agno 2.0.11__py3-none-any.whl → 2.1.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 (93) hide show
  1. agno/agent/agent.py +607 -176
  2. agno/db/in_memory/in_memory_db.py +42 -29
  3. agno/db/mongo/mongo.py +65 -66
  4. agno/db/postgres/postgres.py +6 -4
  5. agno/db/utils.py +50 -22
  6. agno/exceptions.py +62 -1
  7. agno/guardrails/__init__.py +6 -0
  8. agno/guardrails/base.py +19 -0
  9. agno/guardrails/openai.py +144 -0
  10. agno/guardrails/pii.py +94 -0
  11. agno/guardrails/prompt_injection.py +51 -0
  12. agno/knowledge/embedder/aws_bedrock.py +9 -4
  13. agno/knowledge/embedder/azure_openai.py +54 -0
  14. agno/knowledge/embedder/base.py +2 -0
  15. agno/knowledge/embedder/cohere.py +184 -5
  16. agno/knowledge/embedder/google.py +79 -1
  17. agno/knowledge/embedder/huggingface.py +9 -4
  18. agno/knowledge/embedder/jina.py +63 -0
  19. agno/knowledge/embedder/mistral.py +78 -11
  20. agno/knowledge/embedder/ollama.py +5 -0
  21. agno/knowledge/embedder/openai.py +18 -54
  22. agno/knowledge/embedder/voyageai.py +69 -16
  23. agno/knowledge/knowledge.py +11 -4
  24. agno/knowledge/reader/pdf_reader.py +4 -3
  25. agno/knowledge/reader/website_reader.py +3 -2
  26. agno/models/base.py +125 -32
  27. agno/models/cerebras/cerebras.py +1 -0
  28. agno/models/cerebras/cerebras_openai.py +1 -0
  29. agno/models/dashscope/dashscope.py +1 -0
  30. agno/models/google/gemini.py +27 -5
  31. agno/models/openai/chat.py +13 -4
  32. agno/models/openai/responses.py +1 -1
  33. agno/models/perplexity/perplexity.py +2 -3
  34. agno/models/requesty/__init__.py +5 -0
  35. agno/models/requesty/requesty.py +49 -0
  36. agno/models/vllm/vllm.py +1 -0
  37. agno/models/xai/xai.py +1 -0
  38. agno/os/app.py +98 -126
  39. agno/os/interfaces/__init__.py +1 -0
  40. agno/os/interfaces/agui/agui.py +21 -5
  41. agno/os/interfaces/base.py +4 -2
  42. agno/os/interfaces/slack/slack.py +13 -8
  43. agno/os/interfaces/whatsapp/router.py +2 -0
  44. agno/os/interfaces/whatsapp/whatsapp.py +12 -5
  45. agno/os/mcp.py +2 -2
  46. agno/os/middleware/__init__.py +7 -0
  47. agno/os/middleware/jwt.py +233 -0
  48. agno/os/router.py +182 -46
  49. agno/os/routers/home.py +2 -2
  50. agno/os/routers/memory/memory.py +23 -1
  51. agno/os/routers/memory/schemas.py +1 -1
  52. agno/os/routers/session/session.py +20 -3
  53. agno/os/utils.py +74 -8
  54. agno/run/agent.py +120 -77
  55. agno/run/base.py +2 -13
  56. agno/run/team.py +115 -72
  57. agno/run/workflow.py +5 -15
  58. agno/session/summary.py +9 -10
  59. agno/session/team.py +2 -1
  60. agno/team/team.py +721 -169
  61. agno/tools/firecrawl.py +4 -4
  62. agno/tools/function.py +42 -2
  63. agno/tools/knowledge.py +3 -3
  64. agno/tools/searxng.py +2 -2
  65. agno/tools/serper.py +2 -2
  66. agno/tools/spider.py +2 -2
  67. agno/tools/workflow.py +4 -5
  68. agno/utils/events.py +66 -1
  69. agno/utils/hooks.py +57 -0
  70. agno/utils/media.py +11 -9
  71. agno/utils/print_response/agent.py +43 -5
  72. agno/utils/print_response/team.py +48 -12
  73. agno/utils/serialize.py +32 -0
  74. agno/vectordb/cassandra/cassandra.py +44 -4
  75. agno/vectordb/chroma/chromadb.py +79 -8
  76. agno/vectordb/clickhouse/clickhousedb.py +43 -6
  77. agno/vectordb/couchbase/couchbase.py +76 -5
  78. agno/vectordb/lancedb/lance_db.py +38 -3
  79. agno/vectordb/milvus/milvus.py +76 -4
  80. agno/vectordb/mongodb/mongodb.py +76 -4
  81. agno/vectordb/pgvector/pgvector.py +50 -6
  82. agno/vectordb/pineconedb/pineconedb.py +39 -2
  83. agno/vectordb/qdrant/qdrant.py +76 -26
  84. agno/vectordb/singlestore/singlestore.py +77 -4
  85. agno/vectordb/upstashdb/upstashdb.py +42 -2
  86. agno/vectordb/weaviate/weaviate.py +39 -3
  87. agno/workflow/types.py +5 -6
  88. agno/workflow/workflow.py +58 -2
  89. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/METADATA +4 -3
  90. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/RECORD +93 -82
  91. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/WHEEL +0 -0
  92. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/licenses/LICENSE +0 -0
  93. {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/top_level.txt +0 -0
agno/tools/firecrawl.py CHANGED
@@ -3,7 +3,7 @@ from os import getenv
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
5
  from agno.tools import Toolkit
6
- from agno.utils.log import logger
6
+ from agno.utils.log import log_error
7
7
 
8
8
  try:
9
9
  from firecrawl import FirecrawlApp # type: ignore[attr-defined]
@@ -57,7 +57,7 @@ class FirecrawlTools(Toolkit):
57
57
  ):
