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.
Files changed (64) hide show
  1. agno/agent/agent.py +297 -11
  2. agno/db/base.py +214 -0
  3. agno/db/dynamo/dynamo.py +47 -0
  4. agno/db/firestore/firestore.py +47 -0
  5. agno/db/gcs_json/gcs_json_db.py +47 -0
  6. agno/db/in_memory/in_memory_db.py +47 -0
  7. agno/db/json/json_db.py +47 -0
  8. agno/db/mongo/async_mongo.py +229 -0
  9. agno/db/mongo/mongo.py +47 -0
  10. agno/db/mongo/schemas.py +16 -0
  11. agno/db/mysql/async_mysql.py +47 -0
  12. agno/db/mysql/mysql.py +47 -0
  13. agno/db/postgres/async_postgres.py +231 -0
  14. agno/db/postgres/postgres.py +239 -0
  15. agno/db/postgres/schemas.py +19 -0
  16. agno/db/redis/redis.py +47 -0
  17. agno/db/singlestore/singlestore.py +47 -0
  18. agno/db/sqlite/async_sqlite.py +242 -0
  19. agno/db/sqlite/schemas.py +18 -0
  20. agno/db/sqlite/sqlite.py +239 -0
  21. agno/db/surrealdb/surrealdb.py +47 -0
  22. agno/knowledge/chunking/code.py +90 -0
  23. agno/knowledge/chunking/document.py +62 -2
  24. agno/knowledge/chunking/strategy.py +14 -0
  25. agno/knowledge/knowledge.py +7 -1
  26. agno/knowledge/reader/arxiv_reader.py +1 -0
  27. agno/knowledge/reader/csv_reader.py +1 -0
  28. agno/knowledge/reader/docx_reader.py +1 -0
  29. agno/knowledge/reader/firecrawl_reader.py +1 -0
  30. agno/knowledge/reader/json_reader.py +1 -0
  31. agno/knowledge/reader/markdown_reader.py +1 -0
  32. agno/knowledge/reader/pdf_reader.py +1 -0
  33. agno/knowledge/reader/pptx_reader.py +1 -0
  34. agno/knowledge/reader/s3_reader.py +1 -0
  35. agno/knowledge/reader/tavily_reader.py +1 -0
  36. agno/knowledge/reader/text_reader.py +1 -0
  37. agno/knowledge/reader/web_search_reader.py +1 -0
  38. agno/knowledge/reader/website_reader.py +1 -0
  39. agno/knowledge/reader/wikipedia_reader.py +1 -0
  40. agno/knowledge/reader/youtube_reader.py +1 -0
  41. agno/knowledge/utils.py +1 -0
  42. agno/learn/__init__.py +65 -0
  43. agno/learn/config.py +463 -0
  44. agno/learn/curate.py +185 -0
  45. agno/learn/machine.py +690 -0
  46. agno/learn/schemas.py +1043 -0
  47. agno/learn/stores/__init__.py +35 -0
  48. agno/learn/stores/entity_memory.py +3275 -0
  49. agno/learn/stores/learned_knowledge.py +1583 -0
  50. agno/learn/stores/protocol.py +117 -0
  51. agno/learn/stores/session_context.py +1217 -0
  52. agno/learn/stores/user_memory.py +1495 -0
  53. agno/learn/stores/user_profile.py +1220 -0
  54. agno/learn/utils.py +209 -0
  55. agno/models/base.py +59 -0
  56. agno/os/routers/knowledge/knowledge.py +7 -0
  57. agno/tools/browserbase.py +78 -6
  58. agno/tools/google_bigquery.py +11 -2
  59. agno/utils/agent.py +30 -1
  60. {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/METADATA +24 -2
  61. {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/RECORD +64 -50
  62. {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/WHEEL +0 -0
  63. {agno-2.3.24.dist-info → agno-2.3.25.dist-info}/licenses/LICENSE +0 -0
  64. {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("&nbsp;", " ")
222
+ html = html.replace("&amp;", "&")
223
+ html = html.replace("&lt;", "<")
224
+ html = html.replace("&gt;", ">")
225
+ html = html.replace("&quot;", '"')
226
+ html = html.replace("&#39;", "'")
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 HTML content of the current page.
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 content
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
- return self._page.content() if self._page else ""
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 HTML content of the current page asynchronously.
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 content
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
- return await self._async_page.content() if self._async_page else ""
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
@@ -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.replace("\\n", " ").replace("\n", "").replace("\\", "")
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("\\", "").replace("\n", "")
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, cultural_knowledge_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.24
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[st]; extra == "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">