Observability for Google Agent Development Kit (Python) with Opik

Agent Development Kit (ADK) is a flexible and modular framework for developing and deploying AI agents. ADK can be used with popular LLMs and open-source generative AI tools and is designed with a focus on tight integration with the Google ecosystem and Gemini models. ADK makes it easy to get started with simple agents powered by Gemini models and Google AI tools while providing the control and structure needed for more complex agent architectures and orchestration.

In this guide, we will showcase how to integrate Opik with Google ADK so that all the ADK calls are logged as traces in Opik. We’ll cover three key integration patterns:

  1. Basic Agent Example - Simple single-agent setup with Opik tracing
  2. Multi-Agent Example - Complex multi-agent workflow showing hierarchical tracing
  3. Hybrid Tracing - Combining Opik decorators with ADK callbacks for comprehensive observability

Account Setup

Comet provides a hosted version of the Opik platform, simply create an account and grab your API Key.

You can also run the Opik platform locally, see the installation guide for more information.

Opik provides comprehensive integration with ADK, automatically logging traces for all agent executions, tool calls, and LLM interactions with detailed cost tracking and error monitoring.

Key Features

  • Automatic cost tracking for all supported LLM providers including LiteLLM models (OpenAI, Anthropic, Google AI, AWS Bedrock, and more)
  • Full compatibility with the @opik.track decorator for hybrid tracing approaches
  • Thread support for conversational applications using ADK sessions
  • Automatic agent graph visualization with Mermaid diagrams for complex multi-agent workflows
  • Comprehensive error tracking with detailed error information and stack traces
  • Multi-agent instrumentation with track_adk_agent_recursive for complex agent hierarchies

Getting Started

Installation

First, ensure you have both opik and google-adk installed:

$pip install opik google-adk

Configuring Opik

Configure the Opik Python SDK for your deployment type. See the Python SDK Configuration guide for detailed instructions on:

  • CLI configuration: opik configure
  • Code configuration: opik.configure()
  • Self-hosted vs Cloud vs Enterprise setup
  • Configuration files and environment variables

Configuring Google ADK

In order to configure Google ADK, you will need to have your LLM provider API key. For this example, we’ll use OpenAI. You can find or create your OpenAI API Key in this page.

You can set it as an environment variable:

$export OPENAI_API_KEY="YOUR_API_KEY"

Or set it programmatically:

1import os
2import getpass
3
4if "OPENAI_API_KEY" not in os.environ:
5 os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ")

Example 1: Basic Agent Setup with Opik Tracing

The OpikTracer automatically captures detailed information about your ADK agent executions, including inputs, outputs, metadata, token usage, and error information. Here’s a basic example:

