agno 2.0.0rc1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. agno/agent/agent.py +101 -140
  2. agno/db/mongo/mongo.py +8 -3
  3. agno/eval/accuracy.py +12 -5
  4. agno/knowledge/chunking/strategy.py +14 -14
  5. agno/knowledge/knowledge.py +156 -120
  6. agno/knowledge/reader/arxiv_reader.py +5 -5
  7. agno/knowledge/reader/csv_reader.py +6 -77
  8. agno/knowledge/reader/docx_reader.py +5 -5
  9. agno/knowledge/reader/firecrawl_reader.py +5 -5
  10. agno/knowledge/reader/json_reader.py +5 -5
  11. agno/knowledge/reader/markdown_reader.py +31 -9
  12. agno/knowledge/reader/pdf_reader.py +10 -123
  13. agno/knowledge/reader/reader_factory.py +65 -72
  14. agno/knowledge/reader/s3_reader.py +44 -114
  15. agno/knowledge/reader/text_reader.py +5 -5
  16. agno/knowledge/reader/url_reader.py +75 -31
  17. agno/knowledge/reader/web_search_reader.py +6 -29
  18. agno/knowledge/reader/website_reader.py +5 -5
  19. agno/knowledge/reader/wikipedia_reader.py +5 -5
  20. agno/knowledge/reader/youtube_reader.py +6 -6
  21. agno/knowledge/reranker/__init__.py +9 -0
  22. agno/knowledge/utils.py +10 -10
  23. agno/media.py +269 -268
  24. agno/models/aws/bedrock.py +3 -7
  25. agno/models/base.py +50 -54
  26. agno/models/google/gemini.py +11 -10
  27. agno/models/message.py +4 -4
  28. agno/models/ollama/chat.py +1 -1
  29. agno/models/openai/chat.py +33 -14
  30. agno/models/response.py +5 -5
  31. agno/os/app.py +40 -29
  32. agno/os/mcp.py +39 -59
  33. agno/os/router.py +547 -16
  34. agno/os/routers/evals/evals.py +197 -12
  35. agno/os/routers/knowledge/knowledge.py +428 -14
  36. agno/os/routers/memory/memory.py +250 -28
  37. agno/os/routers/metrics/metrics.py +125 -7
  38. agno/os/routers/session/session.py +393 -25
  39. agno/os/schema.py +55 -2
  40. agno/run/agent.py +37 -28
  41. agno/run/base.py +9 -19
  42. agno/run/team.py +110 -19
  43. agno/run/workflow.py +41 -28
  44. agno/team/team.py +808 -1080
  45. agno/tools/brightdata.py +3 -3
  46. agno/tools/cartesia.py +3 -5
  47. agno/tools/dalle.py +7 -4
  48. agno/tools/desi_vocal.py +2 -2
  49. agno/tools/e2b.py +6 -6
  50. agno/tools/eleven_labs.py +3 -3
  51. agno/tools/fal.py +4 -4
  52. agno/tools/function.py +7 -7
  53. agno/tools/giphy.py +2 -2
  54. agno/tools/lumalab.py +3 -3
  55. agno/tools/mcp.py +1 -2
  56. agno/tools/models/azure_openai.py +2 -2
  57. agno/tools/models/gemini.py +3 -3
  58. agno/tools/models/groq.py +3 -5
  59. agno/tools/models/nebius.py +2 -2
  60. agno/tools/models_labs.py +5 -5
  61. agno/tools/openai.py +4 -9
  62. agno/tools/opencv.py +3 -3
  63. agno/tools/replicate.py +7 -7
  64. agno/utils/events.py +5 -5
  65. agno/utils/gemini.py +1 -1
  66. agno/utils/log.py +52 -2
  67. agno/utils/mcp.py +57 -5
  68. agno/utils/models/aws_claude.py +1 -1
  69. agno/utils/models/claude.py +0 -8
  70. agno/utils/models/cohere.py +1 -1
  71. agno/utils/models/watsonx.py +1 -1
  72. agno/utils/openai.py +1 -1
  73. agno/utils/print_response/team.py +177 -73
  74. agno/utils/streamlit.py +27 -0
  75. agno/vectordb/lancedb/lance_db.py +82 -25
  76. agno/workflow/step.py +7 -7
  77. agno/workflow/types.py +13 -13
  78. agno/workflow/workflow.py +37 -28
  79. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/METADATA +140 -1
  80. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/RECORD +83 -84
  81. agno-2.0.1.dist-info/licenses/LICENSE +201 -0
  82. agno/knowledge/reader/gcs_reader.py +0 -67
  83. agno-2.0.0rc1.dist-info/licenses/LICENSE +0 -375
  84. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/WHEEL +0 -0
  85. {agno-2.0.0rc1.dist-info → agno-2.0.1.dist-info}/top_level.txt +0 -0