58
58
  self.api_key: Optional[str] = api_key or getenv("FIRECRAWL_API_KEY")
59
59
  if not self.api_key:
60
- logger.error("FIRECRAWL_API_KEY not set. Please set the FIRECRAWL_API_KEY environment variable.")
60
+ log_error("FIRECRAWL_API_KEY not set. Please set the FIRECRAWL_API_KEY environment variable.")
61
61
 
62
62
  self.formats: Optional[List[str]] = formats
63
63
  self.limit: int = limit
@@ -73,7 +73,7 @@ class FirecrawlTools(Toolkit):
73
73
  if all or enable_mapping:
74
74
  tools.append(self.map_website)
75
75
  if all or enable_search:
76
- tools.append(self.search)
76
+ tools.append(self.search_web)
77
77
 
78
78
  super().__init__(name="firecrawl_tools", tools=tools, **kwargs)
79
79
 
@@ -121,7 +121,7 @@ class FirecrawlTools(Toolkit):
121
121
  map_result = self.app.map(url)
122
122
  return json.dumps(map_result.model_dump(), cls=CustomJSONEncoder)
123
123
 
124
- def search(self, query: str, limit: Optional[int] = None):
124
+ def search_web(self, query: str, limit: Optional[int] = None):
125
125
  """Use this function to search for the web using Firecrawl.
126
126
 
127
127
  Args:
agno/tools/function.py CHANGED
@@ -389,6 +389,9 @@ class Function(BaseModel):
389
389
  if not params_set_by_user:
390
390
  self.parameters = parameters
391
391
 
392
+ if strict:
393
+ self.process_schema_for_strict()
394
+
392
395
  try:
393
396
  self.entrypoint = self._wrap_callable(self.entrypoint)
394
397
  except Exception as e:
@@ -422,7 +425,45 @@ class Function(BaseModel):
422
425
  return wrapped
423
426
 
424
427
  def process_schema_for_strict(self):
425
- self.parameters["additionalProperties"] = False
428
+ """Process the schema to make it strict mode compliant."""
429
+
430
+ def make_nested_strict(schema):
431
+ """Recursively ensure all object schemas have additionalProperties: false"""
432
+ if not isinstance(schema, dict):
433
+ return schema
434
+
435
+ # Make a copy to avoid modifying the original
436
+ result = schema.copy()
437
+
438
+ # If this is an object schema, ensure additionalProperties: false
439
+ if result.get("type") == "object" or "properties" in result:
440
+ result["additionalProperties"] = False
441
+
442
+ # If schema has no type but has other schema properties, give it a type
443
+ if "type" not in result:
444
+ if "properties" in result:
445
+ result["type"] = "object"
446
+ result["additionalProperties"] = False
447
+ elif result.get("title") and not any(
448
+ key in result for key in ["properties", "items", "anyOf", "oneOf", "allOf", "enum"]
449
+ ):
450
+ result["type"] = "string"
451
+
452
+ # Recursively process nested schemas
453
+ for key, value in result.items():
454
+ if key == "properties" and isinstance(value, dict):
455
+ result[key] = {k: make_nested_strict(v) for k, v in value.items()}
456
+ elif key == "items" and isinstance(value, dict):
457
+ # This handles array items like List[KnowledgeFilter]
458
+ result[key] = make_nested_strict(value)
459
+ elif isinstance(value, dict):
460
+ result[key] = make_nested_strict(value)
461
+
462
+ return result
463
+
464
+ # Apply strict mode to the entire schema
465
+ self.parameters = make_nested_strict(self.parameters)
466
+
426
467
  self.parameters["required"] = [
427
468
  name
428
469
  for name in self.parameters["properties"]
@@ -648,7 +689,6 @@ class FunctionCall(BaseModel):
648
689
  entrypoint_args["audios"] = self.function._audios
649
690
  if "files" in signature(self.function.entrypoint).parameters: # type: ignore
650
691
  entrypoint_args["files"] = self.function._files
651
-
652
692
  return entrypoint_args
653
693
 
654
694
  def _build_hook_args(self, hook: Callable, name: str, func: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
agno/tools/knowledge.py CHANGED
@@ -43,15 +43,15 @@ class KnowledgeTools(Toolkit):
43
43
  if enable_think or all:
44
44
  tools.append(self.think)
45
45
  if enable_search or all:
46
- tools.append(self.search)
46
+ tools.append(self.search_knowledge)
47
47
  if enable_analyze or all:
48
48
  tools.append(self.analyze)
49
49
 
50
50
  super().__init__(
51
51
  name="knowledge_tools",
52
+ tools=tools,
52
53
  instructions=self.instructions,
53
54
  add_instructions=add_instructions,
54
- tools=tools,
55
55
  **kwargs,
56
56
  )
57
57
 
@@ -89,7 +89,7 @@ class KnowledgeTools(Toolkit):
89
89
  log_error(f"Error recording thought: {e}")
90
90
  return f"Error recording thought: {e}"
91
91
 
92
- def search(self, session_state: Dict[str, Any], query: str) -> str:
92
+ def search_knowledge(self, session_state: Dict[str, Any], query: str) -> str:
93
93
  """Use this tool to search the knowledge base for relevant information.
