Skip to content

Commit e559f65

Browse files
committed
Add callback tracer plugin
1 parent ba27b83 commit e559f65

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

adk/agenticlayer/agent_to_a2a.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from starlette.requests import Request
1818
from starlette.responses import JSONResponse
1919

20+
from .callback_tracer_plugin import CallbackTracerPlugin
2021
from .otel import setup_otel
2122

2223

@@ -51,6 +52,7 @@ async def create_runner() -> Runner:
5152
session_service=InMemorySessionService(), # type: ignore
5253
memory_service=InMemoryMemoryService(), # type: ignore
5354
credential_service=InMemoryCredentialService(), # type: ignore
55+
plugins=[CallbackTracerPlugin()],
5456
)
5557

5658
# Create A2A components
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import re
2+
from typing import Any, Dict, Optional
3+
4+
from google.adk.agents import BaseAgent
5+
from google.adk.agents.callback_context import CallbackContext
6+
from google.adk.models.llm_request import LlmRequest
7+
from google.adk.models.llm_response import LlmResponse
8+
from google.adk.plugins.base_plugin import BasePlugin
9+
from google.adk.tools.base_tool import BaseTool
10+
from google.adk.tools.tool_context import ToolContext
11+
from google.genai import types
12+
from opentelemetry import trace
13+
14+
# Pattern to match any key containing 'structuredcontent' or 'structured_content', case-insensitive
15+
STRUCTURED_CONTENT_PATTERN = re.compile(r"\.structured_?content", re.IGNORECASE)
16+
17+
18+
def _span_attribute_item(key: str, data: Any) -> tuple[str, Any]:
19+
"""Convert data to a span attribute-compatible type."""
20+
if isinstance(data, (str, bool, int, float)): # only these types are supported by span attributes
21+
return key, data
22+
else:
23+
return key, str(data)
24+
25+
26+
def _flatten_dict(
27+
data: Any, parent_key: str = "", sep: str = ".", parent_key_lower: Optional[str] = None
28+
) -> Dict[str, Any]:
29+
if parent_key_lower is None:
30+
parent_key_lower = parent_key.lower()
31+
32+
if STRUCTURED_CONTENT_PATTERN.search(parent_key_lower):
33+
return {} # skip structured content as it can add too many attributes
34+
35+
items: list[tuple[str, Any]] = []
36+
if isinstance(data, dict):
37+
for k, v in data.items():
38+
new_key = f"{parent_key}{sep}{k}" if parent_key else k
39+
new_key_lower = new_key.lower()
40+
items.extend(_flatten_dict(v, new_key, sep=sep, parent_key_lower=new_key_lower).items())
41+
elif isinstance(data, list):
42+
for i, v in enumerate(data):
43+
new_key = f"{parent_key}{sep}{i}"
44+
new_key_lower = new_key.lower()
45+
items.extend(_flatten_dict(v, new_key, sep=sep, parent_key_lower=new_key_lower).items())
46+
elif data is not None:
47+
items.append(_span_attribute_item(parent_key, data))
48+
return dict(items)
49+
50+
51+
def _set_span_attributes_from_callback_context(span: Any, callback_context: CallbackContext) -> None:
52+
span.set_attribute("conversation_id", callback_context.state.to_dict().get("conversation_id"))
53+
span.set_attribute("invocation_id", callback_context.invocation_id)
54+
span.set_attributes(callback_context.state.to_dict())
55+
56+
if callback_context.user_content:
57+
span.set_attributes(_flatten_dict(callback_context.user_content.model_dump(), parent_key="user_content"))
58+
59+
60+
def _set_span_attributes_for_tool(span: Any, tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext) -> None:
61+
_set_span_attributes_from_callback_context(span, tool_context)
62+
span.set_attributes(_flatten_dict(tool_context.actions.model_dump(), parent_key="tool_context.actions"))
63+
span.set_attribute("tool_name", tool.name)
64+
span.set_attributes(_flatten_dict(args, parent_key="args"))
65+
66+
67+
class CallbackTracerPlugin(BasePlugin):
68+
"""A custom plugin class for the Observability Dashboard."""
69+
70+
def __init__(self) -> None:
71+
super().__init__("AdkCallbackTracerPlugin")
72+
73+
async def before_agent_callback(
74+
self, *, agent: BaseAgent, callback_context: CallbackContext
75+
) -> Optional[types.Content]:
76+
with trace.get_tracer(__name__).start_as_current_span("before_agent_callback") as span:
77+
_set_span_attributes_from_callback_context(span, callback_context)
78+
return None
79+
80+
async def after_agent_callback(
81+
self, *, agent: BaseAgent, callback_context: CallbackContext
82+
) -> Optional[types.Content]:
83+
with trace.get_tracer(__name__).start_as_current_span("after_agent_callback") as span:
84+
_set_span_attributes_from_callback_context(span, callback_context)
85+
return None
86+
87+
async def before_model_callback(
88+
self, *, callback_context: CallbackContext, llm_request: LlmRequest
89+
) -> Optional[LlmResponse]:
90+
with trace.get_tracer(__name__).start_as_current_span("before_model_callback") as span:
91+
_set_span_attributes_from_callback_context(span, callback_context)
92+
span.set_attribute("model", llm_request.model or "unknown")
93+
if llm_request.contents:
94+
span.set_attributes(
95+
_flatten_dict(llm_request.contents[-1].model_dump(), parent_key="llm_request.content")
96+
) # only send the last content part (last user input)
97+
return None
98+
99+
async def after_model_callback(
100+
self, *, callback_context: CallbackContext, llm_response: LlmResponse
101+
) -> Optional[LlmResponse]:
102+
with trace.get_tracer(__name__).start_as_current_span("after_model_callback") as span:
103+
_set_span_attributes_from_callback_context(span, callback_context)
104+
span.set_attributes(_flatten_dict(llm_response.model_dump(), parent_key="llm_response"))
105+
return None
106+
107+
async def before_tool_callback(
108+
self,
109+
*,
110+
tool: BaseTool,
111+
tool_args: Dict[str, Any],
112+
tool_context: ToolContext,
113+
) -> Optional[Dict[str, Any]]:
114+
with trace.get_tracer(__name__).start_as_current_span("before_tool_callback") as span:
115+
_set_span_attributes_for_tool(span, tool, tool_args, tool_context)
116+
return None
117+
118+
async def after_tool_callback(
119+
self,
120+
*,
121+
tool: BaseTool,
122+
tool_args: Dict[str, Any],
123+
tool_context: ToolContext,
124+
result: Dict[str, Any],
125+
) -> Optional[Dict[str, Any]]:
126+
with trace.get_tracer(__name__).start_as_current_span("after_tool_callback") as span:
127+
_set_span_attributes_for_tool(span, tool, tool_args, tool_context)
128+
if isinstance(result, (dict, list)):
129+
span.set_attributes(_flatten_dict(result, parent_key="tool_response"))
130+
return None
131+
132+
async def on_model_error_callback(
133+
self,
134+
*,
135+
callback_context: CallbackContext,
136+
llm_request: LlmRequest,
137+
error: Exception,
138+
) -> Optional[LlmResponse]:
139+
with trace.get_tracer(__name__).start_as_current_span("on_model_error_callback") as span:
140+
_set_span_attributes_from_callback_context(span, callback_context)
141+
span.set_attribute("model", llm_request.model or "unknown")
142+
if llm_request.contents:
143+
span.set_attributes(
144+
_flatten_dict(llm_request.contents[-1].model_dump(), parent_key="llm_request.content")
145+
) # only send the last content part (last user input)
146+
span.set_attribute("error", str(error))
147+
return None
148+
149+
async def on_tool_error_callback(
150+
self,
151+
*,
152+
tool: BaseTool,
153+
tool_args: Dict[str, Any],
154+
tool_context: ToolContext,
155+
error: Exception,
156+
) -> Optional[Dict[str, Any]]:
157+
with trace.get_tracer(__name__).start_as_current_span("on_tool_error_callback") as span:
158+
_set_span_attributes_for_tool(span, tool, tool_args, tool_context)
159+
span.set_attribute("error", str(error))
160+
return None

0 commit comments

Comments
 (0)