Skip to content

Commit 1073c0e

Browse files
committed
bugfix: 修复tool监控丢失的问题
1 parent 74f880e commit 1073c0e

7 files changed

Lines changed: 310 additions & 23 deletions

File tree

tests/storage/test_sql_common.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
import base64
1111
import json
1212
import pickle
13+
from copy import deepcopy
14+
from types import SimpleNamespace
15+
from typing import Iterator
1316
from unittest.mock import MagicMock
1417

1518
import pytest
19+
1620
from sqlalchemy import Text
1721
from sqlalchemy.dialects import mysql
1822
from sqlalchemy.dialects import postgresql
@@ -31,9 +35,11 @@
3135
UTF8MB4String,
3236
decode_content,
3337
decode_grounding_metadata,
38+
decode_grounding_metadata,
39+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY,
40+
TypeDecoratorHookRegistry,
3441
)
3542

36-
3743
# ---------------------------------------------------------------------------
3844
# decode_content
3945
# ---------------------------------------------------------------------------
@@ -459,3 +465,129 @@ def test_all_symbols_reexported(self):
459465
assert _U is UTF8MB4String
460466
assert _dc is decode_content
461467
assert _dg is decode_grounding_metadata
468+
469+
470+
def _build_dialect(name: str) -> SimpleNamespace:
471+
return SimpleNamespace(name=name, type_descriptor=lambda t: t)
472+
473+
474+
@pytest.fixture(autouse=True)
475+
def reset_hook_registry() -> Iterator[None]:
476+
"""Reset global hook registry around each test."""
477+
old_load = deepcopy(TypeDecoratorHookRegistry._load_dialect_hooks)
478+
old_bind = deepcopy(TypeDecoratorHookRegistry._process_bind_hooks)
479+
old_result = deepcopy(TypeDecoratorHookRegistry._process_result_hooks)
480+
try:
481+
TypeDecoratorHookRegistry._load_dialect_hooks = {}
482+
TypeDecoratorHookRegistry._process_bind_hooks = {}
483+
TypeDecoratorHookRegistry._process_result_hooks = {}
484+
yield
485+
finally:
486+
TypeDecoratorHookRegistry._load_dialect_hooks = old_load
487+
TypeDecoratorHookRegistry._process_bind_hooks = old_bind
488+
TypeDecoratorHookRegistry._process_result_hooks = old_result
489+
490+
491+
def test_dynamic_json_all_hooks_can_override() -> None:
492+
"""DynamicJSON supports load/bind/result hook overrides."""
493+
json_type = DynamicJSON()
494+
sqlite = _build_dialect("sqlite")
495+
load_marker = object()
496+
497+
def load_hook(decorator, dialect): # noqa: ANN001
498+
assert decorator is json_type
499+
assert dialect.name == "sqlite"
500+
return load_marker
501+
502+
def bind_hook(decorator, value, dialect): # noqa: ANN001
503+
assert decorator is json_type
504+
assert dialect.name == "sqlite"
505+
return f"hooked-bind-{value}"
506+
507+
def result_hook(decorator, value, dialect): # noqa: ANN001
508+
assert decorator is json_type
509+
assert dialect.name == "sqlite"
510+
return {"hooked_result": value}
511+
512+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_load_dialect_hook(DynamicJSON, load_hook)
513+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(DynamicJSON, bind_hook)
514+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_result_hook(DynamicJSON, result_hook)
515+
516+
assert json_type.load_dialect_impl(sqlite) is load_marker
517+
assert json_type.process_bind_param({"k": "v"}, sqlite) == "hooked-bind-{'k': 'v'}"
518+
assert json_type.process_result_value('{"k":"v"}', sqlite) == {"hooked_result": '{"k":"v"}'}
519+
520+
521+
def test_dynamic_json_hook_none_falls_back_to_default_logic() -> None:
522+
"""Hook skips when returning None."""
523+
json_type = DynamicJSON()
524+
sqlite = _build_dialect("sqlite")
525+
526+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(DynamicJSON, lambda _d, _v, _dialect: None)
527+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_result_hook(DynamicJSON, lambda _d, _v, _dialect: None)
528+
529+
encoded = json_type.process_bind_param({"a": 1}, sqlite)
530+
decoded = json_type.process_result_value(encoded, sqlite)
531+
532+
assert encoded == '{"a": 1}'
533+
assert decoded == {"a": 1}
534+
535+
536+
def test_precise_timestamp_supports_all_three_hooks() -> None:
537+
"""PreciseTimestamp supports load/bind/result hooks."""
538+
ts_type = PreciseTimestamp()
539+
sqlite = _build_dialect("sqlite")
540+
load_marker = object()
541+
542+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_load_dialect_hook(PreciseTimestamp, lambda _d, _dialect: load_marker)
543+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(PreciseTimestamp,
544+
lambda _d, value, _dialect: f"bind-{value}")
545+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_result_hook(PreciseTimestamp,
546+
lambda _d, value, _dialect: f"result-{value}")
547+
548+
assert ts_type.load_dialect_impl(sqlite) is load_marker
549+
assert ts_type.process_bind_param("2026-01-01", sqlite) == "bind-2026-01-01"
550+
assert ts_type.process_result_value("2026-01-01", sqlite) == "result-2026-01-01"
551+
552+
553+
def test_utf8mb4_string_supports_all_three_hooks() -> None:
554+
"""UTF8MB4String supports load/bind/result hooks."""
555+
str_type = UTF8MB4String(length=128)
556+
sqlite = _build_dialect("sqlite")
557+
load_marker = object()
558+
559+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_load_dialect_hook(UTF8MB4String, lambda _d, _dialect: load_marker)
560+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(UTF8MB4String,
561+
lambda _d, value, _dialect: f"bind-{value}")
562+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_result_hook(UTF8MB4String,
563+
lambda _d, value, _dialect: f"result-{value}")
564+
565+
assert str_type.load_dialect_impl(sqlite) is load_marker
566+
assert str_type.process_bind_param("hello", sqlite) == "bind-hello"
567+
assert str_type.process_result_value("hello", sqlite) == "result-hello"
568+
569+
570+
def test_dynamic_pickle_hook_order_uses_first_override_result() -> None:
571+
"""First non-None hook result wins for DynamicPickleType."""
572+
pickle_type = DynamicPickleType()
573+
sqlite = _build_dialect("sqlite")
574+
calls: list[str] = []
575+
576+
def hook_a(_d, _v, _dialect): # noqa: ANN001
577+
calls.append("a")
578+
return None
579+
580+
def hook_b(_d, _v, _dialect): # noqa: ANN001
581+
calls.append("b")
582+
return "override"
583+
584+
def hook_c(_d, _v, _dialect): # noqa: ANN001
585+
calls.append("c")
586+
return "should-not-run"
587+
588+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(DynamicPickleType, hook_a)
589+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(DynamicPickleType, hook_b)
590+
GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY.register_process_bind_hook(DynamicPickleType, hook_c)
591+
592+
assert pickle_type.process_bind_param({"k": "v"}, sqlite) == "override"
593+
assert calls == ["a", "b"]

