Skip to content

Commit 10ecbb9

Browse files
added tracing
1 parent 0ce45b5 commit 10ecbb9

10 files changed

Lines changed: 690 additions & 17 deletions

File tree

examples/tutorials/00_sync/040_pydantic_ai/project/acp.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,37 @@
66

77
from __future__ import annotations
88

9+
import os
910
from typing import AsyncGenerator
1011

1112
from dotenv import load_dotenv
1213

1314
load_dotenv()
1415

16+
import agentex.lib.adk as adk
1517
from project.agent import create_agent
16-
from agentex.lib.adk import convert_pydantic_ai_to_agentex_events
18+
from agentex.lib.adk import (
19+
create_pydantic_ai_tracing_handler,
20+
convert_pydantic_ai_to_agentex_events,
21+
)
1722
from agentex.lib.types.acp import SendMessageParams
23+
from agentex.lib.types.tracing import SGPTracingProcessorConfig
1824
from agentex.lib.utils.logging import make_logger
1925
from agentex.lib.sdk.fastacp.fastacp import FastACP
2026
from agentex.types.task_message_update import TaskMessageUpdate
2127
from agentex.types.task_message_content import TaskMessageContent
28+
from agentex.lib.core.tracing.tracing_processor_manager import add_tracing_processor_config
2229

2330
logger = make_logger(__name__)
2431

32+
add_tracing_processor_config(
33+
SGPTracingProcessorConfig(
34+
sgp_api_key=os.environ.get("SGP_API_KEY", ""),
35+
sgp_account_id=os.environ.get("SGP_ACCOUNT_ID", ""),
36+
sgp_base_url=os.environ.get("SGP_CLIENT_BASE_URL", ""),
37+
)
38+
)
39+
2540
acp = FastACP.create(acp_type="sync")
2641

2742
_agent = None
@@ -41,10 +56,23 @@ async def handle_message_send(
4156
) -> TaskMessageContent | list[TaskMessageContent] | AsyncGenerator[TaskMessageUpdate, None]:
4257
"""Handle incoming messages from Agentex, streaming tokens and tool calls."""
4358
agent = get_agent()
59+
task_id = params.task.id
4460

4561
user_message = params.content.content
46-
logger.info(f"Processing message for task {params.task.id}")
62+
logger.info(f"Processing message for task {task_id}")
4763

48-
async with agent.run_stream_events(user_message) as stream:
49-
async for event in convert_pydantic_ai_to_agentex_events(stream):
50-
yield event
64+
async with adk.tracing.span(
65+
trace_id=task_id,
66+
task_id=task_id,
67+
name="message",
68+
input={"message": user_message},
69+
data={"__span_type__": "AGENT_WORKFLOW"},
70+
) as turn_span:
71+
tracing_handler = create_pydantic_ai_tracing_handler(
72+
trace_id=task_id,
73+
parent_span_id=turn_span.id if turn_span else None,
74+
task_id=task_id,
75+
)
76+
async with agent.run_stream_events(user_message) as stream:
77+
async for event in convert_pydantic_ai_to_agentex_events(stream, tracing_handler=tracing_handler):
78+
yield event

examples/tutorials/10_async/00_base/110_pydantic_ai/project/acp.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
import agentex.lib.adk as adk
1717
from project.agent import create_agent
18-
from agentex.lib.adk import stream_pydantic_ai_events
18+
from agentex.lib.adk import (
19+
stream_pydantic_ai_events,
20+
create_pydantic_ai_tracing_handler,
21+
)
1922
from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams
2023
from agentex.lib.types.fastacp import AsyncACPConfig
2124
from agentex.lib.types.tracing import SGPTracingProcessorConfig
@@ -62,12 +65,18 @@ async def handle_task_event_send(params: SendEventParams):
6265

6366
async with adk.tracing.span(
6467
trace_id=task_id,
68+
task_id=task_id,
6569
name="message",
6670
input={"message": user_message},
6771
data={"__span_type__": "AGENT_WORKFLOW"},
6872
) as turn_span:
73+
tracing_handler = create_pydantic_ai_tracing_handler(
74+
trace_id=task_id,
75+
parent_span_id=turn_span.id if turn_span else None,
76+
task_id=task_id,
77+
)
6978
async with agent.run_stream_events(user_message) as stream:
70-
final_output = await stream_pydantic_ai_events(stream, task_id)
79+
final_output = await stream_pydantic_ai_events(stream, task_id, tracing_handler=tracing_handler)
7180

7281
if turn_span:
7382
turn_span.output = {"final_output": final_output}

examples/tutorials/10_async/10_temporal/110_pydantic_ai/project/agent.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from pydantic_ai.durable_exec.temporal import TemporalAgent
2727

2828
from project.tools import get_weather
29-
from agentex.lib.adk import stream_pydantic_ai_events
29+
from agentex.lib.adk import (
30+
stream_pydantic_ai_events,
31+
create_pydantic_ai_tracing_handler,
32+
)
3033

