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.
- agno/agent/agent.py +607 -176
- agno/db/in_memory/in_memory_db.py +42 -29
- agno/db/mongo/mongo.py +65 -66
- agno/db/postgres/postgres.py +6 -4
- agno/db/utils.py +50 -22
- agno/exceptions.py +62 -1
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +51 -0
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/ollama.py +5 -0
- agno/knowledge/embedder/openai.py +18 -54
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +11 -4
- agno/knowledge/reader/pdf_reader.py +4 -3
- agno/knowledge/reader/website_reader.py +3 -2
- agno/models/base.py +125 -32
- agno/models/cerebras/cerebras.py +1 -0
- agno/models/cerebras/cerebras_openai.py +1 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/google/gemini.py +27 -5
- agno/models/openai/chat.py +13 -4
- agno/models/openai/responses.py +1 -1
- agno/models/perplexity/perplexity.py +2 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +49 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +1 -0
- agno/os/app.py +98 -126
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/agui/agui.py +21 -5
- agno/os/interfaces/base.py +4 -2
- agno/os/interfaces/slack/slack.py +13 -8
- agno/os/interfaces/whatsapp/router.py +2 -0
- agno/os/interfaces/whatsapp/whatsapp.py +12 -5
- agno/os/mcp.py +2 -2
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +182 -46
- agno/os/routers/home.py +2 -2
- agno/os/routers/memory/memory.py +23 -1
- agno/os/routers/memory/schemas.py +1 -1
- agno/os/routers/session/session.py +20 -3
- agno/os/utils.py +74 -8
- agno/run/agent.py +120 -77
- agno/run/base.py +2 -13
- agno/run/team.py +115 -72
- agno/run/workflow.py +5 -15
- agno/session/summary.py +9 -10
- agno/session/team.py +2 -1
- agno/team/team.py +721 -169
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +42 -2
- agno/tools/knowledge.py +3 -3
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/spider.py +2 -2
- agno/tools/workflow.py +4 -5
- agno/utils/events.py +66 -1
- agno/utils/hooks.py +57 -0
- agno/utils/media.py +11 -9
- agno/utils/print_response/agent.py +43 -5
- agno/utils/print_response/team.py +48 -12
- agno/utils/serialize.py +32 -0
- agno/vectordb/cassandra/cassandra.py +44 -4
- agno/vectordb/chroma/chromadb.py +79 -8
- agno/vectordb/clickhouse/clickhousedb.py +43 -6
- agno/vectordb/couchbase/couchbase.py +76 -5
- agno/vectordb/lancedb/lance_db.py +38 -3
- agno/vectordb/milvus/milvus.py +76 -4
- agno/vectordb/mongodb/mongodb.py +76 -4
- agno/vectordb/pgvector/pgvector.py +50 -6
- agno/vectordb/pineconedb/pineconedb.py +39 -2
- agno/vectordb/qdrant/qdrant.py +76 -26
- agno/vectordb/singlestore/singlestore.py +77 -4
- agno/vectordb/upstashdb/upstashdb.py +42 -2
- agno/vectordb/weaviate/weaviate.py +39 -3
- agno/workflow/types.py +5 -6
- agno/workflow/workflow.py +58 -2
- {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/METADATA +4 -3
- {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/RECORD +93 -82
- {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/WHEEL +0 -0
- {agno-2.0.11.dist-info → agno-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
log_warning(f"Error downloading the image: {e}")
|
|
50
52
|
return False
|
|
51
53
|
except IOError as e:
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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
|