agno 2.3.22__py3-none-any.whl → 2.3.24__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 (62) hide show
  1. agno/agent/agent.py +28 -1
  2. agno/agent/remote.py +1 -1
  3. agno/db/mongo/mongo.py +9 -1
  4. agno/db/mysql/async_mysql.py +5 -7
  5. agno/db/mysql/mysql.py +5 -7
  6. agno/db/mysql/schemas.py +39 -21
  7. agno/db/postgres/async_postgres.py +10 -2
  8. agno/db/postgres/postgres.py +5 -7
  9. agno/db/postgres/schemas.py +39 -21
  10. agno/db/singlestore/schemas.py +41 -21
  11. agno/db/singlestore/singlestore.py +14 -3
  12. agno/db/sqlite/async_sqlite.py +7 -2
  13. agno/db/sqlite/schemas.py +36 -21
  14. agno/db/sqlite/sqlite.py +3 -7
  15. agno/knowledge/chunking/markdown.py +94 -8
  16. agno/knowledge/chunking/semantic.py +2 -2
  17. agno/knowledge/knowledge.py +215 -207
  18. agno/models/base.py +32 -8
  19. agno/models/google/gemini.py +27 -4
  20. agno/os/routers/agents/router.py +1 -1
  21. agno/os/routers/evals/evals.py +2 -2
  22. agno/os/routers/knowledge/knowledge.py +21 -5
  23. agno/os/routers/knowledge/schemas.py +1 -1
  24. agno/os/routers/memory/memory.py +4 -4
  25. agno/os/routers/session/session.py +2 -2
  26. agno/os/routers/teams/router.py +2 -2
  27. agno/os/routers/traces/traces.py +3 -3
  28. agno/os/routers/workflows/router.py +1 -1
  29. agno/os/schema.py +1 -1
  30. agno/os/utils.py +1 -1
  31. agno/remote/base.py +1 -1
  32. agno/team/remote.py +1 -1
  33. agno/team/team.py +24 -4
  34. agno/tools/brandfetch.py +27 -18
  35. agno/tools/browserbase.py +150 -13
  36. agno/tools/crawl4ai.py +3 -0
  37. agno/tools/file.py +14 -13
  38. agno/tools/function.py +15 -2
  39. agno/tools/mcp/mcp.py +1 -0
  40. agno/tools/mlx_transcribe.py +10 -7
  41. agno/tools/python.py +14 -6
  42. agno/tools/toolkit.py +122 -23
  43. agno/vectordb/cassandra/cassandra.py +1 -1
  44. agno/vectordb/chroma/chromadb.py +1 -1
  45. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  46. agno/vectordb/couchbase/couchbase.py +1 -1
  47. agno/vectordb/milvus/milvus.py +1 -1
  48. agno/vectordb/mongodb/mongodb.py +13 -3
  49. agno/vectordb/pgvector/pgvector.py +1 -1
  50. agno/vectordb/pineconedb/pineconedb.py +2 -2
  51. agno/vectordb/qdrant/qdrant.py +1 -1
  52. agno/vectordb/redis/redisdb.py +2 -2
  53. agno/vectordb/singlestore/singlestore.py +1 -1
  54. agno/vectordb/surrealdb/surrealdb.py +2 -2
  55. agno/vectordb/weaviate/weaviate.py +1 -1
  56. agno/workflow/remote.py +1 -1
  57. agno/workflow/workflow.py +14 -0
  58. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/METADATA +1 -1
  59. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/RECORD +62 -62
  60. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/WHEEL +0 -0
  61. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/licenses/LICENSE +0 -0
  62. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/top_level.txt +0 -0
agno/tools/browserbase.py CHANGED
@@ -10,13 +10,6 @@ try:
10
10
  except ImportError:
11
11
  raise ImportError("`browserbase` not installed. Please install using `pip install browserbase`")
12
12
 
13
- try:
14
- from playwright.sync_api import sync_playwright
15
- except ImportError:
16
- raise ImportError(
17
- "`playwright` not installed. Please install using `pip install playwright` and run `playwright install`"
18
- )
19
-
20
13
 
