Skip to content

Commit 7908d48

Browse files
committed
Merge branch 'feature/chat-with-document' into 'develop'
Feature/chat with document See merge request genaiic-reusable-assets/engagement-artifacts/genaiic-idp-accelerator!232
2 parents d0511b8 + 26b949f commit 7908d48

File tree

13 files changed

+477
-68
lines changed

13 files changed

+477
-68
lines changed

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ The solution uses a modular architecture with nested CloudFormation stacks to su
4141
- Pattern-specific optimizations and configurations
4242
- Optional features that can be enabled across all patterns:
4343
- Document summarization (controlled by `IsSummarizationEnabled` parameter)
44+
- This feature also enables the "Chat with Document" functionality
45+
- This feature does not use the Bedrock Knowledge Base but stores a full-text text file in S3
4446
- Document Knowledge Base (using Amazon Bedrock)
4547
- Automated accuracy evaluation against baseline data
4648

docs/web-ui.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ The solution includes a responsive web-based user interface built with React tha
2222
- **Confidence threshold configuration** for HITL (Human-in-the-Loop) triggering through the Assessment & HITL Configuration section
2323
- Document upload from local computer
2424
- Knowledge base querying for document collections
25+
- "Chat with document" from the detailed view of the document
2526
- **Document Process Flow visualization** for detailed workflow execution monitoring and troubleshooting
2627
- **Document Analytics** for querying and visualizing processed document data
2728

@@ -97,6 +98,17 @@ The Document Process Flow visualization is particularly useful for troubleshooti
9798
- Analyze execution times to identify performance bottlenecks
9899
- Inspect the input and output of each step to verify data transformation
99100

101+
## Chat with Document
102+
103+
The "Chat with Document" feature is available at the bottom of the Document Detail view. This feature uses the same model that's configured to do the summarization to provide a RAG interface to the document that's the details are displayed for. No other document is taken in to account except the document you're viewing the details of. Note that this feature will only work after the document status is marked as complete.
104+
105+
Your chat history will be saved as you continue your chat but if you leave the document details screen, your chat history is erased. This feature uses prompt caching for the document contents for repeated chat requests for each document.
106+
107+
### How to Use
108+
109+
1. Navigate to a document's detail page and scroll to the bottom
110+
2. In the text area, type in your question and you'll see an answer pop up after the document is analyzed with the model that's configured for summarization
111+
100112
## Authentication Features
101113

102114
The web UI uses Amazon Cognito for secure user authentication and authorization:

lib/idp_common_pkg/idp_common/summarization/service.py

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,16 @@ def process_document(
575575
content_type="application/json",
576576
)
577577

578+
# Store the full text for chat
579+
all_text = self._get_all_text(document)
580+
fulltext_key = f"{document.input_key}/summary/fulltext.txt"
581+
s3.write_content(
582+
content=all_text,
583+
bucket=output_bucket,
584+
key=fulltext_key,
585+
content_type="text/plain",
586+
)
587+
578588
# Create and store the combined markdown summary
579589
md_key = f"{document.input_key}/summary/summary.md"
580590

