@@ -186,6 +186,108 @@ def test_convert_traces_multiple(self):
186186 assert len (results ) == 2
187187 assert all (r .trace_id == "t1" for r in results )
188188
189+ def test_convert_adk_generate_content_llm_spans (self ):
190+ invoke = Span (
191+ trace_id = "t-gc" ,
192+ span_id = "invoke1" ,
193+ parent_span_id = None ,
194+ operation_name = "invoke_agent query_agent" ,
195+ start_time = 1000 ,
196+ duration = 10000 ,
197+ tags = {"gen_ai.operation.name" : "invoke_agent" },
198+ )
199+ llm_1 = Span (
200+ trace_id = "t-gc" ,
201+ span_id = "llm1" ,
202+ parent_span_id = "invoke1" ,
203+ operation_name = "generate_content mockllm-deterministic" ,
204+ start_time = 2000 ,
205+ duration = 1000 ,
206+ tags = {
207+ "gen_ai.operation.name" : "generate_content" ,
208+ "gcp.vertex.agent.llm_request" : json .dumps (
209+ {"Contents" : [{"role" : "user" , "parts" : [{"text" : "inspect pods" }]}]}
210+ ),
211+ "gcp.vertex.agent.llm_response" : json .dumps (
212+ {"Content" : {"role" : "model" , "parts" : [{"text" : "Calling tools." }]}}
213+ ),
214+ },
215+ )
216+ tool_1 = Span (
217+ trace_id = "t-gc" ,
218+ span_id = "tool1" ,
219+ parent_span_id = "invoke1" ,
220+ operation_name = "execute_tool list_pods" ,
221+ start_time = 3000 ,
222+ duration = 500 ,
223+ tags = {
224+ "gen_ai.tool.name" : "list_pods" ,
225+ "gen_ai.tool.call.id" : "call_1" ,
226+ "gcp.vertex.agent.tool_call_args" : json .dumps ({"namespace" : "default" }),
227+ "gcp.vertex.agent.tool_response" : json .dumps ({"pods" : []}),
228+ },
229+ )
230+ llm_2 = Span (
231+ trace_id = "t-gc" ,
232+ span_id = "llm2" ,
233+ parent_span_id = "invoke1" ,
234+ operation_name = "generate_content mockllm-deterministic" ,
235+ start_time = 4000 ,
236+ duration = 1000 ,
237+ tags = {
238+ "gen_ai.operation.name" : "generate_content" ,
239+ "gcp.vertex.agent.llm_request" : json .dumps ({"contents" : []}),
240+ "gcp.vertex.agent.llm_response" : json .dumps (
241+ {
242+ "Content" : {
243+ "role" : "model" ,
244+ "parts" : [
245+ {
246+ "functionCall" : {
247+ "name" : "summarize_pods" ,
248+ "args" : {"namespace" : "default" },
249+ "id" : "call_final" ,
250+ }
251+ }
252+ ],
253+ }
254+ }
255+ ),
256+ },
257+ )
258+ tool_2 = Span (
259+ trace_id = "t-gc" ,
260+ span_id = "tool2" ,
261+ parent_span_id = "invoke1" ,
262+ operation_name = "execute_tool get_events" ,
263+ start_time = 5000 ,
264+ duration = 500 ,
265+ tags = {
266+ "gen_ai.tool.name" : "get_events" ,
267+ "gen_ai.tool.call.id" : "call_2" ,
268+ "gcp.vertex.agent.tool_call_args" : json .dumps ({"namespace" : "default" }),
269+ "gcp.vertex.agent.tool_response" : json .dumps ({"events" : []}),
270+ },
271+ )
272+ invoke .children .extend ([llm_1 , tool_1 , llm_2 , tool_2 ])
273+ trace = Trace (
274+ trace_id = "t-gc" ,
275+ root_spans = [invoke ],
276+ all_spans = [invoke , llm_1 , tool_1 , llm_2 , tool_2 ],
277+ )
278+
279+ result = convert_trace (trace )
280+
281+ assert result .warnings == []
282+ assert len (result .invocations ) == 1
283+ inv = result .invocations [0 ]
284+ assert inv .user_content .parts [0 ].text == "inspect pods"
285+ final_call = inv .final_response .parts [0 ].function_call
286+ assert final_call .name == "summarize_pods"
287+ assert final_call .args == {"namespace" : "default" }
288+ assert final_call .id == "call_final"
289+ assert [t .name for t in inv .intermediate_data .tool_uses ] == ["list_pods" , "get_events" ]
290+
189291 def test_no_invoke_agent_warns (self ):
190292 trace = Trace (
191293 trace_id = "empty" ,
@@ -207,6 +309,35 @@ def test_no_invoke_agent_warns(self):
207309 assert len (result .warnings ) == 1
208310 assert "no invoke_agent" in result .warnings [0 ]
209311
312+ def test_no_llm_descendants_warns_with_compatible_shapes (self ):
313+ invoke = Span (
314+ trace_id = "no-llm" ,
315+ span_id = "invoke-no-llm" ,
316+ parent_span_id = None ,
317+ operation_name = "invoke_agent test_agent" ,
318+ start_time = 1000 ,
319+ duration = 1000 ,
320+ tags = {
321+ "otel.scope.name" : "gcp.vertex.agent" ,
322+ "gen_ai.operation.name" : "invoke_agent" ,
323+ },
324+ )
325+ trace = Trace (
326+ trace_id = "no-llm" ,
327+ root_spans = [invoke ],
328+ all_spans = [invoke ],
329+ )
330+
331+ result = convert_trace (trace )
332+
333+ assert result .invocations == []
334+ assert len (result .warnings ) == 1
335+ warning = result .warnings [0 ]
336+ assert "invoke-no-llm" in warning
337+ assert "no converter-compatible ADK LLM descendants" in warning
338+ assert "call_llm" in warning
339+ assert "ADK generate_content" in warning
340+
210341 def test_no_tool_spans_fallback_to_llm_response (self ):
211342 """When no execute_tool spans exist, function_calls should be
212343 extracted from call_llm responses instead."""
0 commit comments