21
14
  class BrowserbaseTools(Toolkit):
22
15
  def __init__(
@@ -36,7 +29,13 @@ class BrowserbaseTools(Toolkit):
36
29
  Args:
37
30
  api_key (str, optional): Browserbase API key.
38
31
  project_id (str, optional): Browserbase project ID.
39
- base_url (str, optional): Custom Browserbase API endpoint URL (NOT the target website URL). Only use this if you're using a self-hosted Browserbase instance or need to connect to a different region.
32
+ base_url (str, optional): Custom Browserbase API endpoint URL (NOT the target website URL).
33
+ Only use this if you're using a self-hosted Browserbase instance or need to connect to a different region.
34
+ enable_navigate_to (bool): Enable the navigate_to tool. Defaults to True.
35
+ enable_screenshot (bool): Enable the screenshot tool. Defaults to True.
36
+ enable_get_page_content (bool): Enable the get_page_content tool. Defaults to True.
37
+ enable_close_session (bool): Enable the close_session tool. Defaults to True.
38
+ all (bool): Enable all tools. Defaults to False.
40
39
  """
41
40
  self.api_key = api_key or getenv("BROWSERBASE_API_KEY")
42
41
  if not self.api_key:
@@ -59,23 +58,40 @@ class BrowserbaseTools(Toolkit):
59
58
  else:
60
59
  self.app = Browserbase(api_key=self.api_key)
61
60
 
61
+ # Sync playwright state
62
62
  self._playwright = None
63
63
  self._browser = None
64
64
  self._page = None
65
+
66
+ # Async playwright state
67
+ self._async_playwright = None
68
+ self._async_browser = None
69
+ self._async_page = None
70
+
71
+ # Shared session state
65
72
  self._session = None
66
73
  self._connect_url = None
67
74
 
75
+ # Build tools lists
76
+ # sync tools: used by agent.run() and agent.print_response()
77
+ # async tools: used by agent.arun() and agent.aprint_response()
68
78
  tools: List[Any] = []
79
+ async_tools: List[tuple] = []
80
+
69
81
  if all or enable_navigate_to:
70
82
  tools.append(self.navigate_to)
83
+ async_tools.append((self.anavigate_to, "navigate_to"))
71
84
  if all or enable_screenshot:
72
85
  tools.append(self.screenshot)
86
+ async_tools.append((self.ascreenshot, "screenshot"))
73
87
  if all or enable_get_page_content:
74
88
  tools.append(self.get_page_content)
89
+ async_tools.append((self.aget_page_content, "get_page_content"))
75
90
  if all or enable_close_session:
76
91
  tools.append(self.close_session)
92
+ async_tools.append((self.aclose_session, "close_session"))
77
93
 
78
- super().__init__(name="browserbase_tools", tools=tools, **kwargs)
94
+ super().__init__(name="browserbase_tools", tools=tools, async_tools=async_tools, **kwargs)
79
95
 
80
96
  def _ensure_session(self):
81
97
  """Ensures a session exists, creating one if needed."""
@@ -91,9 +107,16 @@ class BrowserbaseTools(Toolkit):
91
107
 
92
108
  def _initialize_browser(self, connect_url: Optional[str] = None):
93
109
  """
94
- Initialize browser connection if not already initialized.
110
+ Initialize sync browser connection if not already initialized.
95
111
  Use provided connect_url or ensure we have a session with a connect_url
96
112
  """
113
+ try:
114
+ from playwright.sync_api import sync_playwright # type: ignore[import-not-found]
115
+ except ImportError:
116
+ raise ImportError(
117
+ "`playwright` not installed. Please install using `pip install playwright` and run `playwright install`"
118
+ )
119
+
97
120
  if connect_url:
98
121
  self._connect_url = connect_url if connect_url else "" # type: ignore
99
122
  elif not self._connect_url:
@@ -107,7 +130,7 @@ class BrowserbaseTools(Toolkit):
107
130
  self._page = context.pages[0] or context.new_page() # type: ignore
108
131
 
109
132
  def _cleanup(self):
110
- """Clean up browser resources."""
133
+ """Clean up sync browser resources."""
111
134
  if self._browser:
112
135
  self._browser.close()
113
136
  self._browser = None
@@ -186,8 +209,7 @@ class BrowserbaseTools(Toolkit):
186
209
 
187
210
  def close_session(self) -> str:
188
211
  """Closes a browser session.
189
- Args:
190
- session_id (str, optional): The session ID to close. If not provided, will use the current session.
212
+
191
213
  Returns:
192
214
  JSON string with closure status
193
215
  """
@@ -207,3 +229,118 @@ class BrowserbaseTools(Toolkit):
207
229
  )
208
230
  except Exception as e:
209
231
  return json.dumps({"status": "warning", "message": f"Cleanup completed with warning: {str(e)}"})
232
+
233
+ async def _ainitialize_browser(self, connect_url: Optional[str] = None):
234
+ """
235
+ Initialize async browser connection if not already initialized.
236
+ Use provided connect_url or ensure we have a session with a connect_url
237
+ """
238
+ try:
239
+ from playwright.async_api import async_playwright # type: ignore[import-not-found]
240
+ except ImportError:
241
+ raise ImportError(
242
+ "`playwright` not installed. Please install using `pip install playwright` and run `playwright install`"
243
+ )
244
+
245
+ if connect_url:
246
+ self._connect_url = connect_url if connect_url else "" # type: ignore
247
+ elif not self._connect_url:
248
+ self._ensure_session()
249
+
250
+ if not self._async_playwright:
251
+ self._async_playwright = await async_playwright().start() # type: ignore
252
+ if self._async_playwright:
253
+ self._async_browser = await self._async_playwright.chromium.connect_over_cdp(self._connect_url)
254
+ context = self._async_browser.contexts[0] if self._async_browser else None
255
+ if context:
256
+ self._async_page = context.pages[0] if context.pages else await context.new_page()
257
+
258
+ async def _acleanup(self):
259
+ """Clean up async browser resources."""
260
+ if self._async_browser:
261
+ await self._async_browser.close()
262
+ self._async_browser = None
263
+ if self._async_playwright:
264
+ await self._async_playwright.stop()
265
+ self._async_playwright = None
266
+ self._async_page = None
267
+
268
+ async def anavigate_to(self, url: str, connect_url: Optional[str] = None) -> str:
269
+ """Navigates to a URL asynchronously.
270
+
271
+ Args:
272
+ url (str): The URL to navigate to
273
+ connect_url (str, optional): The connection URL from an existing session
274
+
275
+ Returns:
276
+ JSON string with navigation status
277
+ """
278
+ try:
279
+ await self._ainitialize_browser(connect_url)
280
+ if self._async_page:
281
+ await self._async_page.goto(url, wait_until="networkidle")
282
+ title = await self._async_page.title() if self._async_page else ""
283
+ result = {"status": "complete", "title": title, "url": url}
284
+ return json.dumps(result)
285
+ except Exception as e:
286
+ await self._acleanup()
287
+ raise e
288
+
289
+ async def ascreenshot(self, path: str, full_page: bool = True, connect_url: Optional[str] = None) -> str:
290
+ """Takes a screenshot of the current page asynchronously.
291
+
292
+ Args:
293
+ path (str): Where to save the screenshot
294
+ full_page (bool): Whether to capture the full page
295
+ connect_url (str, optional): The connection URL from an existing session
296
+
297
+ Returns:
298
+ JSON string confirming screenshot was saved
299
+ """
300
+ try:
301
+ await self._ainitialize_browser(connect_url)
302
+ if self._async_page:
303
+ await self._async_page.screenshot(path=path, full_page=full_page)
304
+ return json.dumps({"status": "success", "path": path})
305
+ except Exception as e:
306
+ await self._acleanup()
307
+ raise e
308
+
309
+ async def aget_page_content(self, connect_url: Optional[str] = None) -> str:
310
+ """Gets the HTML content of the current page asynchronously.
311
+
312
+ Args:
313
+ connect_url (str, optional): The connection URL from an existing session
314
+
315
+ Returns:
316
+ The page HTML content
317
+ """
318
+ try:
319
+ await self._ainitialize_browser(connect_url)
320
+ return await self._async_page.content() if self._async_page else ""
321
+ except Exception as e:
322
+ await self._acleanup()
323
+ raise e
324
+
325
+ async def aclose_session(self) -> str:
326
+ """Closes a browser session asynchronously.
327
+
328
+ Returns:
329
+ JSON string with closure status
330
+ """
331
+ try:
332
+ # First cleanup our local browser resources
333
+ await self._acleanup()
334
+
335
+ # Reset session state
336
+ self._session = None
337
+ self._connect_url = None
338
+
339
+ return json.dumps(
340
+ {
341
+ "status": "closed",
342
+ "message": "Browser resources cleaned up. Session will auto-close if not already closed.",
343
+ }
344
+ )
345
+ except Exception as e:
346
+ return json.dumps({"status": "warning", "message": f"Cleanup completed with warning: {str(e)}"})
agno/tools/crawl4ai.py CHANGED
@@ -20,6 +20,7 @@ class Crawl4aiTools(Toolkit):
20
20
  bm25_threshold: float = 1.0,
21
21
  headless: bool = True,
22
22
  wait_until: str = "domcontentloaded",
23
+ proxy_config: Optional[Dict[str, Any]] = None,
23
24
  enable_crawl: bool = True,
24
25
  all: bool = False,
25
26
  **kwargs,
@@ -36,6 +37,7 @@ class Crawl4aiTools(Toolkit):
36
37
  self.bm25_threshold = bm25_threshold
37
38
  self.wait_until = wait_until
38
39
  self.headless = headless
40
+ self.proxy_config = proxy_config or {}
39
41
 
40
42
  def _build_config(self, search_query: Optional[str] = None) -> Dict[str, Any]:
41
43
  """Build CrawlerRunConfig parameters from toolkit settings."""
@@ -103,6 +105,7 @@ class Crawl4aiTools(Toolkit):
103
105
  browser_config = BrowserConfig(
104
106
  headless=self.headless,
105
107
  verbose=False,
108
+ **self.proxy_config,
106
109
  )
107
110
 
108
111
  async with AsyncWebCrawler(config=browser_config) as crawler:
agno/tools/file.py CHANGED
@@ -24,8 +24,7 @@ class FileTools(Toolkit):
24
24
  all: bool = False,
25
25
  **kwargs,
26
26
  ):
27
- self.base_dir: Path = base_dir or Path.cwd()
28
- self.base_dir = self.base_dir.resolve()
27
+ self.base_dir: Path = (base_dir or Path.cwd()).resolve()
29
28
 
30
29
  tools: List[Any] = []
31
30
  self.max_file_length = max_file_length
@@ -49,6 +48,19 @@ class FileTools(Toolkit):
49
48
 
50
49
  super().__init__(name="file_tools", tools=tools, **kwargs)
51
50
 
51
+ def check_escape(self, relative_path: str) -> Tuple[bool, Path]:
52
+ """Check if the file path is within the base directory.
53
+
54
+ Alias for _check_path maintained for backward compatibility.
55
+
56
+ Args:
57
+ relative_path: The file name or relative path to check.
58
+
59
+ Returns:
60
+ Tuple of (is_safe, resolved_path). If not safe, returns base_dir as the path.
61
+ """
62
+ return self._check_path(relative_path, self.base_dir)
63
+
52
64
  def save_file(self, contents: str, file_name: str, overwrite: bool = True, encoding: str = "utf-8") -> str:
53
65
  """Saves the contents to a file called `file_name` and returns the file name if successful.
54
66
 
@@ -173,17 +185,6 @@ class FileTools(Toolkit):
173
185
  log_error(f"Error removing {file_name}: {e}")
174
186
  return f"Error removing file: {e}"
175
187
 
176
- def check_escape(self, relative_path: str) -> Tuple[bool, Path]:
177
- d = self.base_dir.joinpath(Path(relative_path)).resolve()
178
- if self.base_dir == d:
179
- return True, d
180
- try:
181
- d.relative_to(self.base_dir)
182
- except ValueError:
183
- log_error("Attempted to escape base_dir")
184
- return False, self.base_dir
185
- return True, d
186
-
187
188
  def list_files(self, **kwargs) -> str:
188
189
  """Returns a list of files in directory
189
190
  :param directory: (Optional) name of directory to list.
agno/tools/function.py CHANGED
@@ -54,9 +54,17 @@ class UserInputField:
54
54
 
55
55
  @classmethod
56
56
  def from_dict(cls, data: Dict[str, Any]) -> "UserInputField":
57
+ type_mapping = {"str": str, "int": int, "float": float, "bool": bool, "list": list, "dict": dict}
58
+ field_type_raw = data["field_type"]
59
+ if isinstance(field_type_raw, str):
60
+ field_type = type_mapping.get(field_type_raw, str)
61
+ elif isinstance(field_type_raw, type):
62
+ field_type = field_type_raw
63
+ else:
64
+ field_type = str
57
65
  return cls(
58
66
  name=data["name"],
59
- field_type=eval(data["field_type"]), # Convert string type name to actual type
67
+ field_type=field_type,
60
68
  description=data["description"],
61
69
  value=data["value"],
62
70
  )
@@ -1139,10 +1147,15 @@ class FunctionCall(BaseModel):
1139
1147
  else:
1140
1148
  result = self.function.entrypoint(**entrypoint_args, **self.arguments)
1141
1149
 
1150
+ # Handle both sync and async entrypoints
1142
1151
  if isasyncgenfunction(self.function.entrypoint):
1143
1152
  self.result = result # Store async generator directly
1153
+ elif iscoroutinefunction(self.function.entrypoint):
1154
+ self.result = await result # Await coroutine result
1155
+ elif isgeneratorfunction(self.function.entrypoint):
1156
+ self.result = result # Store sync generator directly
1144
1157
  else:
1145
- self.result = await result
1158
+ self.result = result # Sync function, result is already computed
1146
1159
 
1147
1160
  # Only cache if not a generator
1148
1161
  if self.function.cache_results and not (isgenerator(self.result) or isasyncgen(self.result)):
agno/tools/mcp/mcp.py CHANGED
@@ -127,6 +127,7 @@ class MCPTools(Toolkit):
127
127
 
128
128
  self.transport = transport
129
129
 
130
+ self.header_provider = None
130
131
  if self._is_valid_header_provider(header_provider):
131
132
  self.header_provider = header_provider
132
133
 
@@ -17,7 +17,7 @@ provides high-quality transcription capabilities.
17
17
 
18
18
  import json
19
19
  from pathlib import Path
20
- from typing import Any, Dict, List, Optional, Tuple, Union
20
+ from typing import Any, Dict, List, Optional, Union
21
21
 
22
22
  from agno.tools import Toolkit
23
23
  from agno.utils.log import log_info, logger
@@ -33,9 +33,10 @@ class MLXTranscribeTools(Toolkit):
33
33
  self,
34
34
  base_dir: Optional[Path] = None,
35
35
  enable_read_files_in_base_dir: bool = True,
36
+ restrict_to_base_dir: bool = True,
36
37
  path_or_hf_repo: str = "mlx-community/whisper-large-v3-turbo",
37
38
  verbose: Optional[bool] = None,
38
- temperature: Optional[Union[float, Tuple[float, ...]]] = None,
39
+ temperature: Optional[Union[float, tuple[float, ...]]] = None,
39
40
  compression_ratio_threshold: Optional[float] = None,
40
41
  logprob_threshold: Optional[float] = None,
41
42
  no_speech_threshold: Optional[float] = None,
@@ -50,10 +51,11 @@ class MLXTranscribeTools(Toolkit):
50
51
  all: bool = False,
51
52
  **kwargs,
52
53
  ):
53
- self.base_dir: Path = base_dir or Path.cwd()
54
+ self.base_dir: Path = (base_dir or Path.cwd()).resolve()
55
+ self.restrict_to_base_dir = restrict_to_base_dir
54
56
  self.path_or_hf_repo: str = path_or_hf_repo
55
57
  self.verbose: Optional[bool] = verbose
56
- self.temperature: Optional[Union[float, Tuple[float, ...]]] = temperature
58
+ self.temperature: Optional[Union[float, tuple[float, ...]]] = temperature
57
59
  self.compression_ratio_threshold: Optional[float] = compression_ratio_threshold
58
60
  self.logprob_threshold: Optional[float] = logprob_threshold
59
61
  self.no_speech_threshold: Optional[float] = no_speech_threshold
@@ -83,9 +85,10 @@ class MLXTranscribeTools(Toolkit):
83
85
  str: The transcribed text or an error message if the transcription fails.
84
86
  """
85
87
  try:
86
- audio_file_path = str(self.base_dir.joinpath(file_name))
87
- if audio_file_path is None:
88
- return "No audio file path provided"
88
+ safe, file_path = self._check_path(file_name, self.base_dir, self.restrict_to_base_dir)
89
+ if not safe:
90
+ return f"Error: Path '{file_name}' is outside the allowed base directory"
91
+ audio_file_path = str(file_path)
89
92
 
90
93
  log_info(f"Transcribing audio file {audio_file_path}")
91
94
  transcription_kwargs: Dict[str, Any] = {
agno/tools/python.py CHANGED
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Any, List, Optional
5
5
 
6
6
  from agno.tools import Toolkit
7
- from agno.utils.log import log_debug, log_info, logger
7
+ from agno.utils.log import log_debug, log_error, log_info, logger
8
8
 
9
9
 
10
10
  @functools.lru_cache(maxsize=None)
@@ -18,9 +18,11 @@ class PythonTools(Toolkit):
18
18
  base_dir: Optional[Path] = None,
19
19
  safe_globals: Optional[dict] = None,
20
20
  safe_locals: Optional[dict] = None,
21
+ restrict_to_base_dir: bool = True,
21
22
  **kwargs,
22
23
  ):
23
- self.base_dir: Path = base_dir or Path.cwd()
24
+ self.base_dir: Path = (base_dir or Path.cwd()).resolve()
25
+ self.restrict_to_base_dir = restrict_to_base_dir
24
26
 
25
27
  # Restricted global and local scope
26
28
  self.safe_globals: dict = safe_globals or globals()
@@ -55,7 +57,9 @@ class PythonTools(Toolkit):
55
57
  """
56
58
  try:
57
59
  warn()
58
- file_path = self.base_dir.joinpath(file_name)
60
+ safe, file_path = self._check_path(file_name, self.base_dir, self.restrict_to_base_dir)
61
+ if not safe:
62
+ return f"Error: Path '{file_name}' is outside the allowed base directory"
59
63
  log_debug(f"Saving code to {file_path}")
60
64
  if not file_path.parent.exists():
61
65
  file_path.parent.mkdir(parents=True, exist_ok=True)
@@ -89,8 +93,9 @@ class PythonTools(Toolkit):
89
93
  """
90
94
  try:
91
95
  warn()
92
- file_path = self.base_dir.joinpath(file_name)
93
-
96
+ safe, file_path = self._check_path(file_name, self.base_dir, self.restrict_to_base_dir)
97
+ if not safe:
98
+ return f"Error: Path '{file_name}' is outside the allowed base directory"
94
99
  log_info(f"Running {file_path}")
95
100
  globals_after_run = runpy.run_path(str(file_path), init_globals=self.safe_globals, run_name="__main__")
96
101
  if variable_to_return:
@@ -113,7 +118,10 @@ class PythonTools(Toolkit):
113
118
  """
114
119
  try:
115
120
  log_info(f"Reading file: {file_name}")
116
- file_path = self.base_dir.joinpath(file_name)
121
+ safe, file_path = self._check_path(file_name, self.base_dir, self.restrict_to_base_dir)
122
+ if not safe:
123
+ log_error(f"Attempted to read file outside base directory: {file_name}")
124
+ return "Error reading file: path outside allowed directory"
117
125
  contents = file_path.read_text(encoding="utf-8")
118
126
  return str(contents)
119
127
  except Exception as e: