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/6] 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 3b1bd4991e9a8a79e8da16993986ed8563ed061e Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Sat, 1 Nov 2025 20:32:01 -0700 Subject: [PATCH 2/6] test --- .../api/boundary/web/AgentController.java | 107 ++++++++++++++++++ .../src/main/resources/application.yml | 7 ++ 2 files changed, 114 insertions(+) create mode 100644 spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java new file mode 100644 index 00000000..ed815515 --- /dev/null +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java @@ -0,0 +1,107 @@ +package org.springframework.samples.petclinic.api.boundary.web; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.auth.signer.params.Aws4SignerParams; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.core.SdkBytes; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@RestController +@RequestMapping("/api/agent") +public class AgentController { + + @Value("${agent.primary.arn:}") + private String primaryAgentArn; + + @Value("${agent.nutrition.arn:}") + private String nutritionAgentArn; + + @Value("${aws.region:us-east-1}") + private String awsRegion; + + private final HttpClient httpClient = HttpClient.newHttpClient(); + + @PostMapping(value = "/ask", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono> askAgent(@RequestBody Map request) { + String query = request.get("query"); + + if (query == null || query.trim().isEmpty()) { + return Mono.error(new ResponseStatusException(HttpStatus.BAD_REQUEST, "Query is required")); + } + + if (primaryAgentArn == null || primaryAgentArn.isEmpty()) { + return Mono.error(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "Agent ARN not configured")); + } + + return Mono.fromCallable(() -> invokeAgent(query)); + } + + private Map invokeAgent(String query) throws Exception { + String sessionId = "pet-clinic-web-session-" + UUID.randomUUID().toString(); + String prompt = query; + + if (nutritionAgentArn != null && !nutritionAgentArn.isEmpty()) { + prompt = query + "\n\nNote: Our nutrition specialist agent ARN is " + nutritionAgentArn; + } + + String encodedArn = URLEncoder.encode(primaryAgentArn, StandardCharsets.UTF_8); + String url = String.format("https://bedrock-agentcore.%s.amazonaws.com/runtimes/%s/invocations?qualifier=DEFAULT", + awsRegion, encodedArn); + + String payload = String.format("{\"prompt\": \"%s\"}", prompt.replace("\"", "\\\"")); + + // Sign the request with AWS SigV4 + SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder() + .uri(URI.create(url)) + .method(SdkHttpMethod.POST) + .putHeader("Content-Type", "application/json") + .putHeader("X-Amzn-Bedrock-AgentCore-Runtime-Session-Id", sessionId) + .contentStreamProvider(() -> SdkBytes.fromUtf8String(payload).asInputStream()) + .build(); + + Aws4SignerParams signerParams = Aws4SignerParams.builder() + .awsCredentials(DefaultCredentialsProvider.create().resolveCredentials()) + .signingName("bedrock-agentcore") + .signingRegion(Region.of(awsRegion)) + .build(); + + SdkHttpFullRequest signedRequest = Aws4Signer.create().sign(httpRequest, signerParams); + + // Build HTTP request + HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() + .uri(URI.create(url)) + .POST(HttpRequest.BodyPublishers.ofString(payload)); + + signedRequest.headers().forEach((key, values) -> + values.forEach(value -> requestBuilder.header(key, value)) + ); + + HttpResponse response = httpClient.send(requestBuilder.build(), + HttpResponse.BodyHandlers.ofString()); + + return Map.of( + "query", query, + "response", response.body(), + "sessionId", sessionId + ); + } +} diff --git a/spring-petclinic-api-gateway/src/main/resources/application.yml b/spring-petclinic-api-gateway/src/main/resources/application.yml index 7380a6c3..aba6f179 100644 --- a/spring-petclinic-api-gateway/src/main/resources/application.yml +++ b/spring-petclinic-api-gateway/src/main/resources/application.yml @@ -27,6 +27,13 @@ aws: id: ${APP_MONITOR_ID:default-monitor-id} identity-pool-id: ${APP_MONITOR_IDENTITY_POOL_ID:default-identity-pool-id} +# Agent configuration +agent: + primary: + arn: ${PRIMARY_AGENT_ARN:} + nutrition: + arn: ${NUTRITION_AGENT_ARN:} + --- spring: config: From 732b1b1d6ee01a051fb01c03f7a39d6de3da4021 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 6 Nov 2025 17:44:50 -0800 Subject: [PATCH 3/6] connect pet clinic agents to pet clinic front end --- cdk/agents/app.js | 6 +- .../lambda/traffic-generator/prompts.json | 262 ++++++------------ .../traffic-generator/traffic_generator.py | 46 +-- ...t-clinic-agents-traffic-generator-stack.js | 4 +- .../primary_agent/pet_clinic_agent.py | 7 +- scripts/agents/setup-agents-demo.sh | 30 +- spring-petclinic-api-gateway/pom.xml | 27 ++ .../api/boundary/web/AgentController.java | 100 ++++--- 8 files changed, 201 insertions(+), 281 deletions(-) diff --git a/cdk/agents/app.js b/cdk/agents/app.js index 2fe1b8d2..1808bdec 100644 --- a/cdk/agents/app.js +++ b/cdk/agents/app.js @@ -6,8 +6,11 @@ const { PetClinicAgentsTrafficGeneratorStack } = require('./lib/pet-clinic-agent const app = new cdk.App(); // Deploy Pet Clinic agents +const petClinicUrl = process.env.PET_CLINIC_URL; +const nutritionServiceUrl = petClinicUrl ? `${petClinicUrl.replace(/\/$/, '')}/nutrition` : undefined; + const agentsStack = new PetClinicAgentsStack(app, 'PetClinicAgentsStack', { - nutritionServiceUrl: process.env.NUTRITION_SERVICE_URL, + nutritionServiceUrl: nutritionServiceUrl, env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, @@ -22,6 +25,7 @@ new PetClinicAgentsTrafficGeneratorStack(app, 'PetClinicAgentsTrafficGeneratorSt }, primaryAgentArn: agentsStack.primaryAgentArn, nutritionAgentArn: agentsStack.nutritionAgentArn, + petClinicUrl: petClinicUrl, }); app.synth(); \ No newline at end of file diff --git a/cdk/agents/lambda/traffic-generator/prompts.json b/cdk/agents/lambda/traffic-generator/prompts.json index 886547a5..18293f38 100644 --- a/cdk/agents/lambda/traffic-generator/prompts.json +++ b/cdk/agents/lambda/traffic-generator/prompts.json @@ -1,121 +1,87 @@ { "nutrition-queries": [ - "What should I feed my iguana?", - "Can my dog eat rice?", - "Can my turtle eat lettuce?", - "What's the best diet for my gecko?", - "My bearded dragon needs supplements, which ones?", - "How often should I feed my tarantula?", - "What vegetables can my tortoise eat?", - "My chameleon is losing weight, diet help?", - "Can my frog eat crickets?", - "What's good nutrition for my axolotl?", - "My ferret won't eat, what should I do?", - "What's the best food for indoor cats?", + "What pet food do you recommend for dogs?", + "What dog food is available at your clinic?", + "What should I feed my dog?", + "What's the best dog food you have?", + "Do you have any dog food in stock?", + "What dog nutrition products are available?", + "Can you recommend dog food?", + "What food do you suggest for puppies?", + "What pet food do you recommend for cats?", + "What cat food is available at your clinic?", + "What should I feed my cat?", + "What's the best cat food you have?", + "Do you have any cat food in stock?", + "What cat nutrition products are available?", + "Can you recommend cat food?", + "What food do you suggest for kittens?", + "What pet food do you recommend for birds?", + "What bird food is available at your clinic?", + "What should I feed my bird?", + "What's the best bird food you have?", + "Do you have any bird food in stock?", + "What bird nutrition products are available?", + "What pet food do you recommend for hamsters?", + "What hamster food is available at your clinic?", + "What should I feed my hamster?", + "What's the best hamster food you have?", + "Do you have any hamster food in stock?", + "What pet food do you recommend for lizards?", + "What lizard food is available at your clinic?", + "What should I feed my lizard?", + "What's the best lizard food you have?", + "What should I feed my bearded dragon?", + "What should I feed my gecko?", + "What pet food do you recommend for snakes?", + "What snake food is available at your clinic?", + "What should I feed my snake?", + "What's the best snake food you have?", + "What should I feed my ball python?", + "What dog treats do you recommend?", + "Do you have grain-free dog food?", + "What cat treats are available?", + "Do you have wet cat food?", + "What seeds are best for birds?", + "Do you have bird vitamins?", + "What vegetables can hamsters eat?", + "Do you have hamster treats?", + "What insects do lizards eat?", + "Do you have live food for reptiles?", + "What should I feed my corn snake?", + "Do you have frozen mice for snakes?", + "What's the best food for senior dogs?", + "Do you have puppy food?", + "What's good for cats with sensitive stomachs?", + "Do you have kitten food?", + "What pellets are best for birds?", + "What treats are safe for hamsters?", + "What calcium supplements do you have for lizards?", + "Do you have high-protein dog food?", + "What's the best food for large breed dogs?", + "Do you have hypoallergenic cat food?", + "What pet food do you recommend for rabbits?", "What should I feed my rabbit?", - "My bird has diabetes, what diet do you recommend?", - "What supplements are good for joint health in horses?", - "Is chocolate toxic to pets?", - "My guinea pig won't eat, what should I do?", - "Can dogs eat blueberries?", - "How often should I feed my ferret?", - "What foods are toxic to birds?", - "My hamster is overweight, help with diet plan", - "Can rabbits eat grapes?", - "My fish has swim bladder disease, what diet?", - "How much water should my reptile drink?", - "Can I give my bird human vitamins?", - "What's a good diet for a diabetic rabbit?", - "My horse has allergies, what food is safe?", - "My cat has diabetes, feeding schedule?", - "How do I transition my turtle to new food?", - "Can guinea pigs eat pellets meant for rabbits?", - "What's the best diet for weight loss in ferrets?", - "My iguana has diarrhea, what should I feed?", - "Are raw diets safe for birds?", - "What supplements help with arthritis in horses?", - "Can hamsters eat sunflower seeds?", - "My snake is refusing food, should I be worried?", - "What's good for my dog's coat health?", - "What's the best food for baby rabbits?", - "How do I know if my bird is dehydrated?", - "Can reptiles eat avocado?", - "My parrot won't stop begging for food", - "How often should senior rabbits eat?", - "Can I feed my hamster table scraps?", - "What's the difference between pellets and seeds for birds?", - "My turtle has bad breath, is it diet related?", - "Can cats eat eggs?", - "Can guinea pigs eat fish?", - "What's the best diet for active horses?", - "My bearded dragon is constipated, what foods help?", - "How do I calculate portion sizes for my rabbit?", - "Can birds eat nuts?", - "What's good for a horse's coat health?", - "My ferret has pancreatitis, what diet?", - "Can turtles eat tuna?", - "What's the best food for indoor rabbits?", - "My bird is a picky eater, help!", - "My dog won't eat dry food", - "Can hamsters eat vegetables?", - "What supplements boost immune system in reptiles?", - "My rabbit has GI stasis, what helps?", - "What should I feed my senior cat?", - "Can my dog eat chicken?", - "My cat won't drink water, help?", - "What's the best diet for large breed dogs?", - "Can cats eat tuna?", - "My dog has sensitive stomach", - "What should I feed my chinchilla?", - "Can my hedgehog eat insects?", - "What supplements help with shedding in cats?", - "What's the best diet for my sugar glider?", - "My pet skunk needs nutrition advice", - "How often should I feed my capybara?", - "What vegetables can my pot-bellied pig eat?", - "Can dogs eat peanut butter?", - "My wallaby is losing weight, diet help?", - "Can my kinkajou eat honey?", - "What's good nutrition for my fennec fox?", - "My prairie dog won't eat, what should I do?", - "What should I feed my miniature donkey?", - "My cat is losing weight, help?", - "Can my coatimundi eat fruits?", - "What's the best diet for my alpaca?", - "My serval needs supplements, which ones?", - "How much should I feed my pygmy goat?", - "What's a good diet for my otter?", - "Can my raccoon eat dog food?", - "What's the best diet for senior dogs?", - "What supplements help with my sloth's digestion?", - "My armadillo has dietary restrictions", - "What should I feed my baby opossum?", - "Can my porcupine eat vegetables?", - "What's the best food for my lemur?", - "My flying squirrel is picky, help!", - "Can cats eat cheese?", - "What supplements boost immune system in chinchillas?", - "Can my degu eat seeds?", - "What's good for digestive health in hedgehogs?", - "My marmoset has food allergies", - "How do I feed my pet crow?", - "What's the best diet for my peacock?", - "Can my toucan eat berries?", - "My dog has kidney disease, diet help?", - "What supplements help with anxiety in sugar gliders?", - "My emu is getting old, diet changes?", - "What's good for my llama's coat health?", - "Can my muntjac deer eat hay?", - "What's good for digestive health in cats?", - "What's the best diet for my axolotl?", - "How much should I feed my puppy?", - "Can dogs eat carrots?", - "My cat is overweight, diet plan?", - "What supplements help with joint pain in dogs?", - "Can cats eat salmon?", - "My dog has allergies, what food is safe?", - "Can cats eat yogurt?", - "What's the best diet for working dogs?" + "What rabbit food is available?", + "Do you have rabbit pellets?", + "What's the best rabbit food?", + "What hay do you recommend for rabbits?", + "Do you have timothy hay for rabbits?", + "What vegetables can rabbits eat?", + "What treats are safe for rabbits?", + "Do you have rabbit vitamin supplements?", + "What should I feed my baby rabbit?", + "What's the best diet for adult rabbits?", + "Do you have alfalfa hay for young rabbits?", + "What pellets do you recommend for rabbits?", + "Can rabbits eat carrots daily?", + "What greens are best for rabbits?", + "What pet food do you recommend for guinea pigs?", + "What should I feed my guinea pig?", + "What pet food do you recommend for ferrets?", + "What should I feed my ferret?", + "What pet food do you recommend for turtles?", + "What should I feed my turtle?" ], "non-nutrition-queries": [ "What are your clinic hours?", @@ -157,68 +123,6 @@ "Do you offer microchipping?", "What's your policy on second opinions?", "Can you provide health certificates for travel?", - "Do you offer spay and neuter services?", - "What's your emergency contact information?", - "Can you recommend other veterinary specialists?", - "Do you offer dental cleaning services?", - "What's your policy on prescription refills?", - "Can I get lab results over the phone?", - "Do you offer wellness packages?", - "What's your policy on missed appointments?", - "Can you help with pet adoption paperwork?", - "Do you offer training classes?", - "What's your experience with my pet's breed?", - "Can you provide references from other clients?", - "Do you offer payment plans for expensive procedures?", - "What's your policy on bringing multiple pets?", - "Can family members attend appointments?", - "Do you offer home euthanasia services?", - "What's your policy on controlled substances?", - "Can you help with behavioral training referrals?", - "Do you offer puppy and kitten packages?", - "What's your experience with exotic pet surgery?", - "Can you provide estimates for procedures?", - "Do you offer online appointment scheduling?", - "What's your policy on service animals?", - "Can you help with pet therapy certification?", - "Do you offer mobile veterinary services?", - "What's your policy on emotional support animals?", - "Can you provide vaccination records for boarding?", - "Do you offer pre-surgical consultations?", - "What's your policy on pain management?", - "Can you help with pet insurance claims?", - "Do you offer rehabilitation services?", - "What's your experience with geriatric pets?", - "Can you provide care instructions after surgery?", - "Do you offer alternative medicine treatments?", - "What's your policy on experimental treatments?", - "Can you help coordinate care with specialists?", - "Do you offer end-of-life counseling?", - "What's your policy on client confidentiality?", - "Can you provide medical records to new vets?", - "Do you offer preventive care plans?", - "What's your experience with rescue animals?", - "Can you help with pet behavioral assessments?", - "Do you offer laser therapy treatments?", - "What's your policy on controlled medication refills?", - "Can you provide health guarantees?", - "Do you offer acupuncture services?", - "What's your experience with wildlife rehabilitation?", - "Can you help with breeding health clearances?", - "Do you offer chiropractic services?", - "What's your policy on client education?", - "Can you provide nutritional counseling?", - "Do you offer physical therapy?", - "What's your experience with show animals?", - "Can you help with travel health requirements?", - "Do you offer holistic treatment options?", - "What's your policy on treatment consent?", - "Can you provide emergency contact lists?", - "Do you offer grief counseling services?", - "What's your experience with working animals?", - "Can you help with disability accommodations?", - "Do you offer complementary therapies?", - "What's your policy on treatment refusal?", - "Can you provide continuing education resources?" + "Do you offer spay and neuter services?" ] -} \ No newline at end of file +} diff --git a/cdk/agents/lambda/traffic-generator/traffic_generator.py b/cdk/agents/lambda/traffic-generator/traffic_generator.py index 91801cfb..a21bbe7f 100644 --- a/cdk/agents/lambda/traffic-generator/traffic_generator.py +++ b/cdk/agents/lambda/traffic-generator/traffic_generator.py @@ -1,12 +1,7 @@ import json import os import random -import uuid -import urllib.parse as urlparse from urllib.request import Request, urlopen -import boto3 -from botocore.auth import SigV4Auth -from botocore.awsrequest import AWSRequest def load_prompts(): with open('prompts.json', 'r') as f: @@ -14,55 +9,36 @@ def load_prompts(): def lambda_handler(event, context): """ - Traffic generator that invokes the Primary Agent with random queries. - - Each query includes the Nutrition Agent's ARN in the context, allowing the - Primary Agent to delegate nutrition-related questions to the specialized agent. - - Also generates a random session ID to be reused following runtime's best practices: - https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-sessions.html + Traffic generator that invokes the Pet Clinic frontend agent endpoint with random queries. """ - 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', '1')) - region = os.environ.get('AWS_REGION', 'us-east-1') - session_id = os.environ.get('SESSION_ID', f"pet-clinic-session-{str(uuid.uuid4())}") + pet_clinic_url = os.environ.get('PET_CLINIC_URL') + num_requests = random.randint(1, 4) - if not primary_agent_arn: + if not pet_clinic_url: return { 'statusCode': 500, - 'body': json.dumps({'error': 'PRIMARY_AGENT_ARN environment variable not set'}) + 'body': json.dumps({'error': 'PET_CLINIC_URL environment variable not set'}) } prompts = load_prompts() results = [] - session = boto3.Session() - credentials = session.get_credentials() for _ in range(num_requests): - is_nutrition_query = random.random() <= 0.75 + is_nutrition_query = random.random() <= 0.95 query = random.choice(prompts['nutrition-queries' if is_nutrition_query else 'non-nutrition-queries']) - prompt = f"{query}\n\nNote: Our nutrition specialist agent ARN is {nutrition_agent_arn}" if nutrition_agent_arn else query try: - encoded_arn = urlparse.quote(primary_agent_arn, safe='') - url = f'https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT' - - payload = json.dumps({'prompt': prompt}).encode('utf-8') - request = AWSRequest(method='POST', url=url, data=payload, headers={ - 'Content-Type': 'application/json', - 'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id': session_id - }) - SigV4Auth(credentials, 'bedrock-agentcore', region).add_auth(request) + url = f"{pet_clinic_url.rstrip('/')}/api/agent/ask" + payload = json.dumps({'query': query}).encode('utf-8') + request = Request(url, data=payload, headers={'Content-Type': 'application/json'}) - with urlopen(Request(url, data=payload, headers=dict(request.headers))) as response: + with urlopen(request) as response: body = response.read().decode('utf-8') results.append({ 'query': query, - 'response': body, - 'agent_used': 'primary' + 'response': body }) except Exception as error: 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 2742b88f..fc58057e 100644 --- a/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js +++ b/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js @@ -21,8 +21,8 @@ class PetClinicAgentsTrafficGeneratorStack extends Stack { environment: { PRIMARY_AGENT_ARN: props?.primaryAgentArn || '', NUTRITION_AGENT_ARN: props?.nutritionAgentArn || '', - REQUESTS_PER_INVOKE: '1', - SESSION_ID: `pet-clinic-traffic-generator-${crypto.randomUUID()}` + PET_CLINIC_URL: props?.petClinicUrl || '', + REQUESTS_PER_INVOKE: '1' } }); 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 efc6128e..02d5c6b6 100644 --- a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py +++ b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py @@ -76,16 +76,17 @@ def consult_nutrition_specialist(query): "- Scheduling guidance\n" "- Basic medical guidance and when to seek veterinary care\n\n" "IMPORTANT GUIDELINES:\n" + "- Keep responses CONCISE and to the point\n" + "- When recommending products, clearly list them using bullet points with product names\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" + "- When consulting the nutrition specialist, ONLY say 'Let me consult our nutrition specialist' - do not mention tools, functions, or technical details\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 and direct customers to purchase from our pet clinic\n" + "- For ALL nutrition, food, or diet questions, always provide specific product recommendations first, then advise customers to monitor their pet's behavior and health for a few weeks, and suggest scheduling a consultation if any issues come up\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" ) diff --git a/scripts/agents/setup-agents-demo.sh b/scripts/agents/setup-agents-demo.sh index 08cca0b4..14b1206e 100755 --- a/scripts/agents/setup-agents-demo.sh +++ b/scripts/agents/setup-agents-demo.sh @@ -5,7 +5,7 @@ set -e # Default values REGION="us-east-1" OPERATION="deploy" -NUTRITION_SERVICE_URL="" +PET_CLINIC_URL="" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -18,8 +18,8 @@ while [[ $# -gt 0 ]]; do OPERATION="${1#*=}" shift ;; - --nutrition-service-url=*) - NUTRITION_SERVICE_URL="${1#*=}" + --pet-clinic-url=*) + PET_CLINIC_URL="${1#*=}" shift ;; *) @@ -45,27 +45,27 @@ unset DOCKER_HOST case $OPERATION in deploy) echo "Deploying Pet Clinic Agents..." - if [[ -z "$NUTRITION_SERVICE_URL" ]]; then - echo "Auto-discovering nutrition service URL from EKS cluster..." + if [[ -z "$PET_CLINIC_URL" ]]; then + echo "Auto-discovering Pet Clinic URL from EKS cluster..." if ! command -v kubectl &> /dev/null; then - echo "Warning: kubectl not found. Skipping nutrition service URL discovery." + echo "Warning: kubectl not found. Skipping Pet Clinic URL discovery." elif ! kubectl cluster-info &> /dev/null; then - echo "Warning: Cannot connect to Kubernetes cluster. Skipping nutrition service URL discovery." + echo "Warning: Cannot connect to Kubernetes cluster. Skipping Pet Clinic URL discovery." else - INGRESS_HOST=$(kubectl get ingress -n pet-clinic -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "") + INGRESS_HOST=$(kubectl get svc -n ingress-nginx -o jsonpath='{.items[?(@.metadata.name=="ingress-nginx-controller")].status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "") if [[ -n "$INGRESS_HOST" ]]; then - NUTRITION_SERVICE_URL="http://${INGRESS_HOST}/nutrition" - echo "Discovered nutrition service URL: $NUTRITION_SERVICE_URL" + PET_CLINIC_URL="http://${INGRESS_HOST}" + echo "Discovered Pet Clinic URL: $PET_CLINIC_URL" else - echo "Warning: No ingress found in pet-clinic namespace." - echo "Agent will run without nutrition service integration." + echo "Warning: No ingress found." + echo "Agent will run without Pet Clinic integration." fi fi else - echo "Using provided nutrition service URL: $NUTRITION_SERVICE_URL" + echo "Using provided Pet Clinic URL: $PET_CLINIC_URL" fi - if [[ -n "$NUTRITION_SERVICE_URL" ]]; then - export NUTRITION_SERVICE_URL + if [[ -n "$PET_CLINIC_URL" ]]; then + export PET_CLINIC_URL fi npm install cdk bootstrap --region $REGION diff --git a/spring-petclinic-api-gateway/pom.xml b/spring-petclinic-api-gateway/pom.xml index c8815e3a..645604b0 100644 --- a/spring-petclinic-api-gateway/pom.xml +++ b/spring-petclinic-api-gateway/pom.xml @@ -120,6 +120,33 @@ 1.22.1 + + + software.amazon.awssdk + bedrockagentcore + 2.34.0 + + + software.amazon.awssdk + auth + 2.34.0 + + + software.amazon.awssdk + regions + 2.34.0 + + + software.amazon.awssdk + http-client-spi + 2.34.0 + + + software.amazon.awssdk + sts + 2.34.0 + + org.junit.jupiter diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java index ed815515..d8e95d74 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java @@ -7,22 +7,19 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import reactor.core.publisher.Mono; -import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.auth.signer.Aws4Signer; -import software.amazon.awssdk.auth.signer.params.Aws4SignerParams; -import software.amazon.awssdk.http.SdkHttpFullRequest; -import software.amazon.awssdk.http.SdkHttpMethod; -import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.bedrockagentcore.BedrockAgentCoreClient; +import software.amazon.awssdk.services.bedrockagentcore.model.InvokeAgentRuntimeRequest; +import software.amazon.awssdk.services.bedrockagentcore.model.InvokeAgentRuntimeResponse; -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j @RestController @@ -38,7 +35,15 @@ public class AgentController { @Value("${aws.region:us-east-1}") private String awsRegion; - private final HttpClient httpClient = HttpClient.newHttpClient(); + private final BedrockAgentCoreClient bedrockClient; + private final String sessionId; + + public AgentController(@Value("${aws.region:us-east-1}") String region) { + this.bedrockClient = BedrockAgentCoreClient.builder() + .region(Region.of(region)) + .build(); + this.sessionId = "pet-clinic-web-session-" + UUID.randomUUID().toString(); + } @PostMapping(value = "/ask", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> askAgent(@RequestBody Map request) { @@ -56,52 +61,55 @@ public Mono> askAgent(@RequestBody Map reque } private Map invokeAgent(String query) throws Exception { - String sessionId = "pet-clinic-web-session-" + UUID.randomUUID().toString(); String prompt = query; if (nutritionAgentArn != null && !nutritionAgentArn.isEmpty()) { prompt = query + "\n\nNote: Our nutrition specialist agent ARN is " + nutritionAgentArn; } - String encodedArn = URLEncoder.encode(primaryAgentArn, StandardCharsets.UTF_8); - String url = String.format("https://bedrock-agentcore.%s.amazonaws.com/runtimes/%s/invocations?qualifier=DEFAULT", - awsRegion, encodedArn); - - String payload = String.format("{\"prompt\": \"%s\"}", prompt.replace("\"", "\\\"")); + String payload = String.format("{\"prompt\": \"%s\"}", escapeJson(prompt)); - // Sign the request with AWS SigV4 - SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder() - .uri(URI.create(url)) - .method(SdkHttpMethod.POST) - .putHeader("Content-Type", "application/json") - .putHeader("X-Amzn-Bedrock-AgentCore-Runtime-Session-Id", sessionId) - .contentStreamProvider(() -> SdkBytes.fromUtf8String(payload).asInputStream()) + InvokeAgentRuntimeRequest invokeRequest = InvokeAgentRuntimeRequest.builder() + .agentRuntimeArn(primaryAgentArn) + .qualifier("DEFAULT") + .runtimeSessionId(sessionId) + .contentType("application/json") + .accept("application/json") + .payload(SdkBytes.fromUtf8String(payload)) .build(); - Aws4SignerParams signerParams = Aws4SignerParams.builder() - .awsCredentials(DefaultCredentialsProvider.create().resolveCredentials()) - .signingName("bedrock-agentcore") - .signingRegion(Region.of(awsRegion)) - .build(); + try (ResponseInputStream responseStream = bedrockClient.invokeAgentRuntime(invokeRequest)) { + String responseBody = new BufferedReader( + new InputStreamReader(responseStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); - SdkHttpFullRequest signedRequest = Aws4Signer.create().sign(httpRequest, signerParams); + String formattedResponse = formatForUI(responseBody); - // Build HTTP request - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() - .uri(URI.create(url)) - .POST(HttpRequest.BodyPublishers.ofString(payload)); - - signedRequest.headers().forEach((key, values) -> - values.forEach(value -> requestBuilder.header(key, value)) - ); + return Map.of( + "query", query, + "response", formattedResponse, + "sessionId", sessionId + ); + } + } - HttpResponse response = httpClient.send(requestBuilder.build(), - HttpResponse.BodyHandlers.ofString()); + private String escapeJson(String str) { + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } - return Map.of( - "query", query, - "response", response.body(), - "sessionId", sessionId - ); + private String formatForUI(String response) { + if (response == null || response.isEmpty()) { + return response; + } + String formatted = response.trim(); + if (formatted.startsWith("\"") && formatted.endsWith("\"")) { + formatted = formatted.substring(1, formatted.length() - 1); + } + return formatted.replace("\\n", "\n").replace("\\\"", "\"").replace("\\\\", "\\"); } } From e54468ae28ba94c5801f81a79b6469c31b3fd28a Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 6 Nov 2025 17:48:55 -0800 Subject: [PATCH 4/6] fix readme --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2959fabd..e6f70896 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ The following instructions set up AI agents deployed to Bedrock AgentCore Runtim The setup includes: - **Primary Agent**: A general pet clinic assistant that handles appointment scheduling, clinic information, and emergency contacts. Any nutrition related queries will be delegated to the Nutrition Agent. -- **Nutrition Agent**: A specialized agent focused on pet nutrition, diet recommendations, and feeding guidelines. When deployed with the Pet Clinic EKS demo, it utilizes the nutrition service API at `http:///nutrition` to base its answers on data from the service +- **Nutrition Agent**: A specialized agent focused on pet nutrition, diet recommendations, and feeding guidelines. When deployed with the Pet Clinic EKS demo, it utilizes the pet clinic service API to base its answers on data from the service - **Traffic Generator**: A Lambda function scheduled via AWS EventBridge that sends queries to the Primary Agent on set a cadence. **Prerequisites:** @@ -194,12 +194,17 @@ The setup includes: cd scripts/agents && ./setup-agents-demo.sh --region=region-name ``` - The Nutrition Agent relies on the nutrition service to retrieve pet nutrition information and provide accurate dietary recommendations. To enable this feature, the [EKS demo](#eks-demo) must be set up first. The deployment script will attempt to auto-discover the nutrition service endpoint from your EKS cluster. If auto-discovery fails or you want to specify a custom endpoint, you can manually provide the nutrition service URL using the `--nutrition-service-url` parameter: + The Nutrition Agent relies on the pet clinic service to retrieve pet nutrition information and provide accurate dietary recommendations. To enable this feature: + + - The [EKS demo](#eks-demo) must be set up first + - **The pet clinic service must be exposed as an ingress service** with a publicly accessible URL for the agent to connect to it + + The deployment script will attempt to auto-discover the pet clinic service endpoint from your EKS cluster. If auto-discovery fails or you want to specify a custom endpoint, you can manually provide the pet clinic service URL using the `--pet-clinic-url` parameter: ```shell - export MY_NUTRITION_ENDPOINT=my-load-balancer.us-east-1.elb.amazonaws.com + export MY_PET_CLINIC_ENDPOINT=my-load-balancer.us-east-1.elb.amazonaws.com - cd scripts/agents && ./setup-agents-demo.sh --region=us-east-1 --nutrition-service-url=http://${MY_NUTRITION_ENDPOINT}/nutrition + cd scripts/agents && ./setup-agents-demo.sh --region=us-east-1 --pet-clinic-url=http://${MY_PET_CLINIC_ENDPOINT} ``` 2. **Clean up resources** when finished: From 772a70cd530e3f2ad02396e0343f9e12d39cfb89 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Fri, 7 Nov 2025 11:36:07 -0800 Subject: [PATCH 5/6] test --- .../traffic-generator/traffic_generator.py | 99 ++++++++++++++----- ...t-clinic-agents-traffic-generator-stack.js | 3 +- .../nutrition_agent/nutrition_agent.py | 16 ++- .../primary_agent/pet_clinic_agent.py | 13 +-- 4 files changed, 92 insertions(+), 39 deletions(-) diff --git a/cdk/agents/lambda/traffic-generator/traffic_generator.py b/cdk/agents/lambda/traffic-generator/traffic_generator.py index a21bbe7f..6f94f027 100644 --- a/cdk/agents/lambda/traffic-generator/traffic_generator.py +++ b/cdk/agents/lambda/traffic-generator/traffic_generator.py @@ -1,7 +1,12 @@ import json import os import random +import uuid +import urllib.parse as urlparse from urllib.request import Request, urlopen +import boto3 +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest def load_prompts(): with open('prompts.json', 'r') as f: @@ -10,42 +15,84 @@ def load_prompts(): def lambda_handler(event, context): """ Traffic generator that invokes the Pet Clinic frontend agent endpoint with random queries. + Falls back to direct Primary Agent invocation if PET_CLINIC_URL is not set. """ - pet_clinic_url = os.environ.get('PET_CLINIC_URL') - num_requests = random.randint(1, 4) - - if not pet_clinic_url: + pet_clinic_url = os.environ.get('PET_CLINIC_URL', '') + primary_agent_arn = os.environ.get('PRIMARY_AGENT_ARN') + nutrition_agent_arn = os.environ.get('NUTRITION_AGENT_ARN') + region = os.environ.get('AWS_REGION', 'us-east-1') + session_id = os.environ.get('SESSION_ID', f"pet-clinic-session-{str(uuid.uuid4())}") + + if not pet_clinic_url and not primary_agent_arn: return { - 'statusCode': 500, - 'body': json.dumps({'error': 'PET_CLINIC_URL environment variable not set'}) + 'statusCode': 200, + 'body': json.dumps({'message': 'Neither PET_CLINIC_URL nor PRIMARY_AGENT_ARN set, skipping traffic generation'}) } prompts = load_prompts() results = [] - for _ in range(num_requests): + # Generate queries for all requests + queries = [] + for _ in range(random.randint(1, 4)): is_nutrition_query = random.random() <= 0.95 - query = random.choice(prompts['nutrition-queries' if is_nutrition_query else 'non-nutrition-queries']) + queries.append(random.choice(prompts['nutrition-queries' if is_nutrition_query else 'non-nutrition-queries'])) + + # Fallback to direct agent invocation if PET_CLINIC_URL is not set + if not pet_clinic_url: + session = boto3.Session() + credentials = session.get_credentials() + + for query in queries: + prompt = f"{query}\n\nNote: Our nutrition specialist agent ARN is {nutrition_agent_arn}" if nutrition_agent_arn else query - try: - url = f"{pet_clinic_url.rstrip('/')}/api/agent/ask" - payload = json.dumps({'query': query}).encode('utf-8') - request = Request(url, data=payload, headers={'Content-Type': 'application/json'}) - - with urlopen(request) as response: - body = response.read().decode('utf-8') - - results.append({ - 'query': query, - 'response': body - }) - - except Exception as error: - results.append({ - 'query': query, - 'error': str(error) - }) + try: + encoded_arn = urlparse.quote(primary_agent_arn, safe='') + url = f'https://bedrock-agentcore.{region}.amazonaws.com/runtimes/{encoded_arn}/invocations?qualifier=DEFAULT' + + payload = json.dumps({'prompt': prompt}).encode('utf-8') + request = AWSRequest(method='POST', url=url, data=payload, headers={ + 'Content-Type': 'application/json', + 'X-Amzn-Bedrock-AgentCore-Runtime-Session-Id': session_id + }) + SigV4Auth(credentials, 'bedrock-agentcore', region).add_auth(request) + + with urlopen(Request(url, data=payload, headers=dict(request.headers))) as response: + body = response.read().decode('utf-8') + + results.append({ + 'query': query, + 'response': body, + 'agent_used': 'primary' + }) + + except Exception as error: + results.append({ + 'query': query, + 'error': str(error) + }) + else: + # Use Pet Clinic URL if available + for query in queries: + try: + url = f"{pet_clinic_url.rstrip('/')}/api/agent/ask" + payload = json.dumps({'query': query}).encode('utf-8') + request = Request(url, data=payload, headers={'Content-Type': 'application/json'}) + + with urlopen(request) as response: + body = response.read().decode('utf-8') + + results.append({ + 'query': query, + 'response': body + }) + + except Exception as error: + results.append({ + 'query': query, + 'error': str(error) + }) return { 'statusCode': 200, 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 fc58057e..49d24369 100644 --- a/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js +++ b/cdk/agents/lib/pet-clinic-agents-traffic-generator-stack.js @@ -21,8 +21,7 @@ class PetClinicAgentsTrafficGeneratorStack extends Stack { environment: { PRIMARY_AGENT_ARN: props?.primaryAgentArn || '', NUTRITION_AGENT_ARN: props?.nutritionAgentArn || '', - PET_CLINIC_URL: props?.petClinicUrl || '', - REQUESTS_PER_INVOKE: '1' + PET_CLINIC_URL: props?.petClinicUrl || '' } }); diff --git a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py index 486729b5..725e5146 100644 --- a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py +++ b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py @@ -57,12 +57,18 @@ def get_nutritional_supplements(pet_type): @tool def create_order(product_name, pet_type, quantity=1): - """Create an order for a recommended product. Requires pet_type and quantity.""" + """Create an order for a recommended product. Requires product_name, pet_type, and optional quantity (default 1).""" + product_lower = product_name.lower() data = get_nutrition_data(pet_type) - if data['products'] and product_name.lower() in data['products'].lower(): + + if not data['products']: + return f"I couldn't verify {product_name} in our inventory. Please call (555) 123-PETS to place your order." + + if product_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}." + return f"Order {order_id} created for {quantity}x {product_name}. Total: ${quantity * 29.99:.2f}. Expected delivery: 3-5 business days. You can pick it up at our clinic or we'll ship it to you." + + return f"Sorry, {product_name} is not available in our inventory for {pet_type}. Available products: {data['products']}" def create_nutrition_agent(): model = BedrockModel( @@ -97,7 +103,7 @@ async def invoke(payload, context): 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/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py index 02d5c6b6..599f220a 100644 --- a/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py +++ b/pet_clinic_ai_agents/primary_agent/pet_clinic_agent.py @@ -76,16 +76,17 @@ def consult_nutrition_specialist(query): "- Scheduling guidance\n" "- Basic medical guidance and when to seek veterinary care\n\n" "IMPORTANT GUIDELINES:\n" - "- Keep responses CONCISE and to the point\n" + "- Keep ALL responses BRIEF and CONCISE - aim for 2-3 sentences maximum unless specifically asked for details\n" "- When recommending products, clearly list them using bullet points with product names\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" + "- For product orders: If pet type is NOT mentioned, ask the customer what type of pet they have (dog, cat, bird, etc.) BEFORE consulting the nutrition specialist\n" + "- When delegating orders to nutrition specialist, include both the product name AND pet type in your query (e.g., 'Place an order for BarkBite Complete Nutrition for a dog')\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, ONLY say 'Let me consult our nutrition specialist' - do not mention tools, functions, or technical details\n" + "- NEVER expose or mention agent ARNs, tools, APIs, or any technical details in your responses to users\n" + "- NEVER say things like 'I'm using a tool' or 'Let me look that up' - just respond naturally\n" + "- When consulting the nutrition specialist, ONLY say 'Let me consult our nutrition specialist' - nothing else about the process\n" "- If the specialist returns an error or indicates unavailability, inform the customer that our specialist is currently unavailable\n" - "- For ALL nutrition, food, or diet questions, always provide specific product recommendations first, then advise customers to monitor their pet's behavior and health for a few weeks, and suggest scheduling a consultation if any issues come up\n" + "- For nutrition questions, provide 2-3 product recommendations in a brief bulleted list, then suggest monitoring and consultation if needed\n" "- Always recommend purchasing products from 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 0f2985a1f319dd025c2acaafeddf3901b66b1d6e Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Fri, 7 Nov 2025 16:24:41 -0800 Subject: [PATCH 6/6] test --- .../api/boundary/web/AgentController.java | 16 +++++++--------- .../src/main/resources/application.yml | 7 ------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java index d8e95d74..471f494a 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java @@ -26,21 +26,19 @@ @RequestMapping("/api/agent") public class AgentController { - @Value("${agent.primary.arn:}") - private String primaryAgentArn; + private String primaryAgentArn = "arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU"; - @Value("${agent.nutrition.arn:}") - private String nutritionAgentArn; + private String nutritionAgentArn = "arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU"; - @Value("${aws.region:us-east-1}") - private String awsRegion; + private String awsRegion = "us-east-1"; private final BedrockAgentCoreClient bedrockClient; private final String sessionId; - public AgentController(@Value("${aws.region:us-east-1}") String region) { + public AgentController(String region) { this.bedrockClient = BedrockAgentCoreClient.builder() - .region(Region.of(region)) + .region(Region.of(awsRegion)) + .credentialsProvider(software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.builder().build()) .build(); this.sessionId = "pet-clinic-web-session-" + UUID.randomUUID().toString(); } @@ -70,7 +68,7 @@ private Map invokeAgent(String query) throws Exception { String payload = String.format("{\"prompt\": \"%s\"}", escapeJson(prompt)); InvokeAgentRuntimeRequest invokeRequest = InvokeAgentRuntimeRequest.builder() - .agentRuntimeArn(primaryAgentArn) + .agentRuntimeArn("arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU") .qualifier("DEFAULT") .runtimeSessionId(sessionId) .contentType("application/json") diff --git a/spring-petclinic-api-gateway/src/main/resources/application.yml b/spring-petclinic-api-gateway/src/main/resources/application.yml index aba6f179..7380a6c3 100644 --- a/spring-petclinic-api-gateway/src/main/resources/application.yml +++ b/spring-petclinic-api-gateway/src/main/resources/application.yml @@ -27,13 +27,6 @@ aws: id: ${APP_MONITOR_ID:default-monitor-id} identity-pool-id: ${APP_MONITOR_IDENTITY_POOL_ID:default-identity-pool-id} -# Agent configuration -agent: - primary: - arn: ${PRIMARY_AGENT_ARN:} - nutrition: - arn: ${NUTRITION_AGENT_ARN:} - --- spring: config: