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.
Files changed (57) hide show
  1. agno/agent/agent.py +500 -423
  2. agno/api/os.py +1 -1
  3. agno/culture/manager.py +12 -8
  4. agno/guardrails/prompt_injection.py +1 -0
  5. agno/knowledge/chunking/agentic.py +6 -2
  6. agno/knowledge/embedder/vllm.py +262 -0
  7. agno/knowledge/knowledge.py +37 -5
  8. agno/memory/manager.py +9 -4
  9. agno/models/anthropic/claude.py +1 -2
  10. agno/models/azure/ai_foundry.py +31 -14
  11. agno/models/azure/openai_chat.py +12 -4
  12. agno/models/base.py +106 -65
  13. agno/models/cerebras/cerebras.py +11 -6
  14. agno/models/groq/groq.py +7 -4
  15. agno/models/meta/llama.py +12 -6
  16. agno/models/meta/llama_openai.py +5 -1
  17. agno/models/openai/chat.py +26 -17
  18. agno/models/openai/responses.py +11 -63
  19. agno/models/requesty/requesty.py +5 -2
  20. agno/models/utils.py +254 -8
  21. agno/models/vertexai/claude.py +9 -13
  22. agno/os/app.py +13 -12
  23. agno/os/routers/evals/evals.py +8 -8
  24. agno/os/routers/evals/utils.py +1 -0
  25. agno/os/schema.py +56 -38
  26. agno/os/utils.py +27 -0
  27. agno/run/__init__.py +6 -0
  28. agno/run/agent.py +5 -0
  29. agno/run/base.py +18 -1
  30. agno/run/team.py +13 -9
  31. agno/run/workflow.py +39 -0
  32. agno/session/summary.py +8 -2
  33. agno/session/workflow.py +4 -3
  34. agno/team/team.py +302 -369
  35. agno/tools/exa.py +21 -16
  36. agno/tools/file.py +153 -25
  37. agno/tools/function.py +98 -17
  38. agno/tools/mcp/mcp.py +8 -1
  39. agno/tools/notion.py +204 -0
  40. agno/utils/agent.py +78 -0
  41. agno/utils/events.py +2 -0
  42. agno/utils/hooks.py +1 -1
  43. agno/utils/models/claude.py +25 -8
  44. agno/utils/print_response/workflow.py +115 -16
  45. agno/vectordb/__init__.py +2 -1
  46. agno/vectordb/milvus/milvus.py +5 -0
  47. agno/vectordb/redis/__init__.py +5 -0
  48. agno/vectordb/redis/redisdb.py +687 -0
  49. agno/workflow/__init__.py +2 -0
  50. agno/workflow/agent.py +299 -0
  51. agno/workflow/step.py +13 -2
  52. agno/workflow/workflow.py +969 -72
  53. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
  54. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
  55. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
  56. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
  57. {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[List[Callable[..., Any]], List[BaseGuardrail]]],
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"""
@@ -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 using_filetype:
100
- kind = filetype.guess(content_bytes)
101
- if not kind:
102
- log_error("Unable to determine image type")
103
- return None
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
- img_type = kind.extension
106
- else:
107
- img_type = imghdr.what(None, h=content_bytes) # type: ignore
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")