agno 2.3.22__py3-none-any.whl → 2.3.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. agno/agent/agent.py +28 -1
  2. agno/agent/remote.py +1 -1
  3. agno/db/mongo/mongo.py +9 -1
  4. agno/db/mysql/async_mysql.py +5 -7
  5. agno/db/mysql/mysql.py +5 -7
  6. agno/db/mysql/schemas.py +39 -21
  7. agno/db/postgres/async_postgres.py +10 -2
  8. agno/db/postgres/postgres.py +5 -7
  9. agno/db/postgres/schemas.py +39 -21
  10. agno/db/singlestore/schemas.py +41 -21
  11. agno/db/singlestore/singlestore.py +14 -3
  12. agno/db/sqlite/async_sqlite.py +7 -2
  13. agno/db/sqlite/schemas.py +36 -21
  14. agno/db/sqlite/sqlite.py +3 -7
  15. agno/knowledge/chunking/markdown.py +94 -8
  16. agno/knowledge/chunking/semantic.py +2 -2
  17. agno/knowledge/knowledge.py +215 -207
  18. agno/models/base.py +32 -8
  19. agno/models/google/gemini.py +27 -4
  20. agno/os/routers/agents/router.py +1 -1
  21. agno/os/routers/evals/evals.py +2 -2
  22. agno/os/routers/knowledge/knowledge.py +21 -5
  23. agno/os/routers/knowledge/schemas.py +1 -1
  24. agno/os/routers/memory/memory.py +4 -4
  25. agno/os/routers/session/session.py +2 -2
  26. agno/os/routers/teams/router.py +2 -2
  27. agno/os/routers/traces/traces.py +3 -3
  28. agno/os/routers/workflows/router.py +1 -1
  29. agno/os/schema.py +1 -1
  30. agno/os/utils.py +1 -1
  31. agno/remote/base.py +1 -1
  32. agno/team/remote.py +1 -1
  33. agno/team/team.py +24 -4
  34. agno/tools/brandfetch.py +27 -18
  35. agno/tools/browserbase.py +150 -13
  36. agno/tools/crawl4ai.py +3 -0
  37. agno/tools/file.py +14 -13
  38. agno/tools/function.py +15 -2
  39. agno/tools/mcp/mcp.py +1 -0
  40. agno/tools/mlx_transcribe.py +10 -7
  41. agno/tools/python.py +14 -6
  42. agno/tools/toolkit.py +122 -23
  43. agno/vectordb/cassandra/cassandra.py +1 -1
  44. agno/vectordb/chroma/chromadb.py +1 -1
  45. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  46. agno/vectordb/couchbase/couchbase.py +1 -1
  47. agno/vectordb/milvus/milvus.py +1 -1
  48. agno/vectordb/mongodb/mongodb.py +13 -3
  49. agno/vectordb/pgvector/pgvector.py +1 -1
  50. agno/vectordb/pineconedb/pineconedb.py +2 -2
  51. agno/vectordb/qdrant/qdrant.py +1 -1
  52. agno/vectordb/redis/redisdb.py +2 -2
  53. agno/vectordb/singlestore/singlestore.py +1 -1
  54. agno/vectordb/surrealdb/surrealdb.py +2 -2
  55. agno/vectordb/weaviate/weaviate.py +1 -1
  56. agno/workflow/remote.py +1 -1
  57. agno/workflow/workflow.py +14 -0
  58. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/METADATA +1 -1
  59. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/RECORD +62 -62
  60. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/WHEEL +0 -0
  61. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/licenses/LICENSE +0 -0
  62. {agno-2.3.22.dist-info → agno-2.3.24.dist-info}/top_level.txt +0 -0
agno/db/sqlite/schemas.py CHANGED
@@ -111,25 +111,36 @@ TRACE_TABLE_SCHEMA = {
111
111
  "created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
112
112
  }
113
113
 
