agno 2.0.0a1__py3-none-any.whl → 2.0.0rc2__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 (79) hide show
  1. agno/agent/agent.py +416 -41
  2. agno/api/agent.py +2 -2
  3. agno/api/evals.py +2 -2
  4. agno/api/os.py +1 -1
  5. agno/api/settings.py +2 -2
  6. agno/api/team.py +2 -2
  7. agno/db/dynamo/dynamo.py +0 -6
  8. agno/db/firestore/firestore.py +0 -6
  9. agno/db/in_memory/in_memory_db.py +0 -6
  10. agno/db/json/json_db.py +0 -6
  11. agno/db/mongo/mongo.py +8 -9
  12. agno/db/mysql/utils.py +0 -1
  13. agno/db/postgres/postgres.py +0 -10
  14. agno/db/postgres/utils.py +0 -1
  15. agno/db/redis/redis.py +0 -4
  16. agno/db/singlestore/singlestore.py +0 -10
  17. agno/db/singlestore/utils.py +0 -1
  18. agno/db/sqlite/sqlite.py +0 -4
  19. agno/db/sqlite/utils.py +0 -1
  20. agno/eval/accuracy.py +12 -5
  21. agno/integrations/discord/client.py +5 -1
  22. agno/knowledge/chunking/strategy.py +14 -14
  23. agno/knowledge/embedder/aws_bedrock.py +2 -2
  24. agno/knowledge/knowledge.py +156 -120
  25. agno/knowledge/reader/arxiv_reader.py +5 -5
  26. agno/knowledge/reader/csv_reader.py +6 -77
  27. agno/knowledge/reader/docx_reader.py +5 -5
  28. agno/knowledge/reader/firecrawl_reader.py +5 -5
  29. agno/knowledge/reader/json_reader.py +5 -5
  30. agno/knowledge/reader/markdown_reader.py +31 -9
  31. agno/knowledge/reader/pdf_reader.py +10 -123
  32. agno/knowledge/reader/reader_factory.py +65 -72
  33. agno/knowledge/reader/s3_reader.py +44 -114
  34. agno/knowledge/reader/text_reader.py +5 -5
  35. agno/knowledge/reader/url_reader.py +75 -31
  36. agno/knowledge/reader/web_search_reader.py +6 -29
  37. agno/knowledge/reader/website_reader.py +5 -5
  38. agno/knowledge/reader/wikipedia_reader.py +5 -5
  39. agno/knowledge/reader/youtube_reader.py +6 -6
  40. agno/knowledge/utils.py +10 -10
  41. agno/models/anthropic/claude.py +2 -49
  42. agno/models/aws/bedrock.py +3 -7
  43. agno/models/base.py +37 -6
  44. agno/models/message.py +7 -6
  45. agno/os/app.py +168 -64
  46. agno/os/interfaces/agui/agui.py +1 -1
  47. agno/os/interfaces/agui/utils.py +16 -9
  48. agno/os/interfaces/slack/slack.py +2 -3
  49. agno/os/interfaces/whatsapp/whatsapp.py +2 -3
  50. agno/os/mcp.py +235 -0
  51. agno/os/router.py +576 -19
  52. agno/os/routers/evals/evals.py +201 -12
  53. agno/os/routers/knowledge/knowledge.py +455 -18
  54. agno/os/routers/memory/memory.py +260 -29
  55. agno/os/routers/metrics/metrics.py +127 -7
  56. agno/os/routers/session/session.py +398 -25
  57. agno/os/schema.py +55 -2
  58. agno/os/settings.py +0 -1
  59. agno/run/agent.py +96 -2
  60. agno/run/cancel.py +0 -2
  61. agno/run/team.py +93 -2
  62. agno/run/workflow.py +25 -12
  63. agno/team/team.py +863 -1053
  64. agno/tools/function.py +65 -7
  65. agno/tools/linear.py +1 -1
  66. agno/tools/mcp.py +1 -2
  67. agno/utils/gemini.py +31 -1
  68. agno/utils/log.py +52 -2
  69. agno/utils/mcp.py +55 -3
  70. agno/utils/models/claude.py +41 -0
  71. agno/utils/print_response/team.py +177 -73
  72. agno/utils/streamlit.py +481 -0
  73. agno/workflow/workflow.py +17 -1
  74. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/METADATA +1 -1
  75. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/RECORD +78 -77
  76. agno/knowledge/reader/gcs_reader.py +0 -67
  77. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/WHEEL +0 -0
  78. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  79. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/top_level.txt +0 -0
