Skip to content

feat: Add output function tracing #2191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 16, 2025
Merged

Conversation

bitnahian
Copy link
Contributor

Fixes #2108

  • Adds traces for all output function calls
  • Adds traces with tool span attributes for tool-like output function calls

@bitnahian bitnahian marked this pull request as ready for review July 13, 2025 11:29
@alexmojaki
Copy link
Contributor

@bitnahian I didn't understand many of the design decisions behind TraceContext, please tell me if you see any problems with my simplification in ef8c0af

@@ -529,7 +530,8 @@ async def _handle_tool_calls(
if isinstance(output_schema, _output.ToolOutputSchema):
for call, output_tool in output_schema.find_tool(tool_calls):
try:
result_data = await output_tool.process(call, run_context)
trace_context = _output.build_trace_context(ctx)
result_data = await output_tool.process(call, run_context, trace_context.with_call(call))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like output_tool.process can do the .with_call(call)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move it there

Comment on lines +79 to +86
@dataclass(frozen=True)
class TraceContext:
"""A context for tracing output processing."""

tracer: Tracer
include_content: bool
call: _messages.ToolCallPart | None = None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed a preference towards dataclasses in the code base. Just curious as to why this is the preferred choice? Is it primarily because of serialisation semantics?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a dataclass works nicely here and makes more sense than a 'plain' class.

a pydantic BaseModel would be better suited for data intended to be (de)serialized.

if trace_context.call:
span_manager = trace_context.span(trace_context.call, include_tool_call_id=True)
else:
function_name = getattr(self._function_schema.function, '__name__', 'output_function')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to extract the function name from the schema?

@bitnahian
Copy link
Contributor Author

@alexmojaki thanks for the review. I've made your suggested changes.

@@ -529,7 +530,8 @@ async def _handle_tool_calls(
if isinstance(output_schema, _output.ToolOutputSchema):
for call, output_tool in output_schema.find_tool(tool_calls):
try:
result_data = await output_tool.process(call, run_context)
trace_context = _output.build_trace_context(ctx)
result_data = await output_tool.process(call, run_context, trace_context.with_call(call))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move it there

Comment on lines +79 to +86
@dataclass(frozen=True)
class TraceContext:
"""A context for tracing output processing."""

tracer: Tracer
include_content: bool
call: _messages.ToolCallPart | None = None

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a dataclass works nicely here and makes more sense than a 'plain' class.

a pydantic BaseModel would be better suited for data intended to be (de)serialized.

@bitnahian
Copy link
Contributor Author

Have addressed comments in places I missed. Lmk if any more changes are required.

try:
output = await self._function_schema.call(output, run_context)
output = await trace_context.execute_function_with_span(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible followup: dedupe this whole try/except

@alexmojaki
Copy link
Contributor

Thanks!

@alexmojaki alexmojaki merged commit f9f1c03 into pydantic:main Jul 16, 2025
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Show output functions span in logfire
2 participants