diff --git a/ActionLambda.py b/ActionLambda.py index fe94de4..04cec76 100644 --- a/ActionLambda.py +++ b/ActionLambda.py @@ -1,8 +1,9 @@ import json +import boto3 +from botocore.exceptions import ClientError def lambda_handler(event, context): print(event) - # Mock data for demonstration purposes company_data = [ #Technology Industry @@ -12,11 +13,11 @@ def lambda_handler(event, context): {"companyId": 4, "companyName": "DigitalMyricalDreams Gaming", "industrySector": "Technology", "revenue": 40000, "expenses": 6000, "profit": 34000, "employees": 10}, {"companyId": 5, "companyName": "NanoMedNoLand Pharmaceuticals", "industrySector": "Technology", "revenue": 50000, "expenses": 7000, "profit": 43000, "employees": 10}, {"companyId": 6, "companyName": "RoboSuperBombTech Industries", "industrySector": "Technology", "revenue": 60000, "expenses": 8000, "profit": 52000, "employees": 12}, - {"companyId": 7, "companyName": "FuturePastNet Solutions", "industrySector": "Technology", "revenue": 60000, "expenses": 9000, "profit": 51000, "employees": 10}, + {"companyId": 7, "companyName": "FuturePastNet Solutions", "industrySector": "Technology", "revenue": 60000, "expenses": 9000, "profit": 51000, "employees": 10}, {"companyId": 8, "companyName": "InnovativeCreativeAI Corp", "industrySector": "Technology", "revenue": 65000, "expenses": 10000, "profit": 55000, "employees": 15}, {"companyId": 9, "companyName": "EcoLeekoTech Energy", "industrySector": "Technology", "revenue": 70000, "expenses": 11000, "profit": 59000, "employees": 10}, {"companyId": 10, "companyName": "TechyWealthHealth Systems", "industrySector": "Technology", "revenue": 80000, "expenses": 12000, "profit": 68000, "employees": 10}, - + #Real Estate Industry {"companyId": 11, "companyName": "LuxuryToNiceLiving Real Estate", "industrySector": "Real Estate", "revenue": 90000, "expenses": 13000, "profit": 77000, "employees": 10}, {"companyId": 12, "companyName": "UrbanTurbanDevelopers Inc.", "industrySector": "Real Estate", "revenue": 100000, "expenses": 14000, "profit": 86000, "employees": 10}, @@ -29,62 +30,103 @@ def lambda_handler(event, context): {"companyId": 19, "companyName": "GlobalRegional Properties Alliance", "industrySector": "Real Estate", "revenue": 170000, "expenses": 21000, "profit": 149000, "employees": 11}, {"companyId": 20, "companyName": "NextGenPast Residences", "industrySector": "Real Estate", "revenue": 180000, "expenses": 22000, "profit": 158000, "employees": 260} ] - - + def get_named_parameter(event, name): + '''Function searches through the parameters list in the event dictionary and returns the value of the parameter whose name matches the provided name string''' return next(item for item in event['parameters'] if item['name'] == name)['value'] - - def get_named_property(event, name): - return next(item for item in event['requestBody']['content']['application/json']['properties'] if item['name'] == name)['value'] - def companyResearch(event): + '''Searches for a company based on the 'name' parameter in the lambda event and returns its information if found.''' companyName = get_named_parameter(event, 'name').lower() print("NAME PRINTED: ", companyName) - for company_info in company_data: if company_info["companyName"].lower() == companyName: return company_info - return None - + def createPortfolio(event, company_data): + '''Creates a portfolio of top companies based on the 'numCompanies' and 'industry' parameters in the lambda event.''' numCompanies = int(get_named_parameter(event, 'numCompanies')) industry = get_named_parameter(event, 'industry').lower() - - industry_filtered_companies = [company for company in company_data - if company['industrySector'].lower() == industry] - + industry_filtered_companies = [company for company in company_data if company['industrySector'].lower() == industry] sorted_companies = sorted(industry_filtered_companies, key=lambda x: x['profit'], reverse=True) - top_companies = sorted_companies[:numCompanies] return top_companies - def sendEmail(event, company_data): + '''Sends an email using Amazon SES with a summary report and portfolio details''' + # Get parameters from event emailAddress = get_named_parameter(event, 'emailAddress') fomcSummary = get_named_parameter(event, 'fomcSummary') - - # Retrieve the portfolio data as a string portfolioDataString = get_named_parameter(event, 'portfolio') - + + # Create the email content + SENDER = emailAddress # Replace with your email. Must be verified in SES + RECIPIENT = emailAddress + SUBJECT = "Company Portfolio and Search Results Summary Report" + + # Create the email body + BODY_TEXT = (f"Search Summary Report:\n\n{fomcSummary}\n\n" + f"Portfolio Details:\n{portfolioDataString}") + + # HTML version of the email + BODY_HTML = f""" + +
+ +{fomcSummary}
+{portfolioDataString}+ + + """ + + # The character encoding for the email + CHARSET = "UTF-8" + + try: + # Create a new SES client + ses_client = boto3.client('ses') + + # Send the email + response = ses_client.send_email( + Destination={ + 'ToAddresses': [ + RECIPIENT, + ], + }, + Message={ + 'Body': { + 'Html': { + 'Charset': CHARSET, + 'Data': BODY_HTML, + }, + 'Text': { + 'Charset': CHARSET, + 'Data': BODY_TEXT, + }, + }, + 'Subject': { + 'Charset': CHARSET, + 'Data': SUBJECT, + }, + }, + Source=SENDER + ) + + except ClientError as e: + print(e.response['Error']['Message']) + return f"Error sending email: {str(e)}" + else: + return f"Email sent successfully to {emailAddress}! MessageId: {response['MessageId']}" - # Prepare the email content - email_subject = "Portfolio Creation Summary and FOMC Search Results" - #email_body = f"FOMC Search Summary:\n{fomcSummary}\n\nPortfolio Details:\n{json.dumps(portfolioData, indent=4)}" - - # Email sending code here (commented out for now) - - return "Email sent successfully to {}".format(emailAddress) - - result = '' response_code = 200 action_group = event['actionGroup'] api_path = event['apiPath'] - - print("api_path: ", api_path ) - + print("api_path: ", api_path) + if api_path == '/companyResearch': result = companyResearch(event) elif api_path == '/createPortfolio': @@ -94,13 +136,13 @@ def sendEmail(event, company_data): else: response_code = 404 result = f"Unrecognized api path: {action_group}::{api_path}" - + response_body = { 'application/json': { 'body': result } } - + action_response = { 'actionGroup': event['actionGroup'], 'apiPath': event['apiPath'], diff --git a/cfn/2-bedrock-agent-lambda-template.yaml b/cfn/2-bedrock-agent-lambda-template.yaml index 321f8a6..000185b 100644 --- a/cfn/2-bedrock-agent-lambda-template.yaml +++ b/cfn/2-bedrock-agent-lambda-template.yaml @@ -39,7 +39,11 @@ Resources: Action: - 'sqs:SendMessage' Resource: !GetAtt ActionCallDLQ.Arn - + - Effect: Allow + Action: + - 'ses:SendEmail' + - 'ses:SendRawEmail' + Resource: '*' # IAM Managed Policy for Lambda Invocation LambdaInvokePolicy: Type: 'AWS::IAM::ManagedPolicy' @@ -88,10 +92,11 @@ Resources: Code: ZipFile: | import json - + import boto3 + from botocore.exceptions import ClientError + def lambda_handler(event, context): print(event) - # Mock data for demonstration purposes company_data = [ #Technology Industry @@ -101,11 +106,11 @@ Resources: {"companyId": 4, "companyName": "DigitalMyricalDreams Gaming", "industrySector": "Technology", "revenue": 40000, "expenses": 6000, "profit": 34000, "employees": 10}, {"companyId": 5, "companyName": "NanoMedNoLand Pharmaceuticals", "industrySector": "Technology", "revenue": 50000, "expenses": 7000, "profit": 43000, "employees": 10}, {"companyId": 6, "companyName": "RoboSuperBombTech Industries", "industrySector": "Technology", "revenue": 60000, "expenses": 8000, "profit": 52000, "employees": 12}, - {"companyId": 7, "companyName": "FuturePastNet Solutions", "industrySector": "Technology", "revenue": 60000, "expenses": 9000, "profit": 51000, "employees": 10}, + {"companyId": 7, "companyName": "FuturePastNet Solutions", "industrySector": "Technology", "revenue": 60000, "expenses": 9000, "profit": 51000, "employees": 10}, {"companyId": 8, "companyName": "InnovativeCreativeAI Corp", "industrySector": "Technology", "revenue": 65000, "expenses": 10000, "profit": 55000, "employees": 15}, {"companyId": 9, "companyName": "EcoLeekoTech Energy", "industrySector": "Technology", "revenue": 70000, "expenses": 11000, "profit": 59000, "employees": 10}, {"companyId": 10, "companyName": "TechyWealthHealth Systems", "industrySector": "Technology", "revenue": 80000, "expenses": 12000, "profit": 68000, "employees": 10}, - + #Real Estate Industry {"companyId": 11, "companyName": "LuxuryToNiceLiving Real Estate", "industrySector": "Real Estate", "revenue": 90000, "expenses": 13000, "profit": 77000, "employees": 10}, {"companyId": 12, "companyName": "UrbanTurbanDevelopers Inc.", "industrySector": "Real Estate", "revenue": 100000, "expenses": 14000, "profit": 86000, "employees": 10}, @@ -118,58 +123,103 @@ Resources: {"companyId": 19, "companyName": "GlobalRegional Properties Alliance", "industrySector": "Real Estate", "revenue": 170000, "expenses": 21000, "profit": 149000, "employees": 11}, {"companyId": 20, "companyName": "NextGenPast Residences", "industrySector": "Real Estate", "revenue": 180000, "expenses": 22000, "profit": 158000, "employees": 260} ] - - + def get_named_parameter(event, name): + '''Function searches through the parameters list in the event dictionary and returns the value of the parameter whose name matches the provided name string''' return next(item for item in event['parameters'] if item['name'] == name)['value'] - def companyResearch(event): + '''Searches for a company based on the 'name' parameter in the lambda event and returns its information if found.''' companyName = get_named_parameter(event, 'name').lower() print("NAME PRINTED: ", companyName) - for company_info in company_data: if company_info["companyName"].lower() == companyName: return company_info return None - + def createPortfolio(event, company_data): + '''Creates a portfolio of top companies based on the 'numCompanies' and 'industry' parameters in the lambda event.''' numCompanies = int(get_named_parameter(event, 'numCompanies')) industry = get_named_parameter(event, 'industry').lower() - - industry_filtered_companies = [company for company in company_data - if company['industrySector'].lower() == industry] - + industry_filtered_companies = [company for company in company_data if company['industrySector'].lower() == industry] sorted_companies = sorted(industry_filtered_companies, key=lambda x: x['profit'], reverse=True) - top_companies = sorted_companies[:numCompanies] return top_companies - def sendEmail(event, company_data): + '''Sends an email using Amazon SES with a summary report and portfolio details''' + # Get parameters from event emailAddress = get_named_parameter(event, 'emailAddress') fomcSummary = get_named_parameter(event, 'fomcSummary') - - # Retrieve the portfolio data as a string portfolioDataString = get_named_parameter(event, 'portfolio') - - - # Prepare the email content - email_subject = "Portfolio Creation Summary and FOMC Search Results" - #email_body = f"FOMC Search Summary:\n{fomcSummary}\n\nPortfolio Details:\n{json.dumps(portfolioData, indent=4)}" - - # Email sending code here (commented out for now) - - return "Email sent successfully to {}".format(emailAddress) - - + + # Create the email content + SENDER = emailAddress # Replace with your email. Must be verified in SES + RECIPIENT = emailAddress + SUBJECT = "Company Portfolio and Search Results Summary Report" + + # Create the email body + BODY_TEXT = (f"Search Summary Report:\n\n{fomcSummary}\n\n" + f"Portfolio Details:\n{portfolioDataString}") + + # HTML version of the email + BODY_HTML = f""" + + + +
{fomcSummary}
+{portfolioDataString}+ + + """ + + # The character encoding for the email + CHARSET = "UTF-8" + + try: + # Create a new SES client + ses_client = boto3.client('ses') + + # Send the email + response = ses_client.send_email( + Destination={ + 'ToAddresses': [ + RECIPIENT, + ], + }, + Message={ + 'Body': { + 'Html': { + 'Charset': CHARSET, + 'Data': BODY_HTML, + }, + 'Text': { + 'Charset': CHARSET, + 'Data': BODY_TEXT, + }, + }, + 'Subject': { + 'Charset': CHARSET, + 'Data': SUBJECT, + }, + }, + Source=SENDER + ) + + except ClientError as e: + print(e.response['Error']['Message']) + return f"Error sending email: {str(e)}" + else: + return f"Email sent successfully to {emailAddress}! MessageId: {response['MessageId']}" + result = '' response_code = 200 action_group = event['actionGroup'] api_path = event['apiPath'] - - print("api_path: ", api_path ) - + print("api_path: ", api_path) + if api_path == '/companyResearch': result = companyResearch(event) elif api_path == '/createPortfolio': @@ -179,13 +229,13 @@ Resources: else: response_code = 404 result = f"Unrecognized api path: {action_group}::{api_path}" - + response_body = { 'application/json': { 'body': result } } - + action_response = { 'actionGroup': event['actionGroup'], 'apiPath': event['apiPath'], @@ -193,9 +243,10 @@ Resources: 'httpStatusCode': response_code, 'responseBody': response_body } - + api_response = {'messageVersion': '1.0', 'response': action_response} return api_response + # Lambda Permission for Bedrock to Invoke Lambda LambdaInvokePermission: diff --git a/cfn/3-ec2-streamlit-template.yaml b/cfn/3-ec2-streamlit-template.yaml index 0e49d04..7a66fee 100644 --- a/cfn/3-ec2-streamlit-template.yaml +++ b/cfn/3-ec2-streamlit-template.yaml @@ -197,7 +197,7 @@ Resources: apt-get update -y apt-get upgrade -y apt-get install -y python3-pip git ec2-instance-connect - git clone https://github.com/build-on-aws/bedrock-agents-streamlit.git /home/ubuntu/app + git clone https://github.com/jossai87/bedrock-agents-streamlit-1.git /home/ubuntu/app pip3 install -r /home/ubuntu/app/streamlit_app/requirements.txt cd /home/ubuntu/app/streamlit_app diff --git a/streamlit_app/app.py b/streamlit_app/app.py index fab84fe..582c537 100644 --- a/streamlit_app/app.py +++ b/streamlit_app/app.py @@ -70,12 +70,26 @@ def format_response(response_body): response_data = None try: - # Extract the response and trace data - all_data = format_response(response_data['response']) - the_response = response_data['trace_data'] - except: + # Check if response_data is None or contains error + if response_data is None: + all_data = "No response data" + the_response = "Failed to get response from agent" + elif 'error' in response_data: + all_data = "Error occurred" + the_response = f"Agent error: {response_data['error']}" + elif 'response' in response_data and 'trace_data' in response_data: + # Extract the response and trace data + all_data = format_response(response_data['response']) + the_response = response_data['trace_data'] + else: + all_data = f"Unexpected response format: {list(response_data.keys()) if response_data else 'None'}" + the_response = f"Response data: {response_data}" + except Exception as e: + print(f"Error extracting response data: {e}") + print(f"Response data: {response_data}") + print(f"Full response: {response}") all_data = "..." - the_response = "Apologies, but an error occurred. Please rerun the application" + the_response = f"Error occurred: {str(e)}" # Use trace_data and formatted_response as needed st.sidebar.text_area("", value=all_data, height=300) diff --git a/streamlit_app/invoke_agent.py b/streamlit_app/invoke_agent.py index 7003336..069058a 100644 --- a/streamlit_app/invoke_agent.py +++ b/streamlit_app/invoke_agent.py @@ -11,7 +11,11 @@ from botocore.credentials import Credentials from requests import request -ssm = boto3.client('ssm') +# Region configuration moved here to be available for all clients +theRegion = "us-west-2" +os.environ["AWS_REGION"] = theRegion + +ssm = boto3.client('ssm', region_name=theRegion) # --------------------------------------------------------------------- # Replace with your actual Agent ID and Alias ID below: @@ -25,11 +29,7 @@ #agentAliasId = ssm.get_parameter(Name='/alias-id', WithDecryption=True)['Parameter']['Value'] #valid if CFN infrastructure templates were ran -# --------------------------------------------------------------------- -# REGION CONFIGURATION: -# --------------------------------------------------------------------- -theRegion = "us-west-2" -os.environ["AWS_REGION"] = theRegion +# Region configuration moved to top of file # --------------------------------------------------------------------- # HELPER FUNCTION TO GET AWS CREDENTIALS SAFELY @@ -157,27 +157,64 @@ def decode_response(response): final_response = "" for idx in range(len(split_response)): if "bytes" in split_response[idx]: - encoded_last_response = split_response[idx].split("\"")[3] - decoded = base64.b64decode(encoded_last_response) - final_response_chunk = decoded.decode('utf-8') - print(final_response_chunk) + try: + # More robust extraction of base64 content + segment = split_response[idx] + # Find the bytes field and extract the base64 string + bytes_start = segment.find('"bytes":"') + len('"bytes":"') + bytes_end = segment.find('"', bytes_start) + if bytes_start > len('"bytes":"') - 1 and bytes_end > bytes_start: + encoded_last_response = segment[bytes_start:bytes_end] + # Add padding if needed for base64 decoding + missing_padding = len(encoded_last_response) % 4 + if missing_padding: + encoded_last_response += '=' * (4 - missing_padding) + decoded = base64.b64decode(encoded_last_response) + final_response_chunk = decoded.decode('utf-8') + print(final_response_chunk) + final_response += final_response_chunk + else: + print(f"Could not extract base64 from: {segment}") + except (base64.binascii.Error, UnicodeDecodeError) as e: + print(f"Error decoding base64 at index {idx}: {e}") + print(f"Raw data: {split_response[idx]}") else: print(f"No bytes at index {idx}") print(split_response[idx]) - # Attempt to parse the last part for finalResponse - last_response = split_response[-1] - print(f"Last Response: {last_response}") - if "bytes" in last_response: - print("Bytes in last response") - encoded_last_response = last_response.split("\"")[3] - decoded = base64.b64decode(encoded_last_response) - final_response = decoded.decode('utf-8') - else: - print("No bytes in last response") - part1 = string[string.find('finalResponse')+len('finalResponse":'):] - part2 = part1[:part1.find('"}')+2] - final_response = json.loads(part2)['text'] + # If we didn't get a response from the loop above, try the final response parsing + if not final_response: + last_response = split_response[-1] + print(f"Last Response: {last_response}") + if "bytes" in last_response: + print("Bytes in last response") + try: + # More robust extraction of base64 content + bytes_start = last_response.find('"bytes":"') + len('"bytes":"') + bytes_end = last_response.find('"', bytes_start) + if bytes_start > len('"bytes":"') - 1 and bytes_end > bytes_start: + encoded_last_response = last_response[bytes_start:bytes_end] + # Add padding if needed for base64 decoding + missing_padding = len(encoded_last_response) % 4 + if missing_padding: + encoded_last_response += '=' * (4 - missing_padding) + decoded = base64.b64decode(encoded_last_response) + final_response = decoded.decode('utf-8') + else: + print(f"Could not extract base64 from last response: {last_response}") + final_response = "Could not parse final response" + except (base64.binascii.Error, UnicodeDecodeError) as e: + print(f"Error decoding final response: {e}") + final_response = f"Error decoding response: {e}" + else: + print("No bytes in last response") + try: + part1 = string[string.find('finalResponse')+len('finalResponse":'):] + part2 = part1[:part1.find('"}')+2] + final_response = json.loads(part2)['text'] + except (json.JSONDecodeError, KeyError) as e: + print(f"Error parsing final response JSON: {e}") + final_response = f"Error parsing response: {e}" # Cleanup the final response final_response = final_response.replace("\"", "")