88from dirty_equals import IsInt , IsJson , IsList
99from inline_snapshot import snapshot
1010from pydantic import BaseModel
11- from typing_extensions import NotRequired , TypedDict
11+ from typing_extensions import NotRequired , Self , TypedDict
1212
1313from pydantic_ai import Agent , ModelMessage , ModelRequest , ModelResponse , TextPart , ToolCallPart , UserPromptPart
1414from pydantic_ai ._utils import get_traceparent
1818from pydantic_ai .models .test import TestModel
1919from pydantic_ai .output import PromptedOutput , TextOutput
2020from pydantic_ai .tools import RunContext
21+ from pydantic_ai .toolsets .abstract import ToolsetTool
22+ from pydantic_ai .toolsets .function import FunctionToolset
23+ from pydantic_ai .toolsets .wrapper import WrapperToolset
2124
2225from .conftest import IsStr
2326
2427try :
28+ import logfire
2529 from logfire .testing import CaptureLogfire
2630except ImportError : # pragma: lax no cover
2731 logfire_installed = False
@@ -87,12 +91,37 @@ def test_logfire(
8791 instrument : InstrumentationSettings | bool ,
8892 capfire : CaptureLogfire ,
8993) -> None :
90- my_agent = Agent (model = TestModel (), instrument = instrument )
94+ class InstrumentedToolset (WrapperToolset ):
95+ async def __aenter__ (self ) -> Self :
96+ with logfire .span ('toolset_enter' ): # pyright: ignore[reportPossiblyUnboundVariable]
97+ await super ().__aenter__ ()
98+ return self
9199
92- @my_agent .tool_plain
100+ async def __aexit__ (self , * args : Any ) -> bool | None :
101+ with logfire .span ('toolset_exit' ): # pyright: ignore[reportPossiblyUnboundVariable]
102+ return await super ().__aexit__ (* args )
103+
104+ async def call_tool (
105+ self , name : str , tool_args : dict [str , Any ], ctx : RunContext [Any ], tool : ToolsetTool [Any ]
106+ ) -> Any :
107+ with logfire .span ('toolset_call_tool {name}' , name = name ): # pyright: ignore[reportPossiblyUnboundVariable]
108+ return await super ().call_tool (name , tool_args , ctx , tool )
109+
110+ toolset = FunctionToolset ()
111+
112+ @toolset .tool
93113 async def my_ret (x : int ) -> str :
94114 return str (x + 1 )
95115
116+ if instrument :
117+ toolset = InstrumentedToolset (toolset )
118+
119+ my_agent = Agent (
120+ model = TestModel (),
121+ toolsets = [toolset ],
122+ instrument = instrument ,
123+ )
124+
96125 result = my_agent .run_sync ('Hello' )
97126 assert result .output == snapshot ('{"my_ret":"1"}' )
98127
@@ -109,16 +138,29 @@ async def my_ret(x: int) -> str:
109138 'name' : 'invoke_agent my_agent' ,
110139 'message' : 'my_agent run' ,
111140 'children' : [
112- {'id' : 1 , 'name' : 'chat test' , 'message' : 'chat test' },
141+ {'id' : 1 , 'name' : 'toolset_enter' , 'message' : 'toolset_enter' },
142+ {'id' : 2 , 'name' : 'chat test' , 'message' : 'chat test' },
113143 {
114- 'id' : 2 ,
144+ 'id' : 3 ,
115145 'name' : 'running tools' ,
116146 'message' : 'running 1 tool' ,
117147 'children' : [
118- {'id' : 3 , 'name' : 'execute_tool my_ret' , 'message' : 'running tool: my_ret' },
148+ {
149+ 'id' : 4 ,
150+ 'name' : 'execute_tool my_ret' ,
151+ 'message' : 'running tool: my_ret' ,
152+ 'children' : [
153+ {
154+ 'id' : 5 ,
155+ 'name' : 'toolset_call_tool {name}' ,
156+ 'message' : 'toolset_call_tool my_ret' ,
157+ }
158+ ],
159+ }
119160 ],
120161 },
121- {'id' : 4 , 'name' : 'chat test' , 'message' : 'chat test' },
162+ {'id' : 6 , 'name' : 'chat test' , 'message' : 'chat test' },
163+ {'id' : 7 , 'name' : 'toolset_exit' , 'message' : 'toolset_exit' },
122164 ],
123165 }
124166 ]
@@ -131,16 +173,29 @@ async def my_ret(x: int) -> str:
131173 'name' : 'agent run' ,
132174 'message' : 'my_agent run' ,
133175 'children' : [
134- {'id' : 1 , 'name' : 'chat test' , 'message' : 'chat test' },
176+ {'id' : 1 , 'name' : 'toolset_enter' , 'message' : 'toolset_enter' },
177+ {'id' : 2 , 'name' : 'chat test' , 'message' : 'chat test' },
135178 {
136- 'id' : 2 ,
179+ 'id' : 3 ,
137180 'name' : 'running tools' ,
138181 'message' : 'running 1 tool' ,
139182 'children' : [
140- {'id' : 3 , 'name' : 'running tool' , 'message' : 'running tool: my_ret' },
183+ {
184+ 'id' : 4 ,
185+ 'name' : 'running tool' ,
186+ 'message' : 'running tool: my_ret' ,
187+ 'children' : [
188+ {
189+ 'id' : 5 ,
190+ 'name' : 'toolset_call_tool {name}' ,
191+ 'message' : 'toolset_call_tool my_ret' ,
192+ }
193+ ],
194+ }
141195 ],
142196 },
143- {'id' : 4 , 'name' : 'chat test' , 'message' : 'chat test' },
197+ {'id' : 6 , 'name' : 'chat test' , 'message' : 'chat test' },
198+ {'id' : 7 , 'name' : 'toolset_exit' , 'message' : 'toolset_exit' },
144199 ],
145200 }
146201 ]
@@ -156,14 +211,29 @@ async def my_ret(x: int) -> str:
156211 'name' : 'agent run' ,
157212 'message' : 'my_agent run' ,
158213 'children' : [
159- {'id' : 1 , 'name' : 'chat test' , 'message' : 'chat test' },
214+ {'id' : 1 , 'name' : 'toolset_enter' , 'message' : 'toolset_enter' },
215+ {'id' : 2 , 'name' : 'chat test' , 'message' : 'chat test' },
160216 {
161- 'id' : 2 ,
217+ 'id' : 3 ,
162218 'name' : 'running tools' ,
163219 'message' : 'running 1 tool' ,
164- 'children' : [{'id' : 3 , 'name' : 'running tool' , 'message' : 'running tool: my_ret' }],
220+ 'children' : [
221+ {
222+ 'id' : 4 ,
223+ 'name' : 'running tool' ,
224+ 'message' : 'running tool: my_ret' ,
225+ 'children' : [
226+ {
227+ 'id' : 5 ,
228+ 'name' : 'toolset_call_tool {name}' ,
229+ 'message' : 'toolset_call_tool my_ret' ,
230+ }
231+ ],
232+ }
233+ ],
165234 },
166- {'id' : 4 , 'name' : 'chat test' , 'message' : 'chat test' },
235+ {'id' : 6 , 'name' : 'chat test' , 'message' : 'chat test' },
236+ {'id' : 7 , 'name' : 'toolset_exit' , 'message' : 'toolset_exit' },
167237 ],
168238 }
169239 ]
@@ -176,16 +246,29 @@ async def my_ret(x: int) -> str:
176246 'name' : 'invoke_agent my_agent' ,
177247 'message' : 'my_agent run' ,
178248 'children' : [
179- {'id' : 1 , 'name' : 'chat test' , 'message' : 'chat test' },
249+ {'id' : 1 , 'name' : 'toolset_enter' , 'message' : 'toolset_enter' },
250+ {'id' : 2 , 'name' : 'chat test' , 'message' : 'chat test' },
180251 {
181- 'id' : 2 ,
252+ 'id' : 3 ,
182253 'name' : 'running tools' ,
183254 'message' : 'running 1 tool' ,
184255 'children' : [
185- {'id' : 3 , 'name' : 'execute_tool my_ret' , 'message' : 'running tool: my_ret' }
256+ {
257+ 'id' : 4 ,
258+ 'name' : 'execute_tool my_ret' ,
259+ 'message' : 'running tool: my_ret' ,
260+ 'children' : [
261+ {
262+ 'id' : 5 ,
263+ 'name' : 'toolset_call_tool {name}' ,
264+ 'message' : 'toolset_call_tool my_ret' ,
265+ }
266+ ],
267+ }
186268 ],
187269 },
188- {'id' : 4 , 'name' : 'chat test' , 'message' : 'chat test' },
270+ {'id' : 6 , 'name' : 'chat test' , 'message' : 'chat test' },
271+ {'id' : 7 , 'name' : 'toolset_exit' , 'message' : 'toolset_exit' },
189272 ],
190273 }
191274 ]
@@ -309,7 +392,9 @@ async def my_ret(x: int) -> str:
309392 ),
310393 }
311394 )
312- chat_span_attributes = summary .attributes [1 ]
395+ chat_span_attributes = next (
396+ attrs for attrs in summary .attributes .values () if attrs .get ('gen_ai.operation.name' , None ) == 'chat'
397+ )
313398 if instrument is True or instrument .event_mode == 'attributes' :
314399 if hasattr (capfire , 'get_collected_metrics' ): # pragma: no branch
315400 assert capfire .get_collected_metrics () == snapshot (
0 commit comments