114
- SPAN_TABLE_SCHEMA = {
115
- "span_id": {"type": String, "primary_key": True, "nullable": False},
116
- "trace_id": {
117
- "type": String,
118
- "nullable": False,
119
- "index": True,
120
- "foreign_key": "agno_traces.trace_id", # Foreign key to traces table
121
- },
122
- "parent_span_id": {"type": String, "nullable": True, "index": True},
123
- "name": {"type": String, "nullable": False},
124
- "span_kind": {"type": String, "nullable": False},
125
- "status_code": {"type": String, "nullable": False},
126
- "status_message": {"type": String, "nullable": True},
127
- "start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
128
- "end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
129
- "duration_ms": {"type": BigInteger, "nullable": False},
130
- "attributes": {"type": JSON, "nullable": True},
131
- "created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
132
- }
114
+
115
+ def _get_span_table_schema(traces_table_name: str = "agno_traces") -> dict[str, Any]:
116
+ """Get the span table schema with the correct foreign key reference.
117
+
118
+ Args:
119
+ traces_table_name: The name of the traces table to reference in the foreign key.
120
+
121
+ Returns:
122
+ The span table schema dictionary.
123
+ """
124
+ return {
125
+ "span_id": {"type": String, "primary_key": True, "nullable": False},
126
+ "trace_id": {
127
+ "type": String,
128
+ "nullable": False,
129
+ "index": True,
130
+ "foreign_key": f"{traces_table_name}.trace_id",
131
+ },
132
+ "parent_span_id": {"type": String, "nullable": True, "index": True},
133
+ "name": {"type": String, "nullable": False},
134
+ "span_kind": {"type": String, "nullable": False},
135
+ "status_code": {"type": String, "nullable": False},
136
+ "status_message": {"type": String, "nullable": True},
137
+ "start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
138
+ "end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
139
+ "duration_ms": {"type": BigInteger, "nullable": False},
140
+ "attributes": {"type": JSON, "nullable": True},
141
+ "created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
142
+ }
143
+
133
144
 
134
145
  CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
135
146
  "id": {"type": String, "primary_key": True, "nullable": False},
@@ -152,16 +163,21 @@ VERSIONS_TABLE_SCHEMA = {
152
163
  }
153
164
 
154
165
 
155
- def get_table_schema_definition(table_type: str) -> dict[str, Any]:
166
+ def get_table_schema_definition(table_type: str, traces_table_name: str = "agno_traces") -> dict[str, Any]:
156
167
  """
157
168
  Get the expected schema definition for the given table.
158
169
 
159
170
  Args:
160
171
  table_type (str): The type of table to get the schema for.
172
+ traces_table_name (str): The name of the traces table (used for spans foreign key).
161
173
 
162
174
  Returns:
163
175
  Dict[str, Any]: Dictionary containing column definitions for the table
164
176
  """
177
+ # Handle spans table specially to resolve the foreign key reference
178
+ if table_type == "spans":
179
+ return _get_span_table_schema(traces_table_name)
180
+
165
181
  schemas = {
166
182
  "sessions": SESSION_TABLE_SCHEMA,
167
183
  "evals": EVAL_TABLE_SCHEMA,
@@ -169,7 +185,6 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
169
185
  "memories": USER_MEMORY_TABLE_SCHEMA,
170
186
  "knowledge": KNOWLEDGE_TABLE_SCHEMA,
171
187
  "traces": TRACE_TABLE_SCHEMA,
172
- "spans": SPAN_TABLE_SCHEMA,
173
188
  "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
174
189
  "versions": VERSIONS_TABLE_SCHEMA,
175
190
  }
agno/db/sqlite/sqlite.py CHANGED
@@ -173,7 +173,8 @@ class SqliteDb(BaseDb):
173
173
  Table: SQLAlchemy Table object