agno/tools/function.py CHANGED
@@ -1,14 +1,14 @@
1
1
  from dataclasses import dataclass
2
2
  from functools import partial
3
3
  from importlib.metadata import version
4
- from typing import Any, Callable, Dict, List, Literal, Optional, Type, TypeVar, get_type_hints
4
+ from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Type, TypeVar, get_type_hints
5
5
 
6
6
  from docstring_parser import parse
7
7
  from packaging.version import Version
8
8
  from pydantic import BaseModel, Field, validate_call
9
9
 
10
10
  from agno.exceptions import AgentRunException
11
- from agno.media import AudioArtifact, ImageArtifact, VideoArtifact
11
+ from agno.media import Audio, AudioArtifact, File, Image, ImageArtifact, Video, VideoArtifact
12
12
  from agno.utils.log import log_debug, log_error, log_exception, log_warning
13
13
 
14
14
  T = TypeVar("T")
@@ -125,6 +125,12 @@ class Function(BaseModel):
125
125
  # The session state that the function is associated with
126
126
  _session_state: Optional[Dict[str, Any]] = None
127
127
 
128
+ # Media context that the function is associated with
129
+ _images: Optional[Sequence[Image]] = None
130
+ _videos: Optional[Sequence[Video]] = None
131
+ _audios: Optional[Sequence[Audio]] = None
132
+ _files: Optional[Sequence[File]] = None
133
+
128
134
  def to_dict(self) -> Dict[str, Any]:
