From fd11f3ea202f9c2b60f8705af24689b8cca1b47a Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:12:15 -0500 Subject: [PATCH 1/8] Update invoke_agent.py fixed botocore.exceptions.NoRegionError --- streamlit_app/invoke_agent.py | 58 ++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/streamlit_app/invoke_agent.py b/streamlit_app/invoke_agent.py index 7003336..5fc2172 100644 --- a/streamlit_app/invoke_agent.py +++ b/streamlit_app/invoke_agent.py @@ -4,6 +4,7 @@ import io import sys import boto3 +import requests from boto3.session import Session from botocore.auth import SigV4Auth @@ -11,7 +12,50 @@ from botocore.credentials import Credentials from requests import request -ssm = boto3.client('ssm') +# --------------------------------------------------------------------- +# REGION CONFIGURATION - Dynamic detection with fallback: +# --------------------------------------------------------------------- +def get_current_region(): + """ + Attempt to dynamically determine the current AWS region with multiple fallback options. + """ + # Try to get region from instance metadata (works on EC2) + try: + r = requests.get('http://169.254.169.254/latest/meta-data/placement/region', timeout=0.5) + if r.status_code == 200: + return r.text + except: + pass + + # Try to get region from environment variables + if 'AWS_REGION' in os.environ: + return os.environ['AWS_REGION'] + if 'AWS_DEFAULT_REGION' in os.environ: + return os.environ['AWS_DEFAULT_REGION'] + + # Try to get region from boto3 session + try: + session = boto3.session.Session() + if session.region_name: + return session.region_name + except: + pass + + # Default to us-west-2 if all else fails + return "us-west-2" + +# Set the region dynamically +theRegion = get_current_region() +print(f"Using AWS region: {theRegion}") + +# Set environment variable for other AWS services +os.environ["AWS_REGION"] = theRegion + +# Configure boto3 to use the specified region +boto3.setup_default_session(region_name=theRegion) + +# Now create the SSM client with the region explicitly specified +ssm = boto3.client('ssm', region_name=theRegion) # --------------------------------------------------------------------- # Replace with your actual Agent ID and Alias ID below: @@ -24,13 +68,6 @@ #agentId = ssm.get_parameter(Name='/agent-id', WithDecryption=True)['Parameter']['Value'] #valid if CFN infrastructure templates were ran #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 - # --------------------------------------------------------------------- # HELPER FUNCTION TO GET AWS CREDENTIALS SAFELY # --------------------------------------------------------------------- @@ -39,7 +76,7 @@ def get_frozen_credentials(): Safely obtain frozen AWS credentials from the current Boto3 Session. Raise an error if credentials are not found to clarify what's missing. """ - session = Session() + session = Session(region_name=theRegion) creds = session.get_credentials() if not creds: raise EnvironmentError( @@ -78,7 +115,7 @@ def sigv4_request( The HTTP response (requests.Response object). """ if region is None: - region = os.environ.get("AWS_REGION", "us-west-2") + region = theRegion if credentials is None: credentials = get_frozen_credentials() @@ -226,4 +263,3 @@ def lambda_handler(event, context): "status_code": 500, "body": json.dumps({"error": str(e)}) } - From 8c01a6a97b2ac9829b579cdf59f81357ed40c7ec Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:52:08 -0500 Subject: [PATCH 2/8] Update invoke_agent.py --- streamlit_app/invoke_agent.py | 40 +++-------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/streamlit_app/invoke_agent.py b/streamlit_app/invoke_agent.py index 5fc2172..ab1cf47 100644 --- a/streamlit_app/invoke_agent.py +++ b/streamlit_app/invoke_agent.py @@ -4,7 +4,6 @@ import io import sys import boto3 -import requests from boto3.session import Session from botocore.auth import SigV4Auth @@ -13,42 +12,9 @@ from requests import request # --------------------------------------------------------------------- -# REGION CONFIGURATION - Dynamic detection with fallback: +# REGION CONFIGURATION: # --------------------------------------------------------------------- -def get_current_region(): - """ - Attempt to dynamically determine the current AWS region with multiple fallback options. - """ - # Try to get region from instance metadata (works on EC2) - try: - r = requests.get('http://169.254.169.254/latest/meta-data/placement/region', timeout=0.5) - if r.status_code == 200: - return r.text - except: - pass - - # Try to get region from environment variables - if 'AWS_REGION' in os.environ: - return os.environ['AWS_REGION'] - if 'AWS_DEFAULT_REGION' in os.environ: - return os.environ['AWS_DEFAULT_REGION'] - - # Try to get region from boto3 session - try: - session = boto3.session.Session() - if session.region_name: - return session.region_name - except: - pass - - # Default to us-west-2 if all else fails - return "us-west-2" - -# Set the region dynamically -theRegion = get_current_region() -print(f"Using AWS region: {theRegion}") - -# Set environment variable for other AWS services +theRegion = "us-west-2" os.environ["AWS_REGION"] = theRegion # Configure boto3 to use the specified region @@ -115,7 +81,7 @@ def sigv4_request( The HTTP response (requests.Response object). """ if region is None: - region = theRegion + region = os.environ.get("AWS_REGION", "us-west-2") if credentials is None: credentials = get_frozen_credentials() From d7504a2bc97af3c4763c9ce8cd15e79b59cd1964 Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:52:46 -0500 Subject: [PATCH 3/8] Update 3-ec2-streamlit-template.yaml --- cfn/3-ec2-streamlit-template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 95386c3b79ee11906fcd56ff930df020239b43d1 Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:07:01 -0500 Subject: [PATCH 4/8] Update invoke_agent.py fixed base64 issue --- streamlit_app/invoke_agent.py | 85 ++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/streamlit_app/invoke_agent.py b/streamlit_app/invoke_agent.py index ab1cf47..069058a 100644 --- a/streamlit_app/invoke_agent.py +++ b/streamlit_app/invoke_agent.py @@ -11,16 +11,10 @@ from botocore.credentials import Credentials from requests import request -# --------------------------------------------------------------------- -# REGION CONFIGURATION: -# --------------------------------------------------------------------- +# Region configuration moved here to be available for all clients theRegion = "us-west-2" os.environ["AWS_REGION"] = theRegion -# Configure boto3 to use the specified region -boto3.setup_default_session(region_name=theRegion) - -# Now create the SSM client with the region explicitly specified ssm = boto3.client('ssm', region_name=theRegion) # --------------------------------------------------------------------- @@ -34,6 +28,9 @@ #agentId = ssm.get_parameter(Name='/agent-id', WithDecryption=True)['Parameter']['Value'] #valid if CFN infrastructure templates were ran #agentAliasId = ssm.get_parameter(Name='/alias-id', WithDecryption=True)['Parameter']['Value'] #valid if CFN infrastructure templates were ran + +# Region configuration moved to top of file + # --------------------------------------------------------------------- # HELPER FUNCTION TO GET AWS CREDENTIALS SAFELY # --------------------------------------------------------------------- @@ -42,7 +39,7 @@ def get_frozen_credentials(): Safely obtain frozen AWS credentials from the current Boto3 Session. Raise an error if credentials are not found to clarify what's missing. """ - session = Session(region_name=theRegion) + session = Session() creds = session.get_credentials() if not creds: raise EnvironmentError( @@ -160,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("\"", "") @@ -229,3 +263,4 @@ def lambda_handler(event, context): "status_code": 500, "body": json.dumps({"error": str(e)}) } + From 95b6edfaaba0b39eaaf4c04e5f57507792792766 Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:07:25 -0500 Subject: [PATCH 5/8] Update app.py --- streamlit_app/app.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) 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) From 19fec5279c758171538504571162090ebc4b3787 Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:40:02 -0500 Subject: [PATCH 6/8] Update ActionLambda.py uses email now --- ActionLambda.py | 112 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 35 deletions(-) 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'], From 4e45360a8bdbb658c705b05d90d3a2747e34330c Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:19:01 -0500 Subject: [PATCH 7/8] Update 2-bedrock-agent-lambda-template.yaml --- cfn/2-bedrock-agent-lambda-template.yaml | 115 ++++++++++++++++------- 1 file changed, 81 insertions(+), 34 deletions(-) diff --git a/cfn/2-bedrock-agent-lambda-template.yaml b/cfn/2-bedrock-agent-lambda-template.yaml index 321f8a6..b176ee1 100644 --- a/cfn/2-bedrock-agent-lambda-template.yaml +++ b/cfn/2-bedrock-agent-lambda-template.yaml @@ -88,10 +88,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 +102,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 +119,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 +225,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 +239,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: From a69f925e00d0319fa50cc60e49a15cb88245e3c3 Mon Sep 17 00:00:00 2001 From: jossai87 <60478605+jossai87@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:32:36 -0500 Subject: [PATCH 8/8] Update 2-bedrock-agent-lambda-template.yaml --- cfn/2-bedrock-agent-lambda-template.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cfn/2-bedrock-agent-lambda-template.yaml b/cfn/2-bedrock-agent-lambda-template.yaml index b176ee1..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'