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.
- agno/agent/agent.py +22 -1
- agno/agent/remote.py +1 -1
- agno/db/mysql/async_mysql.py +5 -7
- agno/db/mysql/mysql.py +5 -7
- agno/db/mysql/schemas.py +39 -21
- agno/db/postgres/async_postgres.py +10 -2
- agno/db/postgres/postgres.py +5 -7
- agno/db/postgres/schemas.py +39 -21
- agno/db/singlestore/schemas.py +41 -21
- agno/db/singlestore/singlestore.py +14 -3
- agno/db/sqlite/async_sqlite.py +7 -2
- agno/db/sqlite/schemas.py +36 -21
- agno/db/sqlite/sqlite.py +3 -7
- agno/models/base.py +4 -0
- agno/models/google/gemini.py +27 -4
- agno/os/routers/agents/router.py +1 -1
- agno/os/routers/evals/evals.py +2 -2
- agno/os/routers/knowledge/knowledge.py +2 -2
- agno/os/routers/knowledge/schemas.py +1 -1
- agno/os/routers/memory/memory.py +4 -4
- agno/os/routers/session/session.py +2 -2
- agno/os/routers/teams/router.py +2 -2
- agno/os/routers/traces/traces.py +3 -3
- agno/os/routers/workflows/router.py +1 -1
- agno/os/schema.py +1 -1
- agno/remote/base.py +1 -1
- agno/team/remote.py +1 -1
- agno/team/team.py +19 -1
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +150 -13
- agno/tools/function.py +6 -1
- agno/tools/mcp/mcp.py +1 -0
- agno/tools/toolkit.py +89 -21
- agno/workflow/remote.py +1 -1
- agno/workflow/workflow.py +14 -0
- {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/METADATA +1 -1
- {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/RECORD +40 -40
- {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
- {agno-2.3.22.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
- {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,
|
agno/models/google/gemini.py
CHANGED
|
@@ -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
|
|
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=
|
|
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=
|
|
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=
|
|
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,
|
agno/os/routers/agents/router.py
CHANGED
|
@@ -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.
|
|
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
|
|
agno/os/routers/evals/evals.py
CHANGED
|
@@ -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
|
|
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")
|
agno/os/routers/memory/memory.py
CHANGED
|
@@ -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"),
|
agno/os/routers/teams/router.py
CHANGED
|
@@ -95,7 +95,7 @@ async def team_response_streamer(
|
|
|
95
95
|
)
|
|
96
96
|
yield format_sse_event(error_response)
|
|
97
97
|
|
|
98
|
-
except
|
|
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.
|
|
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
|
|
agno/os/routers/traces/traces.py
CHANGED
|
@@ -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=
|
|
130
|
-
limit: int = Query(default=20, description="Number of traces per page", ge=1
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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).
|
|
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
|
-
|
|
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 =
|
|
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)):
|