3134
MODEL_NAME = "openai:gpt-4o-mini"
3235
SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools.
@@ -50,6 +53,9 @@ class TaskDeps(BaseModel):
5053
"""
5154

5255
task_id: str
56+
# When set, the event handler nests per-tool-call spans under this
57+
# span. Typically the ID of the per-turn span opened by the workflow.
58+
parent_span_id: str | None = None
5359

5460

5561
def _build_base_agent() -> Agent[TaskDeps, str]:
@@ -76,9 +82,19 @@ async def event_handler(
7682
Pydantic AI calls this with the live event stream as soon as the model
7783
activity begins emitting parts. Because the handler runs inside the
7884
activity (not the workflow), it can freely make non-deterministic
79-
Redis writes.
85+
Redis writes — including the tracing HTTP calls that record per-tool-call
86+
spans under the workflow's per-turn span (when ``parent_span_id`` is set).
8087
"""
81-
await stream_pydantic_ai_events(events, run_context.deps.task_id)
88+
tracing_handler = create_pydantic_ai_tracing_handler(
89+
trace_id=run_context.deps.task_id,
90+
parent_span_id=run_context.deps.parent_span_id,
91+
task_id=run_context.deps.task_id,
92+
)
93+
await stream_pydantic_ai_events(
94+
events,
95+
run_context.deps.task_id,
96+
tracing_handler=tracing_handler,
97+
)
8298

8399

84100
# Construct the durable agent at module load time so that the

examples/tutorials/10_async/10_temporal/110_pydantic_ai/project/workflow.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
7474

7575
async with adk.tracing.span(
7676
trace_id=params.task.id,
77+
task_id=params.task.id,
7778
name=f"Turn {self._turn_number}",
7879
input={"message": params.event.content.content},
7980
) as span:
@@ -86,7 +87,10 @@ async def on_task_event_send(self, params: SendEventParams) -> None:
8687
# temporal_agent pushes deltas to Redis so the UI sees tokens.
8788
result = await temporal_agent.run(
8889
params.event.content.content,
89-
deps=TaskDeps(task_id=params.task.id),
90+
deps=TaskDeps(
91+
task_id=params.task.id,
92+
parent_span_id=span.id if span else None,
93+
),
9094
)
9195
if span:
9296
span.output = {"final_output": result.output}

src/agentex/lib/adk/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from agentex.lib.adk._modules._langgraph_sync import convert_langgraph_to_agentex_events
1212
from agentex.lib.adk._modules._pydantic_ai_async import stream_pydantic_ai_events
1313
from agentex.lib.adk._modules._pydantic_ai_sync import convert_pydantic_ai_to_agentex_events
14+
from agentex.lib.adk._modules._pydantic_ai_tracing import create_pydantic_ai_tracing_handler
1415
from agentex.lib.adk._modules.events import EventsModule
1516
from agentex.lib.adk._modules.messages import MessagesModule
1617
from agentex.lib.adk._modules.state import StateModule
@@ -42,17 +43,15 @@
4243
"tracing",
4344
"events",
4445
"agent_task_tracker",
45-
4646
# Checkpointing / LangGraph
4747
"create_checkpointer",
4848
"create_langgraph_tracing_handler",
4949
"stream_langgraph_events",
5050
"convert_langgraph_to_agentex_events",
51-
5251
# Pydantic AI
5352
"stream_pydantic_ai_events",
5453
"convert_pydantic_ai_to_agentex_events",
55-
54+
"create_pydantic_ai_tracing_handler",
5655
# Providers
5756
"providers",
5857
# Utils

src/agentex/lib/adk/_modules/_pydantic_ai_async.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,37 @@
1111
helper's convention). To stream tool-call argument tokens, see the sync
1212
converter at ``agentex.lib.adk._modules._pydantic_ai_sync`` which yields
1313
``ToolRequestDelta`` events.
14+
15+
Tracing is opt-in via a ``tracing_handler`` parameter — see
16+
``create_pydantic_ai_tracing_handler`` in
17+
``agentex.lib.adk._modules._pydantic_ai_tracing``.
1418
"""
1519

20+
from __future__ import annotations
21+
22+
from typing import TYPE_CHECKING
23+
24+
if TYPE_CHECKING:
25+
from agentex.lib.adk._modules._pydantic_ai_tracing import (
26+
AgentexPydanticAITracingHandler,
27+
)
28+
1629

17-
async def stream_pydantic_ai_events(stream, task_id: str) -> str:
30+
async def stream_pydantic_ai_events(
31+
stream,
32+
task_id: str,
33+
tracing_handler: "AgentexPydanticAITracingHandler | None" = None,
34+
) -> str:
1835
"""Stream Pydantic AI events to Agentex via Redis.
1936
2037
Args:
2138
stream: Async iterator yielded by ``agent.run_stream_events(...)``.
2239
task_id: The Agentex task ID to stream messages to.
40+
tracing_handler: Optional handler from
41+
``create_pydantic_ai_tracing_handler(...)``. When provided, each
42+
tool call in the run is also recorded as an Agentex child span
43+
beneath the handler's configured ``parent_span_id``. Streaming
44+
behavior is unchanged when omitted.
2345
2446
Returns:
2547
The accumulated text content of the **last** text part in the run.
@@ -197,6 +219,12 @@ async def _close_reasoning():
197219
author="agent",
198220
),
199221
)
222+
if tracing_handler is not None and tool_call_id:
223+
await tracing_handler.on_tool_start(
224+
tool_call_id=tool_call_id,
225+
tool_name=tool_name,
226+
arguments=args,
227+
)
200228