174
174
  """
175
175
  try:
176
- table_schema = get_table_schema_definition(table_type).copy()
176
+ # Pass traces_table_name for spans table foreign key resolution
177
+ table_schema = get_table_schema_definition(table_type, traces_table_name=self.trace_table_name).copy()
177
178
 
178
179
  columns: List[Column] = []
179
180
  indexes: List[str] = []
@@ -197,12 +198,7 @@ class SqliteDb(BaseDb):
197
198
 
198
199
  # Handle foreign key constraint
199
200
  if "foreign_key" in col_config:
200
- fk_ref = col_config["foreign_key"]
201
- # For spans table, dynamically replace the traces table reference
202
- # with the actual trace table name configured for this db instance
203
- if table_type == "spans" and "trace_id" in fk_ref:
204
- fk_ref = f"{self.trace_table_name}.trace_id"
205
- column_args.append(ForeignKey(fk_ref))
201
+ column_args.append(ForeignKey(col_config["foreign_key"]))
206
202
 
207
203
  columns.append(Column(*column_args, **column_kwargs)) # type: ignore
208
204
 
@@ -1,6 +1,7 @@
1
1
  import os
2
+ import re
2
3
  import tempfile
3
- from typing import List
4
+ from typing import List, Union
4
5
 
5
6
  try:
6
7
  from unstructured.chunking.title import chunk_by_title # type: ignore
@@ -13,17 +14,83 @@ from agno.knowledge.document.base import Document
13
14
 
14
15
 
15
16
  class MarkdownChunking(ChunkingStrategy):
16
- """A chunking strategy that splits markdown based on structure like headers, paragraphs and sections"""
17
-
18
- def __init__(self, chunk_size: int = 5000, overlap: int = 0):
17
+ """A chunking strategy that splits markdown based on structure like headers, paragraphs and sections
18
+
19
+ Args:
20
+ chunk_size: Maximum size of each chunk in characters
21
+ overlap: Number of characters to overlap between chunks
22
+ split_on_headings: Controls heading-based splitting behavior:
23
+ - False: Use size-based chunking (default)
24
+ - True: Split on all headings (H1-H6)
25
+ - int: Split on headings at or above this level (1-6)
26
+ e.g., 2 splits on H1 and H2, keeping H3-H6 content together
27
+ """
28
+
29
+ def __init__(self, chunk_size: int = 5000, overlap: int = 0, split_on_headings: Union[bool, int] = False):
19
30
  self.chunk_size = chunk_size
20
31
  self.overlap = overlap
32
+ self.split_on_headings = split_on_headings
33
+
34
+ # Validate split_on_headings parameter
35
+ # Note: In Python, isinstance(False, int) is True, so we exclude booleans explicitly
36
+ if isinstance(split_on_headings, int) and not isinstance(split_on_headings, bool):
37
+ if not (1 <= split_on_headings <= 6):
38
+ raise ValueError("split_on_headings must be between 1 and 6 when using integer value")
39
+
40
+ def _split_by_headings(self, content: str) -> List[str]:
41
+ """
42
+ Split markdown content by headings, keeping each heading with its content.
43
+ Returns a list of sections where each section starts with a heading.
44
+
45
+ When split_on_headings is an int, only splits on headings at or above that level.
46
+ For example, split_on_headings=2 splits on H1 and H2, keeping H3-H6 content together.
47
+ """
48
+ # Determine which heading levels to split on
49
+ if isinstance(self.split_on_headings, int) and not isinstance(self.split_on_headings, bool):
50
+ # Split on headings at or above this level (1 to split_on_headings)
51
+ max_heading_level = self.split_on_headings
52
+ heading_pattern = rf"^#{{{1},{max_heading_level}}}\s+.+$"
53
+ else:
54
+ # split_on_headings is True: split on all headings (# to ######)
55
+ heading_pattern = r"^#{1,6}\s+.+$"
56
+
57
+ # Split content while keeping the delimiter (heading)
58
+ # Use non-capturing group for the pattern to avoid extra capture groups
59
+ parts = re.split(f"({heading_pattern})", content, flags=re.MULTILINE)
60
+
61
+ sections = []
62
+ current_section = ""
63
+
64
+ for part in parts:
65
+ if not part or not part.strip():
66
+ continue
67
+
68
+ # Check if this part is a heading
69
+ if re.match(heading_pattern, part.strip(), re.MULTILINE):
70
+ # Save previous section if exists
71
+ if current_section.strip():
72
+ sections.append(current_section.strip())
73
+ # Start new section with this heading
74
+ current_section = part
75
+ else:
76
+ # Add content to current section
77
+ current_section += "\n\n" + part if current_section else part
78
+
79
+ # Don't forget the last section
80
+ if current_section.strip():
81
+ sections.append(current_section.strip())
82
+
83
+ return sections if sections else [content]
21
84
 
22
85
  def _partition_markdown_content(self, content: str) -> List[str]:
23
86
  """
