agno 2.2.5__py3-none-any.whl → 2.2.7__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 +500 -423
- agno/api/os.py +1 -1
- agno/culture/manager.py +12 -8
- agno/guardrails/prompt_injection.py +1 -0
- agno/knowledge/chunking/agentic.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +37 -5
- agno/memory/manager.py +9 -4
- agno/models/anthropic/claude.py +1 -2
- agno/models/azure/ai_foundry.py +31 -14
- agno/models/azure/openai_chat.py +12 -4
- agno/models/base.py +106 -65
- agno/models/cerebras/cerebras.py +11 -6
- agno/models/groq/groq.py +7 -4
- agno/models/meta/llama.py +12 -6
- agno/models/meta/llama_openai.py +5 -1
- agno/models/openai/chat.py +26 -17
- agno/models/openai/responses.py +11 -63
- agno/models/requesty/requesty.py +5 -2
- agno/models/utils.py +254 -8
- agno/models/vertexai/claude.py +9 -13
- agno/os/app.py +13 -12
- agno/os/routers/evals/evals.py +8 -8
- agno/os/routers/evals/utils.py +1 -0
- agno/os/schema.py +56 -38
- agno/os/utils.py +27 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +5 -0
- agno/run/base.py +18 -1
- agno/run/team.py +13 -9
- agno/run/workflow.py +39 -0
- agno/session/summary.py +8 -2
- agno/session/workflow.py +4 -3
- agno/team/team.py +302 -369
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -25
- agno/tools/function.py +98 -17
- agno/tools/mcp/mcp.py +8 -1
- agno/tools/notion.py +204 -0
- agno/utils/agent.py +78 -0
- agno/utils/events.py +2 -0
- agno/utils/hooks.py +1 -1
- agno/utils/models/claude.py +25 -8
- agno/utils/print_response/workflow.py +115 -16
- agno/vectordb/__init__.py +2 -1
- agno/vectordb/milvus/milvus.py +5 -0
- agno/vectordb/redis/__init__.py +5 -0
- agno/vectordb/redis/redisdb.py +687 -0
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/step.py +13 -2
- agno/workflow/workflow.py +969 -72
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/top_level.txt +0 -0
agno/tools/mcp/mcp.py
CHANGED
|
@@ -43,6 +43,7 @@ class MCPTools(Toolkit):
|
|
|
43
43
|
include_tools: Optional[list[str]] = None,
|
|
44
44
|
exclude_tools: Optional[list[str]] = None,
|
|
45
45
|
refresh_connection: bool = False,
|
|
46
|
+
tool_name_prefix: Optional[str] = "",
|
|
46
47
|
**kwargs,
|
|
47
48
|
):
|
|
48
49
|
"""
|
|
@@ -71,6 +72,7 @@ class MCPTools(Toolkit):
|
|
|
71
72
|
self.include_tools = include_tools
|
|
72
73
|
self.exclude_tools = exclude_tools
|
|
73
74
|
self.refresh_connection = refresh_connection
|
|
75
|
+
self.tool_name_prefix = tool_name_prefix
|
|
74
76
|
|
|
75
77
|
if session is None and server_params is None:
|
|
76
78
|
if transport == "sse" and url is None:
|
|
@@ -279,6 +281,11 @@ class MCPTools(Toolkit):
|
|
|
279
281
|
if self.include_tools is None or tool.name in self.include_tools:
|
|
280
282
|
filtered_tools.append(tool)
|
|
281
283
|
|
|
284
|
+
# Get tool name prefix if available
|
|
285
|
+
tool_name_prefix = ""
|
|
286
|
+
if self.tool_name_prefix is not None:
|
|
287
|
+
tool_name_prefix = self.tool_name_prefix + "_"
|
|
288
|
+
|
|
282
289
|
# Register the tools with the toolkit
|
|
283
290
|
for tool in filtered_tools:
|
|
284
291
|
try:
|
|
@@ -286,7 +293,7 @@ class MCPTools(Toolkit):
|
|
|
286
293
|
entrypoint = get_entrypoint_for_tool(tool, self.session) # type: ignore
|
|
287
294
|
# Create a Function for the tool
|
|
288
295
|
f = Function(
|
|
289
|
-
name=tool.name,
|
|
296
|
+
name=tool_name_prefix + tool.name,
|
|
290
297
|
description=tool.description,
|
|
291
298
|
parameters=tool.inputSchema,
|
|
292
299
|
entrypoint=entrypoint,
|
agno/tools/notion.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List, Optional, cast
|
|
4
|
+
|
|
5
|
+
from agno.tools import Toolkit
|
|
6
|
+
from agno.utils.log import log_debug, logger
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from notion_client import Client
|
|
10
|
+
except ImportError:
|
|
11
|
+
raise ImportError("`notion-client` not installed. Please install using `pip install notion-client`")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NotionTools(Toolkit):
|
|
15
|
+
"""
|
|
16
|
+
Notion toolkit for creating and managing Notion pages.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
api_key (Optional[str]): Notion API key (integration token). If not provided, uses NOTION_API_KEY env var.
|
|
20
|
+
database_id (Optional[str]): The ID of the database to work with. If not provided, uses NOTION_DATABASE_ID env var.
|
|
21
|
+
enable_create_page (bool): Enable creating pages. Default is True.
|
|
22
|
+
enable_update_page (bool): Enable updating pages. Default is True.
|
|
23
|
+
enable_search_pages (bool): Enable searching pages. Default is True.
|
|
24
|
+
all (bool): Enable all tools. Overrides individual flags when True. Default is False.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_key: Optional[str] = None,
|
|
30
|
+
database_id: Optional[str] = None,
|
|
31
|
+
enable_create_page: bool = True,
|
|
32
|
+
enable_update_page: bool = True,
|
|
33
|
+
enable_search_pages: bool = True,
|
|
34
|
+
all: bool = False,
|
|
35
|
+
**kwargs,
|
|
36
|
+
):
|
|
37
|
+
self.api_key = api_key or os.getenv("NOTION_API_KEY")
|
|
38
|
+
self.database_id = database_id or os.getenv("NOTION_DATABASE_ID")
|
|
39
|
+
|
|
40
|
+
if not self.api_key:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"Notion API key is required. Either pass api_key parameter or set NOTION_API_KEY environment variable."
|
|
43
|
+
)
|
|
44
|
+
if not self.database_id:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Notion database ID is required. Either pass database_id parameter or set NOTION_DATABASE_ID environment variable."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.client = Client(auth=self.api_key)
|
|
50
|
+
|
|
51
|
+
tools: List[Any] = []
|
|
52
|
+
if all or enable_create_page:
|
|
53
|
+
tools.append(self.create_page)
|
|
54
|
+
if all or enable_update_page:
|
|
55
|
+
tools.append(self.update_page)
|
|
56
|
+
if all or enable_search_pages:
|
|
57
|
+
tools.append(self.search_pages)
|
|
58
|
+
|
|
59
|
+
super().__init__(name="notion_tools", tools=tools, **kwargs)
|
|
60
|
+
|
|
61
|
+
def create_page(self, title: str, tag: str, content: str) -> str:
|
|
62
|
+
"""Create a new page in the Notion database with a title, tag, and content.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
title (str): The title of the page
|
|
66
|
+
tag (str): The tag/category for the page (e.g., travel, tech, general-blogs, fashion, documents)
|
|
67
|
+
content (str): The content to add to the page
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
str: JSON string with page creation details
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
log_debug(f"Creating Notion page with title: {title}, tag: {tag}")
|
|
74
|
+
|
|
75
|
+
# Create the page in the database
|
|
76
|
+
new_page = cast(
|
|
77
|
+
Dict[str, Any],
|
|
78
|
+
self.client.pages.create(
|
|
79
|
+
parent={"database_id": self.database_id},
|
|
80
|
+
properties={"Name": {"title": [{"text": {"content": title}}]}, "Tag": {"select": {"name": tag}}},
|
|
81
|
+
children=[
|
|
82
|
+
{
|
|
83
|
+
"object": "block",
|
|
84
|
+
"type": "paragraph",
|
|
85
|
+
"paragraph": {"rich_text": [{"type": "text", "text": {"content": content}}]},
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
result = {"success": True, "page_id": new_page["id"], "url": new_page["url"], "title": title, "tag": tag}
|
|
92
|
+
return json.dumps(result, indent=2)
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.exception(e)
|
|
96
|
+
return json.dumps({"success": False, "error": str(e)})
|
|
97
|
+
|
|
98
|
+
def update_page(self, page_id: str, content: str) -> str:
|
|
99
|
+
"""Add content to an existing Notion page.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
page_id (str): The ID of the page to update
|
|
103
|
+
content (str): The content to append to the page
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
str: JSON string with update status
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
log_debug(f"Updating Notion page: {page_id}")
|
|
110
|
+
|
|
111
|
+
# Append content to the page
|
|
112
|
+
self.client.blocks.children.append(
|
|
113
|
+
block_id=page_id,
|
|
114
|
+
children=[
|
|
115
|
+
{
|
|
116
|
+
"object": "block",
|
|
117
|
+
"type": "paragraph",
|
|
118
|
+
"paragraph": {"rich_text": [{"type": "text", "text": {"content": content}}]},
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
result = {"success": True, "page_id": page_id, "message": "Content added successfully"}
|
|
124
|
+
return json.dumps(result, indent=2)
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.exception(e)
|
|
128
|
+
return json.dumps({"success": False, "error": str(e)})
|
|
129
|
+
|
|
130
|
+
def search_pages(self, tag: str) -> str:
|
|
131
|
+
"""Search for pages in the database by tag.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
tag (str): The tag to search for
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
str: JSON string with list of matching pages
|
|
138
|
+
"""
|
|
139
|
+
try:
|
|
140
|
+
log_debug(f"Searching for pages with tag: {tag}")
|
|
141
|
+
|
|
142
|
+
import httpx
|
|
143
|
+
|
|
144
|
+
headers = {
|
|
145
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
146
|
+
"Notion-Version": "2022-06-28",
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
payload = {"filter": {"property": "Tag", "select": {"equals": tag}}}
|
|
151
|
+
|
|
152
|
+
# The SDK client does not support the query method
|
|
153
|
+
response = httpx.post(
|
|
154
|
+
f"https://api.notion.com/v1/databases/{self.database_id}/query",
|
|
155
|
+
headers=headers,
|
|
156
|
+
json=payload,
|
|
157
|
+
timeout=30.0,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if response.status_code != 200:
|
|
161
|
+
return json.dumps(
|
|
162
|
+
{
|
|
163
|
+
"success": False,
|
|
164
|
+
"error": f"API request failed with status {response.status_code}",
|
|
165
|
+
"message": response.text,
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
data = response.json()
|
|
170
|
+
pages = []
|
|
171
|
+
|
|
172
|
+
for page in data.get("results", []):
|
|
173
|
+
try:
|
|
174
|
+
page_title = "Untitled"
|
|
175
|
+
if page.get("properties", {}).get("Name", {}).get("title"):
|
|
176
|
+
page_title = page["properties"]["Name"]["title"][0]["text"]["content"]
|
|
177
|
+
|
|
178
|
+
page_tag = None
|
|
179
|
+
if page.get("properties", {}).get("Tag", {}).get("select"):
|
|
180
|
+
page_tag = page["properties"]["Tag"]["select"]["name"]
|
|
181
|
+
|
|
182
|
+
page_info = {
|
|
183
|
+
"page_id": page["id"],
|
|
184
|
+
"title": page_title,
|
|
185
|
+
"tag": page_tag,
|
|
186
|
+
"url": page.get("url", ""),
|
|
187
|
+
}
|
|
188
|
+
pages.append(page_info)
|
|
189
|
+
except Exception as page_error:
|
|
190
|
+
log_debug(f"Error parsing page: {page_error}")
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
result = {"success": True, "count": len(pages), "pages": pages}
|
|
194
|
+
return json.dumps(result, indent=2)
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.exception(e)
|
|
198
|
+
return json.dumps(
|
|
199
|
+
{
|
|
200
|
+
"success": False,
|
|
201
|
+
"error": str(e),
|
|
202
|
+
"message": "Failed to search pages. Make sure the database is shared with the integration and has a 'Tag' property.",
|
|
203
|
+
}
|
|
204
|
+
)
|
agno/utils/agent.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, List, Opti
|
|
|
4
4
|
from agno.media import Audio, File, Image, Video
|
|
5
5
|
from agno.models.message import Message
|
|
6
6
|
from agno.models.metrics import Metrics
|
|
7
|
+
from agno.models.response import ModelResponse
|
|
7
8
|
from agno.run.agent import RunEvent, RunInput, RunOutput, RunOutputEvent
|
|
8
9
|
from agno.run.team import RunOutputEvent as TeamRunOutputEvent
|
|
9
10
|
from agno.run.team import TeamRunOutput
|
|
@@ -291,6 +292,83 @@ def collect_joint_files(
|
|
|
291
292
|
return joint_files if joint_files else None
|
|
292
293
|
|
|
293
294
|
|
|
295
|
+
def store_media_util(run_response: Union[RunOutput, TeamRunOutput], model_response: ModelResponse):
|
|
296
|
+
"""Store media from model response in run_response for persistence"""
|
|
297
|
+
# Handle generated media fields from ModelResponse (generated media)
|
|
298
|
+
if model_response.images is not None:
|
|
299
|
+
for image in model_response.images:
|
|
300
|
+
if run_response.images is None:
|
|
301
|
+
run_response.images = []
|
|
302
|
+
run_response.images.append(image) # Generated images go to run_response.images
|
|
303
|
+
|
|
304
|
+
if model_response.videos is not None:
|
|
305
|
+
for video in model_response.videos:
|
|
306
|
+
if run_response.videos is None:
|
|
307
|
+
run_response.videos = []
|
|
308
|
+
run_response.videos.append(video) # Generated videos go to run_response.videos
|
|
309
|
+
|
|
310
|
+
if model_response.audios is not None:
|
|
311
|
+
for audio in model_response.audios:
|
|
312
|
+
if run_response.audio is None:
|
|
313
|
+
run_response.audio = []
|
|
314
|
+
run_response.audio.append(audio) # Generated audio go to run_response.audio
|
|
315
|
+
|
|
316
|
+
if model_response.files is not None:
|
|
317
|
+
for file in model_response.files:
|
|
318
|
+
if run_response.files is None:
|
|
319
|
+
run_response.files = []
|
|
320
|
+
run_response.files.append(file) # Generated files go to run_response.files
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def validate_media_object_id(
|
|
324
|
+
images: Optional[Sequence[Image]] = None,
|
|
325
|
+
videos: Optional[Sequence[Video]] = None,
|
|
326
|
+
audios: Optional[Sequence[Audio]] = None,
|
|
327
|
+
files: Optional[Sequence[File]] = None,
|
|
328
|
+
) -> tuple:
|
|
329
|
+
image_list = None
|
|
330
|
+
if images:
|
|
331
|
+
image_list = []
|
|
332
|
+
for img in images:
|
|
333
|
+
if not img.id:
|
|
334
|
+
from uuid import uuid4
|
|
335
|
+
|
|
336
|
+
img.id = str(uuid4())
|
|
337
|
+
image_list.append(img)
|
|
338
|
+
|
|
339
|
+
video_list = None
|
|
340
|
+
if videos:
|
|
341
|
+
video_list = []
|
|
342
|
+
for vid in videos:
|
|
343
|
+
if not vid.id:
|
|
344
|
+
from uuid import uuid4
|
|
345
|
+
|
|
346
|
+
vid.id = str(uuid4())
|
|
347
|
+
video_list.append(vid)
|
|
348
|
+
|
|
349
|
+
audio_list = None
|
|
350
|
+
if audios:
|
|
351
|
+
audio_list = []
|
|
352
|
+
for aud in audios:
|
|
353
|
+
if not aud.id:
|
|
354
|
+
from uuid import uuid4
|
|
355
|
+
|
|
356
|
+
aud.id = str(uuid4())
|
|
357
|
+
audio_list.append(aud)
|
|
358
|
+
|
|
359
|
+
file_list = None
|
|
360
|
+
if files:
|
|
361
|
+
file_list = []
|
|
362
|
+
for file in files:
|
|
363
|
+
if not file.id:
|
|
364
|
+
from uuid import uuid4
|
|
365
|
+
|
|
366
|
+
file.id = str(uuid4())
|
|
367
|
+
file_list.append(file)
|
|
368
|
+
|
|
369
|
+
return image_list, video_list, audio_list, file_list
|
|
370
|
+
|
|
371
|
+
|
|
294
372
|
def scrub_media_from_run_output(run_response: Union[RunOutput, TeamRunOutput]) -> None:
|
|
295
373
|
"""
|
|
296
374
|
Completely remove all media from RunOutput when store_media=False.
|
agno/utils/events.py
CHANGED
|
@@ -106,6 +106,7 @@ def create_team_run_completed_event(from_run_response: TeamRunOutput) -> TeamRun
|
|
|
106
106
|
member_responses=from_run_response.member_responses, # type: ignore
|
|
107
107
|
metadata=from_run_response.metadata, # type: ignore
|
|
108
108
|
metrics=from_run_response.metrics, # type: ignore
|
|
109
|
+
session_state=from_run_response.session_state, # type: ignore
|
|
109
110
|
)
|
|
110
111
|
|
|
111
112
|
|
|
@@ -130,6 +131,7 @@ def create_run_completed_event(from_run_response: RunOutput) -> RunCompletedEven
|
|
|
130
131
|
reasoning_messages=from_run_response.reasoning_messages, # type: ignore
|
|
131
132
|
metadata=from_run_response.metadata, # type: ignore
|
|
132
133
|
metrics=from_run_response.metrics, # type: ignore
|
|
134
|
+
session_state=from_run_response.session_state, # type: ignore
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
|
agno/utils/hooks.py
CHANGED
|
@@ -5,7 +5,7 @@ from agno.utils.log import log_warning
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def normalize_hooks(
|
|
8
|
-
hooks: Optional[Union[
|
|
8
|
+
hooks: Optional[List[Union[Callable[..., Any], BaseGuardrail]]],
|
|
9
9
|
async_mode: bool = False,
|
|
10
10
|
) -> Optional[List[Callable[..., Any]]]:
|
|
11
11
|
"""Normalize hooks to a list format"""
|
agno/utils/models/claude.py
CHANGED
|
@@ -76,6 +76,17 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
76
76
|
if image.url is not None:
|
|
77
77
|
content_bytes = image.get_content_bytes() # type: ignore
|
|
78
78
|
|
|
79
|
+
# If image URL has a suffix, use it as the type (without dot)
|
|
80
|
+
import os
|
|
81
|
+
from urllib.parse import urlparse
|
|
82
|
+
|
|
83
|
+
img_type = None
|
|
84
|
+
if image.url:
|
|
85
|
+
parsed_url = urlparse(image.url)
|
|
86
|
+
_, ext = os.path.splitext(parsed_url.path)
|
|
87
|
+
if ext:
|
|
88
|
+
img_type = ext.lstrip(".").lower()
|
|
89
|
+
|
|
79
90
|
# Case 2: Image is a local file path
|
|
80
91
|
elif image.filepath is not None:
|
|
81
92
|
from pathlib import Path
|
|
@@ -84,6 +95,11 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
84
95
|
if path.exists() and path.is_file():
|
|
85
96
|
with open(image.filepath, "rb") as f:
|
|
86
97
|
content_bytes = f.read()
|
|
98
|
+
|
|
99
|
+
# If image file path has a suffix, use it as the type (without dot)
|
|
100
|
+
path_ext = path.suffix.lstrip(".")
|
|
101
|
+
if path_ext:
|
|
102
|
+
img_type = path_ext.lower()
|
|
87
103
|
else:
|
|
88
104
|
log_error(f"Image file not found: {image}")
|
|
89
105
|
return None
|
|
@@ -96,15 +112,16 @@ def _format_image_for_message(image: Image) -> Optional[Dict[str, Any]]:
|
|
|
96
112
|
log_error(f"Unsupported image type: {type(image)}")
|
|
97
113
|
return None
|
|
98
114
|
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
if not img_type:
|
|
116
|
+
if using_filetype:
|
|
117
|
+
kind = filetype.guess(content_bytes)
|
|
118
|
+
if not kind:
|
|
119
|
+
log_error("Unable to determine image type")
|
|
120
|
+
return None
|
|
104
121
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
img_type = kind.extension
|
|
123
|
+
else:
|
|
124
|
+
img_type = imghdr.what(None, h=content_bytes) # type: ignore
|
|
108
125
|
|
|
109
126
|
if not img_type:
|
|
110
127
|
log_error("Unable to determine image type")
|