Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The OTLP receiver runs on port 4318 (standard OTLP HTTP port) and accepts both `
|---------|-----------|-------------|
| [zero-code-examples/langchain/](./zero-code-examples/langchain/) | LangChain | OpenAI |
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI |
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini |

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).

Expand Down Expand Up @@ -92,6 +93,7 @@ Detection checks for `gen_ai.request.model` / `gen_ai.input.messages` (GenAI sem
|---------|-----------|-------------|-----------------|-----------------|
| [zero-code-examples/langchain/](./zero-code-examples/langchain/) | LangChain | OpenAI | GenAI semconv (logs) | Standard OTLP export |
| [zero-code-examples/strands/](./zero-code-examples/strands/) | Strands | OpenAI | GenAI semconv (events) | Standard OTLP export |
| [zero-code-examples/adk/](./zero-code-examples/adk/) | Google ADK | Gemini | ADK built-in | Standard OTLP export |
| [langchain_agent](./langchain_agent/) | LangChain | OpenAI | GenAI semconv (logs) | SDK WebSocket |
| [strands_agent](./strands_agent/) | Strands | OpenAI | GenAI semconv (events) | SDK WebSocket |
| [dice_agent](./dice_agent/) | Google ADK | Gemini | ADK built-in | SDK WebSocket |
Expand Down Expand Up @@ -190,6 +192,7 @@ cd ui && npm run dev
# Zero-code OTLP (recommended):
python examples/zero-code-examples/langchain/run.py
python examples/zero-code-examples/strands/run.py
python examples/zero-code-examples/adk/run.py

# SDK examples:
python examples/sdk_example/context_manager_example.py
Expand Down
5 changes: 5 additions & 0 deletions examples/zero-code-examples/adk/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
google-adk>=1.0.0

opentelemetry-sdk>=1.36.0
opentelemetry-exporter-otlp-proto-http>=1.36.0
python-dotenv>=1.0.0
88 changes: 88 additions & 0 deletions examples/zero-code-examples/adk/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Run the ADK dice agent with standard OTLP export -- no agentevals SDK.

Demonstrates zero-code integration: ADK auto-instruments itself through
the global TracerProvider, so the only setup needed is a standard
OTLPSpanExporter pointing at the agentevals receiver.

Unlike the LangChain and Strands zero-code examples, ADK needs no
instrumentor call, no LoggerProvider, and no special environment variables.
ADK puts message content directly on span attributes (gcp.vertex.agent.*).

Prerequisites:
1. pip install -r requirements.txt
2. agentevals serve --dev
3. export GOOGLE_API_KEY="your-key-here"

Usage:
python examples/zero-code-examples/adk/run.py
"""

import asyncio
import os
import sys

from dotenv import load_dotenv
from google.adk.runners import InMemoryRunner
from google.genai import types
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "dice_agent"))
from agent import dice_agent

load_dotenv(override=True)


async def main():
if not os.getenv("GOOGLE_API_KEY"):
print("GOOGLE_API_KEY not set.")
return

endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
print(f"OTLP endpoint: {endpoint}")

os.environ.setdefault(
"OTEL_RESOURCE_ATTRIBUTES",
"agentevals.eval_set_id=dice_agent_eval,agentevals.session_name=adk-zero-code",
)

resource = Resource.create()

tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(), schedule_delay_millis=1000))
trace.set_tracer_provider(tracer_provider)

app_name = "dice_agent_app"
user_id = "demo_user"

runner = InMemoryRunner(agent=dice_agent, app_name=app_name)
session = await runner.session_service.create_session(app_name=app_name, user_id=user_id)

test_queries = [
"Hi! Can you help me?",
"Roll a 20-sided die for me",
"Is the number you rolled prime?",
]

for i, query in enumerate(test_queries, 1):
print(f"\n[{i}/{len(test_queries)}] User: {query}")

content = types.Content(role="user", parts=[types.Part.from_text(text=query)])

agent_response = ""
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
if event.content.parts and event.content.parts[0].text:
agent_response = event.content.parts[0].text

print(f" Agent: {agent_response}")

print()
tracer_provider.force_flush()
print("All traces flushed to OTLP receiver.")


if __name__ == "__main__":
asyncio.run(main())
64 changes: 63 additions & 1 deletion tests/integration/test_live_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
exercising the full pipeline including BatchSpanProcessor/BatchLogRecordProcessor
flush timing, session grouping, and invocation extraction.

Requires API keys (OPENAI_API_KEY for LangChain/Strands).
Requires API keys (OPENAI_API_KEY for LangChain/Strands, GOOGLE_API_KEY for ADK).
Skipped when keys are not available.

Tests are synchronous because:
Expand Down Expand Up @@ -33,6 +33,11 @@
reason="OPENAI_API_KEY not set",
)

_skip_no_google = pytest.mark.skipif(
not os.environ.get("GOOGLE_API_KEY"),
reason="GOOGLE_API_KEY not set",
)


def _run_agent(
script: str,
Expand Down Expand Up @@ -186,6 +191,63 @@ def test_session_visible_via_api(self, live_servers):
assert session_name in session_ids


@_skip_no_google
class TestAdkZeroCode:
"""Run the ADK zero-code OTLP example and verify session grouping."""

def test_session_created_spans_only(self, live_servers):
main_port, otlp_port, mgr = live_servers
session_name = "e2e-adk"

result = _run_agent(
"examples/zero-code-examples/adk/run.py",
otlp_port,
session_name,
)
assert result.returncode == 0, f"Agent failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"

wait_for_session_complete_sync(mgr, session_name, timeout=30)
session = mgr.sessions[session_name]

assert session.is_complete
assert session.source == "otlp"
assert len(session.spans) > 0, "Expected spans from ADK agent"

def test_invocations_extracted(self, live_servers):
main_port, otlp_port, mgr = live_servers
session_name = "e2e-adk-inv"

result = _run_agent(
"examples/zero-code-examples/adk/run.py",
otlp_port,
session_name,
)
assert result.returncode == 0, f"Agent failed:\nstdout: {result.stdout}\nstderr: {result.stderr}"

wait_for_session_complete_sync(mgr, session_name, timeout=30)
session = mgr.sessions[session_name]

assert len(session.invocations) > 0, "Expected extracted invocations"

def test_session_visible_via_api(self, live_servers):
main_port, otlp_port, mgr = live_servers
session_name = "e2e-adk-api"

result = _run_agent(
"examples/zero-code-examples/adk/run.py",
otlp_port,
session_name,
)
assert result.returncode == 0

wait_for_session_complete_sync(mgr, session_name, timeout=30)

resp = httpx.get(f"http://127.0.0.1:{main_port}/api/streaming/sessions")
assert resp.status_code == 200
session_ids = [s["sessionId"] for s in resp.json()["data"]]
assert session_name in session_ids


@_skip_no_openai
class TestAgentRerun:
"""Verify that re-running an agent with the same session_name creates
Expand Down
Loading