129
135
  return self.model_dump(
130
136
  exclude_none=True,
@@ -150,13 +156,23 @@ class Function(BaseModel):
150
156
  del type_hints["team"]
151
157
  if "session_state" in sig.parameters and "session_state" in type_hints:
152
158
  del type_hints["session_state"]
159
+ # Remove media parameters from type hints as they are injected automatically
160
+ if "images" in sig.parameters and "images" in type_hints:
161
+ del type_hints["images"]
162
+ if "videos" in sig.parameters and "videos" in type_hints:
163
+ del type_hints["videos"]
164
+ if "audios" in sig.parameters and "audios" in type_hints:
165
+ del type_hints["audios"]
166
+ if "files" in sig.parameters and "files" in type_hints:
167
+ del type_hints["files"]
153
168
  # log_info(f"Type hints for {function_name}: {type_hints}")
154
169
 
155
170
  # Filter out return type and only process parameters
156
171
  param_type_hints = {
157
172
  name: type_hints.get(name)
158
173
  for name in sig.parameters
159
- if name != "return" and name not in ["agent", "team", "session_state", "self"]
174
+ if name != "return"
175
+ and name not in ["agent", "team", "session_state", "self", "images", "videos", "audios", "files"]
160
176
  }
161
177
 
162
178
  # Parse docstring for parameters
@@ -183,14 +199,17 @@ class Function(BaseModel):
183
199
  # See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas#all-fields-must-be-required
184
200
  if strict:
185
201
  parameters["required"] = [
186
- name for name in parameters["properties"] if name not in ["agent", "team", "session_state", "self"]
202
+ name
203
+ for name in parameters["properties"]
204
+ if name not in ["agent", "team", "session_state", "self", "images", "videos", "audios", "files"]
187
205
  ]
188
206
  else:
189
207
  # Mark a field as required if it has no default value (this would include optional fields)
190
208
  parameters["required"] = [
191
209
  name
192
210
  for name, param in sig.parameters.items()
193
- if param.default == param.empty and name not in ["agent", "team", "session_state", "self"]
211
+ if param.default == param.empty
212
+ and name not in ["agent", "team", "session_state", "self", "images", "videos", "audios", "files"]
194
213
  ]
195
214
 
196
215
  # log_debug(f"JSON schema for {function_name}: {parameters}")
@@ -241,10 +260,28 @@ class Function(BaseModel):
241
260
  del type_hints["team"]
242
261
  if "session_state" in sig.parameters and "session_state" in type_hints:
243
262
  del type_hints["session_state"]
263
+ if "images" in sig.parameters and "images" in type_hints:
264
+ del type_hints["images"]
265
+ if "videos" in sig.parameters and "videos" in type_hints:
266
+ del type_hints["videos"]
267
+ if "audios" in sig.parameters and "audios" in type_hints:
268
+ del type_hints["audios"]
269
+ if "files" in sig.parameters and "files" in type_hints:
270
+ del type_hints["files"]
244
271
  # log_info(f"Type hints for {self.name}: {type_hints}")
245
272
 
246
273
  # Filter out return type and only process parameters
247
- excluded_params = ["return", "agent", "team", "session_state", "self"]
274
+ excluded_params = [
275
+ "return",
276
+ "agent",
277
+ "team",
278
+ "session_state",
279
+ "self",
280
+ "images",
281
+ "videos",
282
+ "audios",
283
+ "files",
284
+ ]
248
285
  if self.requires_user_input and self.user_input_fields:
249
286
  if len(self.user_input_fields) == 0:
250
287
  excluded_params.extend(list(type_hints.keys()))
@@ -357,7 +394,9 @@ class Function(BaseModel):
357
394
  def process_schema_for_strict(self):
358
395
  self.parameters["additionalProperties"] = False
359
396
  self.parameters["required"] = [
360
- name for name in self.parameters["properties"] if name not in ["agent", "team", "session_state", "self"]
397
+ name
398
+ for name in self.parameters["properties"]
399
+ if name not in ["agent", "team", "session_state", "images", "videos", "audios", "files", "self"]
361
400
  ]
362
401
 
363
402
  def _get_cache_key(self, entrypoint_args: Dict[str, Any], call_args: Optional[Dict[str, Any]] = None) -> str:
@@ -372,6 +411,14 @@ class Function(BaseModel):
372
411
  del copy_entrypoint_args["team"]
373
412
  if "session_state" in copy_entrypoint_args:
374
413
  del copy_entrypoint_args["session_state"]
414
+ if "images" in copy_entrypoint_args:
415
+ del copy_entrypoint_args["images"]
416
+ if "videos" in copy_entrypoint_args:
417
+ del copy_entrypoint_args["videos"]
418
+ if "audios" in copy_entrypoint_args:
419
+ del copy_entrypoint_args["audios"]
420
+ if "files" in copy_entrypoint_args:
421
+ del copy_entrypoint_args["files"]
375
422
  args_str = str(copy_entrypoint_args)
376
423
 
377
424
  kwargs_str = str(sorted((call_args or {}).items()))
@@ -554,6 +601,17 @@ class FunctionCall(BaseModel):
554
601
  # Check if the entrypoint has an fc argument
555
602
  if "fc" in signature(self.function.entrypoint).parameters: # type: ignore
556
603
  entrypoint_args["fc"] = self
604
+
605
+ # Check if the entrypoint has media arguments
606
+ if "images" in signature(self.function.entrypoint).parameters: # type: ignore
607
+ entrypoint_args["images"] = self.function._images
608
+ if "videos" in signature(self.function.entrypoint).parameters: # type: ignore
609
+ entrypoint_args["videos"] = self.function._videos
610
+ if "audios" in signature(self.function.entrypoint).parameters: # type: ignore
611
+ entrypoint_args["audios"] = self.function._audios
612
+ if "files" in signature(self.function.entrypoint).parameters: # type: ignore
613
+ entrypoint_args["files"] = self.function._files
614
+
557
615
  return entrypoint_args
558
616
 
559
617
  def _build_hook_args(self, hook: Callable, name: str, func: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
agno/tools/linear.py CHANGED
@@ -202,7 +202,7 @@ class LinearTools(Toolkit):
202
202
  """
203
203
 
204
204
  query = """
205
- mutation IssueCreate ($title: String!, $description: String!, $teamId: String!, $projectId: String!, $assigneeId: String!){
205
+ mutation IssueCreate ($title: String!, $description: String!, $teamId: String!, $projectId: String, $assigneeId: String){
206
206
  issueCreate(
207
207
  input: { title: $title, description: $description, teamId: $teamId, projectId: $projectId, assigneeId: $assigneeId}
208
208
  ) {
agno/tools/mcp.py CHANGED
@@ -365,7 +365,7 @@ class MultiMCPTools(Toolkit):
365
365
  *,
366
366
  env: Optional[dict[str, str]] = None,
367
367
  server_params_list: Optional[
368
- List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
368
+ list[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
369
369
  ] = None,
370
370
  timeout_seconds: int = 5,
371
371
  client=None,
@@ -531,7 +531,6 @@ class MultiMCPTools(Toolkit):
531
531
  session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
532
532
  self._active_contexts.append(session)
533
533
  await self.initialize(session)
534
-
535
534
  # Handle Streamable HTTP connections
536
535
  elif isinstance(server_params, StreamableHTTPClientParams):
537
536
  client_connection = await self._async_exit_stack.enter_async_context(
agno/utils/gemini.py CHANGED
@@ -175,7 +175,37 @@ def convert_schema(schema_dict: Dict[str, Any], root_schema: Optional[Dict[str,
175
175
 
176
176
  elif schema_type == "array" and "items" in schema_dict:
177
177
  items = convert_schema(schema_dict["items"], root_schema)
178
- return Schema(type=Type.ARRAY, description=description, items=items)
178
+ min_items = schema_dict.get("minItems")
179
+ max_items = schema_dict.get("maxItems")
180
+ return Schema(
181
+ type=Type.ARRAY,
182
+ description=description,
183
+ items=items,
184
+ min_items=min_items,
185
+ max_items=max_items,
186
+ )
187
+
188
+ elif schema_type == "string":
189
+ schema_kwargs = {
190
+ "type": Type.STRING,
191
+ "description": description,
192
+ "default": default,
193
+ }
194
+ if "format" in schema_dict:
195
+ schema_kwargs["format"] = schema_dict["format"]
196
+ return Schema(**schema_kwargs)
197
+
198
+ elif schema_type in ("integer", "number"):
199
+ schema_kwargs = {
200
+ "type": schema_type.upper(),
201
+ "description": description,
202
+ "default": default,
203
+ }
204
+ if "maximum" in schema_dict:
205
+ schema_kwargs["maximum"] = schema_dict["maximum"]
206
+ if "minimum" in schema_dict:
207
+ schema_kwargs["minimum"] = schema_dict["minimum"]
208
+ return Schema(**schema_kwargs)
179
209
 
180
210
  elif schema_type == "" and "anyOf" in schema_dict:
181
211
  any_of = []
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
 
@@ -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 = ImageArtifact(
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
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
+
49
101
  img_artifact = ImageArtifact(
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)
@@ -286,3 +286,44 @@ def format_messages(messages: List[Message]) -> Tuple[List[Dict[str, str]], str]
286
286
 
287
287
  chat_messages.append({"role": ROLE_MAP[message.role], "content": content}) # type: ignore
288
288
  return chat_messages, " ".join(system_messages)
289
+
290
+
291
+ def format_tools_for_model(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]:
292
+ """
293
+ Transforms function definitions into a format accepted by the Anthropic API.
294
+ """
295
+ if not tools:
296
+ return None
297
+
298
+ parsed_tools: List[Dict[str, Any]] = []
299
+ for tool_def in tools:
300
+ if tool_def.get("type", "") != "function":
301
+ parsed_tools.append(tool_def)
302
+ continue
303
+
304
+ func_def = tool_def.get("function", {})
305
+ parameters: Dict[str, Any] = func_def.get("parameters", {})
306
+ properties: Dict[str, Any] = parameters.get("properties", {})
307
+ required: List[str] = parameters.get("required", [])
308
+ required_params: List[str] = required
309
+
310
+ input_properties: Dict[str, Any] = {}
311
+ for param_name, param_info in properties.items():
312
+ # Preserve the complete schema structure for complex types
313
+ input_properties[param_name] = param_info.copy()
314
+
315
+ # Ensure description is present (default to empty if missing)
316
+ if "description" not in input_properties[param_name]:
317
+ input_properties[param_name]["description"] = ""
318
+
319
+ tool = {
320
+ "name": func_def.get("name") or "",
321
+ "description": func_def.get("description") or "",
322
+ "input_schema": {
323
+ "type": parameters.get("type", "object"),
324
+ "properties": input_properties,
325
+ "required": required_params,
326
+ },
327
+ }
328
+ parsed_tools.append(tool)
329
+ return parsed_tools