agno/utils/log.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from functools import lru_cache
2
3
  from os import getenv
3
4
  from typing import Any, Literal, Optional
4
5
 
@@ -64,6 +65,11 @@ class AgnoLogger(logging.Logger):
64
65
 
65
66
 
66
67
  def build_logger(logger_name: str, source_type: Optional[str] = None) -> Any:
68
+ # If a logger with the name "agno.{source_type}" is already set, we want to use that one
69
+ _logger = logging.getLogger(f"agno.{logger_name}")
70
+ if _logger.handlers or _logger.level != logging.NOTSET:
71
+ return _logger
72
+
67
73
  # Set the custom logger class as the default for this logger
68
74
  logging.setLoggerClass(AgnoLogger)
69
75
 
@@ -174,6 +180,12 @@ def use_workflow_logger():
174
180
  logger = workflow_logger
175
181
 
176
182
 
183
+ @lru_cache(maxsize=128)
184
+ def _using_default_logger(logger_instance: Any) -> bool:
185
+ """Return True if the currently active logger is our default AgnoLogger"""
186
+ return isinstance(logger_instance, AgnoLogger)
187
+
188
+
177
189
  def log_debug(msg, center: bool = False, symbol: str = "*", log_level: Literal[1, 2] = 1, *args, **kwargs):
178
190
  global logger
179
191
  global debug_on
