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,9 @@
1
1
  from datetime import date, datetime, timedelta, timezone
2
2
  from textwrap import dedent
3
- from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from agno.tracing.schemas import Span, Trace
4
7
 
5
8
  from agno.db.base import BaseDb, SessionType
6
9
  from agno.db.postgres.utils import (
@@ -64,6 +67,8 @@ class SurrealDb(BaseDb):
64
67
  eval_table: Optional[str] = None,
65
68
  knowledge_table: Optional[str] = None,
66
69
  culture_table: Optional[str] = None,
70
+ traces_table: Optional[str] = None,
71
+ spans_table: Optional[str] = None,
67
72
  id: Optional[str] = None,
68
73
  ):
69
74
  """
@@ -71,6 +76,19 @@ class SurrealDb(BaseDb):
71
76
 
72
77
  Args:
73
78
  client: A blocking connection, either HTTP or WS
79
+ db_url: The URL of the SurrealDB database.
80
+ db_creds: The credentials for the SurrealDB database.
81
+ db_ns: The namespace for the SurrealDB database.
82
+ db_db: The database name for the SurrealDB database.
83
+ session_table: The name of the session table.
84
+ memory_table: The name of the memory table.
85
+ metrics_table: The name of the metrics table.
86
+ eval_table: The name of the eval table.
87
+ knowledge_table: The name of the knowledge table.
88
+ culture_table: The name of the culture table.
89
+ traces_table: The name of the traces table.
90
+ spans_table: The name of the spans table.
91
+ id: The ID of the database.
74
92
  """
75
93
  if id is None:
76
94
  base_seed = db_url
@@ -85,6 +103,8 @@ class SurrealDb(BaseDb):
85
103
  eval_table=eval_table,
86
104
  knowledge_table=knowledge_table,
87
105
  culture_table=culture_table,
106
+ traces_table=traces_table,
107
+ spans_table=spans_table,
88
108
  )
89
109
  self._client = client
90
110
  self._db_url = db_url
@@ -111,7 +131,9 @@ class SurrealDb(BaseDb):
111
131
  "knowledge": self.knowledge_table_name,
112
132
  "memories": self.memory_table_name,
113
133
  "sessions": self.session_table_name,
134
+ "spans": self.span_table_name,
114
135
  "teams": self._teams_table_name,
136
+ "traces": self.trace_table_name,
115
137
  "users": self._users_table_name,
116
138
  "workflows": self._workflows_table_name,
117
139
  }
@@ -159,6 +181,13 @@ class SurrealDb(BaseDb):
159
181
  table_name = self.eval_table_name
160
182
  elif table_type == "metrics":
161
183
  table_name = self.metrics_table_name
184
+ elif table_type == "traces":
185
+ table_name = self.trace_table_name
186
+ elif table_type == "spans":
187
+ # Ensure traces table exists before spans (for foreign key-like relationship)
188
+ if create_table_if_not_found:
189
+ self._get_table("traces", create_table_if_not_found=True)
190
+ table_name = self.span_table_name
162
191
  else:
163
192
  raise NotImplementedError(f"Unknown table type: {table_type}")
164
193
 
@@ -167,6 +196,14 @@ class SurrealDb(BaseDb):
167
196
 
168
197
  return table_name
169
198
 
