agno 2.3.24__py3-none-any.whl → 2.3.25__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 +297 -11
- agno/db/base.py +214 -0
- agno/db/dynamo/dynamo.py +47 -0
- agno/db/firestore/firestore.py +47 -0
- agno/db/gcs_json/gcs_json_db.py +47 -0
- agno/db/in_memory/in_memory_db.py +47 -0
- agno/db/json/json_db.py +47 -0
- agno/db/mongo/async_mongo.py +229 -0
- agno/db/mongo/mongo.py +47 -0
- agno/db/mongo/schemas.py +16 -0
- agno/db/mysql/async_mysql.py +47 -0
- agno/db/mysql/mysql.py +47 -0
- agno/db/postgres/async_postgres.py +231 -0
- agno/db/postgres/postgres.py +239 -0
- agno/db/postgres/schemas.py +19 -0
- agno/db/redis/redis.py +47 -0
- agno/db/singlestore/singlestore.py +47 -0
- agno/db/sqlite/async_sqlite.py +242 -0
- agno/db/sqlite/schemas.py +18 -0
- agno/db/sqlite/sqlite.py +239 -0
- agno/db/surrealdb/surrealdb.py +47 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +62 -2
- agno/knowledge/chunking/strategy.py +14 -0
- agno/knowledge/knowledge.py +7 -1
- agno/knowledge/reader/arxiv_reader.py +1 -0
- agno/knowledge/reader/csv_reader.py +1 -0
- agno/knowledge/reader/docx_reader.py +1 -0
- agno/knowledge/reader/firecrawl_reader.py +1 -0
- agno/knowledge/reader/json_reader.py +1 -0
- agno/knowledge/reader/markdown_reader.py +1 -0
- agno/knowledge/reader/pdf_reader.py +1 -0
- agno/knowledge/reader/pptx_reader.py +1 -0
- agno/knowledge/reader/s3_reader.py +1 -0
- agno/knowledge/reader/tavily_reader.py +1 -0
- agno/knowledge/reader/text_reader.py +1 -0
- agno/knowledge/reader/web_search_reader.py +1 -0
- agno/knowledge/reader/website_reader.py +1 -0
- agno/knowledge/reader/wikipedia_reader.py +1 -0
- agno/knowledge/reader/youtube_reader.py +1 -0
- agno/knowledge/utils.py +1 -0
- agno/learn/__init__.py +65 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +690 -0
- agno/learn/schemas.py +1043 -0
- agno/learn/stores/__init__.py +35 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/models/base.py +59 -0
- agno/os/routers/knowledge/knowledge.py +7 -0
- agno/tools/browserbase.py +78 -6
- agno/tools/google_bigquery.py +11 -2
- agno/utils/agent.py +30 -1
- {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/METADATA +24 -2
- {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/RECORD +64 -50
- {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/WHEEL +0 -0
- {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/top_level.txt +0 -0
agno/learn/utils.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Learning Machine Utilities
|
|
3
|
+
==========================
|
|
4
|
+
Helper functions for safe data handling.
|
|
5
|
+
|
|
6
|
+
All functions are designed to never raise exceptions -
|
|
7
|
+
they return None on any failure. This prevents learning
|
|
8
|
+
extraction errors from crashing the main agent.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, fields
|
|
12
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _safe_get(data: Any, key: str, default: Any = None) -> Any:
|
|
18
|
+
"""Safely get a key from dict-like data.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data: Dict or object with attributes.
|
|
22
|
+
key: Key or attribute name to get.
|
|
23
|
+
default: Value to return if not found.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
The value, or default if not found.
|
|
27
|
+
"""
|
|
28
|
+
if isinstance(data, dict):
|
|
29
|
+
return data.get(key, default)
|
|
30
|
+
return getattr(data, key, default)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_json(data: Any) -> Optional[Dict]:
|
|
34
|
+
"""Parse JSON string to dict, or return dict as-is.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
data: JSON string, dict, or None.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Parsed dict, or None if parsing fails.
|
|
41
|
+
"""
|
|
42
|
+
if data is None:
|
|
43
|
+
return None
|
|
44
|
+
if isinstance(data, dict):
|
|
45
|
+
return data
|
|
46
|
+
if isinstance(data, str):
|
|
47
|
+
import json
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return json.loads(data)
|
|
51
|
+
except Exception:
|
|
52
|
+
return None
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def from_dict_safe(cls: Type[T], data: Any) -> Optional[T]:
|
|
57
|
+
"""Safely create a dataclass instance from dict-like data.
|
|
58
|
+
|
|
59
|
+
Works with any dataclass - automatically handles subclass fields.
|
|
60
|
+
Never raises - returns None on any failure.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cls: The dataclass type to instantiate.
|
|
64
|
+
data: Dict, JSON string, or existing instance.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Instance of cls, or None if parsing fails.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> profile = from_dict_safe(UserProfile, {"user_id": "123"})
|
|
71
|
+
>>> profile.user_id
|
|
72
|
+
'123'
|
|
73
|
+
"""
|
|
74
|
+
if data is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
# Already the right type
|
|
78
|
+
if isinstance(data, cls):
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Parse JSON string if needed
|
|
83
|
+
parsed = _parse_json(data)
|
|
84
|
+
if parsed is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# Get valid field names for this class
|
|
88
|
+
field_names = {f.name for f in fields(cls)} # type: ignore
|
|
89
|
+
|
|
90
|
+
# Filter to only valid fields
|
|
91
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
92
|
+
|
|
93
|
+
return cls(**kwargs)
|
|
94
|
+
except Exception:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def print_panel(
|
|
99
|
+
title: str,
|
|
100
|
+
subtitle: str,
|
|
101
|
+
lines: List[str],
|
|
102
|
+
*,
|
|
103
|
+
empty_message: str = "No data",
|
|
104
|
+
raw_data: Any = None,
|
|
105
|
+
raw: bool = False,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Print formatted panel output for learning stores.
|
|
108
|
+
|
|
109
|
+
Uses rich library for formatted output with a bordered panel.
|
|
110
|
+
Falls back to pprint when raw=True or rich is unavailable.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
title: Panel title (e.g., "User Profile", "Session Context")
|
|
114
|
+
subtitle: Panel subtitle (e.g., user_id, session_id)
|
|
115
|
+
lines: Content lines to display inside the panel
|
|
116
|
+
empty_message: Message shown when lines is empty
|
|
117
|
+
raw_data: Object to pprint when raw=True
|
|
118
|
+
raw: If True, use pprint instead of formatted panel
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> print_panel(
|
|
122
|
+
... title="User Profile",
|
|
123
|
+
... subtitle="alice@example.com",
|
|
124
|
+
... lines=["Name: Alice", "Memories:", " [abc123] Loves Python"],
|
|
125
|
+
... raw_data=profile,
|
|
126
|
+
... )
|
|
127
|
+
╭──────────────── User Profile ─────────────────╮
|
|
128
|
+
│ Name: Alice │
|
|
129
|
+
│ Memories: │
|
|
130
|
+
│ [abc123] Loves Python │
|
|
131
|
+
╰─────────────── alice@example.com ─────────────╯
|
|
132
|
+
"""
|
|
133
|
+
if raw and raw_data is not None:
|
|
134
|
+
from pprint import pprint
|
|
135
|
+
|
|
136
|
+
pprint(to_dict_safe(raw_data) or raw_data)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
from rich.console import Console
|
|
141
|
+
from rich.panel import Panel
|
|
142
|
+
|
|
143
|
+
console = Console()
|
|
144
|
+
|
|
145
|
+
if not lines:
|
|
146
|
+
content = f"[dim]{empty_message}[/dim]"
|
|
147
|
+
else:
|
|
148
|
+
content = "\n".join(lines)
|
|
149
|
+
|
|
150
|
+
panel = Panel(
|
|
151
|
+
content,
|
|
152
|
+
title=f"[bold]{title}[/bold]",
|
|
153
|
+
subtitle=f"[dim]{subtitle}[/dim]",
|
|
154
|
+
border_style="blue",
|
|
155
|
+
)
|
|
156
|
+
console.print(panel)
|
|
157
|
+
|
|
158
|
+
except ImportError:
|
|
159
|
+
# Fallback if rich not installed
|
|
160
|
+
from pprint import pprint
|
|
161
|
+
|
|
162
|
+
print(f"=== {title} ({subtitle}) ===")
|
|
163
|
+
if not lines:
|
|
164
|
+
print(f" {empty_message}")
|
|
165
|
+
else:
|
|
166
|
+
for line in lines:
|
|
167
|
+
print(f" {line}")
|
|
168
|
+
print()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def to_dict_safe(obj: Any) -> Optional[Dict[str, Any]]:
|
|
172
|
+
"""Safely convert a dataclass to dict.
|
|
173
|
+
|
|
174
|
+
Works with any dataclass. Never raises - returns None on failure.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
obj: Dataclass instance to convert.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dict representation, or None if conversion fails.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
>>> profile = UserProfile(user_id="123")
|
|
184
|
+
>>> to_dict_safe(profile)
|
|
185
|
+
{'user_id': '123', 'name': None, ...}
|
|
186
|
+
"""
|
|
187
|
+
if obj is None:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Already a dict
|
|
192
|
+
if isinstance(obj, dict):
|
|
193
|
+
return obj
|
|
194
|
+
|
|
195
|
+
# Has to_dict method
|
|
196
|
+
if hasattr(obj, "to_dict"):
|
|
197
|
+
return obj.to_dict()
|
|
198
|
+
|
|
199
|
+
# Is a dataclass
|
|
200
|
+
if hasattr(obj, "__dataclass_fields__"):
|
|
201
|
+
return asdict(obj)
|
|
202
|
+
|
|
203
|
+
# Has __dict__
|
|
204
|
+
if hasattr(obj, "__dict__"):
|
|
205
|
+
return dict(obj.__dict__)
|
|
206
|
+
|
|
207
|
+
return None
|
|
208
|
+
except Exception:
|
|
209
|
+
return None
|
agno/models/base.py
CHANGED
|
@@ -174,6 +174,49 @@ class Model(ABC):
|
|
|
174
174
|
return self.delay_between_retries * (2**attempt)
|
|
175
175
|
return self.delay_between_retries
|
|
176
176
|
|
|
177
|
+
def _is_retryable_error(self, error: ModelProviderError) -> bool:
|
|
178
|
+
"""Determine if an error is worth retrying.
|
|
179
|
+
|
|
180
|
+
Non-retryable errors include:
|
|
181
|
+
- Client errors (400, 401, 403, 413, 422) that won't change on retry
|
|
182
|
+
- Context window/token limit exceeded errors
|
|
183
|
+
- Payload too large errors
|
|
184
|
+
|
|
185
|
+
Retryable errors include:
|
|
186
|
+
- Rate limit errors (429)
|
|
187
|
+
- Server errors (500, 502, 503, 504)
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
error: The ModelProviderError to evaluate.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if the error is transient and worth retrying, False otherwise.
|
|
194
|
+
"""
|
|
195
|
+
# Non-retryable status codes (client errors that won't change)
|
|
196
|
+
non_retryable_codes = {400, 401, 403, 404, 413, 422}
|
|
197
|
+
if error.status_code in non_retryable_codes:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
# Non-retryable error message patterns (context/token limits)
|
|
201
|
+
non_retryable_patterns = [
|
|
202
|
+
"context_length_exceeded",
|
|
203
|
+
"context window",
|
|
204
|
+
"maximum context length",
|
|
205
|
+
"token limit",
|
|
206
|
+
"max_tokens",
|
|
207
|
+
"too many tokens",
|
|
208
|
+
"payload too large",
|
|
209
|
+
"content_too_large",
|
|
210
|
+
"request too large",
|
|
211
|
+
"input too long",
|
|
212
|
+
"exceeds the model",
|
|
213
|
+
]
|
|
214
|
+
error_msg = str(error.message).lower()
|
|
215
|
+
if any(pattern in error_msg for pattern in non_retryable_patterns):
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
return True
|
|
219
|
+
|
|
177
220
|
def _invoke_with_retry(self, **kwargs) -> ModelResponse:
|
|
178
221
|
"""
|
|
179
222
|
Invoke the model with retry logic for ModelProviderError.
|
|
@@ -189,6 +232,10 @@ class Model(ABC):
|
|
|
189
232
|
return self.invoke(**kwargs)
|
|
190
233
|
except ModelProviderError as e:
|
|
191
234
|
last_exception = e
|
|
235
|
+
# Check if error is non-retryable
|
|
236
|
+
if not self._is_retryable_error(e):
|
|
237
|
+
log_error(f"Non-retryable model provider error: {e}")
|
|
238
|
+
raise
|
|
192
239
|
if attempt < self.retries:
|
|
193
240
|
delay = self._get_retry_delay(attempt)
|
|
194
241
|
log_warning(
|
|
@@ -232,6 +279,10 @@ class Model(ABC):
|
|
|
232
279
|
return await self.ainvoke(**kwargs)
|
|
233
280
|
except ModelProviderError as e:
|
|
234
281
|
last_exception = e
|
|
282
|
+
# Check if error is non-retryable
|
|
283
|
+
if not self._is_retryable_error(e):
|
|
284
|
+
log_error(f"Non-retryable model provider error: {e}")
|
|
285
|
+
raise
|
|
235
286
|
if attempt < self.retries:
|
|
236
287
|
delay = self._get_retry_delay(attempt)
|
|
237
288
|
log_warning(
|
|
@@ -277,6 +328,10 @@ class Model(ABC):
|
|
|
277
328
|
return # Success, exit the retry loop
|
|
278
329
|
except ModelProviderError as e:
|
|
279
330
|
last_exception = e
|
|
331
|
+
# Check if error is non-retryable (e.g., context window exceeded, auth errors)
|
|
332
|
+
if not self._is_retryable_error(e):
|
|
333
|
+
log_error(f"Non-retryable model provider error: {e}")
|
|
334
|
+
raise
|
|
280
335
|
if attempt < self.retries:
|
|
281
336
|
delay = self._get_retry_delay(attempt)
|
|
282
337
|
log_warning(
|
|
@@ -325,6 +380,10 @@ class Model(ABC):
|
|
|
325
380
|
return # Success, exit the retry loop
|
|
326
381
|
except ModelProviderError as e:
|
|
327
382
|
last_exception = e
|
|
383
|
+
# Check if error is non-retryable
|
|
384
|
+
if not self._is_retryable_error(e):
|
|
385
|
+
log_error(f"Non-retryable model provider error: {e}")
|
|
386
|
+
raise
|
|
328
387
|
if attempt < self.retries:
|
|
329
388
|
delay = self._get_retry_delay(attempt)
|
|
330
389
|
log_warning(
|
|
@@ -860,6 +860,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
|
|
|
860
860
|
"name": "TextReader",
|
|
861
861
|
"description": "Reads text files",
|
|
862
862
|
"chunkers": [
|
|
863
|
+
"CodeChunker",
|
|
863
864
|
"FixedSizeChunker",
|
|
864
865
|
"AgenticChunker",
|
|
865
866
|
"DocumentChunker",
|
|
@@ -898,6 +899,12 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
|
|
|
898
899
|
"description": "Chunking strategy that uses an LLM to determine natural breakpoints in the text",
|
|
899
900
|
"metadata": {"chunk_size": 5000},
|
|
900
901
|
},
|
|
902
|
+
"CodeChunker": {
|
|
903
|
+
"key": "CodeChunker",
|
|
904
|
+
"name": "CodeChunker",
|
|
905
|
+
"description": "The CodeChunker splits code into chunks based on its structure, leveraging Abstract Syntax Trees (ASTs) to create contextually relevant segments",
|
|
906
|
+
"metadata": {"chunk_size": 2048},
|
|
907
|
+
},
|
|
901
908
|
"DocumentChunker": {
|
|
902
909
|
"key": "DocumentChunker",
|
|
903
910
|
"name": "DocumentChunker",
|
agno/tools/browserbase.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
from os import getenv
|
|
3
4
|
from typing import Any, Dict, List, Optional
|
|
4
5
|
|
|
@@ -22,6 +23,8 @@ class BrowserbaseTools(Toolkit):
|
|
|
22
23
|
enable_get_page_content: bool = True,
|
|
23
24
|
enable_close_session: bool = True,
|
|
24
25
|
all: bool = False,
|
|
26
|
+
parse_html: bool = True,
|
|
27
|
+
max_content_length: Optional[int] = 100000,
|
|
25
28
|
**kwargs,
|
|
26
29
|
):
|
|
27
30
|
"""Initialize BrowserbaseTools.
|
|
@@ -36,7 +39,14 @@ class BrowserbaseTools(Toolkit):
|
|
|
36
39
|
enable_get_page_content (bool): Enable the get_page_content tool. Defaults to True.
|
|
37
40
|
enable_close_session (bool): Enable the close_session tool. Defaults to True.
|
|
38
41
|
all (bool): Enable all tools. Defaults to False.
|
|
42
|
+
parse_html (bool): If True, extract only visible text content instead of raw HTML. Defaults to True.
|
|
43
|
+
This significantly reduces token usage and is recommended for most use cases.
|
|
44
|
+
max_content_length (int, optional): Maximum character length for page content. Defaults to 100000.
|
|
45
|
+
Content exceeding this limit will be truncated with a notice. Set to None for no limit.
|
|
39
46
|
"""
|
|
47
|
+
self.parse_html = parse_html
|
|
48
|
+
self.max_content_length = max_content_length
|
|
49
|
+
|
|
40
50
|
self.api_key = api_key or getenv("BROWSERBASE_API_KEY")
|
|
41
51
|
if not self.api_key:
|
|
42
52
|
raise ValueError(
|
|
@@ -191,18 +201,70 @@ class BrowserbaseTools(Toolkit):
|
|
|
191
201
|
self._cleanup()
|
|
192
202
|
raise e
|
|
193
203
|
|
|
204
|
+
def _extract_text_content(self, html: str) -> str:
|
|
205
|
+
"""Extract visible text content from HTML, removing scripts, styles, and tags.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
html: Raw HTML content
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Cleaned text content
|
|
212
|
+
"""
|
|
213
|
+
# Remove script and style elements
|
|
214
|
+
html = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
215
|
+
html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
216
|
+
# Remove HTML comments
|
|
217
|
+
html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
|
|
218
|
+
# Remove all HTML tags
|
|
219
|
+
html = re.sub(r"<[^>]+>", " ", html)
|
|
220
|
+
# Decode common HTML entities
|
|
221
|
+
html = html.replace(" ", " ")
|
|
222
|
+
html = html.replace("&", "&")
|
|
223
|
+
html = html.replace("<", "<")
|
|
224
|
+
html = html.replace(">", ">")
|
|
225
|
+
html = html.replace(""", '"')
|
|
226
|
+
html = html.replace("'", "'")
|
|
227
|
+
# Normalize whitespace
|
|
228
|
+
html = re.sub(r"\s+", " ", html)
|
|
229
|
+
return html.strip()
|
|
230
|
+
|
|
231
|
+
def _truncate_content(self, content: str) -> str:
|
|
232
|
+
"""Truncate content if it exceeds max_content_length.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
content: The content to potentially truncate
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Original or truncated content with notice
|
|
239
|
+
"""
|
|
240
|
+
if self.max_content_length is None or len(content) <= self.max_content_length:
|
|
241
|
+
return content
|
|
242
|
+
|
|
243
|
+
truncated = content[: self.max_content_length]
|
|
244
|
+
return f"{truncated}\n\n[Content truncated. Original length: {len(content)} characters. Showing first {self.max_content_length} characters.]"
|
|
245
|
+
|
|
194
246
|
def get_page_content(self, connect_url: Optional[str] = None) -> str:
|
|
195
|
-
"""Gets the
|
|
247
|
+
"""Gets the content of the current page.
|
|
196
248
|
|
|
197
249
|
Args:
|
|
198
250
|
connect_url (str, optional): The connection URL from an existing session
|
|
199
251
|
|
|
200
252
|
Returns:
|
|
201
|
-
The page HTML
|
|
253
|
+
The page content (text-only if parse_html=True, otherwise raw HTML)
|
|
202
254
|
"""
|
|
203
255
|
try:
|
|
204
256
|
self._initialize_browser(connect_url)
|
|
205
|
-
|
|
257
|
+
if not self._page:
|
|
258
|
+
return ""
|
|
259
|
+
|
|
260
|
+
raw_content = self._page.content()
|
|
261
|
+
|
|
262
|
+
if self.parse_html:
|
|
263
|
+
content = self._extract_text_content(raw_content)
|
|
264
|
+
else:
|
|
265
|
+
content = raw_content
|
|
266
|
+
|
|
267
|
+
return self._truncate_content(content)
|
|
206
268
|
except Exception as e:
|
|
207
269
|
self._cleanup()
|
|
208
270
|
raise e
|
|
@@ -307,17 +369,27 @@ class BrowserbaseTools(Toolkit):
|
|
|
307
369
|
raise e
|
|
308
370
|
|
|
309
371
|
async def aget_page_content(self, connect_url: Optional[str] = None) -> str:
|
|
310
|
-
"""Gets the
|
|
372
|
+
"""Gets the content of the current page asynchronously.
|
|
311
373
|
|
|
312
374
|
Args:
|
|
313
375
|
connect_url (str, optional): The connection URL from an existing session
|
|
314
376
|
|
|
315
377
|
Returns:
|
|
316
|
-
The page HTML
|
|
378
|
+
The page content (text-only if parse_html=True, otherwise raw HTML)
|
|
317
379
|
"""
|
|
318
380
|
try:
|
|
319
381
|
await self._ainitialize_browser(connect_url)
|
|
320
|
-
|
|
382
|
+
if not self._async_page:
|
|
383
|
+
return ""
|
|
384
|
+
|
|
385
|
+
raw_content = await self._async_page.content()
|
|
386
|
+
|
|
387
|
+
if self.parse_html:
|
|
388
|
+
content = self._extract_text_content(raw_content)
|
|
389
|
+
else:
|
|
390
|
+
content = raw_content
|
|
391
|
+
|
|
392
|
+
return self._truncate_content(content)
|
|
321
393
|
except Exception as e:
|
|
322
394
|
await self._acleanup()
|
|
323
395
|
raise e
|
agno/tools/google_bigquery.py
CHANGED
|
@@ -11,6 +11,15 @@ except ImportError:
|
|
|
11
11
|
raise ImportError("`bigquery` not installed. Please install using `pip install google-cloud-bigquery`")
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _clean_sql(sql: str) -> str:
|
|
15
|
+
"""Clean SQL query by normalizing whitespace while preserving token boundaries.
|
|
16
|
+
|
|
17
|
+
Replaces newlines with spaces (not empty strings) to prevent line comments
|
|
18
|
+
from swallowing subsequent SQL statements.
|
|
19
|
+
"""
|
|
20
|
+
return sql.replace("\\n", " ").replace("\n", " ")
|
|
21
|
+
|
|
22
|
+
|
|
14
23
|
class GoogleBigQueryTools(Toolkit):
|
|
15
24
|
def __init__(
|
|
16
25
|
self,
|
|
@@ -106,12 +115,12 @@ class GoogleBigQueryTools(Toolkit):
|
|
|
106
115
|
"""
|
|
107
116
|
try:
|
|
108
117
|
log_debug(f"Running Google SQL |\n{sql}")
|
|
109
|
-
cleaned_query = sql
|
|
118
|
+
cleaned_query = _clean_sql(sql)
|
|
110
119
|
job_config = bigquery.QueryJobConfig(default_dataset=f"{self.project}.{self.dataset}")
|
|
111
120
|
query_job = self.client.query(cleaned_query, job_config)
|
|
112
121
|
results = query_job.result()
|
|
113
122
|
results_str = str([dict(row) for row in results])
|
|
114
|
-
return results_str.replace("
|
|
123
|
+
return results_str.replace("\n", " ")
|
|
115
124
|
except Exception as e:
|
|
116
125
|
logger.error(f"Error while executing SQL: {e}")
|
|
117
126
|
return ""
|
agno/utils/agent.py
CHANGED
|
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
|
|
|
30
30
|
async def await_for_open_threads(
|
|
31
31
|
memory_task: Optional[Task] = None,
|
|
32
32
|
cultural_knowledge_task: Optional[Task] = None,
|
|
33
|
+
learning_task: Optional[Task] = None,
|
|
33
34
|
) -> None:
|
|
34
35
|
if memory_task is not None:
|
|
35
36
|
try:
|
|
@@ -43,9 +44,17 @@ async def await_for_open_threads(
|
|
|
43
44
|
except Exception as e:
|
|
44
45
|
log_warning(f"Error in cultural knowledge creation: {str(e)}")
|
|
45
46
|
|
|
47
|
+
if learning_task is not None:
|
|
48
|
+
try:
|
|
49
|
+
await learning_task
|
|
50
|
+
except Exception as e:
|
|
51
|
+
log_warning(f"Error in learning extraction: {str(e)}")
|
|
52
|
+
|
|
46
53
|
|
|
47
54
|
def wait_for_open_threads(
|
|
48
|
-
memory_future: Optional[Future] = None,
|
|
55
|
+
memory_future: Optional[Future] = None,
|
|
56
|
+
cultural_knowledge_future: Optional[Future] = None,
|
|
57
|
+
learning_future: Optional[Future] = None,
|
|
49
58
|
) -> None:
|
|
50
59
|
if memory_future is not None:
|
|
51
60
|
try:
|
|
@@ -60,11 +69,18 @@ def wait_for_open_threads(
|
|
|
60
69
|
except Exception as e:
|
|
61
70
|
log_warning(f"Error in cultural knowledge creation: {str(e)}")
|
|
62
71
|
|
|
72
|
+
if learning_future is not None:
|
|
73
|
+
try:
|
|
74
|
+
learning_future.result()
|
|
75
|
+
except Exception as e:
|
|
76
|
+
log_warning(f"Error in learning extraction: {str(e)}")
|
|
77
|
+
|
|
63
78
|
|
|
64
79
|
async def await_for_thread_tasks_stream(
|
|
65
80
|
run_response: Union[RunOutput, TeamRunOutput],
|
|
66
81
|
memory_task: Optional[Task] = None,
|
|
67
82
|
cultural_knowledge_task: Optional[Task] = None,
|
|
83
|
+
learning_task: Optional[Task] = None,
|
|
68
84
|
stream_events: bool = False,
|
|
69
85
|
events_to_skip: Optional[List[RunEvent]] = None,
|
|
70
86
|
store_events: bool = False,
|
|
@@ -111,11 +127,18 @@ async def await_for_thread_tasks_stream(
|
|
|
111
127
|
except Exception as e:
|
|
112
128
|
log_warning(f"Error in cultural knowledge creation: {str(e)}")
|
|
113
129
|
|
|
130
|
+
if learning_task is not None:
|
|
131
|
+
try:
|
|
132
|
+
await learning_task
|
|
133
|
+
except Exception as e:
|
|
134
|
+
log_warning(f"Error in learning extraction: {str(e)}")
|
|
135
|
+
|
|
114
136
|
|
|
115
137
|
def wait_for_thread_tasks_stream(
|
|
116
138
|
run_response: Union[TeamRunOutput, RunOutput],
|
|
117
139
|
memory_future: Optional[Future] = None,
|
|
118
140
|
cultural_knowledge_future: Optional[Future] = None,
|
|
141
|
+
learning_future: Optional[Future] = None,
|
|
119
142
|
stream_events: bool = False,
|
|
120
143
|
events_to_skip: Optional[List[RunEvent]] = None,
|
|
121
144
|
store_events: bool = False,
|
|
@@ -164,6 +187,12 @@ def wait_for_thread_tasks_stream(
|
|
|
164
187
|
except Exception as e:
|
|
165
188
|
log_warning(f"Error in cultural knowledge creation: {str(e)}")
|
|
166
189
|
|
|
190
|
+
if learning_future is not None:
|
|
191
|
+
try:
|
|
192
|
+
learning_future.result()
|
|
193
|
+
except Exception as e:
|
|
194
|
+
log_warning(f"Error in learning extraction: {str(e)}")
|
|
195
|
+
|
|
167
196
|
|
|
168
197
|
def collect_joint_images(
|
|
169
198
|
run_input: Optional[RunInput] = None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agno
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.25
|
|
4
4
|
Summary: Agno: a lightweight library for building Multi-Agent Systems
|
|
5
5
|
Author-email: Ashpreet Bedi <ashpreet@agno.com>
|
|
6
6
|
Project-URL: homepage, https://agno.com
|
|
@@ -264,7 +264,8 @@ Requires-Dist: unstructured; extra == "markdown"
|
|
|
264
264
|
Requires-Dist: markdown; extra == "markdown"
|
|
265
265
|
Requires-Dist: aiofiles; extra == "markdown"
|
|
266
266
|
Provides-Extra: chonkie
|
|
267
|
-
Requires-Dist: chonkie[
|
|
267
|
+
Requires-Dist: chonkie[semantic]; extra == "chonkie"
|
|
268
|
+
Requires-Dist: chonkie[code]; extra == "chonkie"
|
|
268
269
|
Requires-Dist: chonkie; extra == "chonkie"
|
|
269
270
|
Provides-Extra: agui
|
|
270
271
|
Requires-Dist: ag-ui-protocol; extra == "agui"
|
|
@@ -400,6 +401,27 @@ Requires-Dist: yfinance; extra == "integration-tests"
|
|
|
400
401
|
Requires-Dist: sqlalchemy; extra == "integration-tests"
|
|
401
402
|
Requires-Dist: Pillow; extra == "integration-tests"
|
|
402
403
|
Requires-Dist: fastmcp; extra == "integration-tests"
|
|
404
|
+
Provides-Extra: demo
|
|
405
|
+
Requires-Dist: anthropic; extra == "demo"
|
|
406
|
+
Requires-Dist: chromadb; extra == "demo"
|
|
407
|
+
Requires-Dist: ddgs; extra == "demo"
|
|
408
|
+
Requires-Dist: fastapi[standard]; extra == "demo"
|
|
409
|
+
Requires-Dist: google-genai; extra == "demo"
|
|
410
|
+
Requires-Dist: mcp; extra == "demo"
|
|
411
|
+
Requires-Dist: nest_asyncio; extra == "demo"
|
|
412
|
+
Requires-Dist: openai; extra == "demo"
|
|
413
|
+
Requires-Dist: openinference-instrumentation-agno; extra == "demo"
|
|
414
|
+
Requires-Dist: opentelemetry-api; extra == "demo"
|
|
415
|
+
Requires-Dist: opentelemetry-sdk; extra == "demo"
|
|
416
|
+
Requires-Dist: pandas; extra == "demo"
|
|
417
|
+
Requires-Dist: parallel-web; extra == "demo"
|
|
418
|
+
Requires-Dist: pgvector; extra == "demo"
|
|
419
|
+
Requires-Dist: pillow; extra == "demo"
|
|
420
|
+
Requires-Dist: psycopg[binary]; extra == "demo"
|
|
421
|
+
Requires-Dist: pypdf; extra == "demo"
|
|
422
|
+
Requires-Dist: sqlalchemy; extra == "demo"
|
|
423
|
+
Requires-Dist: yfinance; extra == "demo"
|
|
424
|
+
Requires-Dist: youtube-transcript-api; extra == "demo"
|
|
403
425
|
Dynamic: license-file
|
|
404
426
|
|
|
405
427
|
<div align="center" id="top">
|