agno 2.2.13__py3-none-any.whl → 2.4.3__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 (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
@@ -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.
@@ -35,9 +102,9 @@ class MarkdownChunking(ChunkingStrategy):
35
102
  elements = partition_md(filename=temp_file_path)
36
103
 
37
104
  if not elements:
38
- return self.clean_text(content).split("\n\n")
105
+ raw_paragraphs = content.split("\n\n")
106
+ return [self.clean_text(para) for para in raw_paragraphs]
39
107
 
40
- # Chunk by title with some default values
41
108
  chunked_elements = chunk_by_title(
42
109
  elements=elements,
43
110
  max_characters=self.chunk_size,
@@ -57,7 +124,10 @@ class MarkdownChunking(ChunkingStrategy):
57
124
  if chunk_text.strip():
58
125
  text_chunks.append(chunk_text.strip())
59
126
 
60
- return text_chunks if text_chunks else self.clean_text(content).split("\n\n")
127
+ if text_chunks:
128
+ return text_chunks
129
+ raw_paragraphs = content.split("\n\n")
130
+ return [self.clean_text(para) for para in raw_paragraphs]
61
131
 
62
132
  # Always clean up the temporary file
63
133
  finally:
@@ -65,11 +135,18 @@ class MarkdownChunking(ChunkingStrategy):
65
135
 
66
136
  # Fallback to simple paragraph splitting if the markdown chunking fails
67
137
  except Exception:
68
- return self.clean_text(content).split("\n\n")
138
+ raw_paragraphs = content.split("\n\n")
139
+ return [self.clean_text(para) for para in raw_paragraphs]
69
140
 
70
141
  def chunk(self, document: Document) -> List[Document]:
71
142
  """Split markdown document into chunks based on markdown structure"""
72
- 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:
73
150
  return [document]
74
151
 
75
152
  # Split using markdown chunking logic, or fallback to paragraphs
@@ -85,7 +162,20 @@ class MarkdownChunking(ChunkingStrategy):
85
162
  section = section.strip()
86
163
  section_size = len(section)
87
164
 
88
- 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:
89
179
  current_chunk.append(section)
90
180
  current_size += section_size
91
181
  else:
@@ -109,7 +199,8 @@ class MarkdownChunking(ChunkingStrategy):
109
199
  current_chunk = [section]
110
200
  current_size = section_size
111
201
 
112
- if current_chunk:
202
+ # Handle remaining content (only when not split_on_headings)
203
+ if current_chunk and not self.split_on_headings:
113
204
  meta_data = chunk_meta_data.copy()
114
205
  meta_data["chunk"] = chunk_number
115
206
  chunk_id = None
@@ -31,7 +31,7 @@ class RecursiveChunking(ChunkingStrategy):
31
31
  start = 0
32
32
  chunk_meta_data = document.meta_data
33
33
  chunk_number = 1
34
- content = self.clean_text(document.content)
34
+ content = document.content
35
35
 
36
36
  while start < len(content):
37
37
  end = min(start + self.chunk_size, len(content))
@@ -43,7 +43,7 @@ class RecursiveChunking(ChunkingStrategy):
43
43
  end = start + last_sep + 1
44
44
  break
45
45
 
46
- chunk = content[start:end]
46
+ chunk = self.clean_text(content[start:end])
47
47
  meta_data = chunk_meta_data.copy()
48
48
  meta_data["chunk"] = chunk_number
49
49
  chunk_id = None
@@ -1,63 +1,145 @@
1
- import inspect
2
- from typing import Any, Dict, List, Optional
1
+ from typing import Any, Dict, List, Literal, Optional, Union
2
+
3
+ try:
4
+ import numpy as np
5
+ except ImportError:
6
+ raise ImportError("`numpy` not installed. Please install using `pip install numpy`")
7
+
8
+ try:
9
+ from chonkie import SemanticChunker
10
+ from chonkie.embeddings.base import BaseEmbeddings
11
+ except ImportError:
12
+ raise ImportError(
13
+ "`chonkie` is required for semantic chunking. "
14
+ 'Please install it using `pip install "chonkie[semantic]"` to use SemanticChunking.'
15
+ )
3
16
 
4
17
  from agno.knowledge.chunking.strategy import ChunkingStrategy
5
18
  from agno.knowledge.document.base import Document
6
19
  from agno.knowledge.embedder.base import Embedder
7
- from agno.knowledge.embedder.openai import OpenAIEmbedder
20
+ from agno.utils.log import log_debug
8
21
 
9
22
 
10
- class SemanticChunking(ChunkingStrategy):
11
- """Chunking strategy that splits text into semantic chunks using chonkie"""
23
+ def _get_chonkie_embedder_wrapper(embedder: Embedder):
24
+ """Create a wrapper that adapts Agno Embedder to chonkie's BaseEmbeddings interface."""
25
+
26
+ class _ChonkieEmbedderWrapper(BaseEmbeddings):
27
+ """Wrapper to make Agno Embedders compatible with chonkie."""
28
+
29
+ def __init__(self, agno_embedder: Embedder):
30
+ super().__init__()
31
+ self._embedder = agno_embedder
32
+
33
+ def embed(self, text: str):
34
+ embedding = self._embedder.get_embedding(text) # type: ignore[attr-defined]
35
+ return np.array(embedding, dtype=np.float32)
36
+
37
+ def get_tokenizer(self):
38
+ """Return a simple token counter function."""
39
+ return lambda text: len(text.split())
12
40
 
13
- def __init__(self, embedder: Optional[Embedder] = None, chunk_size: int = 5000, similarity_threshold: float = 0.5):
14
- self.embedder = embedder or OpenAIEmbedder(id="text-embedding-3-small") # type: ignore
41
+ @property
42
+ def dimension(self) -> int:
43
+ return getattr(self._embedder, "dimensions")
44
+
45
+ return _ChonkieEmbedderWrapper(embedder)
46
+
47
+
48
+ class SemanticChunking(ChunkingStrategy):
49
+ """Chunking strategy that splits text into semantic chunks using chonkie.
50
+
51
+ Args:
52
+ embedder: The embedder to use for generating embeddings. Can be:
53
+ - A string model identifier (e.g., "minishlab/potion-base-32M") for chonkie's built-in models
54
+ - A chonkie BaseEmbeddings instance (used directly)
55
+ - An Agno Embedder (wrapped for chonkie compatibility)
56
+ chunk_size: Maximum tokens allowed per chunk.
57
+ similarity_threshold: Threshold for semantic similarity (0-1).
58
+ similarity_window: Number of sentences to consider for similarity calculation.
59
+ min_sentences_per_chunk: Minimum number of sentences per chunk.
60
+ min_characters_per_sentence: Minimum number of characters per sentence.
61
+ delimiters: Delimiters to use for sentence splitting.
62
+ include_delimiters: Whether to include delimiter in prev/next sentence or None.
63
+ skip_window: Number of groups to skip when merging (0=disabled).
64
+ filter_window: Window length for the Savitzky-Golay filter.
65
+ filter_polyorder: Polynomial order for the Savitzky-Golay filter.
66
+ filter_tolerance: Tolerance for the Savitzky-Golay filter.
67
+ chunker_params: Additional parameters to pass to chonkie's SemanticChunker.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ embedder: Optional[Union[str, Embedder, BaseEmbeddings]] = None,
73
+ chunk_size: int = 5000,
74
+ similarity_threshold: float = 0.5,
75
+ similarity_window: int = 3,
76
+ min_sentences_per_chunk: int = 1,
77
+ min_characters_per_sentence: int = 24,
78
+ delimiters: Optional[List[str]] = None,
79
+ include_delimiters: Literal["prev", "next", None] = "prev",
80
+ skip_window: int = 0,
81
+ filter_window: int = 5,
82
+ filter_polyorder: int = 3,
83
+ filter_tolerance: float = 0.2,
84
+ chunker_params: Optional[Dict[str, Any]] = None,
85
+ ):
86
+ if embedder is None:
87
+ from agno.knowledge.embedder.openai import OpenAIEmbedder
88
+
89
+ embedder = OpenAIEmbedder() # type: ignore
90
+ log_debug("Embedder not provided, using OpenAIEmbedder as default.")
91
+ self.embedder = embedder
15
92
  self.chunk_size = chunk_size
16
93
  self.similarity_threshold = similarity_threshold
17
- self.chunker = None # Will be initialized lazily when needed
94
+ self.similarity_window = similarity_window
95
+ self.min_sentences_per_chunk = min_sentences_per_chunk
96
+ self.min_characters_per_sentence = min_characters_per_sentence
97
+ self.delimiters = delimiters if delimiters is not None else [". ", "! ", "? ", "\n"]
98
+ self.include_delimiters = include_delimiters
99
+ self.skip_window = skip_window
100
+ self.filter_window = filter_window
101
+ self.filter_polyorder = filter_polyorder
102
+ self.filter_tolerance = filter_tolerance
103
+ self.chunker_params = chunker_params
104
+ self.chunker: Optional[SemanticChunker] = None
18
105
 
19
106
  def _initialize_chunker(self):
20
107
  """Lazily initialize the chunker with chonkie dependency."""
21
- if self.chunker is None:
22
- try:
23
- from chonkie import SemanticChunker
24
- except ImportError:
25
- raise ImportError(
26
- "`chonkie` is required for semantic chunking. "
27
- "Please install it using `pip install chonkie` to use SemanticChunking."
28
- )
29
-
30
- # Build arguments dynamically based on chonkie's supported signature
31
- params: Dict[str, Any] = {
32
- "chunk_size": self.chunk_size,
33
- "threshold": self.similarity_threshold,
34
- }
35
-
36
- try:
37
- sig = inspect.signature(SemanticChunker)
38
- param_names = set(sig.parameters.keys())
39
-
40
- # Prefer passing a callable to avoid Chonkie initializing its own client
41
- if "embedding_fn" in param_names:
42
- params["embedding_fn"] = self.embedder.get_embedding # type: ignore[attr-defined]
43
- # If chonkie allows specifying dimensions, provide them
44
- if "embedding_dimensions" in param_names and getattr(self.embedder, "dimensions", None):
45
- params["embedding_dimensions"] = self.embedder.dimensions # type: ignore[attr-defined]
46
- elif "embedder" in param_names:
47
- # Some versions may accept an embedder object directly
48
- params["embedder"] = self.embedder
49
- else:
50
- # Fallback to model id
51
- params["embedding_model"] = getattr(self.embedder, "id", None) or "text-embedding-3-small"
52
-
53
- self.chunker = SemanticChunker(**params)
54
- except Exception:
55
- # As a final fallback, use the original behavior
56
- self.chunker = SemanticChunker(
57
- embedding_model=getattr(self.embedder, "id", None) or "text-embedding-3-small",
58
- chunk_size=self.chunk_size,
59
- threshold=self.similarity_threshold,
60
- )
108
+ if self.chunker is not None:
109
+ return
110
+
111
+ # Determine embedding model based on type:
112
+ # - str: pass directly to chonkie (uses chonkie's built-in models)
113
+ # - BaseEmbeddings: pass directly to chonkie
114
+ # - Agno Embedder: wrap for chonkie compatibility
115
+ embedding_model: Union[str, BaseEmbeddings]
116
+ if isinstance(self.embedder, str):
117
+ embedding_model = self.embedder
118
+ elif isinstance(self.embedder, BaseEmbeddings):
119
+ embedding_model = self.embedder
120
+ elif isinstance(self.embedder, Embedder):
121
+ embedding_model = _get_chonkie_embedder_wrapper(self.embedder)
122
+ else:
123
+ raise ValueError("Invalid embedder type. Must be a string, BaseEmbeddings, or Embedder instance.")
124
+
125
+ _chunker_params: Dict[str, Any] = {
126
+ "embedding_model": embedding_model,
127
+ "chunk_size": self.chunk_size,
128
+ "threshold": self.similarity_threshold,
129
+ "similarity_window": self.similarity_window,
130
+ "min_sentences_per_chunk": self.min_sentences_per_chunk,
131
+ "min_characters_per_sentence": self.min_characters_per_sentence,
132
+ "delim": self.delimiters,
133
+ "include_delim": self.include_delimiters,
134
+ "skip_window": self.skip_window,
135
+ "filter_window": self.filter_window,
136
+ "filter_polyorder": self.filter_polyorder,
137
+ "filter_tolerance": self.filter_tolerance,
138
+ }
139
+ if self.chunker_params:
140
+ _chunker_params.update(self.chunker_params)
141
+
142
+ self.chunker = SemanticChunker(**_chunker_params)
61
143
 
62
144
  def chunk(self, document: Document) -> List[Document]:
63
145
  """Split document into semantic chunks using chonkie"""
@@ -12,6 +12,10 @@ class ChunkingStrategy(ABC):
12
12
  def chunk(self, document: Document) -> List[Document]:
13
13
  raise NotImplementedError
14
14
 
15
+ async def achunk(self, document: Document) -> List[Document]:
16
+ """Async version of chunk. Override for truly async implementations."""
17
+ return self.chunk(document)
18
+
15
19
  def clean_text(self, text: str) -> str:
16
20
  """Clean the text by replacing multiple newlines with a single newline"""
17
21
  import re
@@ -36,6 +40,7 @@ class ChunkingStrategyType(str, Enum):
36
40
  """Enumeration of available chunking strategies."""
37
41
 
38
42
  AGENTIC_CHUNKER = "AgenticChunker"
43
+ CODE_CHUNKER = "CodeChunker"
39
44
  DOCUMENT_CHUNKER = "DocumentChunker"
40
45
  RECURSIVE_CHUNKER = "RecursiveChunker"
41
46
  SEMANTIC_CHUNKER = "SemanticChunker"
@@ -70,6 +75,7 @@ class ChunkingStrategyFactory:
70
75
  """Create an instance of the chunking strategy with the given parameters."""
71
76
  strategy_map = {
72
77
  ChunkingStrategyType.AGENTIC_CHUNKER: cls._create_agentic_chunking,
78
+ ChunkingStrategyType.CODE_CHUNKER: cls._create_code_chunking,
73
79
  ChunkingStrategyType.DOCUMENT_CHUNKER: cls._create_document_chunking,
74
80
  ChunkingStrategyType.RECURSIVE_CHUNKER: cls._create_recursive_chunking,
75
81
  ChunkingStrategyType.SEMANTIC_CHUNKER: cls._create_semantic_chunking,
@@ -91,6 +97,18 @@ class ChunkingStrategyFactory:
91
97
  # Remove overlap since AgenticChunking doesn't support it
92
98
  return AgenticChunking(**kwargs)
93
99
 
100
+ @classmethod
101
+ def _create_code_chunking(
102
+ cls, chunk_size: Optional[int] = None, overlap: Optional[int] = None, **kwargs
103
+ ) -> ChunkingStrategy:
104
+ from agno.knowledge.chunking.code import CodeChunking
105
+
106
+ # CodeChunking accepts chunk_size but not overlap
107
+ if chunk_size is not None:
108
+ kwargs["chunk_size"] = chunk_size
109
+ # Remove overlap since CodeChunking doesn't support it
110
+ return CodeChunking(**kwargs)
111
+
94
112
  @classmethod
95
113
  def _create_document_chunking(
96
114
  cls, chunk_size: Optional[int] = None, overlap: Optional[int] = None, **kwargs
@@ -18,7 +18,6 @@ except ImportError:
18
18
  @dataclass
19
19
  class AzureOpenAIEmbedder(Embedder):
20
20
  id: str = "text-embedding-3-small" # This has to match the model that you deployed at the provided URL
21
-
22
21
  dimensions: int = 1536
23
22
  encoding_format: Literal["float", "base64"] = "float"
24
23
  user: Optional[str] = None
@@ -15,7 +15,7 @@ except ImportError:
15
15
 
16
16
  @dataclass
17
17
  class GeminiEmbedder(Embedder):
18
- id: str = "gemini-embedding-exp-03-07"
18
+ id: str = "gemini-embedding-001"
19
19
  task_type: str = "RETRIEVAL_QUERY"
20
20
  title: Optional[str] = None
21
21
  dimensions: Optional[int] = 1536
@@ -37,7 +37,7 @@ class MistralEmbedder(Embedder):
37
37
  "api_key": self.api_key,
38
38
  "endpoint": self.endpoint,
39
39
  "max_retries": self.max_retries,
40
- "timeout": self.timeout,
40
+ "timeout_ms": self.timeout * 1000 if self.timeout else None,
41
41
  }
42
42
  _client_params = {k: v for k, v in _client_params.items() if v is not None}
43
43
 
@@ -10,4 +10,4 @@ class NebiusEmbedder(OpenAIEmbedder):
10
10
  id: str = "BAAI/bge-en-icl"
11
11
  dimensions: int = 1024
12
12
  api_key: Optional[str] = getenv("NEBIUS_API_KEY")
13
- base_url: str = "https://api.studio.nebius.com/v1/"
13
+ base_url: str = "https://api.tokenfactory.nebius.com/v1/"
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple
4
4
  from typing_extensions import Literal
5
5
 
6
6
  from agno.knowledge.embedder.base import Embedder
7
- from agno.utils.log import logger
7
+ from agno.utils.log import log_info, log_warning
8
8
 
9
9
  try:
10
10
  from openai import AsyncOpenAI
@@ -71,7 +71,8 @@ class OpenAIEmbedder(Embedder):
71
71
  }
72
72
  if self.user is not None:
73
73
  _request_params["user"] = self.user
74
- if self.id.startswith("text-embedding-3"):
74
+ # Pass dimensions for text-embedding-3 models or when using custom base_url (third-party APIs)
75
+ if self.id.startswith("text-embedding-3") or self.base_url is not None:
75
76
  _request_params["dimensions"] = self.dimensions
76
77
  if self.request_params:
77
78
  _request_params.update(self.request_params)
@@ -82,7 +83,7 @@ class OpenAIEmbedder(Embedder):
82
83
  response: CreateEmbeddingResponse = self.response(text=text)
83
84
  return response.data[0].embedding
84
85
  except Exception as e:
85
- logger.warning(e)
86
+ log_warning(e)
86
87
  return []
87
88
 
88
89
  def get_embedding_and_usage(self, text: str) -> Tuple[List[float], Optional[Dict]]:
@@ -95,7 +96,7 @@ class OpenAIEmbedder(Embedder):
95
96
  return embedding, usage.model_dump()
96
97
  return embedding, None
97
98
  except Exception as e:
98
- logger.warning(e)
99
+ log_warning(e)
99
100
  return [], None
100
101
 
101
102
  async def async_get_embedding(self, text: str) -> List[float]:
@@ -106,7 +107,8 @@ class OpenAIEmbedder(Embedder):
106
107
  }
107
108
  if self.user is not None:
108
109
  req["user"] = self.user
109
- if self.id.startswith("text-embedding-3"):
110
+ # Pass dimensions for text-embedding-3 models or when using custom base_url (third-party APIs)
111
+ if self.id.startswith("text-embedding-3") or self.base_url is not None:
110
112
  req["dimensions"] = self.dimensions
111
113
  if self.request_params:
112
114
  req.update(self.request_params)
@@ -115,7 +117,7 @@ class OpenAIEmbedder(Embedder):
115
117
  response: CreateEmbeddingResponse = await self.aclient.embeddings.create(**req)
116
118
  return response.data[0].embedding
117
119
  except Exception as e:
118
- logger.warning(e)
120
+ log_warning(e)
119
121
  return []
120
122
 
121
123
  async def async_get_embedding_and_usage(self, text: str):
@@ -126,7 +128,8 @@ class OpenAIEmbedder(Embedder):
126
128
  }
127
129
  if self.user is not None:
128
130
  req["user"] = self.user
129
- if self.id.startswith("text-embedding-3"):
131
+ # Pass dimensions for text-embedding-3 models or when using custom base_url (third-party APIs)
132
+ if self.id.startswith("text-embedding-3") or self.base_url is not None:
130
133
  req["dimensions"] = self.dimensions
131
134
  if self.request_params:
132
135
  req.update(self.request_params)
@@ -137,7 +140,7 @@ class OpenAIEmbedder(Embedder):
137
140
  usage = response.usage
138
141
  return embedding, usage.model_dump() if usage else None
139
142
  except Exception as e:
140
- logger.warning(e)
143
+ log_warning(f"Error getting embedding: {e}")
141
144
  return [], None
142
145
 
143
146
  async def async_get_embeddings_batch_and_usage(
@@ -154,7 +157,7 @@ class OpenAIEmbedder(Embedder):
154
157
  """
155
158
  all_embeddings = []
156
159
  all_usage = []
157
- logger.info(f"Getting embeddings and usage for {len(texts)} texts in batches of {self.batch_size} (async)")
160
+ log_info(f"Getting embeddings and usage for {len(texts)} texts in batches of {self.batch_size} (async)")
158
161
 
159
162
  for i in range(0, len(texts), self.batch_size):
160
163
  batch_texts = texts[i : i + self.batch_size]
@@ -166,7 +169,8 @@ class OpenAIEmbedder(Embedder):
166
169
  }
167
170
  if self.user is not None:
168
171
  req["user"] = self.user
169
- if self.id.startswith("text-embedding-3"):
172
+ # Pass dimensions for text-embedding-3 models or when using custom base_url (third-party APIs)
173
+ if self.id.startswith("text-embedding-3") or self.base_url is not None:
170
174
  req["dimensions"] = self.dimensions
171
175
  if self.request_params:
172
176
  req.update(self.request_params)
@@ -180,7 +184,7 @@ class OpenAIEmbedder(Embedder):
180
184
  usage_dict = response.usage.model_dump() if response.usage else None
181
185
  all_usage.extend([usage_dict] * len(batch_embeddings))
182
186
  except Exception as e:
183
- logger.warning(f"Error in async batch embedding: {e}")
187
+ log_warning(f"Error in async batch embedding: {e}")
184
188
  # Fallback to individual calls for this batch
185
189
  for text in batch_texts:
186
190
  try:
@@ -188,7 +192,7 @@ class OpenAIEmbedder(Embedder):
188
192
  all_embeddings.append(embedding)
189
193
  all_usage.append(usage)
190
194
  except Exception as e2:
191
- logger.warning(f"Error in individual async embedding fallback: {e2}")
195
+ log_warning(f"Error in individual async embedding fallback: {e2}")
192
196
  all_embeddings.append([])
193
197
  all_usage.append(None)
194
198