94
94
  After thinking through the question, use this tool as many times as needed to search for relevant information.
95
95
 
agno/tools/searxng.py CHANGED
@@ -21,7 +21,7 @@ class Searxng(Toolkit):
21
21
  self.fixed_max_results = fixed_max_results
22
22
 
23
23
  tools: List[Any] = [
24
- self.search,
24
+ self.search_web,
25
25
  self.image_search,
26
26
  self.it_search,
27
27
  self.map_search,
@@ -33,7 +33,7 @@ class Searxng(Toolkit):
33
33
 
34
34
  super().__init__(name="searxng", tools=tools, **kwargs)
35
35
 
36
- def search(self, query: str, max_results: int = 5) -> str:
36
+ def search_web(self, query: str, max_results: int = 5) -> str:
37
37
  """Use this function to search the web.
38
38
 
39
39
  Args:
agno/tools/serper.py CHANGED
@@ -44,7 +44,7 @@ class SerperTools(Toolkit):
44
44
 
45
45
  tools: List[Any] = []
46
46
  if all or enable_search:
47
- tools.append(self.search)
47
+ tools.append(self.search_web)
48
48
  if all or enable_search_news:
49
49
  tools.append(self.search_news)
50
50
  if all or enable_search_scholar:
@@ -97,7 +97,7 @@ class SerperTools(Toolkit):
97
97
  log_error(f"Serper API error: {str(e)}")
98
98
  return {"success": False, "error": str(e)}
99
99
 
100
- def search(
100
+ def search_web(
101
101
  self,
102
102
  query: str,
103
103
  num_results: Optional[int] = None,
agno/tools/spider.py CHANGED
@@ -42,7 +42,7 @@ class SpiderTools(Toolkit):
42
42
 
43
43
  tools: List[Any] = []
44
44
  if enable_search or all:
45
- tools.append(self.search)
45
+ tools.append(self.search_web)
46
46
  if enable_scrape or all:
47
47
  tools.append(self.scrape)
48
48
  if enable_crawl or all:
@@ -50,7 +50,7 @@ class SpiderTools(Toolkit):
50
50
 
51
51
  super().__init__(name="spider", tools=tools, **kwargs)
52
52
 
53
- def search(self, query: str, max_results: int = 5) -> str:
53
+ def search_web(self, query: str, max_results: int = 5) -> str:
54
54
  """Use this function to search the web.
55
55
  Args:
56
56
  query (str): The query to search the web with.
agno/tools/workflow.py CHANGED
@@ -2,7 +2,7 @@ import json
2
2
  from textwrap import dedent
3
3
  from typing import Any, Dict, Optional
4
4
 
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, Field
6
6
 
7
7
  from agno.tools import Toolkit
8
8
  from agno.utils.log import log_debug, log_error
@@ -10,8 +10,8 @@ from agno.workflow.workflow import Workflow, WorkflowRunOutput
10
10
 
11
11
 
12
12
  class RunWorkflowInput(BaseModel):
13
- input_data: str
14
- additional_data: Optional[Dict[str, Any]] = None
13
+ input_data: str = Field(..., description="The input data for the workflow.")
14
+ additional_data: Optional[Dict[str, Any]] = Field(default=None, description="The additional data for the workflow.")
15
15
 
16
16
 
17
17
  class WorkflowTools(Toolkit):
@@ -131,8 +131,7 @@ class WorkflowTools(Toolkit):
131
131
  """Use this tool to execute the workflow with the specified inputs and parameters.
132
132
  After thinking through the requirements, use this tool to run the workflow with appropriate inputs.
133
133
  Args:
134
- input_data: The input data for the workflow (use a `str` for a simple input)
135
- additional_data: The additional data for the workflow. This is a dictionary of key-value pairs that will be passed to the workflow. E.g. {"topic": "food", "style": "Humour"}
134
+ input_data: The input data for the workflow.
136
135
  """
137
136
  try:
138
137
  log_debug(f"Running workflow with input: {input.input_data}")
agno/utils/events.py CHANGED
@@ -11,6 +11,8 @@ from agno.run.agent import (
11
11
  OutputModelResponseStartedEvent,
12
12
  ParserModelResponseCompletedEvent,
13
13
  ParserModelResponseStartedEvent,
14
+ PreHookCompletedEvent,
15
+ PreHookStartedEvent,
14
16
  ReasoningCompletedEvent,
15
17
  ReasoningStartedEvent,
16
18
  ReasoningStepEvent,
@@ -19,6 +21,7 @@ from agno.run.agent import (
19
21
  RunContentEvent,
20
22
  RunContinuedEvent,
21
23
  RunErrorEvent,
24
+ RunInput,
22
25
  RunOutput,
23
26
  RunPausedEvent,
24
27
  RunStartedEvent,
@@ -31,6 +34,8 @@ from agno.run.team import OutputModelResponseCompletedEvent as TeamOutputModelRe
31
34
  from agno.run.team import OutputModelResponseStartedEvent as TeamOutputModelResponseStartedEvent
32
35
  from agno.run.team import ParserModelResponseCompletedEvent as TeamParserModelResponseCompletedEvent
33
36
  from agno.run.team import ParserModelResponseStartedEvent as TeamParserModelResponseStartedEvent
37
+ from agno.run.team import PreHookCompletedEvent as TeamPreHookCompletedEvent
38
+ from agno.run.team import PreHookStartedEvent as TeamPreHookStartedEvent
34
39
  from agno.run.team import ReasoningCompletedEvent as TeamReasoningCompletedEvent
35
40
  from agno.run.team import ReasoningStartedEvent as TeamReasoningStartedEvent
36
41
  from agno.run.team import ReasoningStepEvent as TeamReasoningStepEvent
@@ -39,7 +44,7 @@ from agno.run.team import RunCompletedEvent as TeamRunCompletedEvent
39
44
  from agno.run.team import RunContentEvent as TeamRunContentEvent
40
45
  from agno.run.team import RunErrorEvent as TeamRunErrorEvent
41
46
  from agno.run.team import RunStartedEvent as TeamRunStartedEvent
42
- from agno.run.team import TeamRunOutput
47
+ from agno.run.team import TeamRunInput, TeamRunOutput
43
48
  from agno.run.team import ToolCallCompletedEvent as TeamToolCallCompletedEvent
44
49
  from agno.run.team import ToolCallStartedEvent as TeamToolCallStartedEvent
45
50
 
@@ -177,6 +182,66 @@ def create_run_cancelled_event(from_run_response: RunOutput, reason: str) -> Run
177
182
  )
178
183
 
179
184
 
185
+ def create_pre_hook_started_event(
186
+ from_run_response: RunOutput, pre_hook_name: Optional[str] = None, run_input: Optional[RunInput] = None
187
+ ) -> PreHookStartedEvent:
188
+ from copy import deepcopy
189
+
190
+ return PreHookStartedEvent(
191
+ session_id=from_run_response.session_id,
192
+ agent_id=from_run_response.agent_id, # type: ignore
193
+ agent_name=from_run_response.agent_name, # type: ignore
194
+ run_id=from_run_response.run_id,
195
+ pre_hook_name=pre_hook_name,
196
+ run_input=deepcopy(run_input),
197
+ )
198
+
199
+
200
+ def create_team_pre_hook_started_event(
201
+ from_run_response: TeamRunOutput, pre_hook_name: Optional[str] = None, run_input: Optional[TeamRunInput] = None
202
+ ) -> TeamPreHookStartedEvent:
203
+ from copy import deepcopy
204
+
205
+ return TeamPreHookStartedEvent(
206
+ session_id=from_run_response.session_id,
207
+ team_id=from_run_response.team_id, # type: ignore
208
+ team_name=from_run_response.team_name, # type: ignore
209
+ run_id=from_run_response.run_id,
210
+ pre_hook_name=pre_hook_name,
211
+ run_input=deepcopy(run_input),
212
+ )
213
+
214
+
215
+ def create_pre_hook_completed_event(
216
+ from_run_response: RunOutput, pre_hook_name: Optional[str] = None, run_input: Optional[RunInput] = None
217
+ ) -> PreHookCompletedEvent:
218
+ from copy import deepcopy
219
+
220
+ return PreHookCompletedEvent(
221
+ session_id=from_run_response.session_id,
222
+ agent_id=from_run_response.agent_id, # type: ignore
223
+ agent_name=from_run_response.agent_name, # type: ignore
224
+ run_id=from_run_response.run_id,
225
+ pre_hook_name=pre_hook_name,
226
+ run_input=deepcopy(run_input),
227
+ )
228
+
229
+
230
+ def create_team_pre_hook_completed_event(
231
+ from_run_response: TeamRunOutput, pre_hook_name: Optional[str] = None, run_input: Optional[TeamRunInput] = None
232
+ ) -> TeamPreHookCompletedEvent:
233
+ from copy import deepcopy
234
+
235
+ return TeamPreHookCompletedEvent(
236
+ session_id=from_run_response.session_id,
237
+ team_id=from_run_response.team_id, # type: ignore
238
+ team_name=from_run_response.team_name, # type: ignore
239
+ run_id=from_run_response.run_id,
240
+ pre_hook_name=pre_hook_name,
241
+ run_input=deepcopy(run_input),
242
+ )
243
+
244
+
180
245
  def create_memory_update_started_event(from_run_response: RunOutput) -> MemoryUpdateStartedEvent:
181
246
  return MemoryUpdateStartedEvent(
182
247
  session_id=from_run_response.session_id,
agno/utils/hooks.py ADDED
@@ -0,0 +1,57 @@
1
+ from typing import Any, Callable, Dict, List, Optional, Union
2
+
3
+ from agno.guardrails.base import BaseGuardrail
4
+ from agno.utils.log import log_warning
5
+
6
+
7
+ def normalize_hooks(
8
+ hooks: Optional[Union[List[Callable[..., Any]], List[BaseGuardrail]]],
9
+ async_mode: bool = False,
10
+ ) -> Optional[List[Callable[..., Any]]]:
11
+ """Normalize hooks to a list format"""
12
+ result_hooks: List[Callable[..., Any]] = []
13
+
14
+ if hooks is not None:
15
+ for hook in hooks:
16
+ if isinstance(hook, BaseGuardrail):
17
+ if async_mode:
18
+ result_hooks.append(hook.async_check)
19
+ else:
20
+ result_hooks.append(hook.check)
21
+ else:
22
+ # Check if the hook is async and used within sync methods
23
+ if not async_mode:
24
+ import asyncio
25
+
26
+ if asyncio.iscoroutinefunction(hook):
27
+ raise ValueError(
28
+ f"Cannot use {hook.__name__} (an async hook) with `run()`. Use `arun()` instead."
29
+ )
30
+
31
+ result_hooks.append(hook)
32
+ return result_hooks if result_hooks else None
33
+
34
+
35
+ def filter_hook_args(hook: Callable[..., Any], all_args: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Filter arguments to only include those that the hook function accepts."""
37
+ import inspect
38
+
39
+ try:
40
+ sig = inspect.signature(hook)
41
+ accepted_params = set(sig.parameters.keys())
42
+
43
+ has_var_keyword = any(param.kind == inspect.Parameter.VAR_KEYWORD for param in sig.parameters.values())
44
+
45
+ # If the function has **kwargs, pass all arguments
46
+ if has_var_keyword:
47
+ return all_args
48
+
49
+ # Otherwise, filter to only include accepted parameters
50
+ filtered_args = {key: value for key, value in all_args.items() if key in accepted_params}
51
+
52
+ return filtered_args
53
+
54
+ except Exception as e:
55
+ log_warning(f"Could not inspect hook signature, passing all arguments: {e}")
56
+ # If signature inspection fails, pass all arguments as fallback
57
+ return all_args
agno/utils/media.py CHANGED
@@ -6,6 +6,8 @@ from typing import List
6
6
 
7
7
  import httpx
8
8
 
9
+ from agno.utils.log import log_info, log_warning
10
+
9
11
 
10
12
  class SampleDataFileExtension(str, Enum):
11
13
  DOCX = "docx"
@@ -30,7 +32,7 @@ def download_image(url: str, output_path: str) -> bool:
30
32
  # Check if the response contains image content
31
33
  content_type = response.headers.get("Content-Type")
32
34
  if not content_type or not content_type.startswith("image"):
33
- print(f"URL does not point to an image. Content-Type: {content_type}")
35
+ log_warning(f"URL does not point to an image. Content-Type: {content_type}")
34
36
  return False
35
37
 
36
38
  path = Path(output_path)
@@ -42,14 +44,14 @@ def download_image(url: str, output_path: str) -> bool:
42
44
  if chunk:
43
45
  file.write(chunk)
44
46
 
45
- print(f"Image successfully downloaded and saved to '{output_path}'.")
47
+ log_info(f"Image successfully downloaded and saved to '{output_path}'.")
46
48
  return True
47
49
 
48
50
  except httpx.HTTPError as e:
49
- print(f"Error downloading the image: {e}")
51
+ log_warning(f"Error downloading the image: {e}")
50
52
  return False
51
53
  except IOError as e:
52
- print(f"Error saving the image to '{output_path}': {e}")
54
+ log_warning(f"Error saving the image to '{output_path}': {e}")
53
55
  return False
54
56
 
55
57
 
@@ -109,7 +111,7 @@ def save_base64_data(base64_data: str, output_path: str) -> bool:
109
111
  with open(path, "wb") as file:
110
112
  file.write(decoded_data)
111
113
 
112
- print(f"Data successfully saved to '{path}'.")
114
+ log_info(f"Data successfully saved to '{path}'.")
113
115
  return True
114
116
  except Exception as e:
115
117
  raise Exception(f"An unexpected error occurred while saving data to '{output_path}': {e}")
@@ -131,25 +133,25 @@ def wait_for_media_ready(url: str, timeout: int = 120, interval: int = 5, verbos
131
133
  max_attempts = timeout // interval
132
134
 
133
135
  if verbose:
134
- print("Media generated! Waiting for upload to complete...")
136
+ log_info("Media generated! Waiting for upload to complete...")
135
137
 
136
138
  for attempt in range(max_attempts):
137
139
  try:
138
140
  response = httpx.head(url, timeout=10)
139
141
  response.raise_for_status()
140
142
  if verbose:
141
- print(f"Media ready: {url}")
143
+ log_info(f"Media ready: {url}")
142
144
  return True
143
145
  except httpx.HTTPError:
144
146
  pass
145
147
 
146
148
  if verbose and (attempt + 1) % 3 == 0:
147
- print(f"Still processing... ({(attempt + 1) * interval}s elapsed)")
149
+ log_info(f"Still processing... ({(attempt + 1) * interval}s elapsed)")
148
150
 
149
151
  time.sleep(interval)
150
152
 
151
153
  if verbose:
152
- print(f"Timeout waiting for media. Try this URL later: {url}")
154
+ log_warning(f"Timeout waiting for media. Try this URL later: {url}")
153
155
  return False
154
156
 
155
157
 
@@ -78,6 +78,8 @@ def print_response_stream(
78
78
  if render:
79
79
  live_log.update(Group(*panels))
80
80
 
81
+ input_content = get_text_from_message(input)
82
+
81
83
  for response_event in agent.run(
82
84
  input=input,
83
85
  session_id=session_id,
@@ -106,6 +108,10 @@ def print_response_stream(
106
108
  live_log.update(Group(*panels))
107
109
  return
108
110
 
111
+ if response_event.event == RunEvent.pre_hook_completed: # type: ignore
112
+ if response_event.run_input is not None: # type: ignore
113
+ input_content = get_text_from_message(response_event.run_input.input_content) # type: ignore
114
+
109
115
  if (
110
116
  response_event.event == RunEvent.tool_call_started # type: ignore
111
117
  and hasattr(response_event, "tool")
@@ -157,9 +163,8 @@ def print_response_stream(
157
163
  panels = [status]
158
164
  if show_message:
159
165
  # Convert message to a panel
160
- message_content = get_text_from_message(input)
161
166
  message_panel = create_panel(
162
- content=Text(message_content, style="green"),
167
+ content=Text(input_content, style="green"),
163
168
  title="Message",
164
169
  border_style="cyan",
165
170
  )
@@ -282,6 +287,8 @@ async def aprint_response_stream(
282
287
  **kwargs,
283
288
  )
284
289
 
290
+ input_content = get_text_from_message(input)
291
+
285
292
  async for resp in result: # type: ignore
286
293
  if isinstance(resp, tuple(get_args(RunOutputEvent))):
287
294
  if resp.is_paused:
@@ -297,6 +304,10 @@ async def aprint_response_stream(
297
304
  ):
298
305
  accumulated_tool_calls.append(resp.tool)
299
306
 
307
+ if resp.event == RunEvent.pre_hook_completed: # type: ignore
308
+ if resp.run_input is not None: # type: ignore
309
+ input_content = get_text_from_message(resp.run_input.input_content) # type: ignore
310
+
300
311
  if resp.event == RunEvent.run_content: # type: ignore
301
312
  if isinstance(resp.content, str):
302
313
  # Don't accumulate text content, parser_model will replace it
@@ -338,12 +349,11 @@ async def aprint_response_stream(
338
349
 
339
350
  panels = [status]
340
351
 
341
- if input and show_message:
352
+ if input_content and show_message:
342
353
  render = True
343
354
  # Convert message to a panel
344
- message_content = get_text_from_message(input)
345
355
  message_panel = create_panel(
346
- content=Text(message_content, style="green"),
356
+ content=Text(input_content, style="green"),
347
357
  title="Message",
348
358
  border_style="cyan",
349
359
  )
@@ -545,6 +555,20 @@ def print_response(
545
555
  )
546
556
  response_timer.stop()
547
557
 
558
+ if run_response.input is not None and run_response.input.input_content != input:
559
+ # Input was modified during the run
560
+ panels = [status]
561
+ if show_message:
562
+ # Convert message to a panel
563
+ message_content = get_text_from_message(run_response.input.input_content)
564
+ message_panel = create_panel(
565
+ content=Text(message_content, style="green"),
566
+ title="Message",
567
+ border_style="cyan",
568
+ )
569
+ panels.append(message_panel) # type: ignore
570
+ live_log.update(Group(*panels))
571
+
548
572
  additional_panels = build_panels(
549
573
  run_response=run_response,
550
574
  output_schema=agent.output_schema, # type: ignore
@@ -649,6 +673,20 @@ async def aprint_response(
649
673
  )
650
674
  response_timer.stop()
651
675
 
676
+ if run_response.input is not None and run_response.input.input_content != input:
677
+ # Input was modified during the run
678
+ panels = [status]
679
+ if show_message:
680
+ # Convert message to a panel
681
+ message_content = get_text_from_message(run_response.input.input_content)
682
+ message_panel = create_panel(
683
+ content=Text(message_content, style="green"),
684
+ title="Message",
685
+ border_style="cyan",
686
+ )
687
+ panels.append(message_panel) # type: ignore
688
+ live_log.update(Group(*panels))
689
+
652
690
  additional_panels = build_panels(
653
691
  run_response=run_response,
654
692
  output_schema=agent.output_schema, # type: ignore