From fa8b9f84889f6a1a039cfb809be3a0133e495b8c Mon Sep 17 00:00:00 2001 From: ADOT Patch workflow Date: Tue, 28 Oct 2025 14:39:11 -0700 Subject: [PATCH 1/3] update pet clinic apps --- cdk/agents/app.js | 1 + .../bedrock-agentcore-deployer/deployer.py | 42 +++-- .../traffic-generator/traffic_generator.py | 31 ++-- cdk/agents/lib/pet-clinic-agents-stack.js | 19 ++- ...t-clinic-agents-traffic-generator-stack.js | 2 +- pet-nutrition-service/db-seed.js | 12 +- pet-nutrition-service/nutrition-fact.js | 3 +- .../nutrition_agent/nutrition_agent.py | 153 ++++++------------ .../nutrition_agent/requirements.txt | 3 +- .../primary_agent/pet_clinic_agent.py | 24 +-- scripts/agents/setup-agents-demo.sh | 9 ++ .../alb-ingress/petclinic-ingress.yaml | 7 + 12 files changed, 146 insertions(+), 160 deletions(-) diff --git a/cdk/agents/app.js b/cdk/agents/app.js index 0e3df05d..2fe1b8d2 100644 --- a/cdk/agents/app.js +++ b/cdk/agents/app.js @@ -7,6 +7,7 @@ const app = new cdk.App(); // Deploy Pet Clinic agents const agentsStack = new PetClinicAgentsStack(app, 'PetClinicAgentsStack', { + nutritionServiceUrl: process.env.NUTRITION_SERVICE_URL, env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, diff --git a/cdk/agents/lambda/bedrock-agentcore-deployer/deployer.py b/cdk/agents/lambda/bedrock-agentcore-deployer/deployer.py index 87b27dce..2fce8683 100644 --- a/cdk/agents/lambda/bedrock-agentcore-deployer/deployer.py +++ b/cdk/agents/lambda/bedrock-agentcore-deployer/deployer.py @@ -27,22 +27,27 @@ def create_agent(properties, event, context): execution_role = properties['ExecutionRole'] try: - response = client.create_agent_runtime( - agentRuntimeName=agent_name, - description=f'{agent_name} agent for Application Signals demo', - agentRuntimeArtifact={ + create_params = { + 'agentRuntimeName': agent_name, + 'description': f'{agent_name} agent for Application Signals demo', + 'agentRuntimeArtifact': { 'containerConfiguration': { 'containerUri': image_uri } }, - roleArn=execution_role, - networkConfiguration={ + 'roleArn': execution_role, + 'networkConfiguration': { 'networkMode': 'PUBLIC' }, - protocolConfiguration={ + 'protocolConfiguration': { 'serverProtocol': 'HTTP' } - ) + } + + if 'EnvironmentVariables' in properties: + create_params['environmentVariables'] = properties['EnvironmentVariables'] + + response = client.create_agent_runtime(**create_params) agent_arn = response['agentRuntimeArn'] @@ -68,22 +73,27 @@ def update_agent(properties, event, context): image_uri = properties['ImageUri'] execution_role = properties['ExecutionRole'] - response = client.update_agent_runtime( - agentRuntimeId=agent_runtime_id, - description=f'{agent_name} agent for Application Signals demo', - agentRuntimeArtifact={ + update_params = { + 'agentRuntimeId': agent_runtime_id, + 'description': f'{agent_name} agent for Application Signals demo', + 'agentRuntimeArtifact': { 'containerConfiguration': { 'containerUri': image_uri } }, - roleArn=execution_role, - networkConfiguration={ + 'roleArn': execution_role, + 'networkConfiguration': { 'networkMode': 'PUBLIC' }, - protocolConfiguration={ + 'protocolConfiguration': { 'serverProtocol': 'HTTP' } - ) + } + + if 'EnvironmentVariables' in properties: + update_params['environmentVariables'] = properties['EnvironmentVariables'] + + response = client.update_agent_runtime(**update_params) agent_arn = response['agentRuntimeArn'] diff --git a/cdk/agents/lambda/traffic-generator/traffic_generator.py b/cdk/agents/lambda/traffic-generator/traffic_generator.py index 5e424f20..e8c3b817 100644 --- a/cdk/agents/lambda/traffic-generator/traffic_generator.py +++ b/cdk/agents/lambda/traffic-generator/traffic_generator.py @@ -15,7 +15,7 @@ def load_prompts(): def lambda_handler(event, context): primary_agent_arn = os.environ.get('PRIMARY_AGENT_ARN') nutrition_agent_arn = os.environ.get('NUTRITION_AGENT_ARN') - num_requests = int(os.environ.get('REQUESTS_PER_INVOKE', '20')) + num_requests = int(os.environ.get('REQUESTS_PER_INVOKE', '1')) # Use environment variable session ID or generate one session_id = os.environ.get('SESSION_ID', f"pet-clinic-session-{str(uuid.uuid4())}") @@ -34,26 +34,27 @@ def lambda_handler(event, context): if is_nutrition_query: query = random.choice(prompts['nutrition-queries']) - enhanced_query = f"{query}\n\nSession ID: {session_id}\nNote: Our nutrition specialist agent ARN is {nutrition_agent_arn}" if nutrition_agent_arn else f"{query}\n\nSession ID: {session_id}" + enhanced_query = f"{query}\n\nNote: Our nutrition specialist agent ARN is {nutrition_agent_arn}" if nutrition_agent_arn else query else: query = random.choice(prompts['non-nutrition-queries']) - enhanced_query = f"{query}\n\nSession ID: {session_id}" + enhanced_query = query try: - encoded_arn = urlparse.quote(primary_agent_arn, safe='') - region = os.environ.get('AWS_REGION', 'us-east-1') - url = f'https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT' + client = boto3.client('bedrock-agentcore') - payload = json.dumps({'prompt': enhanced_query}) - request = AWSRequest(method='POST', url=url, data=payload, headers={'Content-Type': 'application/json'}) - session = boto3.Session() - credentials = session.get_credentials() + response = client.invoke_agent_runtime( + agentRuntimeArn=primary_agent_arn, + runtimeSessionId=session_id, + payload=json.dumps({'prompt': enhanced_query}).encode('utf-8') + ) - SigV4Auth(credentials, 'bedrock-agentcore', region).add_auth(request) - - req = Request(url, data=payload.encode('utf-8'), headers=dict(request.headers)) - with urlopen(req) as response: - body = response.read().decode('utf-8') + # Read the StreamingBody from the response + if 'response' in response: + body = response['response'].read().decode('utf-8') + elif 'body' in response: + body = response['body'].read().decode('utf-8') + else: + body = str(response) results.append({ 'query': query, diff --git a/cdk/agents/lib/pet-clinic-agents-stack.js b/cdk/agents/lib/pet-clinic-agents-stack.js index 1a7a4ed7..6ab875c3 100644 --- a/cdk/agents/lib/pet-clinic-agents-stack.js +++ b/cdk/agents/lib/pet-clinic-agents-stack.js @@ -79,13 +79,26 @@ class PetClinicAgentsStack extends Stack { directory: '../../pet_clinic_ai_agents/primary_agent' }); - // Deploy nutrition agent - const nutritionAgent = new BedrockAgentCoreDeployer(this, 'NutritionAgent', { + // Deploy nutrition agent with optional environment variable + const nutritionAgentProps = { AgentName: 'nutrition_agent', ImageUri: nutritionAgentImage.imageUri, ExecutionRole: agentCoreRole.roleArn, Entrypoint: 'nutrition_agent.py' - }); + }; + + if (props?.nutritionServiceUrl) { + nutritionAgentProps.EnvironmentVariables = { + NUTRITION_SERVICE_URL: props.nutritionServiceUrl, + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: 'sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,system_metrics,google-genai' + }; + } else { + nutritionAgentProps.EnvironmentVariables = { + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: 'sqlalchemy,psycopg2,pymysql,sqlite3,aiopg,asyncpg,mysql_connector,system_metrics,google-genai' + }; + } + + const nutritionAgent = new BedrockAgentCoreDeployer(this, 'NutritionAgent', nutritionAgentProps); // Deploy primary agent const primaryAgent = new BedrockAgentCoreDeployer(this, 'PrimaryAgent', { diff --git a/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js b/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js index b94f6872..2742b88f 100644 --- a/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js +++ b/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js @@ -21,7 +21,7 @@ class PetClinicAgentsTrafficGeneratorStack extends Stack { environment: { PRIMARY_AGENT_ARN: props?.primaryAgentArn || '', NUTRITION_AGENT_ARN: props?.nutritionAgentArn || '', - REQUESTS_PER_INVOKE: '20', + REQUESTS_PER_INVOKE: '1', SESSION_ID: `pet-clinic-traffic-generator-${crypto.randomUUID()}` } }); diff --git a/pet-nutrition-service/db-seed.js b/pet-nutrition-service/db-seed.js index 3f615adb..f2f7465d 100644 --- a/pet-nutrition-service/db-seed.js +++ b/pet-nutrition-service/db-seed.js @@ -12,12 +12,12 @@ module.exports = function(){ .catch(err => logger.error('error dropping collection:', err)); NutritionFact.insertMany([ - { pet_type: 'cat', facts: 'High-protein, grain-free dry or wet food with real meat as the main ingredient' }, - { pet_type: 'dog', facts: 'Balanced dog food with quality proteins, fats, and carbohydrates' }, - { pet_type: 'lizard', facts: 'Insects, leafy greens, and calcium supplements' }, - { pet_type: 'snake', facts: 'Whole prey (mice/rats) based on size' }, - { pet_type: 'bird', facts: 'High-quality seeds, pellets, and fresh fruits/veggies' }, - { pet_type: 'hamster', facts: 'Pellets, grains, fresh vegetables, and occasional fruits' } + { pet_type: 'cat', facts: 'High-protein, grain-free dry or wet food with real meat as the main ingredient', products: 'PurrfectChoice Premium Feline, WhiskerWell Grain-Free Delight, MeowMaster Senior Formula' }, + { pet_type: 'dog', facts: 'Balanced dog food with quality proteins, fats, and carbohydrates', products: 'BarkBite Complete Nutrition, TailWagger Performance Plus, PawsitiveCare Sensitive Blend' }, + { pet_type: 'lizard', facts: 'Insects, leafy greens, and calcium supplements', products: 'ScaleStrong Calcium Boost, CricketCrunch Live Supply, ReptileVitality D3 Formula' }, + { pet_type: 'snake', facts: 'Whole prey (mice/rats) based on size', products: 'SlitherSnack Frozen Mice, CoilCuisine Feeder Rats, SerpentSupreme Multivitamin' }, + { pet_type: 'bird', facts: 'High-quality seeds, pellets, and fresh fruits/veggies', products: 'FeatherFeast Premium Pellets, WingWellness Seed Mix, BeakBoost Cuttlebone Calcium' }, + { pet_type: 'hamster', facts: 'Pellets, grains, fresh vegetables, and occasional fruits', products: 'HamsterHaven Complete Pellets, CheekPouch Gourmet Mix, WhiskerWonder Vitamin Drops' } ]) .then(() => logger.info('collection populated')) .catch(err => logger.error('error populating collection:', err)); diff --git a/pet-nutrition-service/nutrition-fact.js b/pet-nutrition-service/nutrition-fact.js index 75837b90..8cde57c8 100644 --- a/pet-nutrition-service/nutrition-fact.js +++ b/pet-nutrition-service/nutrition-fact.js @@ -2,7 +2,8 @@ const mongoose = require('mongoose'); const NutritionFactSchema = new mongoose.Schema({ pet_type: { type: String, required: true }, - facts: { type: String, required: true } + facts: { type: String, required: true }, + products: { type: String, required: false } }); diff --git a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py index c233609d..41e3aefd 100644 --- a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py +++ b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py @@ -1,96 +1,55 @@ from strands import Agent, tool import uvicorn -import yaml -import random +import requests +import os from strands.models import BedrockModel from bedrock_agentcore.runtime import BedrockAgentCoreApp BEDROCK_MODEL_ID = "us.anthropic.claude-3-5-haiku-20241022-v1:0" - -# Exceptions -class TimeoutException(Exception): - def __init__(self, message, **kwargs): - super().__init__(message) - self.details = kwargs - -class ValidationException(Exception): - def __init__(self, message, **kwargs): - super().__init__(message) - self.details = kwargs - -class ServiceException(Exception): - def __init__(self, message, **kwargs): - super().__init__(message) - self.details = kwargs - -class RateLimitException(Exception): - def __init__(self, message, **kwargs): - super().__init__(message) - self.details = kwargs - -class NetworkException(Exception): - def __init__(self, message, **kwargs): - super().__init__(message) - self.details = kwargs - -try: - with open('pet_database.yaml', 'r') as f: - ANIMAL_DATA = yaml.safe_load(f) -except Exception: - ANIMAL_DATA = None +NUTRITION_SERVICE_URL = os.environ.get('NUTRITION_SERVICE_URL') agent = None agent_app = BedrockAgentCoreApp() -@tool -def get_feeding_guidelines(pet_type, age, weight): - """Get feeding guidelines based on pet type, age, and weight""" - if ANIMAL_DATA is None: - return "Animal database is down, please consult your veterinarian for feeding guidelines." - - animal = ANIMAL_DATA.get(pet_type.lower() + 's') - if not animal: - return f"{pet_type.title()} not found in animal database. Consult veterinarian for specific feeding guidelines" - - calories_per_lb = animal.get('calories_per_pound', '15-20') - schedule = animal.get('feeding_schedule', {}).get(age.lower(), '2 times daily') - +def get_nutrition_data(pet_type): + """Helper function to get nutrition data from the API""" + if not NUTRITION_SERVICE_URL: + return {"facts": "Error: Nutrition service not found", "products": ""} try: - weight = float(weight) - if isinstance(calories_per_lb, str) and '-' in calories_per_lb: - calories = weight * float(calories_per_lb.split('-')[0]) - else: - calories = weight * float(calories_per_lb) - except (ValueError, TypeError): - return f"Feed based on veterinary recommendations for {pet_type}, {schedule}" - - return f"Feed approximately {calories:.0f} calories daily, {schedule}" + response = requests.get(f"{NUTRITION_SERVICE_URL}/{pet_type.lower()}", timeout=5) + if response.status_code == 200: + data = response.json() + return {"facts": data.get('facts', ''), "products": data.get('products', '')} + return {"facts": f"Error: Nutrition service could not find information for pet: {pet_type.lower()}", "products": ""} + except requests.RequestException: + return {"facts": "Error: Nutrition service down", "products": ""} @tool -def get_dietary_restrictions(pet_type, condition): - """Get dietary recommendations for specific health conditions by animal type""" - if ANIMAL_DATA is None: - return "Animal database is down, please consult your veterinarian for dietary advice." - - animal = ANIMAL_DATA.get(pet_type.lower() + 's') - if not animal: - return f"{pet_type.title()} not found in animal database. Consult veterinarian for condition-specific dietary advice" - - restrictions = animal.get('dietary_restrictions', {}) - return restrictions.get(condition.lower(), f"No dietary restrictions for {condition} found in animal database. Consult veterinarian for condition-specific dietary advice") +def get_feeding_guidelines(pet_type): + """Get feeding guidelines based on pet type""" + data = get_nutrition_data(pet_type) + result = f"Nutrition info for {pet_type}: {data['facts']}" + if data['products']: + result += f" Recommended products available at our clinic: {data['products']}" + return result @tool -def get_nutritional_supplements(pet_type, supplement): - """Get supplement recommendations by animal type""" - if ANIMAL_DATA is None: - return "Animal database is down, please consult your veterinarian before adding supplements." - - animal = ANIMAL_DATA.get(pet_type.lower() + 's') - if not animal: - return f"{pet_type.title()} not found in animal database. Consult veterinarian before adding supplements" - - supplements = animal.get('supplements', {}) - return supplements.get(supplement.lower(), f"No information for {supplement} supplement found in animal database. Consult veterinarian before adding supplements") +def get_dietary_restrictions(pet_type): + """Get dietary recommendations for specific health conditions by animal type""" + data = get_nutrition_data(pet_type) + result = f"Dietary info for {pet_type}: {data['facts']}. Consult veterinarian for condition-specific advice." + if data['products']: + result += f" Recommended products available at our clinic: {data['products']}" + return result + +@tool +def get_nutritional_supplements(pet_type): + """Get supplement recommendations by animal type""" + data = get_nutrition_data(pet_type) + result = f"Supplement info for {pet_type}: {data['facts']}. Consult veterinarian for supplements." + if data['products']: + result += f" Recommended products available at our clinic: {data['products']}" + return result def create_nutrition_agent(): model = BedrockModel( @@ -100,44 +59,26 @@ def create_nutrition_agent(): tools = [get_feeding_guidelines, get_dietary_restrictions, get_nutritional_supplements] system_prompt = ( - "You are a specialized pet nutrition expert providing evidence-based dietary guidance.\n\n" - "Your expertise covers:\n" - "- Feeding guidelines for dogs, cats, fish, horses, birds, rabbits, ferrets, hamsters, guinea pigs, reptiles, and amphibians\n" - "- Therapeutic diets for health conditions (diabetes, kidney disease, allergies, obesity, arthritis)\n" - "- Food safety and toxic substances to avoid\n" - "- Nutritional supplements and their proper use\n" - "- Food label interpretation and AAFCO standards\n\n" - "Key principles:\n" - "- Cats are obligate carnivores requiring animal-based nutrients\n" - "- Dogs are omnivores needing balanced animal and plant sources\n" - "- Always recommend veterinary consultation for significant dietary changes\n" - "- Provide specific, actionable advice when possible\n\n" - "Toxic foods to avoid: garlic, onions, chocolate, grapes, xylitol, alcohol, macadamia nuts" + "You are a specialized pet nutrition expert at our veterinary clinic, providing accurate, evidence-based dietary guidance for pets. " + "Never mention using any API, tools, or external services - present all advice as your own expert knowledge.\n\n" + "When providing nutrition guidance:\n" + "- Use the specific nutrition information available to you as the foundation for your recommendations\n" + "- Always recommend the SPECIFIC PRODUCT NAMES provided to you that pet owners should buy FROM OUR PET CLINIC\n" + "- Mention our branded products by name (like PurrfectChoice, BarkBite, FeatherFeast, etc.) when recommending food\n" + "- Emphasize that we carry high-quality, veterinarian-recommended food brands at our clinic\n" + "- Give actionable dietary recommendations including feeding guidelines, restrictions, and supplements\n" + "- Expand on basic nutrition facts with comprehensive guidance for age, weight, and health conditions\n" + "- Always mention that pet owners can purchase the recommended food items directly from our clinic for convenience and quality assurance" ) return Agent(model=model, tools=tools, system_prompt=system_prompt) -def maybe_throw_error(threshold: float=1): - """Randomly throw an error based on threshold probability""" - if random.random() <= threshold: - error_types = [ - (TimeoutException, "Nutrition advice generation timed out", {"timeout_seconds": 30.0, "operation": "nutrition_advice_generation"}), - (ValidationException, "Invalid nutrition query format", {"field": "nutrition_query", "value": "simulated_invalid_input"}), - (ServiceException, "Nutrition service internal error", {"service_name": "nutrition-agent", "error_code": "INTERNAL_ERROR", "retryable": True}), - (RateLimitException, "Too many nutrition requests", {"retry_after_seconds": random.randint(30, 120), "limit_type": "requests_per_minute"}), - (NetworkException, "Network error connecting to nutrition service", {"endpoint": "nutrition-service", "error_code": "CONNECTION_FAILED", "retryable": True}) - ] - - exception_class, message, kwargs = random.choice(error_types) - raise exception_class(message, **kwargs) @agent_app.entrypoint async def invoke(payload, context): """ Invoke the nutrition agent with a payload """ - maybe_throw_error(threshold=0.35) - agent = create_nutrition_agent() msg = payload.get('prompt', '') diff --git a/pet_clinic_ai_agents/nutrition_agent/requirements.txt b/pet_clinic_ai_agents/nutrition_agent/requirements.txt index b4ba9c5a..644dca83 100644 --- a/pet_clinic_ai_agents/nutrition_agent/requirements.txt +++ b/pet_clinic_ai_agents/nutrition_agent/requirements.txt @@ -4,4 +4,5 @@ uv boto3 bedrock-agentcore bedrock-agentcore-starter-toolkit -aws-opentelemetry-distro>=0.12.1 \ No newline at end of file +aws-opentelemetry-distro>=0.12.1 +requests \ No newline at end of file diff --git a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py index 5094fd9e..0115d8fe 100644 --- a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py +++ b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py @@ -1,11 +1,12 @@ import os import boto3 import json -import uuid import uvicorn +import uuid from strands import Agent, tool from strands.models import BedrockModel from bedrock_agentcore.runtime import BedrockAgentCoreApp +from botocore.exceptions import ClientError BEDROCK_MODEL_ID = "us.anthropic.claude-3-5-haiku-20241022-v1:0" @@ -36,7 +37,7 @@ def get_appointment_availability(): return "We have appointments available: Today 3:00 PM, Tomorrow 10:00 AM and 2:30 PM. Call (555) 123-PETS to schedule." @tool -def consult_nutrition_specialist(query, agent_arn, session_id=None): +def consult_nutrition_specialist(query, agent_arn, context=None): """Delegate nutrition questions to the specialized nutrition agent. Requires the nutrition agent ARN as a parameter.""" if not agent_arn: @@ -44,6 +45,7 @@ def consult_nutrition_specialist(query, agent_arn, session_id=None): try: region = os.environ.get('AWS_REGION') or os.environ.get('AWS_DEFAULT_REGION', 'us-east-1') + session_id = os.environ.get('CURRENT_SESSION_ID') or str(uuid.uuid4()) client = boto3.client('bedrock-agentcore', region_name=region) response = client.invoke_agent_runtime( agentRuntimeArn=agent_arn, @@ -57,13 +59,13 @@ def consult_nutrition_specialist(query, agent_arn, session_id=None): return body else: return "Our nutrition specialist is experiencing high demand. Please try again in a few moments or call (555) 123-PETS ext. 201." + except ClientError as e: + return str(e) except Exception as e: - print(f"Error calling nutrition specialist: {e}") return "Unable to reach our nutrition specialist. Please call (555) 123-PETS ext. 201." agent = None agent_app = BedrockAgentCoreApp() -session_id = f"pet-clinic-primary-agent-session-{str(uuid.uuid4())}" system_prompt = ( "You are a helpful pet clinic assistant. You can help with:\n" @@ -76,12 +78,9 @@ def consult_nutrition_specialist(query, agent_arn, session_id=None): "- ONLY use the consult_nutrition_specialist tool for EXPLICIT nutrition-related questions (diet, feeding, supplements, food recommendations, what to feed, can pets eat X, nutrition advice)\n" "- DO NOT use the nutrition agent for general clinic questions, appointments, hours, emergencies, or non-nutrition medical issues\n" "- NEVER expose or mention agent ARNs in your responses to users\n" - "- If the user query contains 'session id', extract and use that session ID when calling consult_nutrition_specialist\n" - "- If no session ID is provided in the query, use the default session ID\n" + "- When the nutrition specialist recommends any food products or supplements, fully endorse those recommendations and emphasize that these products are available at our pet clinic\n" "- For medical concerns, provide general guidance and recommend scheduling a veterinary appointment\n" - "- For emergencies, immediately provide emergency contact information\n" - "- Always recommend consulting with a veterinarian for proper diagnosis and treatment\n\n" - f"Your default session ID is: {session_id}. When calling consult_nutrition_specialist, use the session ID from the query if provided, otherwise use this default session_id parameter." + "- For emergencies, immediately provide emergency contact information" ) def create_clinic_agent(): @@ -97,12 +96,15 @@ def create_clinic_agent(): async def invoke(payload, context): """ Invoke the clinic agent with a payload - """ + """ + if context and hasattr(context, 'session_id'): + os.environ['CURRENT_SESSION_ID'] = context.session_id + agent = create_clinic_agent() msg = payload.get('prompt', '') response_data = [] - async for event in agent.stream_async(msg): + async for event in agent.stream_async(msg, context=context): if 'data' in event: response_data.append(event['data']) diff --git a/scripts/agents/setup-agents-demo.sh b/scripts/agents/setup-agents-demo.sh index 9998dd12..ee77e951 100755 --- a/scripts/agents/setup-agents-demo.sh +++ b/scripts/agents/setup-agents-demo.sh @@ -5,6 +5,7 @@ set -e # Default values REGION="us-east-1" OPERATION="deploy" +NUTRITION_SERVICE_URL="" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -17,6 +18,10 @@ while [[ $# -gt 0 ]]; do OPERATION="${1#*=}" shift ;; + --nutrition-service-url=*) + NUTRITION_SERVICE_URL="${1#*=}" + shift + ;; *) echo "Unknown option $1" exit 1 @@ -40,6 +45,10 @@ unset DOCKER_HOST case $OPERATION in deploy) echo "Deploying Pet Clinic Agents..." + if [[ -n "$NUTRITION_SERVICE_URL" ]]; then + echo "Using nutrition service URL: $NUTRITION_SERVICE_URL" + export NUTRITION_SERVICE_URL + fi npm install cdk bootstrap --region $REGION cdk deploy --all --require-approval never diff --git a/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml b/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml index 000e3b9f..a97c3924 100644 --- a/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml +++ b/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml @@ -39,3 +39,10 @@ spec: name: visits-service-java port: number: 8082 + - path: /nutrition + pathType: Prefix + backend: + service: + name: nutrition-service-nodejs + port: + number: 80 From 0d79c6e39644de0414168b0880b2d28300f58ead Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Fri, 31 Oct 2025 11:51:41 -0700 Subject: [PATCH 2/3] add order tool to nutrition agent --- cdk/agents/lib/pet-clinic-agents-stack.js | 5 ++++- .../nutrition_agent/nutrition_agent.py | 15 +++++++++++++-- .../primary_agent/pet_clinic_agent.py | 12 ++++++++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/cdk/agents/lib/pet-clinic-agents-stack.js b/cdk/agents/lib/pet-clinic-agents-stack.js index 6ab875c3..c8d250fc 100644 --- a/cdk/agents/lib/pet-clinic-agents-stack.js +++ b/cdk/agents/lib/pet-clinic-agents-stack.js @@ -105,7 +105,10 @@ class PetClinicAgentsStack extends Stack { AgentName: 'pet_clinic_agent', ImageUri: primaryAgentImage.imageUri, ExecutionRole: agentCoreRole.roleArn, - Entrypoint: 'pet_clinic_agent.py' + Entrypoint: 'pet_clinic_agent.py', + EnvironmentVariables: { + NUTRITION_AGENT_ARN: nutritionAgent.agentArn + } }); this.nutritionAgentImageUri = nutritionAgentImage.imageUri; diff --git a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py index 4c0246b5..486729b5 100644 --- a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py +++ b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py @@ -3,6 +3,7 @@ import requests import os import boto3 +import uuid from strands.models import BedrockModel from bedrock_agentcore.runtime import BedrockAgentCoreApp @@ -54,12 +55,21 @@ def get_nutritional_supplements(pet_type): result += f" Recommended products available at our clinic: {data['products']}" return result +@tool +def create_order(product_name, pet_type, quantity=1): + """Create an order for a recommended product. Requires pet_type and quantity.""" + data = get_nutrition_data(pet_type) + if data['products'] and product_name.lower() in data['products'].lower(): + order_id = f"ORD-{uuid.uuid4().hex[:8].upper()}" + return f"Order {order_id} created for {quantity}x {product_name}. Total: ${quantity * 29.99:.2f}. Expected delivery: 3-5 business days." + return f"Sorry, can't make the order. {product_name} is not available in our inventory for {pet_type}." + def create_nutrition_agent(): model = BedrockModel( model_id=BEDROCK_MODEL_ID, ) - tools = [get_feeding_guidelines, get_dietary_restrictions, get_nutritional_supplements] + tools = [get_feeding_guidelines, get_dietary_restrictions, get_nutritional_supplements, create_order] system_prompt = ( "You are a specialized pet nutrition expert at our veterinary clinic, providing accurate, evidence-based dietary guidance for pets. " @@ -71,7 +81,8 @@ def create_nutrition_agent(): "- Emphasize that we carry high-quality, veterinarian-recommended food brands at our clinic\n" "- Give actionable dietary recommendations including feeding guidelines, restrictions, and supplements\n" "- Expand on basic nutrition facts with comprehensive guidance for age, weight, and health conditions\n" - "- Always mention that pet owners can purchase the recommended food items directly from our clinic for convenience and quality assurance" + "- Always mention that pet owners can purchase the recommended food items directly from our clinic for convenience and quality assurance\n" + "- If asked to order or purchase a product, use the create_order tool to place the order" ) return Agent(model=model, tools=tools, system_prompt=system_prompt) diff --git a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py index d2a62735..efc6128e 100644 --- a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py +++ b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py @@ -37,9 +37,10 @@ def get_appointment_availability(): return "We have appointments available: Today 3:00 PM, Tomorrow 10:00 AM and 2:30 PM. Call (555) 123-PETS to schedule." @tool -def consult_nutrition_specialist(query, agent_arn, context=None): - """Delegate nutrition questions to the specialized nutrition agent. Requires the nutrition agent ARN as a parameter.""" +def consult_nutrition_specialist(query): + """Delegate nutrition questions to the specialized nutrition agent.""" + agent_arn = os.environ.get('NUTRITION_AGENT_ARN') if not agent_arn: return "Nutrition specialist configuration error. Please call (555) 123-PETS ext. 201." @@ -68,7 +69,7 @@ def consult_nutrition_specialist(query, agent_arn, context=None): agent_app = BedrockAgentCoreApp() system_prompt = ( - "You are a helpful pet clinic assistant. You can help with:\n" + "You are a helpful assistant at our pet clinic. We offer comprehensive veterinary services including:\n" "- General clinic information (hours, contact info)\n" "- Emergency situations and contacts\n" "- Directing clients to appropriate specialists\n" @@ -76,12 +77,15 @@ def consult_nutrition_specialist(query, agent_arn, context=None): "- Basic medical guidance and when to seek veterinary care\n\n" "IMPORTANT GUIDELINES:\n" "- ONLY use the consult_nutrition_specialist tool for EXPLICIT nutrition-related questions (diet, feeding, supplements, food recommendations, what to feed, can pets eat X, nutrition advice)\n" + "- Delegate pet food product orders to the nutrition specialist using the consult_nutrition_specialist tool\n" "- DO NOT use the nutrition agent for general clinic questions, appointments, hours, emergencies, or non-nutrition medical issues\n" "- NEVER expose or mention agent ARNs in your responses to users\n" "- NEVER mention using tools, APIs, or external services - present all information as your own knowledge\n" "- When consulting the nutrition specialist, explain to the customer that you need to speak with our nutrition specialist\n" "- If the specialist returns an error or indicates unavailability, inform the customer that our specialist is currently unavailable\n" - "- For nutrition questions, always include a specific product recommendation\n" + "- For nutrition questions, always include a specific product recommendation and direct customers to purchase from our pet clinic\n" + "- Always recommend purchasing products from our pet clinic\n" + "- If a requested product is not available, politely inform the customer that we don't currently carry that item at our pet clinic\n" "- For medical concerns, provide general guidance and recommend scheduling a veterinary appointment\n" "- For emergencies, immediately provide emergency contact information" ) From 1e1f13372d63d994b71d6fb61f5cd633d7987f96 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Fri, 31 Oct 2025 12:13:08 -0700 Subject: [PATCH 3/3] remove nutrition from ingress yaml --- .../sample-app/alb-ingress/petclinic-ingress.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml b/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml index a97c3924..000e3b9f 100644 --- a/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml +++ b/scripts/eks/appsignals/sample-app/alb-ingress/petclinic-ingress.yaml @@ -39,10 +39,3 @@ spec: name: visits-service-java port: number: 8082 - - path: /nutrition - pathType: Prefix - backend: - service: - name: nutrition-service-nodejs - port: - number: 80