Skip to content

Commit acb22d9

Browse files
committed
feat(examples): add AgentCore zero-code OTLP example using BedrockAgentCoreApp
Wraps a Strands agent with BedrockAgentCoreApp from bedrock-agentcore-sdk-python. The handler uses @app.entrypoint and the server starts with app.run(), matching the AgentCore runtime pattern. E2E tests use subprocess.Popen, wait for /ping, then POST to /invocations. Tests pass without AWS credentials because Strands exports OTLP spans even when BedrockModel raises NoCredentialsError.
1 parent da3ca5e commit acb22d9

4 files changed

Lines changed: 139 additions & 2 deletions

File tree

examples/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ agentevals accepts OTLP/HTTP on port 4318 (`http/protobuf` and `http/json`) and
3030
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI |
3131
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini |
3232
| [zero-code-examples/pydantic-ai/](./zero-code-examples/pydantic-ai/) | Pydantic AI | OpenAI |
33+
| [zero-code-examples/agentcore/](./zero-code-examples/agentcore/) | AWS AgentCore | Amazon Bedrock |
3334

3435
This approach works with any framework that has OTel instrumentation: LangChain, Strands, Google ADK, etc. If your framework already emits OTel spans, you only need to add `OTLPSpanExporter` (and `OTLPLogExporter` if it uses GenAI log-based content delivery).
3536