24
87
  Partition markdown content and return a list of text chunks.
25
88
  Falls back to paragraph splitting if the markdown chunking fails.
26
89
  """
90
+ # When split_on_headings is True or an int, use regex-based splitting to preserve headings
91
+ if self.split_on_headings:
92
+ return self._split_by_headings(content)
93
+
27
94
  try:
28
95
  # Create a temporary file with the markdown content.
29
96
  # This is the recommended usage of the unstructured library.
@@ -38,7 +105,6 @@ class MarkdownChunking(ChunkingStrategy):
38
105
  raw_paragraphs = content.split("\n\n")
39
106
  return [self.clean_text(para) for para in raw_paragraphs]
40
107
 
41
- # Chunk by title with some default values
42
108
  chunked_elements = chunk_by_title(
43
109
  elements=elements,
44
110
  max_characters=self.chunk_size,
@@ -74,7 +140,13 @@ class MarkdownChunking(ChunkingStrategy):
74
140
 
75
141
  def chunk(self, document: Document) -> List[Document]:
76
142
  """Split markdown document into chunks based on markdown structure"""
77
- if not document.content or len(document.content) <= self.chunk_size:
143
+ # If content is empty, return as-is
144
+ if not document.content:
145
+ return [document]
146
+
147
+ # When split_on_headings is enabled, always split by headings regardless of size
148
+ # Only skip chunking for small content when using size-based chunking
149
+ if not self.split_on_headings and len(document.content) <= self.chunk_size:
78
150
  return [document]
79
151
 
80
152
  # Split using markdown chunking logic, or fallback to paragraphs
@@ -90,7 +162,20 @@ class MarkdownChunking(ChunkingStrategy):
90
162
  section = section.strip()
91
163
  section_size = len(section)
92
164
 
93
- if current_size + section_size <= self.chunk_size:
165
+ # When split_on_headings is True or an int, each section becomes its own chunk
166
+ if self.split_on_headings:
167
+ meta_data = chunk_meta_data.copy()
168
+ meta_data["chunk"] = chunk_number
169
+ chunk_id = None
170
+ if document.id:
171
+ chunk_id = f"{document.id}_{chunk_number}"
172
+ elif document.name:
173
+ chunk_id = f"{document.name}_{chunk_number}"
174
+ meta_data["chunk_size"] = section_size
175
+
176
+ chunks.append(Document(id=chunk_id, name=document.name, meta_data=meta_data, content=section))
177
+ chunk_number += 1
178
+ elif current_size + section_size <= self.chunk_size:
94
179
  current_chunk.append(section)
95
180
  current_size += section_size
96
181
  else:
@@ -114,7 +199,8 @@ class MarkdownChunking(ChunkingStrategy):
114
199
  current_chunk = [section]
115
200
  current_size = section_size
116
201
 
117
- if current_chunk:
202
+ # Handle remaining content (only when not split_on_headings)
203
+ if current_chunk and not self.split_on_headings:
118
204
  meta_data = chunk_meta_data.copy()
119
205
  meta_data["chunk"] = chunk_number
120
206
  chunk_id = None
@@ -17,7 +17,7 @@ except ImportError:
17
17
  from agno.knowledge.chunking.strategy import ChunkingStrategy
18
18
  from agno.knowledge.document.base import Document
19
19
  from agno.knowledge.embedder.base import Embedder
20
- from agno.utils.log import log_info
20
+ from agno.utils.log import log_debug
21
21
 
22
22
 
23
23
  def _get_chonkie_embedder_wrapper(embedder: Embedder):
@@ -87,7 +87,7 @@ class SemanticChunking(ChunkingStrategy):
87
87
  from agno.knowledge.embedder.openai import OpenAIEmbedder
88
88
 
89
89
  embedder = OpenAIEmbedder() # type: ignore
90
- log_info("Embedder not provided, using OpenAIEmbedder as default.")
90
+ log_debug("Embedder not provided, using OpenAIEmbedder as default.")
91
91
  self.embedder = embedder
92
92
  self.chunk_size = chunk_size
93
93
  self.similarity_threshold = similarity_threshold