agno 2.3.22__py3-none-any.whl → 2.3.23__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 (40) hide show
  1. agno/agent/agent.py +22 -1
  2. agno/agent/remote.py +1 -1
  3. agno/db/mysql/async_mysql.py +5 -7
  4. agno/db/mysql/mysql.py +5 -7
  5. agno/db/mysql/schemas.py +39 -21
  6. agno/db/postgres/async_postgres.py +10 -2
  7. agno/db/postgres/postgres.py +5 -7
  8. agno/db/postgres/schemas.py +39 -21
  9. agno/db/singlestore/schemas.py +41 -21
  10. agno/db/singlestore/singlestore.py +14 -3
  11. agno/db/sqlite/async_sqlite.py +7 -2
  12. agno/db/sqlite/schemas.py +36 -21
  13. agno/db/sqlite/sqlite.py +3 -7
  14. agno/models/base.py +4 -0
  15. agno/models/google/gemini.py +27 -4
  16. agno/os/routers/agents/router.py +1 -1
  17. agno/os/routers/evals/evals.py +2 -2
  18. agno/os/routers/knowledge/knowledge.py +2 -2
  19. agno/os/routers/knowledge/schemas.py +1 -1
  20. agno/os/routers/memory/memory.py +4 -4
  21. agno/os/routers/session/session.py +2 -2
  22. agno/os/routers/teams/router.py +2 -2
  23. agno/os/routers/traces/traces.py +3 -3
  24. agno/os/routers/workflows/router.py +1 -1
  25. agno/os/schema.py +1 -1
  26. agno/remote/base.py +1 -1
  27. agno/team/remote.py +1 -1
  28. agno/team/team.py +19 -1
  29. agno/tools/brandfetch.py +27 -18
  30. agno/tools/browserbase.py +150 -13
  31. agno/tools/function.py +6 -1
  32. agno/tools/mcp/mcp.py +1 -0
  33. agno/tools/toolkit.py +89 -21
  34. agno/workflow/remote.py +1 -1
  35. agno/workflow/workflow.py +14 -0
  36. {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/METADATA +1 -1
  37. {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/RECORD +40 -40
  38. {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
  39. {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
  40. {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
agno/models/base.py CHANGED
@@ -1016,6 +1016,8 @@ class Model(ABC):
1016
1016
  model_response.extra.update(provider_response.extra)
1017
1017
  if provider_response.provider_data is not None:
1018
1018
  model_response.provider_data = provider_response.provider_data
1019
+ if provider_response.response_usage is not None:
1020
+ model_response.response_usage = provider_response.response_usage
1019
1021
 
1020
1022
  async def _aprocess_model_response(
1021
1023
  self,
@@ -1073,6 +1075,8 @@ class Model(ABC):
1073
1075
  model_response.extra.update(provider_response.extra)
1074
1076
  if provider_response.provider_data is not None:
1075
1077
  model_response.provider_data = provider_response.provider_data
1078
+ if provider_response.response_usage is not None:
1079
+ model_response.response_usage = provider_response.response_usage
1076
1080
 
1077
1081
  def _populate_assistant_message(
1078
1082
  self,
@@ -466,7 +466,12 @@ class Gemini(Model):
466
466
 
467
467
  except (ClientError, ServerError) as e:
468
468
  log_error(f"Error from Gemini API: {e}")
469
- error_message = str(e.response) if hasattr(e, "response") else str(e)
469
+ error_message = str(e)
470
+ if hasattr(e, "response"):
471
+ if hasattr(e.response, "text"):
472
+ error_message = e.response.text
473
+ else:
474
+ error_message = str(e.response)
470
475
  raise ModelProviderError(
471
476
  message=error_message,
472
477
  status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
@@ -518,8 +523,14 @@ class Gemini(Model):
518
523
 
519
524
  except (ClientError, ServerError) as e:
520
525
  log_error(f"Error from Gemini API: {e}")
526
+ error_message = str(e)
527
+ if hasattr(e, "response"):
528
+ if hasattr(e.response, "text"):
529
+ error_message = e.response.text
530
+ else:
531
+ error_message = str(e.response)
521
532
  raise ModelProviderError(
522
- message=str(e.response) if hasattr(e, "response") else str(e),
533
+ message=error_message,
523
534
  status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
524
535
  model_name=self.name,
525
536
  model_id=self.id,
@@ -574,8 +585,14 @@ class Gemini(Model):
574
585
 
575
586
  except (ClientError, ServerError) as e:
576
587
  log_error(f"Error from Gemini API: {e}")
588
+ error_message = str(e)
589
+ if hasattr(e, "response"):
590
+ if hasattr(e.response, "text"):
591
+ error_message = e.response.text
592
+ else:
593
+ error_message = str(e.response)
577
594
  raise ModelProviderError(
578
- message=str(e.response) if hasattr(e, "response") else str(e),
595
+ message=error_message,
579
596
  status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
580
597
  model_name=self.name,
581
598
  model_id=self.id,
@@ -628,8 +645,14 @@ class Gemini(Model):
628
645
 
629
646
  except (ClientError, ServerError) as e:
630
647
  log_error(f"Error from Gemini API: {e}")
648
+ error_message = str(e)
649
+ if hasattr(e, "response"):
650
+ if hasattr(e.response, "text"):
651
+ error_message = e.response.text
652
+ else:
653
+ error_message = str(e.response)
631
654
  raise ModelProviderError(
632
- message=str(e.response) if hasattr(e, "response") else str(e),
655
+ message=error_message,
633
656
  status_code=e.code if hasattr(e, "code") and e.code is not None else 502,
634
657
  model_name=self.name,
635
658
  model_id=self.id,
@@ -409,7 +409,7 @@ def get_agent_router(
409
409
  if agent is None:
410
410
  raise HTTPException(status_code=404, detail="Agent not found")
411
411
 
412
- cancelled = agent.cancel_run(run_id=run_id)
412
+ cancelled = await agent.acancel_run(run_id=run_id)
413
413
  if not cancelled:
414
414
  raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
415
415
 
@@ -118,8 +118,8 @@ def attach_routes(
118
118
  model_id: Optional[str] = Query(default=None, description="Model ID"),
119
119
  filter_type: Optional[EvalFilterType] = Query(default=None, description="Filter type", alias="type"),
120
120
  eval_types: Optional[List[EvalType]] = Depends(parse_eval_types_filter),
121
- limit: Optional[int] = Query(default=20, description="Number of eval runs to return"),
122
- page: Optional[int] = Query(default=1, description="Page number"),
121
+ limit: Optional[int] = Query(default=20, description="Number of eval runs to return", ge=1),
122
+ page: Optional[int] = Query(default=1, description="Page number", ge=0),
123
123
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort by"),
124
124
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
125
125
  db_id: Optional[str] = Query(default=None, description="The ID of the database to use"),
@@ -344,8 +344,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
344
344
  )
345
345
  async def get_content(
346
346
  request: Request,
347
- limit: Optional[int] = Query(default=20, description="Number of content entries to return"),
348
- page: Optional[int] = Query(default=1, description="Page number"),
347
+ limit: Optional[int] = Query(default=20, description="Number of content entries to return", ge=1),
348
+ page: Optional[int] = Query(default=1, description="Page number", ge=0),
349
349
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort by"),
350
350
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
351
351
  db_id: Optional[str] = Query(default=None, description="The ID of the database to use"),
@@ -156,7 +156,7 @@ class VectorSearchRequestSchema(BaseModel):
156
156
  class Meta(BaseModel):
157
157
  """Inline metadata schema for pagination."""
158
158
 
159
- limit: int = Field(20, description="Number of results per page", ge=1, le=100)
159
+ limit: int = Field(20, description="Number of results per page", ge=1)
160
160
  page: int = Field(1, description="Page number", ge=1)
161
161
 
162
162
  query: str = Field(..., description="The search query text")
@@ -256,8 +256,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
256
256
  team_id: Optional[str] = Query(default=None, description="Filter memories by team ID"),
257
257
  topics: Optional[List[str]] = Depends(parse_topics),
258
258
  search_content: Optional[str] = Query(default=None, description="Fuzzy search within memory content"),
259
- limit: Optional[int] = Query(default=20, description="Number of memories to return per page"),
260
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
259
+ limit: Optional[int] = Query(default=20, description="Number of memories to return per page", ge=1),
260
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
261
261
  sort_by: Optional[str] = Query(default="updated_at", description="Field to sort memories by"),
262
262
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
263
263
  db_id: Optional[str] = Query(default=None, description="Database ID to query memories from"),
@@ -558,8 +558,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
558
558
  )
559
559
  async def get_user_memory_stats(
560
560
  request: Request,
561
- limit: Optional[int] = Query(default=20, description="Number of user statistics to return per page"),
562
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
561
+ limit: Optional[int] = Query(default=20, description="Number of user statistics to return per page", ge=1),
562
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
563
563
  db_id: Optional[str] = Query(default=None, description="Database ID to query statistics from"),
564
564
  table: Optional[str] = Query(default=None, description="Table to query statistics from"),
565
565
  ) -> PaginatedResponse[UserStatsSchema]:
@@ -107,8 +107,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
107
107
  ),
108
108
  user_id: Optional[str] = Query(default=None, description="Filter sessions by user ID"),
109
109
  session_name: Optional[str] = Query(default=None, description="Filter sessions by name (partial match)"),
110
- limit: Optional[int] = Query(default=20, description="Number of sessions to return per page"),
111
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
110
+ limit: Optional[int] = Query(default=20, description="Number of sessions to return per page", ge=1),
111
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
112
112
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort sessions by"),
113
113
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
114
114
  db_id: Optional[str] = Query(default=None, description="Database ID to query sessions from"),
@@ -95,7 +95,7 @@ async def team_response_streamer(
95
95
  )
96
96
  yield format_sse_event(error_response)
97
97
 
98
- except Exception as e:
98
+ except BaseException as e:
99
99
  import traceback
100
100
 
101
101
  traceback.print_exc()
@@ -325,7 +325,7 @@ def get_team_router(
325
325
  if team is None:
326
326
  raise HTTPException(status_code=404, detail="Team not found")
327
327
 
328
- cancelled = team.cancel_run(run_id=run_id)
328
+ cancelled = await team.acancel_run(run_id=run_id)
329
329
  if not cancelled:
330
330
  raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
331
331
 
@@ -126,8 +126,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
126
126
  default=None,
127
127
  description="Filter traces ending before this time (ISO 8601 format with timezone, e.g., '2025-11-19T11:00:00Z' or '2025-11-19T16:30:00+05:30'). Times are converted to UTC for comparison.",
128
128
  ),
129
- page: int = Query(default=1, description="Page number (1-indexed)", ge=1),
130
- limit: int = Query(default=20, description="Number of traces per page", ge=1, le=100),
129
+ page: int = Query(default=1, description="Page number (1-indexed)", ge=0),
130
+ limit: int = Query(default=20, description="Number of traces per page", ge=1),
131
131
  db_id: Optional[str] = Query(default=None, description="Database ID to query traces from"),
132
132
  ):
133
133
  """Get list of traces with optional filters and pagination"""
@@ -455,7 +455,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
455
455
  description="Filter sessions with traces created before this time (ISO 8601 format with timezone, e.g., '2025-11-19T11:00:00Z' or '2025-11-19T16:30:00+05:30'). Times are converted to UTC for comparison.",
456
456
  ),
457
457
  page: int = Query(default=1, description="Page number (1-indexed)", ge=1),
458
- limit: int = Query(default=20, description="Number of sessions per page", ge=1, le=100),
458
+ limit: int = Query(default=20, description="Number of sessions per page", ge=1),
459
459
  db_id: Optional[str] = Query(default=None, description="Database ID to query statistics from"),
460
460
  ):
461
461
  """Get trace statistics grouped by session"""
@@ -721,7 +721,7 @@ def get_workflow_router(
721
721
  if workflow is None:
722
722
  raise HTTPException(status_code=404, detail="Workflow not found")
723
723
 
724
- cancelled = workflow.cancel_run(run_id=run_id)
724
+ cancelled = await workflow.acancel_run(run_id=run_id)
725
725
  if not cancelled:
726
726
  raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
727
727
 
agno/os/schema.py CHANGED
@@ -549,7 +549,7 @@ class SortOrder(str, Enum):
549
549
 
550
550
  class PaginationInfo(BaseModel):
551
551
  page: int = Field(0, description="Current page number (0-indexed)", ge=0)
552
- limit: int = Field(20, description="Number of items per page", ge=1, le=100)
552
+ limit: int = Field(20, description="Number of items per page", ge=1)
553
553
  total_pages: int = Field(0, description="Total number of pages", ge=0)
554
554
  total_count: int = Field(0, description="Total count of items", ge=0)
555
555
  search_time_ms: float = Field(0, description="Search execution time in milliseconds", ge=0)
agno/remote/base.py CHANGED
@@ -577,5 +577,5 @@ class BaseRemote:
577
577
  raise NotImplementedError("acontinue_run method must be implemented by the subclass")
578
578
 
579
579
  @abstractmethod
580
- async def cancel_run(self, run_id: str) -> bool:
580
+ async def acancel_run(self, run_id: str) -> bool:
581
581
  raise NotImplementedError("cancel_run method must be implemented by the subclass")
agno/team/remote.py CHANGED
@@ -425,7 +425,7 @@ class RemoteTeam(BaseRemote):
425
425
  )
426
426
  return map_task_result_to_team_run_output(task_result, team_id=self.team_id, user_id=user_id)
427
427
 
428
- async def cancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
428
+ async def acancel_run(self, run_id: str, auth_token: Optional[str] = None) -> bool:
429
429
  """Cancel a running team execution.
430
430
 
431
431
  Args:
agno/team/team.py CHANGED
@@ -56,6 +56,9 @@ from agno.models.utils import get_model
56
56
  from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
57
57
  from agno.run import RunContext, RunStatus
58
58
  from agno.run.agent import RunEvent, RunOutput, RunOutputEvent
59
+ from agno.run.cancel import (
60
+ acancel_run as acancel_run_global,
61
+ )
59
62
  from agno.run.cancel import (
60
63
  acleanup_run,
61
64
  araise_if_cancelled,
@@ -1011,6 +1014,18 @@ class Team:
1011
1014
  """
1012
1015
  return cancel_run_global(run_id)
1013
1016
 
1017
+ @staticmethod
1018
+ async def acancel_run(run_id: str) -> bool:
1019
+ """Cancel a running team execution.
1020
+
1021
+ Args:
1022
+ run_id (str): The run_id to cancel.
1023
+
1024
+ Returns:
1025
+ bool: True if the run was found and marked for cancellation, False otherwise.
1026
+ """
1027
+ return await acancel_run_global(run_id)
1028
+
1014
1029
  async def _connect_mcp_tools(self) -> None:
1015
1030
  """Connect the MCP tools to the agent."""
1016
1031
  if self.tools is not None:
@@ -2652,6 +2667,8 @@ class Team:
2652
2667
  """
2653
2668
  log_debug(f"Team Run Start: {run_response.run_id}", center=True)
2654
2669
 
2670
+ await aregister_run(run_context.run_id)
2671
+
2655
2672
  memory_task = None
2656
2673
 
2657
2674
  try:
@@ -5367,7 +5384,8 @@ class Team:
5367
5384
 
5368
5385
  elif isinstance(tool, Toolkit):
5369
5386
  # For each function in the toolkit and process entrypoint
5370
- for name, _func in tool.functions.items():
5387
+ toolkit_functions = tool.get_async_functions() if async_mode else tool.get_functions()
5388
+ for name, _func in toolkit_functions.items():
5371
5389
  if name in _function_names:
5372
5390
  continue
5373
5391
  _function_names.append(name)
agno/tools/brandfetch.py CHANGED
@@ -1,9 +1,10 @@
1
1
  """
2
- Going to contribute this to agno toolkits.
2
+ Brandfetch API toolkit for retrieving brand data and searching brands.
3
3
  """
4
4
 
5
+ import warnings
5
6
  from os import getenv
6
- from typing import Any, Optional
7
+ from typing import Any, List, Optional
7
8
 
8
9
  try:
9
10
  import httpx
@@ -31,8 +32,6 @@ class BrandfetchTools(Toolkit):
31
32
  all: bool - if True, will use all tools
32
33
  enable_search_by_identifier: bool - if True, will use search by identifier
33
34
  enable_search_by_brand: bool - if True, will use search by brand
34
- enable_asearch_by_identifier: bool - if True, will use async search by identifier
35
- enable_asearch_by_brand: bool - if True, will use async search by brand
36
35
  """
37
36
 
38
37
  def __init__(
@@ -44,9 +43,18 @@ class BrandfetchTools(Toolkit):
44
43
  enable_search_by_identifier: bool = True,
45
44
  enable_search_by_brand: bool = False,
46
45
  all: bool = False,
47
- async_tools: bool = False,
46
+ async_tools: bool = False, # Deprecated
48
47
  **kwargs,
49
48
  ):
49
+ # Handle deprecated async_tools parameter
50
+ if async_tools:
51
+ warnings.warn(
52
+ "The 'async_tools' parameter is deprecated and will be removed in a future version. "
53
+ "Async tools are now automatically used when calling agent.arun() or agent.aprint_response().",
54
+ DeprecationWarning,
55
+ stacklevel=2,
56
+ )
57
+
50
58
  self.api_key = api_key or getenv("BRANDFETCH_API_KEY")
51
59
  self.client_id = client_id or getenv("BRANDFETCH_CLIENT_ID")
52
60
  self.base_url = base_url
@@ -54,20 +62,21 @@ class BrandfetchTools(Toolkit):
54
62
  self.search_url = f"{self.base_url}/search"
55
63
  self.brand_url = f"{self.base_url}/brands"
56
64
 
57
- tools: list[Any] = []
58
- # Backward-compat mapping: prefer new enable_* flags, but honor legacy toggles
59
- if async_tools:
60
- if all or enable_search_by_identifier:
61
- tools.append(self.asearch_by_identifier)
62
- if all or enable_search_by_brand:
63
- tools.append(self.asearch_by_brand)
64
- else:
65
- if all or enable_search_by_identifier:
66
- tools.append(self.search_by_identifier)
67
- if all or enable_search_by_brand:
68
- tools.append(self.search_by_brand)
65
+ # Build tools lists
66
+ # sync tools: used by agent.run() and agent.print_response()
67
+ # async tools: used by agent.arun() and agent.aprint_response()
68
+ tools: List[Any] = []
69
+ async_tools_list: List[tuple] = []
70
+
71
+ if all or enable_search_by_identifier:
72
+ tools.append(self.search_by_identifier)
73
+ async_tools_list.append((self.asearch_by_identifier, "search_by_identifier"))
74
+ if all or enable_search_by_brand:
75
+ tools.append(self.search_by_brand)
76
+ async_tools_list.append((self.asearch_by_brand, "search_by_brand"))
77
+
69
78
  name = kwargs.pop("name", "brandfetch_tools")
70
- super().__init__(name=name, tools=tools, **kwargs)
79
+ super().__init__(name=name, tools=tools, async_tools=async_tools_list, **kwargs)
71
80
 
72
81
  async def asearch_by_identifier(self, identifier: str) -> dict[str, Any]:
73
82
  """
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/function.py CHANGED
@@ -1139,10 +1139,15 @@ class FunctionCall(BaseModel):
1139
1139
  else:
1140
1140
  result = self.function.entrypoint(**entrypoint_args, **self.arguments)
1141
1141
 
1142
+ # Handle both sync and async entrypoints
1142
1143
  if isasyncgenfunction(self.function.entrypoint):
1143
1144
  self.result = result # Store async generator directly
1145
+ elif iscoroutinefunction(self.function.entrypoint):
1146
+ self.result = await result # Await coroutine result
1147
+ elif isgeneratorfunction(self.function.entrypoint):
1148
+ self.result = result # Store sync generator directly
1144
1149
  else:
1145
- self.result = await result
1150
+ self.result = result # Sync function, result is already computed
1146
1151
 
1147
1152
  # Only cache if not a generator
1148
1153
  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