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: 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..6f94f027 100644 --- a/cdk/agents/lambda/traffic-generator/traffic_generator.py +++ b/cdk/agents/lambda/traffic-generator/traffic_generator.py @@ -14,62 +14,85 @@ 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. + Falls back to direct Primary Agent invocation if PET_CLINIC_URL is not set. """ + 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') - 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())}") - - if not primary_agent_arn: + + if not pet_clinic_url and not primary_agent_arn: return { - 'statusCode': 500, - 'body': json.dumps({'error': 'PRIMARY_AGENT_ARN 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 = [] - session = boto3.Session() - credentials = session.get_credentials() - for _ in range(num_requests): - is_nutrition_query = random.random() <= 0.75 - 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 + # Generate queries for all requests + queries = [] + for _ in range(random.randint(1, 4)): + is_nutrition_query = random.random() <= 0.95 + 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: - 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) - }) + 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 2742b88f..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 || '', - REQUESTS_PER_INVOKE: '1', - SESSION_ID: `pet-clinic-traffic-generator-${crypto.randomUUID()}` + 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..83878743 100644 --- a/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py +++ b/pet_clinic_ai_agents/nutrition_agent/nutrition_agent.py @@ -57,12 +57,14 @@ 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(): 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 +99,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 efc6128e..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,18 @@ def consult_nutrition_specialist(query): "- Scheduling guidance\n" "- Basic medical guidance and when to seek veterinary care\n\n" "IMPORTANT GUIDELINES:\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, explain to the customer that you need to speak with our nutrition specialist\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 nutrition questions, always include a specific product recommendation and direct customers to purchase from our pet clinic\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" - "- 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/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 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 new file mode 100644 index 00000000..471f494a --- /dev/null +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/AgentController.java @@ -0,0 +1,113 @@ +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.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.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 +@RequestMapping("/api/agent") +public class AgentController { + + private String primaryAgentArn = "arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU"; + + private String nutritionAgentArn = "arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU"; + + private String awsRegion = "us-east-1"; + + private final BedrockAgentCoreClient bedrockClient; + private final String sessionId; + + public AgentController(String region) { + this.bedrockClient = BedrockAgentCoreClient.builder() + .region(Region.of(awsRegion)) + .credentialsProvider(software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.builder().build()) + .build(); + this.sessionId = "pet-clinic-web-session-" + UUID.randomUUID().toString(); + } + + @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 prompt = query; + + if (nutritionAgentArn != null && !nutritionAgentArn.isEmpty()) { + prompt = query + "\n\nNote: Our nutrition specialist agent ARN is " + nutritionAgentArn; + } + + String payload = String.format("{\"prompt\": \"%s\"}", escapeJson(prompt)); + + InvokeAgentRuntimeRequest invokeRequest = InvokeAgentRuntimeRequest.builder() + .agentRuntimeArn("arn:aws:bedrock-agentcore:us-east-1:140023401067:runtime/pet_clinic_agent-1237f9CoGU") + .qualifier("DEFAULT") + .runtimeSessionId(sessionId) + .contentType("application/json") + .accept("application/json") + .payload(SdkBytes.fromUtf8String(payload)) + .build(); + + try (ResponseInputStream responseStream = bedrockClient.invokeAgentRuntime(invokeRequest)) { + String responseBody = new BufferedReader( + new InputStreamReader(responseStream, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + + String formattedResponse = formatForUI(responseBody); + + return Map.of( + "query", query, + "response", formattedResponse, + "sessionId", sessionId + ); + } + } + + private String escapeJson(String str) { + return str.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + + 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("\\\\", "\\"); + } +}