199
+ def get_latest_schema_version(self):
200
+ """Get the latest version of the database schema."""
201
+ pass
202
+
203
+ def upsert_schema_version(self, version: str) -> None:
204
+ """Upsert the schema version into the database."""
205
+ pass
206
+
170
207
  def _query(
171
208
  self,
172
209
  query: str,
@@ -1351,3 +1388,568 @@ class SurrealDb(BaseDb):
1351
1388
  if not raw or not deserialize:
1352
1389
  return raw
1353
1390
  return deserialize_eval_run_record(raw)
1391
+
1392
+ # --- Traces ---
1393
+ def upsert_trace(self, trace: "Trace") -> None:
1394
+ """Create or update a single trace record in the database.
1395
+
1396
+ Args:
1397
+ trace: The Trace object to store (one per trace_id).
1398
+ """
1399
+ try:
1400
+ table = self._get_table("traces", create_table_if_not_found=True)
1401
+ record = RecordID(table, trace.trace_id)
1402
+
1403
+ # Check if trace exists
1404
+ existing = self._query_one("SELECT * FROM ONLY $record", {"record": record}, dict)
1405
+
1406
+ if existing:
1407
+ # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
1408
+ def get_component_level(workflow_id: Any, team_id: Any, agent_id: Any, name: str) -> int:
1409
+ is_root_name = ".run" in name or ".arun" in name
1410
+ if not is_root_name:
1411
+ return 0
1412
+ elif workflow_id:
1413
+ return 3
1414
+ elif team_id:
1415
+ return 2
1416
+ elif agent_id:
1417
+ return 1
1418
+ else:
1419
+ return 0
1420
+
1421
+ existing_level = get_component_level(
1422
+ existing.get("workflow_id"),
1423
+ existing.get("team_id"),
1424
+ existing.get("agent_id"),
1425
+ existing.get("name", ""),
1426
+ )
1427
+ new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
1428
+ should_update_name = new_level > existing_level
1429
+
1430
+ # Parse existing start_time to calculate correct duration
1431
+ existing_start_time = existing.get("start_time")
1432
+ if isinstance(existing_start_time, datetime):
1433
+ recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
1434
+ else:
1435
+ recalculated_duration_ms = trace.duration_ms
1436
+
1437
+ # Build update query
1438
+ update_fields = [
1439
+ "end_time = $end_time",
1440
+ "duration_ms = $duration_ms",
1441
+ "status = $status",
1442
+ ]
1443
+ update_vars: Dict[str, Any] = {
1444
+ "record": record,
1445
+ "end_time": trace.end_time,
1446
+ "duration_ms": recalculated_duration_ms,
1447
+ "status": trace.status,
1448
+ }
1449
+
1450
+ if should_update_name:
1451
+ update_fields.append("name = $name")
1452
+ update_vars["name"] = trace.name
1453
+
1454
+ # Update context fields only if new value is not None
1455
+ if trace.run_id is not None:
1456
+ update_fields.append("run_id = $run_id")
1457
+ update_vars["run_id"] = trace.run_id
1458
+ if trace.session_id is not None:
1459
+ update_fields.append("session_id = $session_id")
1460
+ update_vars["session_id"] = trace.session_id
1461
+ if trace.user_id is not None:
1462
+ update_fields.append("user_id = $user_id")
1463
+ update_vars["user_id"] = trace.user_id
1464
+ if trace.agent_id is not None:
1465
+ update_fields.append("agent_id = $agent_id")
1466
+ update_vars["agent_id"] = trace.agent_id
1467
+ if trace.team_id is not None:
1468
+ update_fields.append("team_id = $team_id")
1469
+ update_vars["team_id"] = trace.team_id
1470
+ if trace.workflow_id is not None:
1471
+ update_fields.append("workflow_id = $workflow_id")
1472
+ update_vars["workflow_id"] = trace.workflow_id
1473
+
1474
+ update_query = f"UPDATE ONLY $record SET {', '.join(update_fields)}"
1475
+ self._query_one(update_query, update_vars, dict)
1476
+ else:
1477
+ # Create new trace
1478
+ trace_dict = trace.to_dict()
1479
+ trace_dict.pop("total_spans", None)
1480
+ trace_dict.pop("error_count", None)
1481
+
1482
+ # Convert datetime fields
1483
+ if isinstance(trace_dict.get("start_time"), str):
1484
+ trace_dict["start_time"] = datetime.fromisoformat(trace_dict["start_time"].replace("Z", "+00:00"))
1485
+ if isinstance(trace_dict.get("end_time"), str):
1486
+ trace_dict["end_time"] = datetime.fromisoformat(trace_dict["end_time"].replace("Z", "+00:00"))
1487
+ if isinstance(trace_dict.get("created_at"), str):
1488
+ trace_dict["created_at"] = datetime.fromisoformat(trace_dict["created_at"].replace("Z", "+00:00"))
1489
+
1490
+ self._query_one(
1491
+ "CREATE ONLY $record CONTENT $content",
1492
+ {"record": record, "content": trace_dict},
1493
+ dict,
1494
+ )
1495
+
1496
+ except Exception as e:
1497
+ log_error(f"Error creating trace: {e}")
1498
+
1499
+ def get_trace(
1500
+ self,
1501
+ trace_id: Optional[str] = None,
1502
+ run_id: Optional[str] = None,
1503
+ ):
1504
+ """Get a single trace by trace_id or other filters.
1505
+
1506
+ Args:
1507
+ trace_id: The unique trace identifier.
1508
+ run_id: Filter by run ID (returns first match).
1509
+
1510
+ Returns:
1511
+ Optional[Trace]: The trace if found, None otherwise.
1512
+
1513
+ Note:
1514
+ If multiple filters are provided, trace_id takes precedence.
1515
+ For other filters, the most recent trace is returned.
1516
+ """
1517
+ try:
1518
+ table = self._get_table("traces", create_table_if_not_found=False)
1519
+ spans_table = self._get_table("spans", create_table_if_not_found=False)
1520
+
1521
+ if trace_id:
1522
+ record = RecordID(table, trace_id)
1523
+ trace_data = self._query_one("SELECT * FROM ONLY $record", {"record": record}, dict)
1524
+ elif run_id:
1525
+ query = dedent(f"""
1526
+ SELECT * FROM {table}
1527
+ WHERE run_id = $run_id
1528
+ ORDER BY start_time DESC
1529
+ LIMIT 1
1530
+ """)
1531
+ trace_data = self._query_one(query, {"run_id": run_id}, dict)
1532
+ else:
1533
+ log_debug("get_trace called without any filter parameters")
1534
+ return None
1535
+
1536
+ if not trace_data:
1537
+ return None
1538
+
1539
+ # Calculate total_spans and error_count
1540
+ id_obj = trace_data.get("id")
1541
+ trace_id_val = trace_data.get("trace_id") or (id_obj.id if id_obj is not None else None)
1542
+ if trace_id_val:
1543
+ count_query = f"SELECT count() as total FROM {spans_table} WHERE trace_id = $trace_id GROUP ALL"
1544
+ count_result = self._query_one(count_query, {"trace_id": trace_id_val}, dict)
1545
+ trace_data["total_spans"] = count_result.get("total", 0) if count_result else 0
1546
+
1547
+ error_query = f"SELECT count() as total FROM {spans_table} WHERE trace_id = $trace_id AND status_code = 'ERROR' GROUP ALL"
1548
+ error_result = self._query_one(error_query, {"trace_id": trace_id_val}, dict)
1549
+ trace_data["error_count"] = error_result.get("total", 0) if error_result else 0
1550
+
1551
+ # Deserialize
1552
+ return self._deserialize_trace(trace_data)
1553
+
1554
+ except Exception as e:
1555
+ log_error(f"Error getting trace: {e}")
1556
+ return None
1557
+
1558
+ def get_traces(
1559
+ self,
1560
+ run_id: Optional[str] = None,
1561
+ session_id: Optional[str] = None,
1562
+ user_id: Optional[str] = None,
1563
+ agent_id: Optional[str] = None,
1564
+ team_id: Optional[str] = None,
1565
+ workflow_id: Optional[str] = None,
1566
+ status: Optional[str] = None,
1567
+ start_time: Optional[datetime] = None,
1568
+ end_time: Optional[datetime] = None,
1569
+ limit: Optional[int] = 20,
1570
+ page: Optional[int] = 1,
1571
+ ) -> tuple[List, int]:
1572
+ """Get traces matching the provided filters with pagination.
1573
+
1574
+ Args:
1575
+ run_id: Filter by run ID.
1576
+ session_id: Filter by session ID.
1577
+ user_id: Filter by user ID.
1578
+ agent_id: Filter by agent ID.
1579
+ team_id: Filter by team ID.
1580
+ workflow_id: Filter by workflow ID.
1581
+ status: Filter by status (OK, ERROR, UNSET).
1582
+ start_time: Filter traces starting after this datetime.
1583
+ end_time: Filter traces ending before this datetime.
1584
+ limit: Maximum number of traces to return per page.
1585
+ page: Page number (1-indexed).
1586
+
1587
+ Returns:
1588
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
1589
+ """
1590
+ try:
1591
+ table = self._get_table("traces", create_table_if_not_found=False)
1592
+ spans_table = self._get_table("spans", create_table_if_not_found=False)
1593
+
1594
+ # Build where clause
1595
+ where = WhereClause()
1596
+ if run_id:
1597
+ where.and_("run_id", run_id)
1598
+ if session_id:
1599
+ where.and_("session_id", session_id)
1600
+ if user_id:
1601
+ where.and_("user_id", user_id)
1602
+ if agent_id:
1603
+ where.and_("agent_id", agent_id)
1604
+ if team_id:
1605
+ where.and_("team_id", team_id)
1606
+ if workflow_id:
1607
+ where.and_("workflow_id", workflow_id)
1608
+ if status:
1609
+ where.and_("status", status)
1610
+ if start_time:
1611
+ where.and_("start_time", start_time, ">=")
1612
+ if end_time:
1613
+ where.and_("end_time", end_time, "<=")
1614
+
1615
+ where_clause, where_vars = where.build()
1616
+
1617
+ # Total count
1618
+ total_count = self._count(table, where_clause, where_vars)
1619
+
1620
+ # Query with pagination
1621
+ order_limit_start_clause = order_limit_start("start_time", "DESC", limit, page)
1622
+ query = dedent(f"""
1623
+ SELECT * FROM {table}
1624
+ {where_clause}
1625
+ {order_limit_start_clause}
1626
+ """)
1627
+ traces_raw = self._query(query, where_vars, dict)
1628
+
1629
+ # Add total_spans and error_count to each trace
1630
+ result_traces = []
1631
+ for trace_data in traces_raw:
1632
+ id_obj = trace_data.get("id")
1633
+ trace_id_val = trace_data.get("trace_id") or (id_obj.id if id_obj is not None else None)
1634
+ if trace_id_val:
1635
+ count_query = f"SELECT count() as total FROM {spans_table} WHERE trace_id = $trace_id GROUP ALL"
1636
+ count_result = self._query_one(count_query, {"trace_id": trace_id_val}, dict)
1637
+ trace_data["total_spans"] = count_result.get("total", 0) if count_result else 0
1638
+
1639
+ error_query = f"SELECT count() as total FROM {spans_table} WHERE trace_id = $trace_id AND status_code = 'ERROR' GROUP ALL"
1640
+ error_result = self._query_one(error_query, {"trace_id": trace_id_val}, dict)
1641
+ trace_data["error_count"] = error_result.get("total", 0) if error_result else 0
1642
+
1643
+ result_traces.append(self._deserialize_trace(trace_data))
1644
+
1645
+ return result_traces, total_count
1646
+
1647
+ except Exception as e:
1648
+ log_error(f"Error getting traces: {e}")
1649
+ return [], 0
1650
+
1651
+ def get_trace_stats(
1652
+ self,
1653
+ user_id: Optional[str] = None,
1654
+ agent_id: Optional[str] = None,
1655
+ team_id: Optional[str] = None,
1656
+ workflow_id: Optional[str] = None,
1657
+ start_time: Optional[datetime] = None,
1658
+ end_time: Optional[datetime] = None,
1659
+ limit: Optional[int] = 20,
1660
+ page: Optional[int] = 1,
1661
+ ) -> tuple[List[Dict[str, Any]], int]:
1662
+ """Get trace statistics grouped by session.
1663
+
1664
+ Args:
1665
+ user_id: Filter by user ID.
1666
+ agent_id: Filter by agent ID.
1667
+ team_id: Filter by team ID.
1668
+ workflow_id: Filter by workflow ID.
1669
+ start_time: Filter sessions with traces created after this datetime.
1670
+ end_time: Filter sessions with traces created before this datetime.
1671
+ limit: Maximum number of sessions to return per page.
1672
+ page: Page number (1-indexed).
1673
+
1674
+ Returns:
1675
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
1676
+ Each dict contains: session_id, user_id, agent_id, team_id, workflow_id, total_traces,
1677
+ first_trace_at, last_trace_at.
1678
+ """
1679
+ try:
1680
+ table = self._get_table("traces", create_table_if_not_found=False)
1681
+
1682
+ # Build where clause
1683
+ where = WhereClause()
1684
+ where.and_("!!session_id", True, "=") # Ensure session_id is not null
1685
+ if user_id:
1686
+ where.and_("user_id", user_id)
1687
+ if agent_id:
1688
+ where.and_("agent_id", agent_id)
1689
+ if team_id:
1690
+ where.and_("team_id", team_id)
1691
+ if workflow_id:
1692
+ where.and_("workflow_id", workflow_id)
1693
+ if start_time:
1694
+ where.and_("created_at", start_time, ">=")
1695
+ if end_time:
1696
+ where.and_("created_at", end_time, "<=")
1697
+
1698
+ where_clause, where_vars = where.build()
1699
+
1700
+ # Get total count of unique sessions
1701
+ count_query = dedent(f"""
1702
+ SELECT count() as total FROM (
1703
+ SELECT session_id FROM {table}
1704
+ {where_clause}
1705
+ GROUP BY session_id
1706
+ ) GROUP ALL
1707
+ """)
1708
+ count_result = self._query_one(count_query, where_vars, dict)
1709
+ total_count = count_result.get("total", 0) if count_result else 0
1710
+
1711
+ # Query with aggregation
1712
+ order_limit_start_clause = order_limit_start("last_trace_at", "DESC", limit, page)
1713
+ query = dedent(f"""
1714
+ SELECT
1715
+ session_id,
1716
+ user_id,
1717
+ agent_id,
1718
+ team_id,
1719
+ workflow_id,
1720
+ count() AS total_traces,
1721
+ time::min(created_at) AS first_trace_at,
1722
+ time::max(created_at) AS last_trace_at
1723
+ FROM {table}
1724
+ {where_clause}
1725
+ GROUP BY session_id, user_id, agent_id, team_id, workflow_id
1726
+ {order_limit_start_clause}
1727
+ """)
1728
+ results = self._query(query, where_vars, dict)
1729
+
1730
+ # Convert datetime objects
1731
+ stats_list = []
1732
+ for row in results:
1733
+ stat = dict(row)
1734
+ if isinstance(stat.get("first_trace_at"), datetime):
1735
+ pass # Keep as datetime
1736
+ if isinstance(stat.get("last_trace_at"), datetime):
1737
+ pass # Keep as datetime
1738
+ stats_list.append(stat)
1739
+
1740
+ return stats_list, total_count
1741
+
1742
+ except Exception as e:
1743
+ log_error(f"Error getting trace stats: {e}")
1744
+ return [], 0
1745
+
1746
+ def _deserialize_trace(self, trace_data: dict) -> "Trace":
1747
+ """Helper to deserialize a trace record from SurrealDB."""
1748
+ from agno.tracing.schemas import Trace
1749
+
1750
+ # Handle RecordID for id field
1751
+ if isinstance(trace_data.get("id"), RecordID):
1752
+ if "trace_id" not in trace_data or not trace_data["trace_id"]:
1753
+ trace_data["trace_id"] = trace_data["id"].id
1754
+ del trace_data["id"]
1755
+
1756
+ # Convert datetime to ISO string for Trace.from_dict
1757
+ for field in ["start_time", "end_time", "created_at"]:
1758
+ if isinstance(trace_data.get(field), datetime):
1759
+ trace_data[field] = trace_data[field].isoformat()
1760
+
1761
+ return Trace.from_dict(trace_data)
1762
+
1763
+ # --- Spans ---
1764
+ def create_span(self, span: "Span") -> None:
1765
+ """Create a single span in the database.
1766
+
1767
+ Args:
1768
+ span: The Span object to store.
1769
+ """
1770
+ try:
1771
+ table = self._get_table("spans", create_table_if_not_found=True)
1772
+ record = RecordID(table, span.span_id)
1773
+
1774
+ span_dict = span.to_dict()
1775
+
1776
+ # Convert datetime fields
1777
+ if isinstance(span_dict.get("start_time"), str):
1778
+ span_dict["start_time"] = datetime.fromisoformat(span_dict["start_time"].replace("Z", "+00:00"))
1779
+ if isinstance(span_dict.get("end_time"), str):
1780
+ span_dict["end_time"] = datetime.fromisoformat(span_dict["end_time"].replace("Z", "+00:00"))
1781
+ if isinstance(span_dict.get("created_at"), str):
1782
+ span_dict["created_at"] = datetime.fromisoformat(span_dict["created_at"].replace("Z", "+00:00"))
1783
+
1784
+ self._query_one(
1785
+ "CREATE ONLY $record CONTENT $content",
1786
+ {"record": record, "content": span_dict},
1787
+ dict,
1788
+ )
1789
+
1790
+ except Exception as e:
1791
+ log_error(f"Error creating span: {e}")
1792
+
1793
+ def create_spans(self, spans: List) -> None:
1794
+ """Create multiple spans in the database as a batch.
1795
+
1796
+ Args:
1797
+ spans: List of Span objects to store.
1798
+ """
1799
+ if not spans:
1800
+ return
1801
+
1802
+ try:
1803
+ table = self._get_table("spans", create_table_if_not_found=True)
1804
+
1805
+ for span in spans:
1806
+ record = RecordID(table, span.span_id)
1807
+ span_dict = span.to_dict()
1808
+
1809
+ # Convert datetime fields
1810
+ if isinstance(span_dict.get("start_time"), str):
1811
+ span_dict["start_time"] = datetime.fromisoformat(span_dict["start_time"].replace("Z", "+00:00"))
1812
+ if isinstance(span_dict.get("end_time"), str):
1813
+ span_dict["end_time"] = datetime.fromisoformat(span_dict["end_time"].replace("Z", "+00:00"))
1814
+ if isinstance(span_dict.get("created_at"), str):
1815
+ span_dict["created_at"] = datetime.fromisoformat(span_dict["created_at"].replace("Z", "+00:00"))
1816
+
1817
+ self._query_one(
1818
+ "CREATE ONLY $record CONTENT $content",
1819
+ {"record": record, "content": span_dict},
1820
+ dict,
1821
+ )
1822
+
1823
+ except Exception as e:
1824
+ log_error(f"Error creating spans batch: {e}")
1825
+
1826
+ def get_span(self, span_id: str):
1827
+ """Get a single span by its span_id.
1828
+
1829
+ Args:
1830
+ span_id: The unique span identifier.
1831
+
1832
+ Returns:
1833
+ Optional[Span]: The span if found, None otherwise.
1834
+ """
1835
+ try:
1836
+ table = self._get_table("spans", create_table_if_not_found=False)
1837
+ record = RecordID(table, span_id)
1838
+
1839
+ span_data = self._query_one("SELECT * FROM ONLY $record", {"record": record}, dict)
1840
+ if not span_data:
1841
+ return None
1842
+
1843
+ return self._deserialize_span(span_data)
1844
+
1845
+ except Exception as e:
1846
+ log_error(f"Error getting span: {e}")
1847
+ return None
1848
+
1849
+ def get_spans(
1850
+ self,
1851
+ trace_id: Optional[str] = None,
1852
+ parent_span_id: Optional[str] = None,
1853
+ limit: Optional[int] = 1000,
1854
+ ) -> List:
1855
+ """Get spans matching the provided filters.
1856
+
1857
+ Args:
1858
+ trace_id: Filter by trace ID.
1859
+ parent_span_id: Filter by parent span ID.
1860
+ limit: Maximum number of spans to return.
1861
+
1862
+ Returns:
1863
+ List[Span]: List of matching spans.
1864
+ """
1865
+ try:
1866
+ table = self._get_table("spans", create_table_if_not_found=False)
1867
+
1868
+ # Build where clause
1869
+ where = WhereClause()
1870
+ if trace_id:
1871
+ where.and_("trace_id", trace_id)
1872
+ if parent_span_id:
1873
+ where.and_("parent_span_id", parent_span_id)
1874
+
1875
+ where_clause, where_vars = where.build()
1876
+
1877
+ # Query
1878
+ limit_clause = f"LIMIT {limit}" if limit else ""
1879
+ query = dedent(f"""
1880
+ SELECT * FROM {table}
1881
+ {where_clause}
1882
+ ORDER BY start_time ASC
1883
+ {limit_clause}
1884
+ """)
1885
+ spans_raw = self._query(query, where_vars, dict)
1886
+
1887
+ return [self._deserialize_span(s) for s in spans_raw]
1888
+
1889
+ except Exception as e:
1890
+ log_error(f"Error getting spans: {e}")
1891
+ return []
1892
+
1893
+ def _deserialize_span(self, span_data: dict) -> "Span":
1894
+ """Helper to deserialize a span record from SurrealDB."""
1895
+ from agno.tracing.schemas import Span
1896
+
1897
+ # Handle RecordID for id field
1898
+ if isinstance(span_data.get("id"), RecordID):
1899
+ if "span_id" not in span_data or not span_data["span_id"]:
1900
+ span_data["span_id"] = span_data["id"].id
1901
+ del span_data["id"]
1902
+
1903
+ # Convert datetime to ISO string for Span.from_dict
1904
+ for field in ["start_time", "end_time", "created_at"]:
1905
+ if isinstance(span_data.get(field), datetime):
1906
+ span_data[field] = span_data[field].isoformat()
1907
+
1908
+ return Span.from_dict(span_data)
1909
+
1910
+ # -- Learning methods (stubs) --
1911
+ def get_learning(
1912
+ self,
1913
+ learning_type: str,
1914
+ user_id: Optional[str] = None,
1915
+ agent_id: Optional[str] = None,
1916
+ team_id: Optional[str] = None,
1917
+ session_id: Optional[str] = None,
1918
+ namespace: Optional[str] = None,
1919
+ entity_id: Optional[str] = None,
1920
+ entity_type: Optional[str] = None,
1921
+ ) -> Optional[Dict[str, Any]]:
1922
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1923
+
1924
+ def upsert_learning(
1925
+ self,
1926
+ id: str,
1927
+ learning_type: str,
1928
+ content: Dict[str, Any],
1929
+ user_id: Optional[str] = None,
1930
+ agent_id: Optional[str] = None,
1931
+ team_id: Optional[str] = None,
1932
+ session_id: Optional[str] = None,
1933
+ namespace: Optional[str] = None,
1934
+ entity_id: Optional[str] = None,
1935
+ entity_type: Optional[str] = None,
1936
+ metadata: Optional[Dict[str, Any]] = None,
1937
+ ) -> None:
1938
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1939
+
1940
+ def delete_learning(self, id: str) -> bool:
1941
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1942
+
1943
+ def get_learnings(
1944
+ self,
1945
+ learning_type: Optional[str] = None,
1946
+ user_id: Optional[str] = None,
1947
+ agent_id: Optional[str] = None,
1948
+ team_id: Optional[str] = None,
1949
+ session_id: Optional[str] = None,
1950
+ namespace: Optional[str] = None,
1951
+ entity_id: Optional[str] = None,
1952
+ entity_type: Optional[str] = None,
1953
+ limit: Optional[int] = None,
1954
+ ) -> List[Dict[str, Any]]:
1955
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")