1import datetime
2from zoneinfo import ZoneInfo
3
4from google.adk.agents import LlmAgent
5from google.adk.models.lite_llm import LiteLlm
6from opik.integrations.adk import OpikTracer
7
8def get_weather(city: str) -> dict:
9 """Get weather information for a city."""
10 if city.lower() == "new york":
11 return {
12 "status": "success",
13 "report": "The weather in New York is sunny with a temperature of 25 °C (77 °F).",
14 }
15 elif city.lower() == "london":
16 return {
17 "status": "success",
18 "report": "The weather in London is cloudy with a temperature of 18 °C (64 °F).",
19 }
20 return {"status": "error", "error_message": f"Weather info for '{city}' is unavailable."}
21
22def get_current_time(city: str) -> dict:
23 """Get current time for a city."""
24 if city.lower() == "new york":
25 tz = ZoneInfo("America/New_York")
26 now = datetime.datetime.now(tz)
27 return {
28 "status": "success",
29 "report": now.strftime(f"The current time in {city} is %Y-%m-%d %H:%M:%S %Z%z."),
30 }
31 elif city.lower() == "london":
32 tz = ZoneInfo("Europe/London")
33 now = datetime.datetime.now(tz)
34 return {
35 "status": "success",
36 "report": now.strftime(f"The current time in {city} is %Y-%m-%d %H:%M:%S %Z%z."),
37 }
38 return {"status": "error", "error_message": f"No timezone info for '{city}'."}
39
40# Configure Opik tracer
41opik_tracer = OpikTracer(
42 name="basic-weather-agent",
43 tags=["basic", "weather", "time", "single-agent"],
44 metadata={
45 "environment": "development",
46 "model": "gpt-4o",
47 "framework": "google-adk",
48 "example": "basic"
49 },
50 project_name="adk-basic-demo"
51)
52
53# Initialize LiteLLM with OpenAI gpt-4o
54llm = LiteLlm(model="openai/gpt-4o")
55
56# Create the basic agent with Opik callbacks
57basic_agent = LlmAgent(
58 name="weather_time_agent",
59 model=llm,
60 description="Agent for answering time & weather questions",
61 instruction="Answer questions about the time or weather in a city. Be helpful and provide clear information.",
62 tools=[get_weather, get_current_time],
63 before_agent_callback=opik_tracer.before_agent_callback,
64 after_agent_callback=opik_tracer.after_agent_callback,
65 before_model_callback=opik_tracer.before_model_callback,
66 after_model_callback=opik_tracer.after_model_callback,
67 before_tool_callback=opik_tracer.before_tool_callback,
68 after_tool_callback=opik_tracer.after_tool_callback,
69)

Each agent execution will now be automatically logged to the Opik platform with the detailed trace information:

Example 2: Multi-Agent Setup with Hierarchical Tracing

This example demonstrates a more complex multi-agent setup where we have specialized agents for different tasks. The key insight is that you only need to add Opik callbacks to the top-level agent - all child agent calls will be automatically traced in the same trace tree.