201229
elif isinstance(event, FunctionToolResultEvent):
202230
await _close_text()
@@ -221,6 +249,11 @@ async def _close_reasoning():
221249
author="agent",
222250
),
223251
)
252+
if tracing_handler is not None and tool_call_id:
253+
await tracing_handler.on_tool_end(
254+
tool_call_id=tool_call_id,
255+
result=content_str,
256+
)
224257

225258
# FunctionToolCallEvent / FinalResultEvent / AgentRunResultEvent
226259
# are intentionally ignored — same as the sync converter.

src/agentex/lib/adk/_modules/_pydantic_ai_sync.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ async def handle_message_send(params):
2121
from __future__ import annotations
2222

2323
import json
24-
from typing import Any, AsyncIterator
24+
from typing import TYPE_CHECKING, Any, AsyncIterator
2525

2626
from pydantic_ai.run import AgentRunResultEvent
27+
28+
if TYPE_CHECKING:
29+
from agentex.lib.adk._modules._pydantic_ai_tracing import (
30+
AgentexPydanticAITracingHandler,
31+
)
2732
from pydantic_ai.messages import (
2833
TextPart,
2934
PartEndEvent,
@@ -98,6 +103,7 @@ def _tool_return_content(result: ToolReturnPart | Any) -> Any:
98103

99104
async def convert_pydantic_ai_to_agentex_events(
100105
stream_response: AsyncIterator[Any],
106+
tracing_handler: "AgentexPydanticAITracingHandler | None" = None,
101107
) -> AsyncIterator[StreamTaskMessageStart | StreamTaskMessageDelta | StreamTaskMessageFull | StreamTaskMessageDone]:
102108
"""Convert a Pydantic AI agent event stream into Agentex stream events.
103109
@@ -120,6 +126,11 @@ async def convert_pydantic_ai_to_agentex_events(
120126
stream_response: The async iterator yielded by Pydantic AI's
121127
``agent.run_stream_events(...)`` context manager (or a stream of
122128
``AgentStreamEvent`` items received in an ``event_stream_handler``).
129+
tracing_handler: Optional handler from
130+
``create_pydantic_ai_tracing_handler(...)``. When provided, each
131+
tool call in the run is also recorded as an Agentex child span
132+
beneath the handler's configured ``parent_span_id``. Streaming
133+
behavior is unchanged when omitted.
123134
124135
Yields:
125136
Agentex ``StreamTaskMessage*`` events suitable for forwarding back over
@@ -265,13 +276,34 @@ async def convert_pydantic_ai_to_agentex_events(
265276
if message_index is None:
266277
continue
267278
yield StreamTaskMessageDone(type="done", index=message_index)
279+
# Tool-call parts end with the model's full args known. Open a
280+
# tracing child span for the tool execution now; close it when
281+
# FunctionToolResultEvent arrives below.
282+
if tracing_handler is not None and isinstance(event.part, ToolCallPart) and event.part.tool_call_id:
283+
args: dict[str, Any] | str | None
284+
raw_args = event.part.args
285+
if isinstance(raw_args, dict):
286+
args = dict(raw_args)
287+
elif isinstance(raw_args, str):
288+
try:
289+
args = json.loads(raw_args) if raw_args else {}
290+
except json.JSONDecodeError:
291+
args = {"_raw": raw_args}
292+
else:
293+
args = {}
294+
await tracing_handler.on_tool_start(
295+
tool_call_id=event.part.tool_call_id,
296+
tool_name=event.part.tool_name,
297+
arguments=args,
298+
)
268299

269300
elif isinstance(event, FunctionToolResultEvent):
270301
result = event.part
271302
tool_call_id = result.tool_call_id
272303
tool_name = getattr(result, "tool_name", "") or ""
273304
message_index = next_message_index
274305
next_message_index += 1
306+
content_payload = _tool_return_content(result)
275307
yield StreamTaskMessageFull(
276308
type="full",
277309
index=message_index,
@@ -280,9 +312,14 @@ async def convert_pydantic_ai_to_agentex_events(
280312
author="agent",
281313
tool_call_id=tool_call_id,
282314
name=tool_name,
283-
content=_tool_return_content(result),
315+
content=content_payload,
284316
),
285317
)
318+
if tracing_handler is not None and tool_call_id:
319+
await tracing_handler.on_tool_end(
320+
tool_call_id=tool_call_id,
321+
result=content_payload,
322+
)
286323

287324
elif isinstance(event, (FunctionToolCallEvent, FinalResultEvent, AgentRunResultEvent)):
288325
# Already covered by PartStart/PartDelta/PartEnd events above, or

0 commit comments

Comments
 (0)