Skip to content

Commit b680246

Browse files
areibmancursoragentalex
authored
Fix sse streaming function call error with agentops (#1206)
* Fix text extraction and handling for Google GenAI instrumentation Co-authored-by: alex <alex@agentops.ai> * Move SSE function call example to correct examples directory Co-authored-by: alex <alex@agentops.ai> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: alex <alex@agentops.ai>
1 parent 0d633eb commit b680246

4 files changed

Lines changed: 120 additions & 13 deletions

File tree

agentops/instrumentation/agentic/google_adk/patch.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ def _extract_messages_from_contents(contents: list) -> dict:
114114
# Extract content from parts
115115
text_parts = []
116116
for part in parts:
117-
if "text" in part:
118-
text_parts.append(part["text"])
117+
if "text" in part and part.get("text") is not None:
118+
text_parts.append(str(part["text"]))
119119
elif "function_call" in part:
120120
# Function calls in prompts are typically from the model's previous responses
121121
func_call = part["function_call"]
@@ -227,8 +227,8 @@ def _extract_llm_attributes(llm_request_dict: dict, llm_response: Any) -> dict:
227227
# Extract content from parts
228228
text_parts = []
229229
for part in parts:
230-
if "text" in part:
231-
text_parts.append(part["text"])
230+
if "text" in part and part.get("text") is not None:
231+
text_parts.append(str(part["text"]))
232232
elif "function_call" in part:
233233
# Function calls in prompts are typically from the model's previous responses
234234
func_call = part["function_call"]
@@ -299,8 +299,8 @@ def _extract_llm_attributes(llm_request_dict: dict, llm_response: Any) -> dict:
299299
text_parts = []
300300
tool_call_index = 0
301301
for part in parts:
302-
if "text" in part:
303-
text_parts.append(part["text"])
302+
if "text" in part and part.get("text") is not None:
303+
text_parts.append(str(part["text"]))
304304
elif "function_call" in part:
305305
# This is a function call in the response
306306
func_call = part["function_call"]

agentops/instrumentation/providers/google_genai/attributes/model.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,22 @@ def _extract_content_from_prompt(content: Any) -> str:
3434
if isinstance(item, str):
3535
text += item + "\n"
3636
elif isinstance(item, dict) and "text" in item:
37-
text += item["text"] + "\n"
37+
if item.get("text") is not None:
38+
text += str(item["text"]) + "\n"
3839
elif hasattr(item, "text"):
39-
text += item.text + "\n"
40+
part_text = getattr(item, "text", None)
41+
if part_text:
42+
text += part_text + "\n"
4043
# Handle content as a list with mixed types
4144
elif hasattr(item, "parts"):
4245
parts = item.parts
4346
for part in parts:
4447
if isinstance(part, str):
4548
text += part + "\n"
4649
elif hasattr(part, "text"):
47-
text += part.text + "\n"
50+
part_text = getattr(part, "text", None)
51+
if part_text:
52+
text += part_text + "\n"
4853
return text
4954

5055
# Dict with text key
@@ -62,7 +67,9 @@ def _extract_content_from_prompt(content: Any) -> str:
6267
if isinstance(part, str):
6368
text += part + "\n"
6469
elif hasattr(part, "text"):
65-
text += part.text + "\n"
70+
part_text = getattr(part, "text", None)
71+
if part_text:
72+
text += part_text + "\n"
6673
return text
6774

6875
# Other object types - try to convert to string
@@ -155,7 +162,9 @@ def _set_response_attributes(attributes: AttributeMap, response: Any) -> None:
155162
if isinstance(part, str):
156163
text += part
157164
elif hasattr(part, "text"):
158-
text += part.text
165+
part_text = getattr(part, "text", None)
166+
if part_text:
167+
text += part_text
159168

160169
attributes[MessageAttributes.COMPLETION_CONTENT.format(i=i)] = text
161170
attributes[MessageAttributes.COMPLETION_ROLE.format(i=i)] = "assistant"

agentops/instrumentation/providers/google_genai/stream_wrapper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ def instrumented_stream():
9292

9393
# Track token count (approximate by word count if metadata not available)
9494
if hasattr(chunk, "text"):
95-
full_text += chunk.text
95+
text_value = getattr(chunk, "text", None)
96+
if text_value:
97+
full_text += text_value
9698

9799
yield chunk
98100

@@ -195,7 +197,9 @@ async def instrumented_stream():
195197
last_chunk_with_metadata = chunk
196198

197199
if hasattr(chunk, "text"):
198-
full_text += chunk.text
200+
text_value = getattr(chunk, "text", None)
201+
if text_value:
202+
full_text += text_value
199203

200204
yield chunk
201205

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
import asyncio
3+
from typing import Optional
4+
5+
import agentops
6+
from google.adk.agents import LlmAgent
7+
from google.adk.tools import FunctionTool
8+
from google.adk.runners import Runner
9+
from google.adk.sessions import InMemorySessionService
10+
from google.genai import types
11+
12+
13+
# Attempt to import RunConfig/StreamingMode from likely ADK locations
14+
RunConfig: Optional[object] = None
15+
StreamingMode: Optional[object] = None
16+
try:
17+
from google.adk.runners import RunConfig as _RunConfig, StreamingMode as _StreamingMode # type: ignore
18+
RunConfig = _RunConfig
19+
StreamingMode = _StreamingMode
20+
except Exception:
21+
try:
22+
from google.adk.types import RunConfig as _RunConfig2, StreamingMode as _StreamingMode2 # type: ignore
23+
RunConfig = _RunConfig2
24+
StreamingMode = _StreamingMode2
25+
except Exception:
26+
RunConfig = None
27+
StreamingMode = None
28+
29+
30+
# Initialize AgentOps (set AGENTOPS_API_KEY in your environment)
31+
agentops.init(api_key=os.getenv("AGENTOPS_API_KEY"), trace_name="adk_sse_text_function_call")
32+
33+
APP_NAME = "adk_sse_text_function_call_app"
34+
USER_ID = "user_sse_text_fc"
35+
SESSION_ID = "session_sse_text_fc"
36+
MODEL_NAME = "gemini-2.0-flash"
37+
38+
39+
# Simple tool to trigger a function call
40+
async def get_weather(location: str) -> str:
41+
return f"Weather for {location}: sunny and 25°C."
42+
43+
44+
weather_tool = FunctionTool(func=get_weather)
45+
46+
# Agent configured with the tool so the model can trigger a function call
47+
agent = LlmAgent(
48+
model=MODEL_NAME,
49+
name="WeatherAgent",
50+
description="Provides weather using a tool",
51+
instruction=(
52+
"You are a helpful assistant. When asked about weather, call the get_weather tool with the given location."
53+
),
54+
tools=[weather_tool],
55+
output_key="weather_output",
56+
)
57+
58+
# Session service and runner
59+
session_service = InMemorySessionService()
60+
runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
61+
62+
63+
async def main():
64+
# Ensure session exists
65+
await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
66+
67+
# Create user message
68+
user_message = types.Content(role="user", parts=[types.Part(text="What's the weather in Paris?")])
69+
70+
# Configure SSE streaming with TEXT modality, as reported by the user
71+
run_config_kw = {}
72+
if RunConfig is not None and StreamingMode is not None:
73+
run_config_kw["run_config"] = RunConfig(streaming_mode=StreamingMode.SSE, response_modalities=["TEXT"]) # type: ignore
74+
75+
final_text = None
76+
async for event in runner.run_async(user_id=USER_ID, session_id=SESSION_ID, new_message=user_message, **run_config_kw):
77+
# Print out any parts safely; this will include function_call parts when they occur
78+
if hasattr(event, "content") and event.content and getattr(event.content, "parts", None):
79+
for part in event.content.parts:
80+
text = getattr(part, "text", None)
81+
func_call = getattr(part, "function_call", None)
82+
if text:
83+
print(f"Assistant: {text}")
84+
final_text = text
85+
elif func_call is not None:
86+
name = getattr(func_call, "name", "<unknown>")
87+
args = getattr(func_call, "args", {})
88+
print(f"Function call: {name} args={args}")
89+
90+
print("Final text:", final_text)
91+
92+
93+
if __name__ == "__main__":
94+
asyncio.run(main())

0 commit comments

Comments
 (0)