1def get_detailed_weather(city: str) -> dict:
2 """Get detailed weather information including forecast."""
3 weather_data = {
4 "new york": {
5 "current": "Sunny, 25°C (77°F)",
6 "humidity": "65%",
7 "wind": "10 km/h NW",
8 "forecast": "Partly cloudy tomorrow, high of 27°C"
9 },
10 "london": {
11 "current": "Cloudy, 18°C (64°F)",
12 "humidity": "78%",
13 "wind": "15 km/h SW",
14 "forecast": "Light rain expected tomorrow, high of 16°C"
15 },
16 "tokyo": {
17 "current": "Partly cloudy, 22°C (72°F)",
18 "humidity": "70%",
19 "wind": "8 km/h E",
20 "forecast": "Sunny tomorrow, high of 25°C"
21 }
22 }
23
24 city_lower = city.lower()
25 if city_lower in weather_data:
26 data = weather_data[city_lower]
27 return {
28 "status": "success",
29 "report": f"Weather in {city}: {data['current']}. Humidity: {data['humidity']}, Wind: {data['wind']}. {data['forecast']}"
30 }
31 return {"status": "error", "error_message": f"Detailed weather for '{city}' is unavailable."}
32
33def get_world_time(city: str) -> dict:
34 """Get time information for major world cities."""
35 timezones = {
36 "new york": "America/New_York",
37 "london": "Europe/London",
38 "tokyo": "Asia/Tokyo",
39 "sydney": "Australia/Sydney",
40 "paris": "Europe/Paris"
41 }
42
43 city_lower = city.lower()
44 if city_lower in timezones:
45 tz = ZoneInfo(timezones[city_lower])
46 now = datetime.datetime.now(tz)
47 return {
48 "status": "success",
49 "report": now.strftime(f"Current time in {city}: %A, %B %d, %Y at %I:%M %p %Z")
50 }
51 return {"status": "error", "error_message": f"Time zone info for '{city}' is unavailable."}
52
53def get_travel_info(from_city: str, to_city: str) -> dict:
54 """Get basic travel information between cities."""
55 travel_data = {
56 ("new york", "london"): {"flight_time": "7 hours", "time_diff": "+5 hours"},
57 ("london", "new york"): {"flight_time": "8 hours", "time_diff": "-5 hours"},
58 ("new york", "tokyo"): {"flight_time": "14 hours", "time_diff": "+14 hours"},
59 ("tokyo", "new york"): {"flight_time": "13 hours", "time_diff": "-14 hours"},
60 ("london", "tokyo"): {"flight_time": "12 hours", "time_diff": "+9 hours"},
61 ("tokyo", "london"): {"flight_time": "11 hours", "time_diff": "-9 hours"},
62 }
63
64 route = (from_city.lower(), to_city.lower())
65 if route in travel_data:
66 data = travel_data[route]
67 return {
68 "status": "success",
69 "report": f"Travel from {from_city} to {to_city}: Approximately {data['flight_time']} flight time. Time difference: {data['time_diff']}"
70 }
71 return {"status": "error", "error_message": f"Travel info for '{from_city}' to '{to_city}' is unavailable."}
72
73# Weather specialist agent (no Opik callbacks needed)
74weather_agent = LlmAgent(
75 name="weather_specialist",
76 model=llm,
77 description="Specialized agent for detailed weather information",
78 instruction="Provide comprehensive weather information including current conditions and forecasts. Be detailed and informative.",
79 tools=[get_detailed_weather]
80)
81
82# Time specialist agent (no Opik callbacks needed)
83time_agent = LlmAgent(
84 name="time_specialist",
85 model=llm,
86 description="Specialized agent for world time information",
87 instruction="Provide accurate time information for cities around the world. Include day of week and full date.",
88 tools=[get_world_time]
89)
90
91# Travel specialist agent (no Opik callbacks needed)
92travel_agent = LlmAgent(
93 name="travel_specialist",
94 model=llm,
95 description="Specialized agent for travel information",
96 instruction="Provide helpful travel information including flight times and time zone differences.",
97 tools=[get_travel_info]
98)
99
100# Configure Opik tracer for multi-agent example
101multi_agent_tracer = OpikTracer(
102 name="multi-agent-coordinator",
103 tags=["multi-agent", "coordinator", "weather", "time", "travel"],
104 metadata={
105 "environment": "development",
106 "model": "gpt-4o",
107 "framework": "google-adk",
108 "example": "multi-agent",
109 "agent_count": 4
110 },
111 project_name="adk-multi-agent-demo"
112)
113
114# Coordinator agent with sub-agents
115coordinator_agent = LlmAgent(
116 name="travel_coordinator",
117 model=llm,
118 description="Coordinator agent that delegates to specialized agents for weather, time, and travel information",
119 instruction="""You are a travel coordinator that helps users with weather, time, and travel information.
120
121 You have access to three specialized agents:
122 - weather_specialist: For detailed weather information
123 - time_specialist: For world time information
124 - travel_specialist: For travel planning information
125
126 Delegate appropriate queries to the right specialist agents and compile comprehensive responses for the user.""",
127 tools=[], # No direct tools, delegates to sub-agents
128 sub_agents=[weather_agent, time_agent, travel_agent],
129)
130
131# Use the experimental recursive tracking feature to instrument all agents at once
132from opik.integrations.adk import track_adk_agent_recursive
133track_adk_agent_recursive(coordinator_agent, multi_agent_tracer)

The trace can now be viewed in the UI:

This approach is particularly useful for:

  • Sequential agents with multiple processing steps
  • Parallel agents executing tasks concurrently
  • Loop agents with iterative workflows
  • Agent tools that contain nested agents
  • Complex hierarchies with deeply nested agent structures

Cost Tracking

Opik automatically tracks token usage and cost for all LLM calls during the agent execution, not only for the Gemini LLMs, but including the models accessed via LiteLLM.

View the complete list of supported models and providers on the Supported Models page.

Agent Graph Visualization