@@ -639,6 +649,30 @@ def process_document(
639649

640650
return document
641651

652+
def _get_all_text(self, document: Document) -> str:
653+
"""
654+
Retrieve all text content from a document's pages.
655+
656+
Args:
657+
document: Document object to process
658+
659+
Returns:
660+
str: Combined text content from all pages
661+
"""
662+
all_text = ""
663+
for page_id, page in sorted(document.pages.items()):
664+
if page.parsed_text_uri:
665+
try:
666+
page_text = s3.get_text_content(page.parsed_text_uri)
667+
all_text += f"<page-number>{page_id}</page-number>\n{page_text}\n\n"
668+
except Exception as e:
669+
logger.warning(
670+
f"Failed to load text content from {page.parsed_text_uri}: {e}"
671+
)
672+
# Continue with other pages
673+
674+
return all_text
675+
642676
def _process_document_as_whole(
643677
self, document: Document, store_results: bool = True
644678
) -> Document:
@@ -659,19 +693,7 @@ def _process_document_as_whole(
659693
start_time = time.time()
660694

661695
# Combine text from all pages
662-
all_text = ""
663-
for page_id, page in sorted(document.pages.items()):
664-
if page.parsed_text_uri:
665-
try:
666-
page_text = s3.get_text_content(page.parsed_text_uri)
667-
all_text += (
668-
f"<page-number>{page_id}</page-number>\n{page_text}\n\n"
669-
)
670-
except Exception as e:
671-
logger.warning(
672-
f"Failed to load text content from {page.parsed_text_uri}: {e}"
673-
)
674-
# Continue with other pages
696+
all_text = self._get_all_text(document)
675697

676698
if not all_text:
677699
logger.warning("No text content found in document pages")
@@ -708,7 +730,7 @@ def _process_document_as_whole(
708730
content_type="application/json",
709731
)
710732

711-
# Store the raw text
733+
# Store the full text for chat
712734
fulltext_key = f"{document.input_key}/summary/fulltext.txt"
713735
s3.write_content(
714736
content=all_text,

lib/idp_common_pkg/tests/unit/summarization/test_summarization_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -450,8 +450,8 @@ def test_process_document(
450450
# Verify executor was used to process sections in parallel
451451
assert mock_executor_instance.submit.call_count == 2
452452

453-
# Verify write_content was called for combined results
454-
assert mock_write_content.call_count == 2
453+
# Verify write_content was called for combined results (JSON, fulltext, and markdown)
454+
assert mock_write_content.call_count == 3
455455

456456
# Verify document has summarization_result
457457
assert result.summarization_result is not None

src/api/schema.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ type Query @aws_cognito_user_pools @aws_iam {
203203
getFileContents(s3Uri: String!): FileContentsResponse
204204
getConfiguration: ConfigurationResponse
205205
queryKnowledgeBase(input: String!, sessionId: String): String
206-
chatWithDocument(s3Uri: String!, prompt: String!): String
206+
chatWithDocument(s3Uri: String!, prompt: String!, history: AWSJSON!, modelId: String!): String
207207
getStepFunctionExecution(executionArn: String!): StepFunctionExecutionResponse
208208
submitAnalyticsQuery(query: String!): AnalyticsJob @aws_cognito_user_pools
209209
getAnalyticsJobStatus(jobId: ID!): AnalyticsJob @aws_cognito_user_pools
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import json
2+
import boto3
3+
import logging
4+
import botocore
5+
import html
6+
import mimetypes
7+
import base64
8+
import hashlib
9+
import os
10+
from urllib.parse import urlparse
11+
from botocore.exceptions import ClientError
12+
13+
# Set up logging
14+
logger = logging.getLogger()
15+
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
16+
# Get LOG_LEVEL from environment variable with INFO as default
17+
18+
def get_summarization_model():
19+
"""Get the summarization model from configuration table"""
20+
try:
21+
dynamodb = boto3.resource('dynamodb')
22+
config_table = dynamodb.Table(os.environ['CONFIGURATION_TABLE_NAME'])
23+
24+
# Query for the Default configuration
25+
response = config_table.get_item(
26+
Key={'Configuration': 'Default'}
27+
)
28+
29+
if 'Item' in response:
30+
config_data = response['Item']
31+
# Extract summarization model from the configuration
32+
if 'summarization' in config_data and 'model' in config_data['summarization']:
33+
return config_data['summarization']['model']
34+
35+
# Fallback to a default model if not found in config
36+
return 'us.amazon.nova-pro-v1:0'
37+
38+
except Exception as e:
39+
logger.error(f"Error getting summarization model from config: {str(e)}")
40+
return 'us.amazon.nova-pro-v1:0' # Fallback default
41+
42+
def handler(event, context):
43+
response_data = {}
44+
45+
try:
46+
# logger.info(f"Received event: {json.dumps(event)}")
47+
48+
objectKey = event['arguments']['s3Uri']
49+
prompt = event['arguments']['prompt']
50+
history = event['arguments']['history']
51+
52+
full_prompt = "You are an assistant that's responsible for getting details from document text attached here based on questions from the user.\n\n"
53+
full_prompt += "If you don't know the answer, just say that you don't know. Don't try to make up an answer.\n\n"
54+
full_prompt += "Additionally, use the user and assistant responses in the following JSON object to see what's been asked and what the resposes were in the past.\n\n"
55+
full_prompt += "The JSON object is: " + json.dumps(history) + ".\n\n"
56+
full_prompt += "The user's question is: " + prompt
57+
58+
# this feature is not enabled until the model can be selected on the chat screen
59+
# selectedModelId = event['arguments']['modelId']
60+
selectedModelId = get_summarization_model()
61+
62+
logger.info(f"Processing S3 URI: {objectKey}")
63+
64+
output_bucket = os.environ['OUTPUT_BUCKET']
65+
66+
bedrock_runtime = boto3.client('bedrock-runtime', region_name='us-west-2')
67+
68+
# Call Bedrock Runtime to get Python code based on the prompt
69+
if (len(objectKey)):
70+
encoded_string = objectKey.encode()
71+
md5_hash = hashlib.md5(encoded_string, usedforsecurity=False)
72+
hex_representation = md5_hash.hexdigest()
73+
74+
# full text key
75+
fulltext_key = objectKey + '/summary/fulltext.txt'
76+
77+
logger.info(f"Output Bucket: {output_bucket}")
78+
logger.info(f"Full Text Key: {fulltext_key}")
79+
80+
# read full contents of the object as text
81+
s3 = boto3.client('s3')
82+
response = s3.get_object(Bucket=output_bucket, Key=fulltext_key)
83+
content_str = response['Body'].read().decode('utf-8')
84+
85+
message = [
86+
{
87+
"role":"user",
88+
"content": [
89+
{
90+
"text": content_str
91+
},
92+
{
93+
"cachePoint" : {
94+
'type': 'default'
95+
}
96+
}
97+
]
98+
},
99+
{
100+
"role":"user",
101+
"content": [
102+
{
103+
"text": full_prompt
104+
}
105+
]
106+
}
107+
]
108+
109+
# print('invoking model converse')
110+
111+
response = bedrock_runtime.converse(
112+
modelId=selectedModelId,
113+
messages=message
114+
)
115+
116+
token_usage = response['usage']
117+
# print(f"Input tokens: {token_usage['inputTokens']}")
118+
# print(f"Output tokens: {token_usage['outputTokens']}")
119+
# print(f"Total tokens: {token_usage['totalTokens']}")
120+
# print(f"cacheReadInputTokens: {token_usage['cacheReadInputTokens']}")
121+
# print(f"cacheWriteInputTokens: {token_usage['cacheWriteInputTokens']}")
122+
# print(f"Stop reason: {response['stopReason']}")
123+
124+
output_message = response['output']['message']
125+
126+
model_response_text = ''
127+
for content in output_message['content']:
128+
model_response_text += content['text']
129+
130+
# print output_message
131+
132+
chat_response = {"cr" : output_message }
133+
return json.dumps(chat_response)
134+
135+
136+
except ClientError as e:
137+
error_code = e.response['Error']['Code']
138+
error_message = e.response['Error']['Message']
139+
logger.error(f"S3 ClientError: {error_code} - {error_message}")
140+
141+
if error_code == 'NoSuchKey':
142+
raise Exception(f"File not found: {objectKey}")
143+
elif error_code == 'NoSuchBucket':
144+
raise Exception(f"Bucket not found: {output_bucket}")
145+
else:
146+
raise Exception(error_message)
147+
148+
except Exception as e:
149+
logger.error(f"Unexpected error: {str(e)}")
150+
raise Exception(f"Error fetching file: {str(e)}")
151+
152+
return response_data
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
boto3>=1.38.45

src/ui/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ui/src/components/chat-panel/ChatPanel.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,34 @@
2323
white-space: pre-wrap;
2424
}
2525

26+
.chat-assistant.error {
27+
background-color: rgb(245, 178, 178);
28+
}
29+
2630
.chat-composer-container {
2731
margin-top: 10px;
32+
}
33+
34+
.time {
35+
font-size: 9px;
36+
}
37+
38+
/* HTML: <div class="loader"></div> */
39+
.loader {
40+
width: 60px;
41+
aspect-ratio: 2;
42+
--_g: no-repeat radial-gradient(circle closest-side,#000 90%,#0000);
43+
background:
44+
var(--_g) 0% 50%,
45+
var(--_g) 50% 50%,
46+
var(--_g) 100% 50%;
47+
background-size: calc(100%/5) 60%;
48+
animation: l3 1s infinite linear;
49+
margin: 10px 5px;
50+
}
51+
@keyframes l3 {
52+
20%{background-position:0% 0%, 50% 50%,100% 50%}
53+
40%{background-position:0% 100%, 50% 0%,100% 50%}
54+
60%{background-position:0% 50%, 50% 100%,100% 0%}
55+
80%{background-position:0% 50%, 50% 50%,100% 100%}
2856
}

0 commit comments

Comments
 (0)