trpc_agent_sdk/agents/core/_llm_processor.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,36 @@ async def call_llm_async(self,
7979
return
8080

8181
# Step 2: Call the model and process responses with telemetry tracing.
82-
# Avoid start_as_current_span in async generators because cancellation can
83-
# close the generator from a different context, which may trigger
84-
# "Token was created in a different Context" during detach.
85-
span = tracer.start_span('call_llm')
86-
try:
82+
with tracer.start_as_current_span('call_llm'):
8783
event_id = Event.new_id()
8884
final_llm_response = None
85+
aggregated_raw_function_calls: list[dict] = []
86+
aggregated_event_function_calls: list[dict] = []
87+
88+
def _append_function_calls(target: list[dict], calls: list) -> None:
89+
for call in calls or []:
90+
# Keep only telemetry-safe fields for trace attributes.
91+
target.append({
92+
"id": getattr(call, "id", None),
93+
"name": getattr(call, "name", None),
94+
"args": getattr(call, "args", None),
95+
})
8996

9097
async for llm_response in self.model.generate_async(request, stream=stream, ctx=context):
98+
# Collect raw model-level function calls from every chunk.
99+
raw_calls = []
100+
if llm_response.content and llm_response.content.parts:
101+
for part in llm_response.content.parts:
102+
if part.function_call:
103+
raw_calls.append(part.function_call)
104+
_append_function_calls(aggregated_raw_function_calls, raw_calls)
105+
91106
# Create Event directly from LlmResponse
92107
event = self._create_event_from_response(context, event_id, llm_response)
93108

94109
# Process response with planner if available
95110
event = self._process_planning_response(event, context)
111+
_append_function_calls(aggregated_event_function_calls, event.get_function_calls())
96112

97113
# Track the latest non-partial response for tracing
98114
# In streaming mode, only the final (non-partial) response
@@ -111,9 +127,9 @@ async def call_llm_async(self,
111127
event_id,
112128
request,
113129
final_llm_response,
114-
instruction_metadata=instruction_metadata)
115-
finally:
116-
span.end()
130+
instruction_metadata=instruction_metadata,
131+
stream_function_calls_raw=aggregated_raw_function_calls,
132+
stream_function_calls_post_planner=aggregated_event_function_calls)
117133