Opik automatically generates visual representations of your agent workflows using Mermaid diagrams. The graph shows:

  • Agent hierarchy and relationships
  • Sequential execution flows
  • Parallel processing branches
  • Loop structures and iterations
  • Tool connections and dependencies

The graph is automatically computed and stored with each trace, providing a clear visual understanding of your agent’s execution flow:

For weather time agent the graph will look like that:

For more complex agent architectures displaying a graph may be even more beneficial:

Example 3: Hybrid Tracing - Combining Opik Decorators with ADK Callbacks

This advanced example shows how to combine Opik’s @opik.track decorator with ADK’s callback system. This is powerful when you have complex multi-step tools that perform their own internal operations that you want to trace separately, while still maintaining the overall agent trace context.

1from opik import track
2
3@track(name="weather_data_processing", tags=["data-processing", "weather"])
4def process_weather_data(raw_data: dict) -> dict:
5 """Process raw weather data with additional computations."""
6 # Simulate some data processing steps that we want to trace separately
7 processed = {
8 "temperature_celsius": raw_data.get("temp_c", 0),
9 "temperature_fahrenheit": raw_data.get("temp_c", 0) * 9/5 + 32,
10 "conditions": raw_data.get("condition", "unknown"),
11 "comfort_index": "comfortable" if 18 <= raw_data.get("temp_c", 0) <= 25 else "less comfortable"
12 }
13 return processed
14
15@track(name="location_validation", tags=["validation", "location"])
16def validate_location(city: str) -> dict:
17 """Validate and normalize city names."""
18 # Simulate location validation logic that we want to trace
19 normalized_cities = {
20 "nyc": "New York",
21 "ny": "New York",
22 "new york city": "New York",
23 "london uk": "London",
24 "london england": "London",
25 "tokyo japan": "Tokyo"
26 }
27
28 city_lower = city.lower().strip()
29 validated_city = normalized_cities.get(city_lower, city.title())
30
31 return {
32 "original": city,
33 "validated": validated_city,
34 "is_valid": city_lower in ["new york", "london", "tokyo"] or city_lower in normalized_cities
35 }
36
37@track(name="advanced_weather_lookup", tags=["weather", "api-simulation"])
38def get_advanced_weather(city: str) -> dict:
39 """Get weather with internal processing steps tracked by Opik decorators."""
40
41 # Step 1: Validate location (traced by @opik.track)
42 location_result = validate_location(city)
43
44 if not location_result["is_valid"]:
45 return {
46 "status": "error",
47 "error_message": f"Invalid location: {city}"
48 }
49
50 validated_city = location_result["validated"]
51
52 # Step 2: Get raw weather data (simulated)
53 raw_weather_data = {
54 "New York": {"temp_c": 25, "condition": "sunny", "humidity": 65},
55 "London": {"temp_c": 18, "condition": "cloudy", "humidity": 78},
56 "Tokyo": {"temp_c": 22, "condition": "partly cloudy", "humidity": 70}
57 }
58
59 if validated_city not in raw_weather_data:
60 return {
61 "status": "error",
62 "error_message": f"Weather data unavailable for {validated_city}"
63 }
64
65 raw_data = raw_weather_data[validated_city]
66
67 # Step 3: Process the data (traced by @opik.track)
68 processed_data = process_weather_data(raw_data)
69
70 return {
71 "status": "success",
72 "city": validated_city,
73 "report": f"Weather in {validated_city}: {processed_data['conditions']}, {processed_data['temperature_celsius']}°C ({processed_data['temperature_fahrenheit']:.1f}°F). Comfort level: {processed_data['comfort_index']}.",
74 "raw_humidity": raw_data["humidity"]
75 }
76
77# Configure Opik tracer for hybrid example
78hybrid_tracer = OpikTracer(
79 name="hybrid-tracing-agent",
80 tags=["hybrid", "decorators", "callbacks", "advanced"],
81 metadata={
82 "environment": "development",
83 "model": "gpt-4o",
84 "framework": "google-adk",
85 "example": "hybrid-tracing",
86 "tracing_methods": ["decorators", "callbacks"]
87 },
88 project_name="adk-hybrid-demo"
89)
90
91# Create hybrid agent that combines both tracing approaches
92hybrid_agent = LlmAgent(
93 name="advanced_weather_time_agent",
94 model=llm,
95 description="Advanced agent with hybrid Opik tracing using both decorators and callbacks",
96 instruction="""You are an advanced weather and time agent that provides detailed information with comprehensive internal processing.
97
98 Your tools perform multi-step operations that are individually traced, giving detailed visibility into the processing pipeline.
99 Use the advanced weather and time tools to provide thorough, well-processed information to users.""",
100 tools=[get_advanced_weather],
101 # ADK callbacks for agent-level tracing
102 before_agent_callback=hybrid_tracer.before_agent_callback,
103 after_agent_callback=hybrid_tracer.after_agent_callback,
104 before_model_callback=hybrid_tracer.before_model_callback,
105 after_model_callback=hybrid_tracer.after_model_callback,
106 before_tool_callback=hybrid_tracer.before_tool_callback,
107 after_tool_callback=hybrid_tracer.after_tool_callback,
108)