@@ -181,12 +193,18 @@ def log_debug(msg, center: bool = False, symbol: str = "*", log_level: Literal[1
181
193
 
182
194
  if debug_on:
183
195
  if debug_level >= log_level:
184
- logger.debug(msg, center, symbol, *args, **kwargs)
196
+ if _using_default_logger(logger):
197
+ logger.debug(msg, center, symbol, *args, **kwargs)
198
+ else:
199
+ logger.debug(msg, *args, **kwargs)
185
200
 
186
201
 
187
202
  def log_info(msg, center: bool = False, symbol: str = "*", *args, **kwargs):
188
203
  global logger
189
- logger.info(msg, center, symbol, *args, **kwargs)
204
+ if _using_default_logger(logger):
205
+ logger.info(msg, center, symbol, *args, **kwargs)
206
+ else:
207
+ logger.info(msg, *args, **kwargs)
190
208
 
191
209
 
192
210
  def log_warning(msg, *args, **kwargs):
@@ -202,3 +220,35 @@ def log_error(msg, *args, **kwargs):
202
220
  def log_exception(msg, *args, **kwargs):
203
221
  global logger
204
222
  logger.exception(msg, *args, **kwargs)
223
+
224
+
225
+ def configure_agno_logging(
226
+ custom_default_logger: Optional[Any] = None,
227
+ custom_agent_logger: Optional[Any] = None,
228
+ custom_team_logger: Optional[Any] = None,
229
+ custom_workflow_logger: Optional[Any] = None,
230
+ ) -> None:
231
+ """
232
+ Util to set custom loggers. These will be used everywhere across the Agno library.
233
+
234
+ Args:
235
+ custom_default_logger: Default logger to use (overrides agent_logger for default)
236
+ custom_agent_logger: Custom logger for agent operations
237
+ custom_team_logger: Custom logger for team operations
238
+ custom_workflow_logger: Custom logger for workflow operations
239
+ """
240
+ if custom_default_logger is not None:
241
+ global logger
242
+ logger = custom_default_logger
243
+
244
+ if custom_agent_logger is not None:
245
+ global agent_logger
246
+ agent_logger = custom_agent_logger
247
+
248
+ if custom_team_logger is not None:
249
+ global team_logger
250
+ team_logger = custom_team_logger
251
+
252
+ if custom_workflow_logger is not None:
253
+ global workflow_logger
254
+ workflow_logger = custom_workflow_logger
agno/utils/mcp.py CHANGED
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from functools import partial
2
3
  from uuid import uuid4
3
4
 
@@ -11,7 +12,7 @@ except (ImportError, ModuleNotFoundError):
11
12
  raise ImportError("`mcp` not installed. Please install using `pip install mcp`")
12
13
 
13
14
 
14
- from agno.media import ImageArtifact
15
+ from agno.media import Image
15
16
  from agno.tools.function import ToolResult
16
17
 
17
18
 
@@ -43,13 +44,64 @@ def get_entrypoint_for_tool(tool: MCPTool, session: ClientSession):
43
44
 
44
45
  for content_item in result.content:
45
46
  if isinstance(content_item, TextContent):
46
- response_str += content_item.text + "\n"
47
+ text_content = content_item.text
48
+
49
+ # Parse as JSON to check for custom image format
50
+ try:
51
+ parsed_json = json.loads(text_content)
52
+ if (
53
+ isinstance(parsed_json, dict)
54
+ and parsed_json.get("type") == "image"
55
+ and "data" in parsed_json
56
+ ):
57
+ log_debug("Found custom JSON image format in TextContent")
58
+
59
+ # Extract image data
60
+ image_data = parsed_json.get("data")
61
+ mime_type = parsed_json.get("mimeType", "image/png")
62
+
63
+ if image_data and isinstance(image_data, str):
64
+ import base64
65
+
66
+ try:
67
+ image_bytes = base64.b64decode(image_data)
68
+ except Exception as e:
69
+ log_debug(f"Failed to decode base64 image data: {e}")
70
+ image_bytes = None
71
+
72
+ if image_bytes:
73
+ img_artifact = Image(
74
+ id=str(uuid4()),
75
+ url=None,
76
+ content=image_bytes,
77
+ mime_type=mime_type,
78
+ )
79
+ images.append(img_artifact)
80
+ response_str += "Image has been generated and added to the response.\n"
81
+ continue
82
+
83
+ except (json.JSONDecodeError, TypeError):
84
+ pass
85
+
86
+ response_str += text_content + "\n"
87
+
47
88
  elif isinstance(content_item, ImageContent):
48
- # Handle image content if present
49
- img_artifact = ImageArtifact(
89
+ # Handle standard MCP ImageContent
90
+ image_data = getattr(content_item, "data", None)
91
+
92
+ if image_data and isinstance(image_data, str):
93
+ import base64
94
+
95
+ try:
96
+ image_data = base64.b64decode(image_data)
97
+ except Exception as e:
98
+ log_debug(f"Failed to decode base64 image data: {e}")
99
+ image_data = None
100
+
101
+ img_artifact = Image(
50
102
  id=str(uuid4()),
51
103
  url=getattr(content_item, "url", None),
52
- content=getattr(content_item, "data", None),
104
+ content=image_data,
53
105
  mime_type=getattr(content_item, "mimeType", "image/png"),
54
106
  )
55
107
  images.append(img_artifact)
@@ -53,7 +53,7 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
53
53
  try:
54
54
  # Case 1: Image is a URL
55
55
  if image.url is not None:
56
- content_bytes = image.image_url_content
56
+ content_bytes = image.get_content_bytes() # type: ignore
57
57
 
58
58
  # Case 2: Image is a local file path
59
59
  elif image.filepath is not None:
@@ -307,14 +307,6 @@ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Opti
307
307
  required: List[str] = parameters.get("required", [])
308
308
  required_params: List[str] = required
309
309
 
310
- if not required_params:
311
- for param_name, param_info in properties.items():
312
- param_type = param_info.get("type", "")
313
- param_type_list: List[str] = [param_type] if isinstance(param_type, str) else param_type or []
314
-
315
- if "null" not in param_type_list:
316
- required_params.append(param_name)
317
-
318
310
  input_properties: Dict[str, Any] = {}
319
311
  for param_name, param_info in properties.items():
320
312
  # Preserve the complete schema structure for complex types
@@ -20,7 +20,7 @@ def _format_images_for_message(message: Message, images: Sequence[Image]) -> Lis
20
20
  if image.content is not None:
21
21
  image_content = image.content
22
22
  elif image.url is not None:
23
- image_content = image.image_url_content
23
+ image_content = image.get_content_bytes() # type: ignore
24
24
  elif image.filepath is not None:
25
25
  if isinstance(image.filepath, Path):
26
26
  image_content = image.filepath.read_bytes()
@@ -19,7 +19,7 @@ def format_images_for_message(message: Message, images: Sequence[Image]) -> Mess
19
19
  if image.content is not None:
20
20
  image_content = image.content
21
21
  elif image.url is not None:
22
- image_content = image.image_url_content
22
+ image_content = image.get_content_bytes() # type: ignore
23
23
  else:
24
24
  log_warning(f"Unsupported image format: {image}")
25
25
  continue
agno/utils/openai.py CHANGED
@@ -39,7 +39,7 @@ def audio_to_message(audio: Sequence[Audio]) -> List[Dict[str, Any]]:
39
39
 
40
40
  # The audio is a URL
41
41
  elif audio_snippet.url:
42
- audio_bytes = audio_snippet.audio_url_content
42
+ audio_bytes = audio_snippet.get_content_bytes()
43
43
  if audio_bytes is not None:
44
44
  encoded_string = base64.b64encode(audio_bytes).decode("utf-8")
45
45
  if not audio_format:
@@ -6,7 +6,7 @@ from agno.media import Audio, File, Image, Video
6
6
  from agno.models.message import Message
7
7
  from agno.models.response import ToolExecution
8
8
  from agno.reasoning.step import ReasoningStep
9
- from agno.run.agent import RunEvent, RunOutput, ToolCallCompletedEvent
9
+ from agno.run.agent import RunOutput
10
10
  from agno.run.team import TeamRunEvent, TeamRunOutput, TeamRunOutputEvent
11
11
  from agno.utils.log import log_warning
12
12
  from agno.utils.message import get_text_from_message
@@ -184,7 +184,7 @@ def print_response(
184
184
  elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
185
185
  show_markdown = member_markdown.get(member_response.team_id, False)
186
186
 
187
- member_response_content: Union[str, JSON, Markdown] = team._parse_response_content( # type: ignore
187
+ member_response_content: Union[str, JSON, Markdown] = _parse_response_content( # type: ignore
188
188
  member_response,
189
189
  tags_to_include_in_markdown,
190
190
  show_markdown=show_markdown,
@@ -247,7 +247,7 @@ def print_response(
247
247
  panels.append(team_tool_calls_panel)
248
248
  live_console.update(Group(*panels))
249
249
 
250
- response_content_batch: Union[str, JSON, Markdown] = team._parse_response_content( # type: ignore
250
+ response_content_batch: Union[str, JSON, Markdown] = _parse_response_content( # type: ignore
251
251
  run_response, tags_to_include_in_markdown, show_markdown=team_markdown
252
252
  )
253
253
 
@@ -388,6 +388,7 @@ def print_response_stream(
388
388
  dependencies=dependencies,
389
389
  metadata=metadata,
390
390
  debug_mode=debug_mode,
391
+ yield_run_response=True,
391
392
  **kwargs,
392
393
  )
393
394
 
@@ -397,9 +398,8 @@ def print_response_stream(
397
398
  # Dict to track member response panels by member_id
398
399
  member_response_panels = {}
399
400
 
400
- run_id = None
401
+ final_run_response = None
401
402
  for resp in stream_resp:
402
- run_id = resp.run_id
403
403
  if team_markdown is None:
404
404
  if markdown:
405
405
  team_markdown = True
@@ -409,6 +409,10 @@ def print_response_stream(
409
409
  if team.output_schema is not None:
410
410
  team_markdown = False
411
411
 
412
+ if isinstance(resp, TeamRunOutput):
413
+ final_run_response = resp
414
+ continue
415
+
412
416
  if isinstance(resp, tuple(get_args(TeamRunOutputEvent))):
413
417
  if resp.event == TeamRunEvent.run_content:
414
418
  if isinstance(resp.content, str):
@@ -424,8 +428,8 @@ def print_response_stream(
424
428
  reasoning_steps = resp.reasoning_steps # type: ignore
425
429
 
426
430
  # Collect team tool calls, avoiding duplicates
427
- if isinstance(resp, ToolCallCompletedEvent) and resp.tool:
428
- tool = resp.tool
431
+ if resp.event == TeamRunEvent.tool_call_completed and resp.tool: # type: ignore
432
+ tool = resp.tool # type: ignore
429
433
  # Generate a unique ID for this tool call
430
434
  if tool.tool_call_id:
431
435
  tool_id = tool.tool_call_id
@@ -537,7 +541,7 @@ def print_response_stream(
537
541
  if markdown:
538
542
  show_markdown = True
539
543
 
540
- member_response_content = team._parse_response_content(
544
+ member_response_content = _parse_response_content(
541
545
  member_response,
542
546
  tags_to_include_in_markdown,
543
547
  show_markdown=show_markdown,
@@ -597,7 +601,7 @@ def print_response_stream(
597
601
  live_console.update(Group(*panels))
598
602
 
599
603
  response_timer.stop()
600
- run_response = team.get_run_output(run_id=run_id) # type: ignore
604
+ run_response = final_run_response
601
605
 
602
606
  # Add citations
603
607
  if hasattr(resp, "citations") and resp.citations is not None and resp.citations.urls is not None:
@@ -725,7 +729,7 @@ def print_response_stream(
725
729
  elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
726
730
  show_markdown = member_markdown.get(member_response.team_id, False)
727
731
 
728
- member_response_content = team._parse_response_content( # type: ignore
732
+ member_response_content = _parse_response_content( # type: ignore
729
733
  member_response,
730
734
  tags_to_include_in_markdown,
731
735
  show_markdown=show_markdown,
@@ -985,7 +989,7 @@ async def aprint_response(
985
989
  elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
986
990
  show_markdown = member_markdown.get(member_response.team_id, False)
987
991
 
988
- member_response_content: Union[str, JSON, Markdown] = team._parse_response_content( # type: ignore
992
+ member_response_content: Union[str, JSON, Markdown] = _parse_response_content( # type: ignore
989
993
  member_response,
990
994
  tags_to_include_in_markdown,
991
995
  show_markdown=show_markdown,
@@ -1046,7 +1050,7 @@ async def aprint_response(
1046
1050
  panels.append(team_tool_calls_panel)
1047
1051
  live_console.update(Group(*panels))
1048
1052
 
1049
- response_content_batch: Union[str, JSON, Markdown] = team._parse_response_content( # type: ignore
1053
+ response_content_batch: Union[str, JSON, Markdown] = _parse_response_content( # type: ignore
1050
1054
  run_response, tags_to_include_in_markdown, show_markdown=team_markdown
1051
1055
  )
1052
1056
 
@@ -1175,7 +1179,10 @@ async def aprint_response_stream(
1175
1179
  team_markdown = None
1176
1180
  member_markdown = {}
1177
1181
 
1178
- run_id = None
1182
+ # Dict to track member response panels by member_id
1183
+ member_response_panels = {}
1184
+
1185
+ final_run_response = None
1179
1186
  async for resp in team.arun( # type: ignore
1180
1187
  input=input,
1181
1188
  audio=audio,
@@ -1192,9 +1199,9 @@ async def aprint_response_stream(
1192
1199
  dependencies=dependencies,
1193
1200
  metadata=metadata,
1194
1201
  debug_mode=debug_mode,
1202
+ yield_run_response=True,
1195
1203
  **kwargs,
1196
1204
  ):
1197
- run_id = resp.run_id
1198
1205
  if team_markdown is None:
1199
1206
  if markdown:
1200
1207
  team_markdown = True
@@ -1204,6 +1211,10 @@ async def aprint_response_stream(
1204
1211
  if team.output_schema is not None:
1205
1212
  team_markdown = False
1206
1213
 
1214
+ if isinstance(resp, TeamRunOutput):
1215
+ final_run_response = resp
1216
+ continue
1217
+
1207
1218
  if isinstance(resp, tuple(get_args(TeamRunOutputEvent))):
1208
1219
  if resp.event == TeamRunEvent.run_content:
1209
1220
  if isinstance(resp.content, str):
@@ -1219,8 +1230,8 @@ async def aprint_response_stream(
1219
1230
  reasoning_steps = resp.reasoning_steps # type: ignore
1220
1231
 
1221
1232
  # Collect team tool calls, avoiding duplicates
1222
- if isinstance(resp, ToolCallCompletedEvent) and resp.tool:
1223
- tool = resp.tool
1233
+ if resp.event == TeamRunEvent.tool_call_completed and resp.tool: # type: ignore
1234
+ tool = resp.tool # type: ignore
1224
1235
  # Generate a unique ID for this tool call
1225
1236
  if tool.tool_call_id is not None:
1226
1237
  tool_id = tool.tool_call_id
@@ -1259,7 +1270,7 @@ async def aprint_response_stream(
1259
1270
  response_content_stream = Markdown(escaped_content)
1260
1271
 
1261
1272
  # Create new panels for each chunk
1262
- panels = [status]
1273
+ panels = []
1263
1274
 
1264
1275
  if input and show_message:
1265
1276
  render = True
@@ -1271,8 +1282,6 @@ async def aprint_response_stream(
1271
1282
  border_style="cyan",
1272
1283
  )
1273
1284
  panels.append(message_panel)
1274
- if render:
1275
- live_console.update(Group(*panels))
1276
1285
 
1277
1286
  if len(reasoning_steps) > 0 and show_reasoning:
1278
1287
  render = True
@@ -1280,8 +1289,6 @@ async def aprint_response_stream(
1280
1289
  for i, step in enumerate(reasoning_steps, 1):
1281
1290
  reasoning_panel = build_reasoning_step_panel(i, step, show_full_reasoning)
1282
1291
  panels.append(reasoning_panel)
1283
- if render:
1284
- live_console.update(Group(*panels))
1285
1292
 
1286
1293
  if len(_response_reasoning_content) > 0:
1287
1294
  render = True
@@ -1292,29 +1299,95 @@ async def aprint_response_stream(
1292
1299
  border_style="green",
1293
1300
  )
1294
1301
  panels.append(thinking_panel)
1295
- if render:
1296
- live_console.update(Group(*panels))
1302
+ elif _response_content == "":
1303
+ # Keep showing status if no content yet
1304
+ panels.append(status)
1297
1305
 
1298
- # Add tool calls panel if available
1299
- if (
1300
- resp is not None
1301
- and hasattr(resp, "tool")
1302
- and resp.tool is not None
1303
- and resp.event == RunEvent.tool_call_started
1304
- ):
1305
- render = True
1306
- # Create bullet points for each tool call
1307
- tool_calls_content = Text()
1308
- formatted_tool_call = format_tool_calls([resp.tool])
1309
- tool_calls_content.append(f"• {formatted_tool_call}\n")
1310
-
1311
- tool_calls_panel = create_panel(
1312
- content=tool_calls_content.plain.rstrip(),
1313
- title="Tool Calls",
1314
- border_style="yellow",
1315
- )
1316
- panels.append(tool_calls_panel)
1306
+ # Process member responses and their tool calls
1307
+ for member_response in resp.member_responses if hasattr(resp, "member_responses") else []:
1308
+ member_id = None
1309
+ member_name = "Team Member"
1310
+ if isinstance(member_response, RunOutput) and member_response.agent_id is not None:
1311
+ member_id = member_response.agent_id
1312
+ member_name = team._get_member_name(member_id)
1313
+ elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
1314
+ member_id = member_response.team_id
1315
+
1316
+ member_name = team._get_member_name(member_id)
1317
+
1318
+ # If we have tool calls for this member, display them
1319
+ if member_id in member_tool_calls and member_tool_calls[member_id]:
1320
+ formatted_calls = format_tool_calls(member_tool_calls[member_id])
1321
+ if formatted_calls:
1322
+ console_width = console.width if console else 80
1323
+ panel_width = console_width + 30
1324
+
1325
+ lines = []
1326
+ for call in formatted_calls:
1327
+ wrapped_call = textwrap.fill(f"• {call}", width=panel_width, subsequent_indent=" ")
1328
+ lines.append(wrapped_call)
1329
+
1330
+ tool_calls_text = "\n\n".join(lines)
1331
+
1332
+ member_tool_calls_panel = create_panel(
1333
+ content=tool_calls_text,
1334
+ title=f"{member_name} Tool Calls",
1335
+ border_style="yellow",
1336
+ )
1337
+ panels.append(member_tool_calls_panel)
1338
+
1339
+ # Process member response content
1340
+ if team.show_members_responses and member_id is not None:
1341
+ show_markdown = False
1342
+ if markdown:
1343
+ show_markdown = True
1344
+
1345
+ member_response_content = _parse_response_content(
1346
+ member_response,
1347
+ tags_to_include_in_markdown,
1348
+ show_markdown=show_markdown,
1349
+ )
1350
+
1351
+ member_response_panel = create_panel(
1352
+ content=member_response_content,
1353
+ title=f"{member_name} Response",
1354
+ border_style="magenta",
1355
+ )
1356
+
1357
+ panels.append(member_response_panel)
1358
+
1359
+ # Store for reference
1360
+ if member_id is not None:
1361
+ member_response_panels[member_id] = member_response_panel
1362
+
1363
+ # Add team tool calls panel if available (before the team response)
1364
+ if team_tool_calls:
1365
+ formatted_calls = format_tool_calls(team_tool_calls)
1366
+ if formatted_calls:
1367
+ console_width = console.width if console else 80
1368
+ panel_width = console_width + 30
1369
+
1370
+ lines = []
1371
+ # Create a set to track already added calls by their string representation
1372
+ added_calls = set()
1373
+ for call in formatted_calls:
1374
+ if call not in added_calls:
1375
+ added_calls.add(call)
1376
+ # Wrap the call text to fit within the panel
1377
+ wrapped_call = textwrap.fill(f"• {call}", width=panel_width, subsequent_indent=" ")
1378
+ lines.append(wrapped_call)
1379
+
1380
+ # Join with blank lines between items
1381
+ tool_calls_text = "\n\n".join(lines)
1382
+
1383
+ team_tool_calls_panel = create_panel(
1384
+ content=tool_calls_text,
1385
+ title="Team Tool Calls",
1386
+ border_style="yellow",
1387
+ )
1388
+ panels.append(team_tool_calls_panel)
1317
1389
 
1390
+ # Add the team response panel at the end
1318
1391
  if response_content_stream:
1319
1392
  render = True
1320
1393
  # Create panel for response
@@ -1324,11 +1397,13 @@ async def aprint_response_stream(
1324
1397
  border_style="blue",
1325
1398
  )
1326
1399
  panels.append(response_panel)
1327
- if render:
1400
+
1401
+ if render or len(panels) > 0:
1328
1402
  live_console.update(Group(*panels))
1403
+
1329
1404
  response_timer.stop()
1330
1405
 
1331
- run_response = team.get_run_output(run_id=run_id) # type: ignore
1406
+ run_response = final_run_response
1332
1407
 
1333
1408
  # Add citations
1334
1409
  if hasattr(resp, "citations") and resp.citations is not None and resp.citations.urls is not None:
@@ -1415,6 +1490,7 @@ async def aprint_response_stream(
1415
1490
  elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
1416
1491
  member_id = member_response.team_id
1417
1492
 
1493
+ # Print tool calls
1418
1494
  if member_id:
1419
1495
  # First add tool calls if any
1420
1496
  if member_id in member_tool_calls and member_tool_calls[member_id]:
@@ -1472,39 +1548,39 @@ async def aprint_response_stream(
1472
1548
  elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
1473
1549
  show_markdown = member_markdown.get(member_response.team_id, False)
1474
1550
 
1475
- member_response_content = team._parse_response_content( # type: ignore
1476
- member_response,
1477
- tags_to_include_in_markdown,
1478
- show_markdown=show_markdown,
1479
- )
1551
+ member_response_content = _parse_response_content( # type: ignore
1552
+ member_response,
1553
+ tags_to_include_in_markdown,
1554
+ show_markdown=show_markdown,
1555
+ )
1480
1556
 
1481
- member_name = "Team Member"
1482
- if isinstance(member_response, RunOutput) and member_response.agent_id is not None:
1483
- member_name = team._get_member_name(member_response.agent_id)
1484
- elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
1485
- member_name = team._get_member_name(member_response.team_id)
1557
+ member_name = "Team Member"
1558
+ if isinstance(member_response, RunOutput) and member_response.agent_id is not None:
1559
+ member_name = team._get_member_name(member_response.agent_id)
1560
+ elif isinstance(member_response, TeamRunOutput) and member_response.team_id is not None:
1561
+ member_name = team._get_member_name(member_response.team_id)
1486
1562
 
1487
- member_response_panel = create_panel(
1488
- content=member_response_content,
1489
- title=f"{member_name} Response",
1490
- border_style="magenta",
1491
- )
1492
- final_panels.append(member_response_panel)
1563
+ member_response_panel = create_panel(
1564
+ content=member_response_content,
1565
+ title=f"{member_name} Response",
1566
+ border_style="magenta",
1567
+ )
1568
+ final_panels.append(member_response_panel)
1493
1569
 
1494
- # Add citations if any
1495
- if member_response.citations is not None and member_response.citations.urls is not None:
1496
- md_content = "\n".join(
1497
- f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
1498
- for i, citation in enumerate(member_response.citations.urls)
1499
- if citation.url # Only include citations with valid URLs
1570
+ # Add citations if any
1571
+ if member_response.citations is not None and member_response.citations.urls is not None:
1572
+ md_content = "\n".join(
1573
+ f"{i + 1}. [{citation.title or citation.url}]({citation.url})"
1574
+ for i, citation in enumerate(member_response.citations.urls)
1575
+ if citation.url # Only include citations with valid URLs
1576
+ )
1577
+ if md_content: # Only create panel if there are citations
1578
+ citations_panel = create_panel(
1579
+ content=Markdown(md_content),
1580
+ title="Citations",
1581
+ border_style="magenta",
1500
1582
  )
1501
- if md_content: # Only create panel if there are citations
1502
- citations_panel = create_panel(
1503
- content=Markdown(md_content),
1504
- title="Citations",
1505
- border_style="magenta",
1506
- )
1507
- final_panels.append(citations_panel)
1583
+ final_panels.append(citations_panel)
1508
1584
 
1509
1585
  # Add team tool calls before team response
1510
1586
  if team_tool_calls:
@@ -1563,3 +1639,31 @@ async def aprint_response_stream(
1563
1639
 
1564
1640
  # Final update with correctly ordered panels
1565
1641
  live_console.update(Group(*final_panels))
1642
+
1643
+
1644
+ def _parse_response_content(
1645
+ run_response: Union[TeamRunOutput, RunOutput],
1646
+ tags_to_include_in_markdown: Set[str],
1647
+ show_markdown: bool = True,
1648
+ ) -> Any:
1649
+ from rich.json import JSON
1650
+ from rich.markdown import Markdown
1651
+
1652
+ if isinstance(run_response.content, str):
1653
+ if show_markdown:
1654
+ escaped_content = escape_markdown_tags(run_response.content, tags_to_include_in_markdown)
1655
+ return Markdown(escaped_content)
1656
+ else:
1657
+ return run_response.get_content_as_string(indent=4)
1658
+ elif isinstance(run_response.content, BaseModel):
1659
+ try:
1660
+ return JSON(run_response.content.model_dump_json(exclude_none=True), indent=2)
1661
+ except Exception as e:
1662
+ log_warning(f"Failed to convert response to JSON: {e}")
1663
+ else:
1664
+ import json
1665
+
1666
+ try:
1667
+ return JSON(json.dumps(run_response.content), indent=4)
1668
+ except Exception as e:
1669
+ log_warning(f"Failed to convert response to JSON: {e}")