118134
except Exception as ex: # pylint: disable=broad-except
119135
logger.error("LLM call failed for agent %s: %s", author, ex)

trpc_agent_sdk/agents/core/_tools_processor.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,10 @@ async def execute_tools_async(
221221
state_end.update(merged_event.actions.state_delta)
222222

223223
# Add merged tool call tracing
224-
with tracer.start_as_current_span("execute_tool (merged)"):
224+
with tracer.start_as_current_span(
225+
"execute_tool (merged)",
226+
attributes={"gen_ai.operation.name": "execute_tool"},
227+
):
225228
trace_merged_tool_calls(
226229
response_event_id=merged_event.id,
227230
function_response_event=merged_event,
@@ -285,8 +288,17 @@ async def _execute_tool(self, tool_call: FunctionCall, tool: BaseTool, context:
285288
Event: The result of tool execution
286289
"""
287290

288-
# Wrap tool execution in telemetry span
289-
with tracer.start_as_current_span(f"execute_tool {tool.name}"):
291+
# Wrap tool execution in telemetry span.
292+
# Pass initial attributes so the Galileo sampler can make a sampling
293+
# decision at span-creation time (before trace_tool_call sets them).
294+
with tracer.start_as_current_span(
295+
f"execute_tool {tool.name}",
296+
attributes={
297+
"gen_ai.operation.name": "execute_tool",
298+
"gen_ai.tool.name": tool.name,
299+
"gen_ai.tool.description": tool.description or "",
300+
},
301+
):
290302
# Capture state before tool execution
291303
state_begin = dict(context.session.state)
292304

trpc_agent_sdk/filter/_run_filter.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ async def run_stream_filters(ctx: AgentContext, req: Any, filters: list[BaseFilt
5151
if handle is None:
5252
raise ValueError("handle must be provided")
5353
current_handle = partial(stream_handler_adapter, handle)
54-
filters.reverse()
55-
for filter in filters:
54+
for filter in reversed(filters):
5655
current_handle = partial(filter.run_stream, ctx, req, current_handle)
5756
async for event in current_handle():
5857
yield event.rsp
@@ -95,9 +94,8 @@ async def run_filters(ctx: AgentContext, req: Any, filters: list[BaseFilter],
9594
"""
9695
if handle is None:
9796
raise ValueError("handle must be provided")
98-
filters.reverse()
9997
current_handle = partial(coroutine_handler_adapter, handle)
100-
for filter in filters:
98+
for filter in reversed(filters):
10199
current_handle = partial(filter.run, ctx, req, current_handle)
102100
rsp, error = await current_handle()
103101
if error:

trpc_agent_sdk/storage/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from ._sql_common import decode_content
3131
from ._sql_common import decode_grounding_metadata
3232
from ._sql_common import decode_usage_metadata
33+
from ._sql_common import GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY
34+
from ._sql_common import TypeDecoratorHookRegistry
3335

3436
__all__ = [
3537
"EXPIRE_METHOD",
@@ -57,4 +59,6 @@
5759
"decode_content",
5860
"decode_grounding_metadata",
5961
"decode_usage_metadata",
62+
"GLOBAL_TYPE_DECORATOR_HOOK_REGISTRY",
63+
"TypeDecoratorHookRegistry",
6064
]

0 commit comments

Comments
 (0)