The trace can now be viewed in the UI:

Compatibility with @track Decorator

The OpikTracer is fully compatible with the @track decorator, allowing you to create hybrid tracing approaches that combine ADK agent tracking with custom function tracing. You can both invoke your agent from inside another tracked function and call tracked functions inside your tool functions, all the spans and traces parent-child relationships will be preserved!

Thread Support

The Opik integration automatically handles ADK sessions and maps them to Opik threads for conversational applications:

1from opik.integrations.adk import OpikTracer
2from google.adk import sessions as adk_sessions, runners as adk_runners
3
4# ADK session management
5session_service = adk_sessions.InMemorySessionService()
6session = session_service.create_session_sync(
7 app_name="my_app",
8 user_id="user_123",
9 session_id="conversation_456"
10)
11
12opik_tracer = OpikTracer()
13runner = adk_runners.Runner(
14 agent=your_agent,
15 app_name="my_app",
16 session_service=session_service
17)
18
19# All traces will be automatically grouped by session_id as thread_id

The integration automatically:

  • Uses the ADK session ID as the Opik thread ID
  • Groups related conversations and interactions
  • Logs app_name and user_id as metadata
  • Maintains conversation context across multiple interactions

You can view your session as a whole conversation and easily navigate to any specific trace you need.

Error Tracking

The OpikTracer provides comprehensive error tracking and monitoring:

  • Automatic error capture for agent execution failures
  • Detailed stack traces with full context information
  • Tool execution errors with input/output data
  • Model call failures with provider-specific error details

Error information is automatically logged to spans and traces, making it easy to debug issues in production:

Troubleshooting: Missing Trace

When using Runner.run_async, make sure to process all events completely, even after finding the final response (when event.is_final_response() is True). If you exit the loop too early, OpikTracer won’t log the final response and your trace will be incomplete. Don’t use code that stops processing events prematurely:

1async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=content):
2 if event.is_final_response():
3 ...
4 break # Stop processing events once the final response is found

There is an upstream discussion about how to best solve this source of confusion: https://github.com/google/adk-python/issues/1695.

Our team tried to address those issues and make the integration as robust as possible. If you are facing similar problems, the first thing we recommend is to update both opik and google-adk to the latest versions. We are actively working on improving this integration, so with the most recent versions you’ll most likely get the best UX!.

Flushing Traces

The OpikTracer object has a flush method that ensures all traces are logged to the Opik platform before you exit a script:

1from opik.integrations.adk import OpikTracer
2
3opik_tracer = OpikTracer()
4
5# Your ADK agent execution code here...
6
7# Ensure all traces are sent before script exits
8opik_tracer.flush()