@@ -105,6 +106,7 @@ Detection checks for `gen_ai.request.model` / `gen_ai.input.messages` (GenAI sem
105106
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI | GenAI semconv (events*) | Standard OTLP export |
106107
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini | ADK built-in | Standard OTLP export |
107108
| [zero-code-examples/pydantic-ai/](./zero-code-examples/pydantic-ai/) | Pydantic AI | OpenAI | GenAI semconv (span attrs) | Standard OTLP export |
109+
| [zero-code-examples/agentcore/](./zero-code-examples/agentcore/) | AWS AgentCore | Amazon Bedrock | GenAI semconv (events*) | Standard OTLP export |
108110
| [langchain_agent](./langchain_agent/) | LangChain | OpenAI | GenAI semconv (logs) | SDK WebSocket |
109111
| [strands_agent](./strands_agent/) | Strands | OpenAI | GenAI semconv (events*) | SDK WebSocket |
110112
| [dice_agent](./dice_agent/) | Google ADK | Gemini | ADK built-in | SDK WebSocket |
@@ -221,6 +223,10 @@ python examples/zero-code-examples/strands/run.py
221223
python examples/zero-code-examples/adk/run.py
222224
python examples/zero-code-examples/pydantic-ai/run.py
223225

226+
# AgentCore starts a server (AWS credentials required):
227+
python examples/zero-code-examples/agentcore/run.py &
228+
curl http://localhost:8080/invocations -d '{"prompt": "Roll a 20-sided die for me"}'
229+
224230
# SDK examples:
225231
python examples/sdk_example/context_manager_example.py
226232
python examples/sdk_example/decorator_example.py
@@ -235,7 +241,7 @@ python examples/strands_agent/main.py
235241
Traces stream to the dev server in real-time. Evaluation runs automatically when the session completes.
236242

237243
See each example's README for prerequisites and detailed instructions:
238-
- [zero-code-examples/](./zero-code-examples/) (LangChain, Strands, ADK, OpenAI Agents, Pydantic AI standard OTLP)
244+
- [zero-code-examples/](./zero-code-examples/) (LangChain, Strands, ADK, OpenAI Agents, Pydantic AI, AWS AgentCore, standard OTLP)
239245
- [dice_agent/README.md](./dice_agent/README.md) (Google ADK + Gemini)
240246
- [langchain_agent/README.md](./langchain_agent/README.md) (LangChain + OpenAI, SDK)
241247
- [strands_agent/](./strands_agent/) (Strands + OpenAI, SDK)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bedrock-agentcore>=1.8.0
2+
strands-agents>=1.35.0
3+
boto3>=1.38.0
4+
opentelemetry-sdk>=1.36.0
5+
opentelemetry-exporter-otlp-proto-http>=1.36.0
6+
python-dotenv>=1.0.0
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""AWS AgentCore zero-code OTLP example -- no agentevals SDK.
2+
3+
Setup:
4+
pip install -r examples/zero-code-examples/agentcore/requirements.txt
5+
export AWS_DEFAULT_REGION=us-east-1
6+
agentevals serve --dev
7+
8+
Run:
9+
python examples/zero-code-examples/agentcore/run.py
10+
curl http://localhost:8080/invocations -d '{"prompt": "Roll a 20-sided die"}'
11+
agentcore dev # alternative: npm install -g @aws/agentcore
12+
"""
13+
14+
import os
15+
import random
16+
17+
from bedrock_agentcore import BedrockAgentCoreApp
18+
from dotenv import load_dotenv
19+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
20+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
21+
from strands import Agent, tool
22+
from strands.models import BedrockModel
23+
from strands.telemetry import StrandsTelemetry
24+
25+
load_dotenv(override=True)
26+
os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_latest_experimental")
27+
os.environ.setdefault("OTEL_RESOURCE_ATTRIBUTES",
28+
"agentevals.eval_set_id=agentcore_eval,agentevals.session_name=agentcore-zero-code")
29+
30+
_telemetry = StrandsTelemetry()
31+
_telemetry.tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(), schedule_delay_millis=1000))
32+
33+
app = BedrockAgentCoreApp()
34+
35+
36+
@tool
37+
def roll_die(sides: int = 6) -> dict:
38+
"""Roll a die with the given number of sides."""
39+
result = random.randint(1, sides)
40+
return {"sides": sides, "result": result, "message": f"Rolled a {sides}-sided die and got {result}"}
41+
42+
43+
@tool
44+
def check_prime(n: int) -> bool:
45+
"""Return True if number is prime."""
46+
return n >= 2 and all(n % i for i in range(2, int(n**0.5) + 1))
47+
48+
49+
@app.entrypoint
50+
async def handler(payload):
51+
prompt = payload.get("prompt", "Hello!")
52+
agent = Agent(
53+
model=BedrockModel(model_id="us.amazon.nova-pro-v1:0"),
54+
tools=[roll_die, check_prime],
55+
system_prompt="Use roll_die when asked to roll dice. Use check_prime when asked about prime numbers.",
56+
name="dice_agent",
57+
)
58+
async for event in agent.stream_async(prompt):
59+
yield event
60+
61+
62+
app.run()

tests/integration/test_live_agents.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
from __future__ import annotations
1717

18+
import contextlib
1819
import os
1920
import subprocess
2021
import sys
22+
import time
2123

2224
import httpx
2325
import pytest
@@ -38,7 +40,6 @@
3840
reason="GOOGLE_API_KEY not set",
3941
)
4042

41-
4243
def _run_agent(
4344
script: str,
4445
otlp_http_port: int,
@@ -64,6 +65,40 @@ def _run_agent(
6465
)
6566

6667

68+
_AGENTCORE_ENV = {"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental"}
69+
_AGENTCORE_SCRIPT = "examples/zero-code-examples/agentcore/run.py"
70+
71+
72+
@contextlib.contextmanager
73+
def _agentcore_server(otlp_http_port: int, session_name: str, extra_env: dict | None = None):
74+
env = {**os.environ,
75+
"OTEL_EXPORTER_OTLP_ENDPOINT": f"http://127.0.0.1:{otlp_http_port}",
76+
"OTEL_RESOURCE_ATTRIBUTES": f"agentevals.eval_set_id=e2e-test,agentevals.session_name={session_name}",
77+
**(extra_env or {})}
78+
proc = subprocess.Popen([sys.executable, os.path.join(REPO_ROOT, _AGENTCORE_SCRIPT)], env=env, cwd=REPO_ROOT)
79+
try:
80+
for _ in range(20):
81+
if proc.poll() is not None:
82+
raise RuntimeError(f"agentcore server exited early (code {proc.returncode})")
83+
try:
84+
httpx.get("http://127.0.0.1:8080/ping", timeout=1)
85+
break
86+
except Exception:
87+
time.sleep(0.5)
88+
else:
89+
proc.kill()
90+
raise RuntimeError("agentcore server did not start within 10s")
91+
yield proc
92+
finally:
93+
time.sleep(2)
94+
proc.terminate()
95+
try:
96+
proc.wait(timeout=5)
97+
except subprocess.TimeoutExpired:
98+
proc.kill()
99+
proc.wait()
100+
101+
67102
@_skip_no_openai
68103
class TestLangchainZeroCode:
69104
"""Run the LangChain zero-code OTLP example and verify session grouping."""
@@ -365,6 +400,34 @@ def test_session_visible_via_api(self, live_servers):
365400
assert session_name in session_ids
366401

367402

403+
class TestAgentCoreZeroCode:
404+
def test_session_created_spans_only(self, live_servers):
405+
main_port, otlp_http_port, mgr = live_servers
406+
session_name = "e2e-agentcore"
407+
with _agentcore_server(otlp_http_port, session_name, extra_env=_AGENTCORE_ENV):
408+
httpx.post("http://127.0.0.1:8080/invocations", json={"prompt": "Roll a 20-sided die"}, timeout=60)
409+
wait_for_session_complete_sync(mgr, session_name, timeout=60)
410+
s = mgr.sessions[session_name]
411+
assert s.is_complete and s.source == "otlp" and len(s.spans) > 0
412+
413+
def test_invocations_extracted(self, live_servers):
414+
main_port, otlp_http_port, mgr = live_servers
415+
session_name = "e2e-agentcore-inv"
416+
with _agentcore_server(otlp_http_port, session_name, extra_env=_AGENTCORE_ENV):
417+
httpx.post("http://127.0.0.1:8080/invocations", json={"prompt": "Is 17 prime?"}, timeout=60)
418+
wait_for_session_complete_sync(mgr, session_name, timeout=60)
419+
assert len(mgr.sessions[session_name].invocations) > 0
420+
421+
def test_session_visible_via_api(self, live_servers):
422+
main_port, otlp_http_port, mgr = live_servers
423+
session_name = "e2e-agentcore-api"
424+
with _agentcore_server(otlp_http_port, session_name, extra_env=_AGENTCORE_ENV):
425+
httpx.post("http://127.0.0.1:8080/invocations", json={"prompt": "Hello!"}, timeout=60)
426+
wait_for_session_complete_sync(mgr, session_name, timeout=60)
427+
data = httpx.get(f"http://127.0.0.1:{main_port}/api/streaming/sessions").json()["data"]
428+
assert session_name in [s["sessionId"] for s in data]
429+
430+
368431
@_skip_no_openai
369432
class TestAgentRerun:
370433
"""Verify that re-running an agent with the same session_name creates

0 commit comments

Comments
 (0)