agno 0.1.2__py3-none-any.whl → 2.3.13__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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1791 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1312 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/db/mongo/mongo.py ADDED
@@ -0,0 +1,2597 @@
1
+ import time
2
+ from datetime import date, datetime, timedelta, timezone
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
4
+ from uuid import uuid4
5
+
6
+ if TYPE_CHECKING:
7
+ from agno.tracing.schemas import Span, Trace
8
+
9
+ from agno.db.base import BaseDb, SessionType
10
+ from agno.db.mongo.utils import (
11
+ apply_pagination,
12
+ apply_sorting,
13
+ bulk_upsert_metrics,
14
+ calculate_date_metrics,
15
+ create_collection_indexes,
16
+ deserialize_cultural_knowledge_from_db,
17
+ fetch_all_sessions_data,
18
+ get_dates_to_calculate_metrics_for,
19
+ serialize_cultural_knowledge_for_db,
20
+ )
21
+ from agno.db.schemas.culture import CulturalKnowledge
22
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
23
+ from agno.db.schemas.knowledge import KnowledgeRow
24
+ from agno.db.schemas.memory import UserMemory
25
+ from agno.db.utils import deserialize_session_json_fields
26
+ from agno.session import AgentSession, Session, TeamSession, WorkflowSession
27
+ from agno.utils.log import log_debug, log_error, log_info
28
+ from agno.utils.string import generate_id
29
+
30
+ try:
31
+ from pymongo import MongoClient, ReturnDocument
32
+ from pymongo.collection import Collection
33
+ from pymongo.database import Database
34
+ from pymongo.errors import OperationFailure
35
+ except ImportError:
36
+ raise ImportError("`pymongo` not installed. Please install it using `pip install pymongo`")
37
+
38
+
39
+ class MongoDb(BaseDb):
40
+ def __init__(
41
+ self,
42
+ db_client: Optional[MongoClient] = None,
43
+ db_name: Optional[str] = None,
44
+ db_url: Optional[str] = None,
45
+ session_collection: Optional[str] = None,
46
+ memory_collection: Optional[str] = None,
47
+ metrics_collection: Optional[str] = None,
48
+ eval_collection: Optional[str] = None,
49
+ knowledge_collection: Optional[str] = None,
50
+ culture_collection: Optional[str] = None,
51
+ traces_collection: Optional[str] = None,
52
+ spans_collection: Optional[str] = None,
53
+ id: Optional[str] = None,
54
+ ):
55
+ """
56
+ Interface for interacting with a MongoDB database.
57
+
58
+ Args:
59
+ db_client (Optional[MongoClient]): The MongoDB client to use.
60
+ db_name (Optional[str]): The name of the database to use.
61
+ db_url (Optional[str]): The database URL to connect to.
62
+ session_collection (Optional[str]): Name of the collection to store sessions.
63
+ memory_collection (Optional[str]): Name of the collection to store memories.
64
+ metrics_collection (Optional[str]): Name of the collection to store metrics.
65
+ eval_collection (Optional[str]): Name of the collection to store evaluation runs.
66
+ knowledge_collection (Optional[str]): Name of the collection to store knowledge documents.
67
+ culture_collection (Optional[str]): Name of the collection to store cultural knowledge.
68
+ traces_collection (Optional[str]): Name of the collection to store traces.
69
+ spans_collection (Optional[str]): Name of the collection to store spans.
70
+ id (Optional[str]): ID of the database.
71
+
72
+ Raises:
73
+ ValueError: If neither db_url nor db_client is provided.
74
+ """
75
+ if id is None:
76
+ base_seed = db_url or str(db_client)
77
+ db_name_suffix = db_name if db_name is not None else "agno"
78
+ seed = f"{base_seed}#{db_name_suffix}"
79
+ id = generate_id(seed)
80
+
81
+ super().__init__(
82
+ id=id,
83
+ session_table=session_collection,
84
+ memory_table=memory_collection,
85
+ metrics_table=metrics_collection,
86
+ eval_table=eval_collection,
87
+ knowledge_table=knowledge_collection,
88
+ culture_table=culture_collection,
89
+ traces_table=traces_collection,
90
+ spans_table=spans_collection,
91
+ )
92
+
93
+ _client: Optional[MongoClient] = db_client
94
+ if _client is None and db_url is not None:
95
+ _client = MongoClient(db_url)
96
+ if _client is None:
97
+ raise ValueError("One of db_url or db_client must be provided")
98
+
99
+ self.db_url: Optional[str] = db_url
100
+ self.db_client: MongoClient = _client
101
+ self.db_name: str = db_name if db_name is not None else "agno"
102
+
103
+ self._database: Optional[Database] = None
104
+
105
+ @property
106
+ def database(self) -> Database:
107
+ if self._database is None:
108
+ self._database = self.db_client[self.db_name]
109
+ return self._database
110
+
111
+ # -- DB methods --
112
+ def table_exists(self, table_name: str) -> bool:
113
+ """Check if a collection with the given name exists in the MongoDB database.
114
+
115
+ Args:
116
+ table_name: Name of the collection to check
117
+
118
+ Returns:
119
+ bool: True if the collection exists in the database, False otherwise
120
+ """
121
+ return table_name in self.database.list_collection_names()
122
+
123
+ def _create_all_tables(self):
124
+ """Create all configured MongoDB collections if they don't exist."""
125
+ collections_to_create = [
126
+ ("sessions", self.session_table_name),
127
+ ("memories", self.memory_table_name),
128
+ ("metrics", self.metrics_table_name),
129
+ ("evals", self.eval_table_name),
130
+ ("knowledge", self.knowledge_table_name),
131
+ ("culture", self.culture_table_name),
132
+ ]
133
+
134
+ for collection_type, collection_name in collections_to_create:
135
+ if collection_name and not self.table_exists(collection_name):
136
+ self._get_collection(collection_type, create_collection_if_not_found=True)
137
+
138
+ def _get_collection(
139
+ self, table_type: str, create_collection_if_not_found: Optional[bool] = True
140
+ ) -> Optional[Collection]:
141
+ """Get or create a collection based on table type.
142
+
143
+ Args:
144
+ table_type (str): The type of table to get or create.
145
+
146
+ Returns:
147
+ Collection: The collection object.
148
+ """
149
+ if table_type == "sessions":
150
+ if not hasattr(self, "session_collection"):
151
+ if self.session_table_name is None:
152
+ raise ValueError("Session collection was not provided on initialization")
153
+ self.session_collection = self._get_or_create_collection(
154
+ collection_name=self.session_table_name,
155
+ collection_type="sessions",
156
+ create_collection_if_not_found=create_collection_if_not_found,
157
+ )
158
+ return self.session_collection
159
+
160
+ if table_type == "memories":
161
+ if not hasattr(self, "memory_collection"):
162
+ if self.memory_table_name is None:
163
+ raise ValueError("Memory collection was not provided on initialization")
164
+ self.memory_collection = self._get_or_create_collection(
165
+ collection_name=self.memory_table_name,
166
+ collection_type="memories",
167
+ create_collection_if_not_found=create_collection_if_not_found,
168
+ )
169
+ return self.memory_collection
170
+
171
+ if table_type == "metrics":
172
+ if not hasattr(self, "metrics_collection"):
173
+ if self.metrics_table_name is None:
174
+ raise ValueError("Metrics collection was not provided on initialization")
175
+ self.metrics_collection = self._get_or_create_collection(
176
+ collection_name=self.metrics_table_name,
177
+ collection_type="metrics",
178
+ create_collection_if_not_found=create_collection_if_not_found,
179
+ )
180
+ return self.metrics_collection
181
+
182
+ if table_type == "evals":
183
+ if not hasattr(self, "eval_collection"):
184
+ if self.eval_table_name is None:
185
+ raise ValueError("Eval collection was not provided on initialization")
186
+ self.eval_collection = self._get_or_create_collection(
187
+ collection_name=self.eval_table_name,
188
+ collection_type="evals",
189
+ create_collection_if_not_found=create_collection_if_not_found,
190
+ )
191
+ return self.eval_collection
192
+
193
+ if table_type == "knowledge":
194
+ if not hasattr(self, "knowledge_collection"):
195
+ if self.knowledge_table_name is None:
196
+ raise ValueError("Knowledge collection was not provided on initialization")
197
+ self.knowledge_collection = self._get_or_create_collection(
198
+ collection_name=self.knowledge_table_name,
199
+ collection_type="knowledge",
200
+ create_collection_if_not_found=create_collection_if_not_found,
201
+ )
202
+ return self.knowledge_collection
203
+
204
+ if table_type == "culture":
205
+ if not hasattr(self, "culture_collection"):
206
+ if self.culture_table_name is None:
207
+ raise ValueError("Culture collection was not provided on initialization")
208
+ self.culture_collection = self._get_or_create_collection(
209
+ collection_name=self.culture_table_name,
210
+ collection_type="culture",
211
+ create_collection_if_not_found=create_collection_if_not_found,
212
+ )
213
+ return self.culture_collection
214
+
215
+ if table_type == "traces":
216
+ if not hasattr(self, "traces_collection"):
217
+ if self.trace_table_name is None:
218
+ raise ValueError("Traces collection was not provided on initialization")
219
+ self.traces_collection = self._get_or_create_collection(
220
+ collection_name=self.trace_table_name,
221
+ collection_type="traces",
222
+ create_collection_if_not_found=create_collection_if_not_found,
223
+ )
224
+ return self.traces_collection
225
+
226
+ if table_type == "spans":
227
+ if not hasattr(self, "spans_collection"):
228
+ if self.span_table_name is None:
229
+ raise ValueError("Spans collection was not provided on initialization")
230
+ self.spans_collection = self._get_or_create_collection(
231
+ collection_name=self.span_table_name,
232
+ collection_type="spans",
233
+ create_collection_if_not_found=create_collection_if_not_found,
234
+ )
235
+ return self.spans_collection
236
+
237
+ raise ValueError(f"Unknown table type: {table_type}")
238
+
239
+ def _get_or_create_collection(
240
+ self, collection_name: str, collection_type: str, create_collection_if_not_found: Optional[bool] = True
241
+ ) -> Optional[Collection]:
242
+ """Get or create a collection with proper indexes.
243
+
244
+ Args:
245
+ collection_name (str): The name of the collection to get or create.
246
+ collection_type (str): The type of collection to get or create.
247
+ create_collection_if_not_found (Optional[bool]): Whether to create the collection if it doesn't exist.
248
+
249
+ Returns:
250
+ Optional[Collection]: The collection object.
251
+ """
252
+ try:
253
+ collection = self.database[collection_name]
254
+
255
+ if not hasattr(self, f"_{collection_name}_initialized"):
256
+ if not create_collection_if_not_found:
257
+ return None
258
+ create_collection_indexes(collection, collection_type)
259
+ setattr(self, f"_{collection_name}_initialized", True)
260
+ log_debug(f"Initialized collection '{collection_name}'")
261
+ else:
262
+ log_debug(f"Collection '{collection_name}' already initialized")
263
+
264
+ return collection
265
+
266
+ except Exception as e:
267
+ log_error(f"Error getting collection {collection_name}: {e}")
268
+ raise
269
+
270
+ def get_latest_schema_version(self):
271
+ """Get the latest version of the database schema."""
272
+ pass
273
+
274
+ def upsert_schema_version(self, version: str) -> None:
275
+ """Upsert the schema version into the database."""
276
+ pass
277
+
278
+ # -- Session methods --
279
+
280
+ def delete_session(self, session_id: str) -> bool:
281
+ """Delete a session from the database.
282
+
283
+ Args:
284
+ session_id (str): The ID of the session to delete.
285
+
286
+ Returns:
287
+ bool: True if the session was deleted, False otherwise.
288
+
289
+ Raises:
290
+ Exception: If there is an error deleting the session.
291
+ """
292
+ try:
293
+ collection = self._get_collection(table_type="sessions")
294
+ if collection is None:
295
+ return False
296
+
297
+ result = collection.delete_one({"session_id": session_id})
298
+ if result.deleted_count == 0:
299
+ log_debug(f"No session found to delete with session_id: {session_id}")
300
+ return False
301
+ else:
302
+ log_debug(f"Successfully deleted session with session_id: {session_id}")
303
+ return True
304
+
305
+ except Exception as e:
306
+ log_error(f"Error deleting session: {e}")
307
+ raise e
308
+
309
+ def delete_sessions(self, session_ids: List[str]) -> None:
310
+ """Delete multiple sessions from the database.
311
+
312
+ Args:
313
+ session_ids (List[str]): The IDs of the sessions to delete.
314
+ """
315
+ try:
316
+ collection = self._get_collection(table_type="sessions")
317
+ if collection is None:
318
+ return
319
+
320
+ result = collection.delete_many({"session_id": {"$in": session_ids}})
321
+ log_debug(f"Successfully deleted {result.deleted_count} sessions")
322
+
323
+ except Exception as e:
324
+ log_error(f"Error deleting sessions: {e}")
325
+ raise e
326
+
327
+ def get_session(
328
+ self,
329
+ session_id: str,
330
+ session_type: SessionType,
331
+ user_id: Optional[str] = None,
332
+ deserialize: Optional[bool] = True,
333
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
334
+ """Read a session from the database.
335
+
336
+ Args:
337
+ session_id (str): The ID of the session to get.
338
+ session_type (SessionType): The type of session to get.
339
+ user_id (Optional[str]): The ID of the user to get the session for.
340
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
341
+
342
+ Returns:
343
+ Union[Session, Dict[str, Any], None]:
344
+ - When deserialize=True: Session object
345
+ - When deserialize=False: Session dictionary
346
+
347
+ Raises:
348
+ Exception: If there is an error reading the session.
349
+ """
350
+ try:
351
+ collection = self._get_collection(table_type="sessions")
352
+ if collection is None:
353
+ return None
354
+
355
+ query = {"session_id": session_id}
356
+ if user_id is not None:
357
+ query["user_id"] = user_id
358
+
359
+ result = collection.find_one(query)
360
+ if result is None:
361
+ return None
362
+
363
+ session = deserialize_session_json_fields(result)
364
+ if not deserialize:
365
+ return session
366
+
367
+ if session_type == SessionType.AGENT:
368
+ return AgentSession.from_dict(session)
369
+ elif session_type == SessionType.TEAM:
370
+ return TeamSession.from_dict(session)
371
+ elif session_type == SessionType.WORKFLOW:
372
+ return WorkflowSession.from_dict(session)
373
+ else:
374
+ raise ValueError(f"Invalid session type: {session_type}")
375
+
376
+ except Exception as e:
377
+ log_error(f"Exception reading session: {e}")
378
+ raise e
379
+
380
+ def get_sessions(
381
+ self,
382
+ session_type: Optional[SessionType] = None,
383
+ user_id: Optional[str] = None,
384
+ component_id: Optional[str] = None,
385
+ session_name: Optional[str] = None,
386
+ start_timestamp: Optional[int] = None,
387
+ end_timestamp: Optional[int] = None,
388
+ limit: Optional[int] = None,
389
+ page: Optional[int] = None,
390
+ sort_by: Optional[str] = None,
391
+ sort_order: Optional[str] = None,
392
+ deserialize: Optional[bool] = True,
393
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
394
+ """Get all sessions.
395
+
396
+ Args:
397
+ session_type (Optional[SessionType]): The type of session to get.
398
+ user_id (Optional[str]): The ID of the user to get the session for.
399
+ component_id (Optional[str]): The ID of the component to get the session for.
400
+ session_name (Optional[str]): The name of the session to filter by.
401
+ start_timestamp (Optional[int]): The start timestamp to filter sessions by.
402
+ end_timestamp (Optional[int]): The end timestamp to filter sessions by.
403
+ limit (Optional[int]): The limit of the sessions to get.
404
+ page (Optional[int]): The page number to get.
405
+ sort_by (Optional[str]): The field to sort the sessions by.
406
+ sort_order (Optional[str]): The order to sort the sessions by.
407
+ deserialize (Optional[bool]): Whether to serialize the sessions. Defaults to True.
408
+ create_table_if_not_found (Optional[bool]): Whether to create the collection if it doesn't exist.
409
+
410
+ Returns:
411
+ Union[List[AgentSession], List[TeamSession], List[WorkflowSession], Tuple[List[Dict[str, Any]], int]]:
412
+ - When deserialize=True: List of Session objects
413
+ - When deserialize=False: List of session dictionaries and the total count
414
+
415
+ Raises:
416
+ Exception: If there is an error reading the sessions.
417
+ """
418
+ try:
419
+ collection = self._get_collection(table_type="sessions")
420
+ if collection is None:
421
+ return [] if deserialize else ([], 0)
422
+
423
+ # Filtering
424
+ query: Dict[str, Any] = {}
425
+ if user_id is not None:
426
+ query["user_id"] = user_id
427
+ if session_type is not None:
428
+ query["session_type"] = session_type
429
+ if component_id is not None:
430
+ if session_type == SessionType.AGENT:
431
+ query["agent_id"] = component_id
432
+ elif session_type == SessionType.TEAM:
433
+ query["team_id"] = component_id
434
+ elif session_type == SessionType.WORKFLOW:
435
+ query["workflow_id"] = component_id
436
+ if start_timestamp is not None:
437
+ query["created_at"] = {"$gte": start_timestamp}
438
+ if end_timestamp is not None:
439
+ if "created_at" in query:
440
+ query["created_at"]["$lte"] = end_timestamp
441
+ else:
442
+ query["created_at"] = {"$lte": end_timestamp}
443
+ if session_name is not None:
444
+ query["session_data.session_name"] = {"$regex": session_name, "$options": "i"}
445
+
446
+ # Get total count
447
+ total_count = collection.count_documents(query)
448
+
449
+ cursor = collection.find(query)
450
+
451
+ # Sorting
452
+ sort_criteria = apply_sorting({}, sort_by, sort_order)
453
+ if sort_criteria:
454
+ cursor = cursor.sort(sort_criteria)
455
+
456
+ # Pagination
457
+ query_args = apply_pagination({}, limit, page)
458
+ if query_args.get("skip"):
459
+ cursor = cursor.skip(query_args["skip"])
460
+ if query_args.get("limit"):
461
+ cursor = cursor.limit(query_args["limit"])
462
+
463
+ records = list(cursor)
464
+ if records is None:
465
+ return [] if deserialize else ([], 0)
466
+ sessions_raw = [deserialize_session_json_fields(record) for record in records]
467
+
468
+ if not deserialize:
469
+ return sessions_raw, total_count
470
+
471
+ sessions: List[Union[AgentSession, TeamSession, WorkflowSession]] = []
472
+ for record in sessions_raw:
473
+ if session_type == SessionType.AGENT.value:
474
+ agent_session = AgentSession.from_dict(record)
475
+ if agent_session is not None:
476
+ sessions.append(agent_session)
477
+ elif session_type == SessionType.TEAM.value:
478
+ team_session = TeamSession.from_dict(record)
479
+ if team_session is not None:
480
+ sessions.append(team_session)
481
+ elif session_type == SessionType.WORKFLOW.value:
482
+ workflow_session = WorkflowSession.from_dict(record)
483
+ if workflow_session is not None:
484
+ sessions.append(workflow_session)
485
+
486
+ return sessions
487
+
488
+ except Exception as e:
489
+ log_error(f"Exception reading sessions: {e}")
490
+ raise e
491
+
492
+ def rename_session(
493
+ self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
494
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
495
+ """Rename a session in the database.
496
+
497
+ Args:
498
+ session_id (str): The ID of the session to rename.
499
+ session_type (SessionType): The type of session to rename.
500
+ session_name (str): The new name of the session.
501
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
502
+
503
+ Returns:
504
+ Optional[Union[Session, Dict[str, Any]]]:
505
+ - When deserialize=True: Session object
506
+ - When deserialize=False: Session dictionary
507
+
508
+ Raises:
509
+ Exception: If there is an error renaming the session.
510
+ """
511
+ try:
512
+ collection = self._get_collection(table_type="sessions")
513
+ if collection is None:
514
+ return None
515
+
516
+ try:
517
+ result = collection.find_one_and_update(
518
+ {"session_id": session_id},
519
+ {"$set": {"session_data.session_name": session_name, "updated_at": int(time.time())}},
520
+ return_document=ReturnDocument.AFTER,
521
+ upsert=False,
522
+ )
523
+ except OperationFailure:
524
+ # If the update fails because session_data doesn't contain a session_name yet, we initialize session_data
525
+ result = collection.find_one_and_update(
526
+ {"session_id": session_id},
527
+ {"$set": {"session_data": {"session_name": session_name}, "updated_at": int(time.time())}},
528
+ return_document=ReturnDocument.AFTER,
529
+ upsert=False,
530
+ )
531
+ if not result:
532
+ return None
533
+
534
+ deserialized_session = deserialize_session_json_fields(result)
535
+
536
+ if not deserialize:
537
+ return deserialized_session
538
+
539
+ if session_type == SessionType.AGENT.value:
540
+ return AgentSession.from_dict(deserialized_session)
541
+ elif session_type == SessionType.TEAM.value:
542
+ return TeamSession.from_dict(deserialized_session)
543
+ else:
544
+ return WorkflowSession.from_dict(deserialized_session)
545
+
546
+ except Exception as e:
547
+ log_error(f"Exception renaming session: {e}")
548
+ raise e
549
+
550
+ def upsert_session(
551
+ self, session: Session, deserialize: Optional[bool] = True
552
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
553
+ """Insert or update a session in the database.
554
+
555
+ Args:
556
+ session (Session): The session to upsert.
557
+
558
+ Returns:
559
+ Optional[Session]: The upserted session.
560
+
561
+ Raises:
562
+ Exception: If there is an error upserting the session.
563
+ """
564
+ try:
565
+ collection = self._get_collection(table_type="sessions", create_collection_if_not_found=True)
566
+ if collection is None:
567
+ return None
568
+
569
+ session_dict = session.to_dict()
570
+
571
+ if isinstance(session, AgentSession):
572
+ record = {
573
+ "session_id": session_dict.get("session_id"),
574
+ "session_type": SessionType.AGENT.value,
575
+ "agent_id": session_dict.get("agent_id"),
576
+ "user_id": session_dict.get("user_id"),
577
+ "runs": session_dict.get("runs"),
578
+ "agent_data": session_dict.get("agent_data"),
579
+ "session_data": session_dict.get("session_data"),
580
+ "summary": session_dict.get("summary"),
581
+ "metadata": session_dict.get("metadata"),
582
+ "created_at": session_dict.get("created_at"),
583
+ "updated_at": int(time.time()),
584
+ }
585
+
586
+ result = collection.find_one_and_replace(
587
+ filter={"session_id": session_dict.get("session_id")},
588
+ replacement=record,
589
+ upsert=True,
590
+ return_document=ReturnDocument.AFTER,
591
+ )
592
+ if not result:
593
+ return None
594
+
595
+ session = result # type: ignore
596
+
597
+ if not deserialize:
598
+ return session
599
+
600
+ return AgentSession.from_dict(session) # type: ignore
601
+
602
+ elif isinstance(session, TeamSession):
603
+ record = {
604
+ "session_id": session_dict.get("session_id"),
605
+ "session_type": SessionType.TEAM.value,
606
+ "team_id": session_dict.get("team_id"),
607
+ "user_id": session_dict.get("user_id"),
608
+ "runs": session_dict.get("runs"),
609
+ "team_data": session_dict.get("team_data"),
610
+ "session_data": session_dict.get("session_data"),
611
+ "summary": session_dict.get("summary"),
612
+ "metadata": session_dict.get("metadata"),
613
+ "created_at": session_dict.get("created_at"),
614
+ "updated_at": int(time.time()),
615
+ }
616
+
617
+ result = collection.find_one_and_replace(
618
+ filter={"session_id": session_dict.get("session_id")},
619
+ replacement=record,
620
+ upsert=True,
621
+ return_document=ReturnDocument.AFTER,
622
+ )
623
+ if not result:
624
+ return None
625
+
626
+ # MongoDB stores native objects, no deserialization needed for document fields
627
+ session = result # type: ignore
628
+
629
+ if not deserialize:
630
+ return session
631
+
632
+ return TeamSession.from_dict(session) # type: ignore
633
+
634
+ else:
635
+ record = {
636
+ "session_id": session_dict.get("session_id"),
637
+ "session_type": SessionType.WORKFLOW.value,
638
+ "workflow_id": session_dict.get("workflow_id"),
639
+ "user_id": session_dict.get("user_id"),
640
+ "runs": session_dict.get("runs"),
641
+ "workflow_data": session_dict.get("workflow_data"),
642
+ "session_data": session_dict.get("session_data"),
643
+ "summary": session_dict.get("summary"),
644
+ "metadata": session_dict.get("metadata"),
645
+ "created_at": session_dict.get("created_at"),
646
+ "updated_at": int(time.time()),
647
+ }
648
+
649
+ result = collection.find_one_and_replace(
650
+ filter={"session_id": session_dict.get("session_id")},
651
+ replacement=record,
652
+ upsert=True,
653
+ return_document=ReturnDocument.AFTER,
654
+ )
655
+ if not result:
656
+ return None
657
+
658
+ session = result # type: ignore
659
+
660
+ if not deserialize:
661
+ return session
662
+
663
+ return WorkflowSession.from_dict(session) # type: ignore
664
+
665
+ except Exception as e:
666
+ log_error(f"Exception upserting session: {e}")
667
+ raise e
668
+
669
+ def upsert_sessions(
670
+ self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
671
+ ) -> List[Union[Session, Dict[str, Any]]]:
672
+ """
673
+ Bulk upsert multiple sessions for improved performance on large datasets.
674
+
675
+ Args:
676
+ sessions (List[Session]): List of sessions to upsert.
677
+ deserialize (Optional[bool]): Whether to deserialize the sessions. Defaults to True.
678
+ preserve_updated_at (bool): If True, preserve the updated_at from the session object.
679
+
680
+ Returns:
681
+ List[Union[Session, Dict[str, Any]]]: List of upserted sessions.
682
+
683
+ Raises:
684
+ Exception: If an error occurs during bulk upsert.
685
+ """
686
+ if not sessions:
687
+ return []
688
+
689
+ try:
690
+ collection = self._get_collection(table_type="sessions", create_collection_if_not_found=True)
691
+ if collection is None:
692
+ log_info("Sessions collection not available, falling back to individual upserts")
693
+ return [
694
+ result
695
+ for session in sessions
696
+ if session is not None
697
+ for result in [self.upsert_session(session, deserialize=deserialize)]
698
+ if result is not None
699
+ ]
700
+
701
+ from pymongo import ReplaceOne
702
+
703
+ operations = []
704
+ results: List[Union[Session, Dict[str, Any]]] = []
705
+
706
+ for session in sessions:
707
+ if session is None:
708
+ continue
709
+
710
+ session_dict = session.to_dict()
711
+
712
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
713
+ updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
714
+
715
+ if isinstance(session, AgentSession):
716
+ record = {
717
+ "session_id": session_dict.get("session_id"),
718
+ "session_type": SessionType.AGENT.value,
719
+ "agent_id": session_dict.get("agent_id"),
720
+ "user_id": session_dict.get("user_id"),
721
+ "runs": session_dict.get("runs"),
722
+ "agent_data": session_dict.get("agent_data"),
723
+ "session_data": session_dict.get("session_data"),
724
+ "summary": session_dict.get("summary"),
725
+ "metadata": session_dict.get("metadata"),
726
+ "created_at": session_dict.get("created_at"),
727
+ "updated_at": updated_at,
728
+ }
729
+ elif isinstance(session, TeamSession):
730
+ record = {
731
+ "session_id": session_dict.get("session_id"),
732
+ "session_type": SessionType.TEAM.value,
733
+ "team_id": session_dict.get("team_id"),
734
+ "user_id": session_dict.get("user_id"),
735
+ "runs": session_dict.get("runs"),
736
+ "team_data": session_dict.get("team_data"),
737
+ "session_data": session_dict.get("session_data"),
738
+ "summary": session_dict.get("summary"),
739
+ "metadata": session_dict.get("metadata"),
740
+ "created_at": session_dict.get("created_at"),
741
+ "updated_at": updated_at,
742
+ }
743
+ elif isinstance(session, WorkflowSession):
744
+ record = {
745
+ "session_id": session_dict.get("session_id"),
746
+ "session_type": SessionType.WORKFLOW.value,
747
+ "workflow_id": session_dict.get("workflow_id"),
748
+ "user_id": session_dict.get("user_id"),
749
+ "runs": session_dict.get("runs"),
750
+ "workflow_data": session_dict.get("workflow_data"),
751
+ "session_data": session_dict.get("session_data"),
752
+ "summary": session_dict.get("summary"),
753
+ "metadata": session_dict.get("metadata"),
754
+ "created_at": session_dict.get("created_at"),
755
+ "updated_at": updated_at,
756
+ }
757
+ else:
758
+ continue
759
+
760
+ operations.append(
761
+ ReplaceOne(filter={"session_id": record["session_id"]}, replacement=record, upsert=True)
762
+ )
763
+
764
+ if operations:
765
+ # Execute bulk write
766
+ collection.bulk_write(operations)
767
+
768
+ # Fetch the results
769
+ session_ids = [session.session_id for session in sessions if session and session.session_id]
770
+ cursor = collection.find({"session_id": {"$in": session_ids}})
771
+
772
+ for doc in cursor:
773
+ session_dict = doc
774
+
775
+ if deserialize:
776
+ session_type = doc.get("session_type")
777
+ if session_type == SessionType.AGENT.value:
778
+ deserialized_agent_session = AgentSession.from_dict(session_dict)
779
+ if deserialized_agent_session is None:
780
+ continue
781
+ results.append(deserialized_agent_session)
782
+
783
+ elif session_type == SessionType.TEAM.value:
784
+ deserialized_team_session = TeamSession.from_dict(session_dict)
785
+ if deserialized_team_session is None:
786
+ continue
787
+ results.append(deserialized_team_session)
788
+
789
+ elif session_type == SessionType.WORKFLOW.value:
790
+ deserialized_workflow_session = WorkflowSession.from_dict(session_dict)
791
+ if deserialized_workflow_session is None:
792
+ continue
793
+ results.append(deserialized_workflow_session)
794
+ else:
795
+ results.append(session_dict)
796
+
797
+ return results
798
+
799
+ except Exception as e:
800
+ log_error(f"Exception during bulk session upsert, falling back to individual upserts: {e}")
801
+
802
+ # Fallback to individual upserts
803
+ return [
804
+ result
805
+ for session in sessions
806
+ if session is not None
807
+ for result in [self.upsert_session(session, deserialize=deserialize)]
808
+ if result is not None
809
+ ]
810
+
811
+ # -- Memory methods --
812
+
813
+ def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None):
814
+ """Delete a user memory from the database.
815
+
816
+ Args:
817
+ memory_id (str): The ID of the memory to delete.
818
+ user_id (Optional[str]): The ID of the user to verify ownership. If provided, only delete if the memory belongs to this user.
819
+
820
+ Returns:
821
+ bool: True if the memory was deleted, False otherwise.
822
+
823
+ Raises:
824
+ Exception: If there is an error deleting the memory.
825
+ """
826
+ try:
827
+ collection = self._get_collection(table_type="memories")
828
+ if collection is None:
829
+ return
830
+
831
+ query = {"memory_id": memory_id}
832
+ if user_id is not None:
833
+ query["user_id"] = user_id
834
+
835
+ result = collection.delete_one(query)
836
+
837
+ success = result.deleted_count > 0
838
+ if success:
839
+ log_debug(f"Successfully deleted memory id: {memory_id}")
840
+ else:
841
+ log_debug(f"No memory found with id: {memory_id}")
842
+
843
+ except Exception as e:
844
+ log_error(f"Error deleting memory: {e}")
845
+ raise e
846
+
847
+ def delete_user_memories(self, memory_ids: List[str], user_id: Optional[str] = None) -> None:
848
+ """Delete user memories from the database.
849
+
850
+ Args:
851
+ memory_ids (List[str]): The IDs of the memories to delete.
852
+ user_id (Optional[str]): The ID of the user to verify ownership. If provided, only delete memories that belong to this user.
853
+
854
+ Raises:
855
+ Exception: If there is an error deleting the memories.
856
+ """
857
+ try:
858
+ collection = self._get_collection(table_type="memories")
859
+ if collection is None:
860
+ return
861
+
862
+ query: Dict[str, Any] = {"memory_id": {"$in": memory_ids}}
863
+ if user_id is not None:
864
+ query["user_id"] = user_id
865
+
866
+ result = collection.delete_many(query)
867
+
868
+ if result.deleted_count == 0:
869
+ log_debug(f"No memories found with ids: {memory_ids}")
870
+
871
+ except Exception as e:
872
+ log_error(f"Error deleting memories: {e}")
873
+ raise e
874
+
875
+ def get_all_memory_topics(self) -> List[str]:
876
+ """Get all memory topics from the database.
877
+
878
+ Returns:
879
+ List[str]: The topics.
880
+
881
+ Raises:
882
+ Exception: If there is an error getting the topics.
883
+ """
884
+ try:
885
+ collection = self._get_collection(table_type="memories")
886
+ if collection is None:
887
+ return []
888
+
889
+ topics = collection.distinct("topics", {})
890
+ return [topic for topic in topics if topic]
891
+
892
+ except Exception as e:
893
+ log_error(f"Exception reading from collection: {e}")
894
+ raise e
895
+
896
+ def get_user_memory(
897
+ self, memory_id: str, deserialize: Optional[bool] = True, user_id: Optional[str] = None
898
+ ) -> Optional[UserMemory]:
899
+ """Get a memory from the database.
900
+
901
+ Args:
902
+ memory_id (str): The ID of the memory to get.
903
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
904
+ user_id (Optional[str]): The ID of the user to verify ownership. If provided, only return the memory if it belongs to this user.
905
+
906
+ Returns:
907
+ Optional[UserMemory]:
908
+ - When deserialize=True: UserMemory object
909
+ - When deserialize=False: Memory dictionary
910
+
911
+ Raises:
912
+ Exception: If there is an error getting the memory.
913
+ """
914
+ try:
915
+ collection = self._get_collection(table_type="memories")
916
+ if collection is None:
917
+ return None
918
+
919
+ query = {"memory_id": memory_id}
920
+ if user_id is not None:
921
+ query["user_id"] = user_id
922
+
923
+ result = collection.find_one(query)
924
+ if result is None or not deserialize:
925
+ return result
926
+
927
+ # Remove MongoDB's _id field before creating UserMemory object
928
+ result_filtered = {k: v for k, v in result.items() if k != "_id"}
929
+ return UserMemory.from_dict(result_filtered)
930
+
931
+ except Exception as e:
932
+ log_error(f"Exception reading from collection: {e}")
933
+ raise e
934
+
935
+ def get_user_memories(
936
+ self,
937
+ user_id: Optional[str] = None,
938
+ agent_id: Optional[str] = None,
939
+ team_id: Optional[str] = None,
940
+ topics: Optional[List[str]] = None,
941
+ search_content: Optional[str] = None,
942
+ limit: Optional[int] = None,
943
+ page: Optional[int] = None,
944
+ sort_by: Optional[str] = None,
945
+ sort_order: Optional[str] = None,
946
+ deserialize: Optional[bool] = True,
947
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
948
+ """Get all memories from the database as UserMemory objects.
949
+
950
+ Args:
951
+ user_id (Optional[str]): The ID of the user to get the memories for.
952
+ agent_id (Optional[str]): The ID of the agent to get the memories for.
953
+ team_id (Optional[str]): The ID of the team to get the memories for.
954
+ topics (Optional[List[str]]): The topics to filter the memories by.
955
+ search_content (Optional[str]): The content to filter the memories by.
956
+ limit (Optional[int]): The limit of the memories to get.
957
+ page (Optional[int]): The page number to get.
958
+ sort_by (Optional[str]): The field to sort the memories by.
959
+ sort_order (Optional[str]): The order to sort the memories by.
960
+ deserialize (Optional[bool]): Whether to serialize the memories. Defaults to True.
961
+ create_table_if_not_found: Whether to create the collection if it doesn't exist.
962
+
963
+ Returns:
964
+ Tuple[List[Dict[str, Any]], int]: A tuple containing the memories and the total count.
965
+
966
+ Raises:
967
+ Exception: If there is an error getting the memories.
968
+ """
969
+ try:
970
+ collection = self._get_collection(table_type="memories")
971
+ if collection is None:
972
+ return [] if deserialize else ([], 0)
973
+
974
+ query: Dict[str, Any] = {}
975
+ if user_id is not None:
976
+ query["user_id"] = user_id
977
+ if agent_id is not None:
978
+ query["agent_id"] = agent_id
979
+ if team_id is not None:
980
+ query["team_id"] = team_id
981
+ if topics is not None:
982
+ query["topics"] = {"$in": topics}
983
+ if search_content is not None:
984
+ query["memory"] = {"$regex": search_content, "$options": "i"}
985
+
986
+ # Get total count
987
+ total_count = collection.count_documents(query)
988
+
989
+ # Apply sorting
990
+ sort_criteria = apply_sorting({}, sort_by, sort_order)
991
+
992
+ # Apply pagination
993
+ query_args = apply_pagination({}, limit, page)
994
+
995
+ cursor = collection.find(query)
996
+ if sort_criteria:
997
+ cursor = cursor.sort(sort_criteria)
998
+ if query_args.get("skip"):
999
+ cursor = cursor.skip(query_args["skip"])
1000
+ if query_args.get("limit"):
1001
+ cursor = cursor.limit(query_args["limit"])
1002
+
1003
+ records = list(cursor)
1004
+ if not deserialize:
1005
+ return records, total_count
1006
+
1007
+ # Remove MongoDB's _id field before creating UserMemory objects
1008
+ return [UserMemory.from_dict({k: v for k, v in record.items() if k != "_id"}) for record in records]
1009
+
1010
+ except Exception as e:
1011
+ log_error(f"Exception reading from collection: {e}")
1012
+ raise e
1013
+
1014
+ def get_user_memory_stats(
1015
+ self,
1016
+ limit: Optional[int] = None,
1017
+ page: Optional[int] = None,
1018
+ user_id: Optional[str] = None,
1019
+ ) -> Tuple[List[Dict[str, Any]], int]:
1020
+ """Get user memories stats.
1021
+
1022
+ Args:
1023
+ limit (Optional[int]): The limit of the memories to get.
1024
+ page (Optional[int]): The page number to get.
1025
+ user_id (Optional[str]): User ID for filtering.
1026
+
1027
+ Returns:
1028
+ Tuple[List[Dict[str, Any]], int]: A tuple containing the memories stats and the total count.
1029
+
1030
+ Raises:
1031
+ Exception: If there is an error getting the memories stats.
1032
+ """
1033
+ try:
1034
+ collection = self._get_collection(table_type="memories")
1035
+ if collection is None:
1036
+ return [], 0
1037
+
1038
+ match_stage: Dict[str, Any] = {"user_id": {"$ne": None}}
1039
+ if user_id is not None:
1040
+ match_stage["user_id"] = user_id
1041
+
1042
+ pipeline: List[Dict[str, Any]] = [
1043
+ {"$match": match_stage},
1044
+ {
1045
+ "$group": {
1046
+ "_id": "$user_id",
1047
+ "total_memories": {"$sum": 1},
1048
+ "last_memory_updated_at": {"$max": "$updated_at"},
1049
+ }
1050
+ },
1051
+ {"$sort": {"last_memory_updated_at": -1}},
1052
+ ]
1053
+
1054
+ # Get total count
1055
+ count_pipeline = pipeline + [{"$count": "total"}]
1056
+ count_result = list(collection.aggregate(count_pipeline)) # type: ignore
1057
+ total_count = count_result[0]["total"] if count_result else 0
1058
+
1059
+ # Apply pagination
1060
+ if limit is not None:
1061
+ if page is not None:
1062
+ pipeline.append({"$skip": (page - 1) * limit})
1063
+ pipeline.append({"$limit": limit})
1064
+
1065
+ results = list(collection.aggregate(pipeline)) # type: ignore
1066
+
1067
+ formatted_results = [
1068
+ {
1069
+ "user_id": result["_id"],
1070
+ "total_memories": result["total_memories"],
1071
+ "last_memory_updated_at": result["last_memory_updated_at"],
1072
+ }
1073
+ for result in results
1074
+ ]
1075
+
1076
+ return formatted_results, total_count
1077
+
1078
+ except Exception as e:
1079
+ log_error(f"Exception getting user memory stats: {e}")
1080
+ raise e
1081
+
1082
+ def upsert_user_memory(
1083
+ self, memory: UserMemory, deserialize: Optional[bool] = True
1084
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
1085
+ """Upsert a user memory in the database.
1086
+
1087
+ Args:
1088
+ memory (UserMemory): The memory to upsert.
1089
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
1090
+
1091
+ Returns:
1092
+ Optional[Union[UserMemory, Dict[str, Any]]]:
1093
+ - When deserialize=True: UserMemory object
1094
+ - When deserialize=False: Memory dictionary
1095
+
1096
+ Raises:
1097
+ Exception: If there is an error upserting the memory.
1098
+ """
1099
+ try:
1100
+ collection = self._get_collection(table_type="memories", create_collection_if_not_found=True)
1101
+ if collection is None:
1102
+ return None
1103
+
1104
+ if memory.memory_id is None:
1105
+ memory.memory_id = str(uuid4())
1106
+
1107
+ update_doc = {
1108
+ "user_id": memory.user_id,
1109
+ "agent_id": memory.agent_id,
1110
+ "team_id": memory.team_id,
1111
+ "memory_id": memory.memory_id,
1112
+ "memory": memory.memory,
1113
+ "topics": memory.topics,
1114
+ "updated_at": int(time.time()),
1115
+ }
1116
+
1117
+ result = collection.replace_one({"memory_id": memory.memory_id}, update_doc, upsert=True)
1118
+
1119
+ if result.upserted_id:
1120
+ update_doc["_id"] = result.upserted_id
1121
+
1122
+ if not deserialize:
1123
+ return update_doc
1124
+
1125
+ # Remove MongoDB's _id field before creating UserMemory object
1126
+ update_doc_filtered = {k: v for k, v in update_doc.items() if k != "_id"}
1127
+ return UserMemory.from_dict(update_doc_filtered)
1128
+
1129
+ except Exception as e:
1130
+ log_error(f"Exception upserting user memory: {e}")
1131
+ raise e
1132
+
1133
+ def upsert_memories(
1134
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
1135
+ ) -> List[Union[UserMemory, Dict[str, Any]]]:
1136
+ """
1137
+ Bulk upsert multiple user memories for improved performance on large datasets.
1138
+
1139
+ Args:
1140
+ memories (List[UserMemory]): List of memories to upsert.
1141
+ deserialize (Optional[bool]): Whether to deserialize the memories. Defaults to True.
1142
+
1143
+ Returns:
1144
+ List[Union[UserMemory, Dict[str, Any]]]: List of upserted memories.
1145
+
1146
+ Raises:
1147
+ Exception: If an error occurs during bulk upsert.
1148
+ """
1149
+ if not memories:
1150
+ return []
1151
+
1152
+ try:
1153
+ collection = self._get_collection(table_type="memories", create_collection_if_not_found=True)
1154
+ if collection is None:
1155
+ log_info("Memories collection not available, falling back to individual upserts")
1156
+ return [
1157
+ result
1158
+ for memory in memories
1159
+ if memory is not None
1160
+ for result in [self.upsert_user_memory(memory, deserialize=deserialize)]
1161
+ if result is not None
1162
+ ]
1163
+
1164
+ from pymongo import ReplaceOne
1165
+
1166
+ operations = []
1167
+ results: List[Union[UserMemory, Dict[str, Any]]] = []
1168
+
1169
+ current_time = int(time.time())
1170
+ for memory in memories:
1171
+ if memory is None:
1172
+ continue
1173
+
1174
+ if memory.memory_id is None:
1175
+ memory.memory_id = str(uuid4())
1176
+
1177
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
1178
+ updated_at = memory.updated_at if preserve_updated_at else current_time
1179
+
1180
+ record = {
1181
+ "user_id": memory.user_id,
1182
+ "agent_id": memory.agent_id,
1183
+ "team_id": memory.team_id,
1184
+ "memory_id": memory.memory_id,
1185
+ "memory": memory.memory,
1186
+ "input": memory.input,
1187
+ "feedback": memory.feedback,
1188
+ "topics": memory.topics,
1189
+ "created_at": memory.created_at,
1190
+ "updated_at": updated_at,
1191
+ }
1192
+
1193
+ operations.append(ReplaceOne(filter={"memory_id": memory.memory_id}, replacement=record, upsert=True))
1194
+
1195
+ if operations:
1196
+ # Execute bulk write
1197
+ collection.bulk_write(operations)
1198
+
1199
+ # Fetch the results
1200
+ memory_ids = [memory.memory_id for memory in memories if memory and memory.memory_id]
1201
+ cursor = collection.find({"memory_id": {"$in": memory_ids}})
1202
+
1203
+ for doc in cursor:
1204
+ if deserialize:
1205
+ # Remove MongoDB's _id field before creating UserMemory object
1206
+ doc_filtered = {k: v for k, v in doc.items() if k != "_id"}
1207
+ results.append(UserMemory.from_dict(doc_filtered))
1208
+ else:
1209
+ results.append(doc)
1210
+
1211
+ return results
1212
+
1213
+ except Exception as e:
1214
+ log_error(f"Exception during bulk memory upsert, falling back to individual upserts: {e}")
1215
+
1216
+ # Fallback to individual upserts
1217
+ return [
1218
+ result
1219
+ for memory in memories
1220
+ if memory is not None
1221
+ for result in [self.upsert_user_memory(memory, deserialize=deserialize)]
1222
+ if result is not None
1223
+ ]
1224
+
1225
+ def clear_memories(self) -> None:
1226
+ """Delete all memories from the database.
1227
+
1228
+ Raises:
1229
+ Exception: If an error occurs during deletion.
1230
+ """
1231
+ try:
1232
+ collection = self._get_collection(table_type="memories")
1233
+ if collection is None:
1234
+ return
1235
+
1236
+ collection.delete_many({})
1237
+
1238
+ except Exception as e:
1239
+ log_error(f"Exception deleting all memories: {e}")
1240
+ raise e
1241
+
1242
+ # -- Cultural Knowledge methods --
1243
+ def clear_cultural_knowledge(self) -> None:
1244
+ """Delete all cultural knowledge from the database.
1245
+
1246
+ Raises:
1247
+ Exception: If an error occurs during deletion.
1248
+ """
1249
+ try:
1250
+ collection = self._get_collection(table_type="culture")
1251
+ if collection is None:
1252
+ return
1253
+
1254
+ collection.delete_many({})
1255
+
1256
+ except Exception as e:
1257
+ log_error(f"Exception deleting all cultural knowledge: {e}")
1258
+ raise e
1259
+
1260
+ def delete_cultural_knowledge(self, id: str) -> None:
1261
+ """Delete cultural knowledge by ID.
1262
+
1263
+ Args:
1264
+ id (str): The ID of the cultural knowledge to delete.
1265
+
1266
+ Raises:
1267
+ Exception: If an error occurs during deletion.
1268
+ """
1269
+ try:
1270
+ collection = self._get_collection(table_type="culture")
1271
+ if collection is None:
1272
+ return
1273
+
1274
+ collection.delete_one({"id": id})
1275
+ log_debug(f"Deleted cultural knowledge with ID: {id}")
1276
+
1277
+ except Exception as e:
1278
+ log_error(f"Error deleting cultural knowledge: {e}")
1279
+ raise e
1280
+
1281
+ def get_cultural_knowledge(
1282
+ self, id: str, deserialize: Optional[bool] = True
1283
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1284
+ """Get cultural knowledge by ID.
1285
+
1286
+ Args:
1287
+ id (str): The ID of the cultural knowledge to retrieve.
1288
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
1289
+
1290
+ Returns:
1291
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
1292
+
1293
+ Raises:
1294
+ Exception: If an error occurs during retrieval.
1295
+ """
1296
+ try:
1297
+ collection = self._get_collection(table_type="culture")
1298
+ if collection is None:
1299
+ return None
1300
+
1301
+ result = collection.find_one({"id": id})
1302
+ if result is None:
1303
+ return None
1304
+
1305
+ # Remove MongoDB's _id field
1306
+ result_filtered = {k: v for k, v in result.items() if k != "_id"}
1307
+
1308
+ if not deserialize:
1309
+ return result_filtered
1310
+
1311
+ return deserialize_cultural_knowledge_from_db(result_filtered)
1312
+
1313
+ except Exception as e:
1314
+ log_error(f"Error getting cultural knowledge: {e}")
1315
+ raise e
1316
+
1317
+ def get_all_cultural_knowledge(
1318
+ self,
1319
+ agent_id: Optional[str] = None,
1320
+ team_id: Optional[str] = None,
1321
+ name: Optional[str] = None,
1322
+ limit: Optional[int] = None,
1323
+ page: Optional[int] = None,
1324
+ sort_by: Optional[str] = None,
1325
+ sort_order: Optional[str] = None,
1326
+ deserialize: Optional[bool] = True,
1327
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1328
+ """Get all cultural knowledge with filtering and pagination.
1329
+
1330
+ Args:
1331
+ agent_id (Optional[str]): Filter by agent ID.
1332
+ team_id (Optional[str]): Filter by team ID.
1333
+ name (Optional[str]): Filter by name (case-insensitive partial match).
1334
+ limit (Optional[int]): Maximum number of results to return.
1335
+ page (Optional[int]): Page number for pagination.
1336
+ sort_by (Optional[str]): Field to sort by.
1337
+ sort_order (Optional[str]): Sort order ('asc' or 'desc').
1338
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
1339
+
1340
+ Returns:
1341
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1342
+ - When deserialize=True: List of CulturalKnowledge objects
1343
+ - When deserialize=False: Tuple with list of dictionaries and total count
1344
+
1345
+ Raises:
1346
+ Exception: If an error occurs during retrieval.
1347
+ """
1348
+ try:
1349
+ collection = self._get_collection(table_type="culture")
1350
+ if collection is None:
1351
+ if not deserialize:
1352
+ return [], 0
1353
+ return []
1354
+
1355
+ # Build query
1356
+ query: Dict[str, Any] = {}
1357
+ if agent_id is not None:
1358
+ query["agent_id"] = agent_id
1359
+ if team_id is not None:
1360
+ query["team_id"] = team_id
1361
+ if name is not None:
1362
+ query["name"] = {"$regex": name, "$options": "i"}
1363
+
1364
+ # Get total count for pagination
1365
+ total_count = collection.count_documents(query)
1366
+
1367
+ # Apply sorting
1368
+ sort_criteria = apply_sorting({}, sort_by, sort_order)
1369
+
1370
+ # Apply pagination
1371
+ query_args = apply_pagination({}, limit, page)
1372
+
1373
+ cursor = collection.find(query)
1374
+ if sort_criteria:
1375
+ cursor = cursor.sort(sort_criteria)
1376
+ if query_args.get("skip"):
1377
+ cursor = cursor.skip(query_args["skip"])
1378
+ if query_args.get("limit"):
1379
+ cursor = cursor.limit(query_args["limit"])
1380
+
1381
+ # Remove MongoDB's _id field from all results
1382
+ results_filtered = [{k: v for k, v in item.items() if k != "_id"} for item in cursor]
1383
+
1384
+ if not deserialize:
1385
+ return results_filtered, total_count
1386
+
1387
+ return [deserialize_cultural_knowledge_from_db(item) for item in results_filtered]
1388
+
1389
+ except Exception as e:
1390
+ log_error(f"Error getting all cultural knowledge: {e}")
1391
+ raise e
1392
+
1393
+ def upsert_cultural_knowledge(
1394
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
1395
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1396
+ """Upsert cultural knowledge in MongoDB.
1397
+
1398
+ Args:
1399
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
1400
+ deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
1401
+
1402
+ Returns:
1403
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
1404
+
1405
+ Raises:
1406
+ Exception: If an error occurs during upsert.
1407
+ """
1408
+ try:
1409
+ collection = self._get_collection(table_type="culture", create_collection_if_not_found=True)
1410
+ if collection is None:
1411
+ return None
1412
+
1413
+ # Serialize content, categories, and notes into a dict for DB storage
1414
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
1415
+
1416
+ # Create the document with serialized content
1417
+ update_doc = {
1418
+ "id": cultural_knowledge.id,
1419
+ "name": cultural_knowledge.name,
1420
+ "summary": cultural_knowledge.summary,
1421
+ "content": content_dict if content_dict else None,
1422
+ "metadata": cultural_knowledge.metadata,
1423
+ "input": cultural_knowledge.input,
1424
+ "created_at": cultural_knowledge.created_at,
1425
+ "updated_at": int(time.time()),
1426
+ "agent_id": cultural_knowledge.agent_id,
1427
+ "team_id": cultural_knowledge.team_id,
1428
+ }
1429
+
1430
+ result = collection.replace_one({"id": cultural_knowledge.id}, update_doc, upsert=True)
1431
+
1432
+ if result.upserted_id:
1433
+ update_doc["_id"] = result.upserted_id
1434
+
1435
+ # Remove MongoDB's _id field
1436
+ doc_filtered = {k: v for k, v in update_doc.items() if k != "_id"}
1437
+
1438
+ if not deserialize:
1439
+ return doc_filtered
1440
+
1441
+ return deserialize_cultural_knowledge_from_db(doc_filtered)
1442
+
1443
+ except Exception as e:
1444
+ log_error(f"Error upserting cultural knowledge: {e}")
1445
+ raise e
1446
+
1447
+ # -- Metrics methods --
1448
+
1449
+ def _get_all_sessions_for_metrics_calculation(
1450
+ self, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None
1451
+ ) -> List[Dict[str, Any]]:
1452
+ """Get all sessions of all types for metrics calculation."""
1453
+ try:
1454
+ collection = self._get_collection(table_type="sessions")
1455
+ if collection is None:
1456
+ return []
1457
+
1458
+ query = {}
1459
+ if start_timestamp is not None:
1460
+ query["created_at"] = {"$gte": start_timestamp}
1461
+ if end_timestamp is not None:
1462
+ if "created_at" in query:
1463
+ query["created_at"]["$lte"] = end_timestamp
1464
+ else:
1465
+ query["created_at"] = {"$lte": end_timestamp}
1466
+
1467
+ projection = {
1468
+ "user_id": 1,
1469
+ "session_data": 1,
1470
+ "runs": 1,
1471
+ "created_at": 1,
1472
+ "session_type": 1,
1473
+ }
1474
+
1475
+ results = list(collection.find(query, projection))
1476
+ return results
1477
+
1478
+ except Exception as e:
1479
+ log_error(f"Exception reading from sessions collection: {e}")
1480
+ return []
1481
+
1482
+ def _get_metrics_calculation_starting_date(self, collection: Collection) -> Optional[date]:
1483
+ """Get the first date for which metrics calculation is needed."""
1484
+ try:
1485
+ result = collection.find_one({}, sort=[("date", -1)], limit=1)
1486
+
1487
+ if result is not None:
1488
+ result_date = datetime.strptime(result["date"], "%Y-%m-%d").date()
1489
+ if result.get("completed"):
1490
+ return result_date + timedelta(days=1)
1491
+ else:
1492
+ return result_date
1493
+
1494
+ # No metrics records. Return the date of the first recorded session.
1495
+ first_session_result = self.get_sessions(sort_by="created_at", sort_order="asc", limit=1, deserialize=False)
1496
+ first_session_date = first_session_result[0][0]["created_at"] if first_session_result[0] else None # type: ignore
1497
+
1498
+ if first_session_date is None:
1499
+ return None
1500
+
1501
+ return datetime.fromtimestamp(first_session_date, tz=timezone.utc).date()
1502
+
1503
+ except Exception as e:
1504
+ log_error(f"Exception getting metrics calculation starting date: {e}")
1505
+ return None
1506
+
1507
+ def calculate_metrics(self) -> Optional[list[dict]]:
1508
+ """Calculate metrics for all dates without complete metrics."""
1509
+ try:
1510
+ collection = self._get_collection(table_type="metrics", create_collection_if_not_found=True)
1511
+ if collection is None:
1512
+ return None
1513
+
1514
+ starting_date = self._get_metrics_calculation_starting_date(collection)
1515
+ if starting_date is None:
1516
+ log_info("No session data found. Won't calculate metrics.")
1517
+ return None
1518
+
1519
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
1520
+ if not dates_to_process:
1521
+ log_info("Metrics already calculated for all relevant dates.")
1522
+ return None
1523
+
1524
+ start_timestamp = int(
1525
+ datetime.combine(dates_to_process[0], datetime.min.time()).replace(tzinfo=timezone.utc).timestamp()
1526
+ )
1527
+ end_timestamp = int(
1528
+ datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time())
1529
+ .replace(tzinfo=timezone.utc)
1530
+ .timestamp()
1531
+ )
1532
+
1533
+ sessions = self._get_all_sessions_for_metrics_calculation(
1534
+ start_timestamp=start_timestamp, end_timestamp=end_timestamp
1535
+ )
1536
+ all_sessions_data = fetch_all_sessions_data(
1537
+ sessions=sessions, dates_to_process=dates_to_process, start_timestamp=start_timestamp
1538
+ )
1539
+ if not all_sessions_data:
1540
+ log_info("No new session data found. Won't calculate metrics.")
1541
+ return None
1542
+
1543
+ results = []
1544
+ metrics_records = []
1545
+
1546
+ for date_to_process in dates_to_process:
1547
+ date_key = date_to_process.isoformat()
1548
+ sessions_for_date = all_sessions_data.get(date_key, {})
1549
+
1550
+ # Skip dates with no sessions
1551
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
1552
+ continue
1553
+
1554
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
1555
+ metrics_records.append(metrics_record)
1556
+
1557
+ if metrics_records:
1558
+ results = bulk_upsert_metrics(collection, metrics_records)
1559
+
1560
+ return results
1561
+
1562
+ except Exception as e:
1563
+ log_error(f"Error calculating metrics: {e}")
1564
+ raise e
1565
+
1566
+ def get_metrics(
1567
+ self,
1568
+ starting_date: Optional[date] = None,
1569
+ ending_date: Optional[date] = None,
1570
+ ) -> Tuple[List[dict], Optional[int]]:
1571
+ """Get all metrics matching the given date range."""
1572
+ try:
1573
+ collection = self._get_collection(table_type="metrics")
1574
+ if collection is None:
1575
+ return [], None
1576
+
1577
+ query = {}
1578
+ if starting_date:
1579
+ query["date"] = {"$gte": starting_date.isoformat()}
1580
+ if ending_date:
1581
+ if "date" in query:
1582
+ query["date"]["$lte"] = ending_date.isoformat()
1583
+ else:
1584
+ query["date"] = {"$lte": ending_date.isoformat()}
1585
+
1586
+ records = list(collection.find(query))
1587
+ if not records:
1588
+ return [], None
1589
+
1590
+ # Get the latest updated_at
1591
+ latest_updated_at = max(record.get("updated_at", 0) for record in records)
1592
+
1593
+ return records, latest_updated_at
1594
+
1595
+ except Exception as e:
1596
+ log_error(f"Error getting metrics: {e}")
1597
+ raise e
1598
+
1599
+ # -- Knowledge methods --
1600
+
1601
+ def delete_knowledge_content(self, id: str):
1602
+ """Delete a knowledge row from the database.
1603
+
1604
+ Args:
1605
+ id (str): The ID of the knowledge row to delete.
1606
+
1607
+ Raises:
1608
+ Exception: If an error occurs during deletion.
1609
+ """
1610
+ try:
1611
+ collection = self._get_collection(table_type="knowledge")
1612
+ if collection is None:
1613
+ return
1614
+
1615
+ collection.delete_one({"id": id})
1616
+
1617
+ log_debug(f"Deleted knowledge content with id '{id}'")
1618
+
1619
+ except Exception as e:
1620
+ log_error(f"Error deleting knowledge content: {e}")
1621
+ raise e
1622
+
1623
+ def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1624
+ """Get a knowledge row from the database.
1625
+
1626
+ Args:
1627
+ id (str): The ID of the knowledge row to get.
1628
+
1629
+ Returns:
1630
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1631
+
1632
+ Raises:
1633
+ Exception: If an error occurs during retrieval.
1634
+ """
1635
+ try:
1636
+ collection = self._get_collection(table_type="knowledge")
1637
+ if collection is None:
1638
+ return None
1639
+
1640
+ result = collection.find_one({"id": id})
1641
+ if result is None:
1642
+ return None
1643
+
1644
+ return KnowledgeRow.model_validate(result)
1645
+
1646
+ except Exception as e:
1647
+ log_error(f"Error getting knowledge content: {e}")
1648
+ raise e
1649
+
1650
+ def get_knowledge_contents(
1651
+ self,
1652
+ limit: Optional[int] = None,
1653
+ page: Optional[int] = None,
1654
+ sort_by: Optional[str] = None,
1655
+ sort_order: Optional[str] = None,
1656
+ ) -> Tuple[List[KnowledgeRow], int]:
1657
+ """Get all knowledge contents from the database.
1658
+
1659
+ Args:
1660
+ limit (Optional[int]): The maximum number of knowledge contents to return.
1661
+ page (Optional[int]): The page number.
1662
+ sort_by (Optional[str]): The column to sort by.
1663
+ sort_order (Optional[str]): The order to sort by.
1664
+ create_table_if_not_found (Optional[bool]): Whether to create the collection if it doesn't exist.
1665
+
1666
+ Returns:
1667
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
1668
+
1669
+ Raises:
1670
+ Exception: If an error occurs during retrieval.
1671
+ """
1672
+ try:
1673
+ collection = self._get_collection(table_type="knowledge")
1674
+ if collection is None:
1675
+ return [], 0
1676
+
1677
+ query: Dict[str, Any] = {}
1678
+
1679
+ # Get total count
1680
+ total_count = collection.count_documents(query)
1681
+
1682
+ # Apply sorting
1683
+ sort_criteria = apply_sorting({}, sort_by, sort_order)
1684
+
1685
+ # Apply pagination
1686
+ query_args = apply_pagination({}, limit, page)
1687
+
1688
+ cursor = collection.find(query)
1689
+ if sort_criteria:
1690
+ cursor = cursor.sort(sort_criteria)
1691
+ if query_args.get("skip"):
1692
+ cursor = cursor.skip(query_args["skip"])
1693
+ if query_args.get("limit"):
1694
+ cursor = cursor.limit(query_args["limit"])
1695
+
1696
+ records = list(cursor)
1697
+ knowledge_rows = [KnowledgeRow.model_validate(record) for record in records]
1698
+
1699
+ return knowledge_rows, total_count
1700
+
1701
+ except Exception as e:
1702
+ log_error(f"Error getting knowledge contents: {e}")
1703
+ raise e
1704
+
1705
+ def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
1706
+ """Upsert knowledge content in the database.
1707
+
1708
+ Args:
1709
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
1710
+
1711
+ Returns:
1712
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
1713
+
1714
+ Raises:
1715
+ Exception: If an error occurs during upsert.
1716
+ """
1717
+ try:
1718
+ collection = self._get_collection(table_type="knowledge", create_collection_if_not_found=True)
1719
+ if collection is None:
1720
+ return None
1721
+
1722
+ update_doc = knowledge_row.model_dump()
1723
+ collection.replace_one({"id": knowledge_row.id}, update_doc, upsert=True)
1724
+
1725
+ return knowledge_row
1726
+
1727
+ except Exception as e:
1728
+ log_error(f"Error upserting knowledge content: {e}")
1729
+ raise e
1730
+
1731
+ # -- Eval methods --
1732
+
1733
+ def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
1734
+ """Create an EvalRunRecord in the database."""
1735
+ try:
1736
+ collection = self._get_collection(table_type="evals", create_collection_if_not_found=True)
1737
+ if collection is None:
1738
+ return None
1739
+
1740
+ current_time = int(time.time())
1741
+ eval_dict = eval_run.model_dump()
1742
+ eval_dict["created_at"] = current_time
1743
+ eval_dict["updated_at"] = current_time
1744
+
1745
+ collection.insert_one(eval_dict)
1746
+
1747
+ log_debug(f"Created eval run with id '{eval_run.run_id}'")
1748
+
1749
+ return eval_run
1750
+
1751
+ except Exception as e:
1752
+ log_error(f"Error creating eval run: {e}")
1753
+ raise e
1754
+
1755
+ def delete_eval_run(self, eval_run_id: str) -> None:
1756
+ """Delete an eval run from the database."""
1757
+ try:
1758
+ collection = self._get_collection(table_type="evals")
1759
+ if collection is None:
1760
+ return
1761
+
1762
+ result = collection.delete_one({"run_id": eval_run_id})
1763
+
1764
+ if result.deleted_count == 0:
1765
+ log_debug(f"No eval run found with ID: {eval_run_id}")
1766
+ else:
1767
+ log_debug(f"Deleted eval run with ID: {eval_run_id}")
1768
+
1769
+ except Exception as e:
1770
+ log_error(f"Error deleting eval run {eval_run_id}: {e}")
1771
+ raise e
1772
+
1773
+ def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1774
+ """Delete multiple eval runs from the database."""
1775
+ try:
1776
+ collection = self._get_collection(table_type="evals")
1777
+ if collection is None:
1778
+ return
1779
+
1780
+ result = collection.delete_many({"run_id": {"$in": eval_run_ids}})
1781
+
1782
+ if result.deleted_count == 0:
1783
+ log_debug(f"No eval runs found with IDs: {eval_run_ids}")
1784
+ else:
1785
+ log_debug(f"Deleted {result.deleted_count} eval runs")
1786
+
1787
+ except Exception as e:
1788
+ log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
1789
+ raise e
1790
+
1791
+ def get_eval_run_raw(self, eval_run_id: str) -> Optional[Dict[str, Any]]:
1792
+ """Get an eval run from the database as a raw dictionary."""
1793
+ try:
1794
+ collection = self._get_collection(table_type="evals")
1795
+ if collection is None:
1796
+ return None
1797
+
1798
+ result = collection.find_one({"run_id": eval_run_id})
1799
+ return result
1800
+
1801
+ except Exception as e:
1802
+ log_error(f"Exception getting eval run {eval_run_id}: {e}")
1803
+ raise e
1804
+
1805
+ def get_eval_run(self, eval_run_id: str, deserialize: Optional[bool] = True) -> Optional[EvalRunRecord]:
1806
+ """Get an eval run from the database.
1807
+
1808
+ Args:
1809
+ eval_run_id (str): The ID of the eval run to get.
1810
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1811
+
1812
+ Returns:
1813
+ Optional[EvalRunRecord]:
1814
+ - When deserialize=True: EvalRunRecord object
1815
+ - When deserialize=False: EvalRun dictionary
1816
+
1817
+ Raises:
1818
+ Exception: If there is an error getting the eval run.
1819
+ """
1820
+ try:
1821
+ collection = self._get_collection(table_type="evals")
1822
+ if collection is None:
1823
+ return None
1824
+
1825
+ eval_run_raw = collection.find_one({"run_id": eval_run_id})
1826
+
1827
+ if not eval_run_raw:
1828
+ return None
1829
+
1830
+ if not deserialize:
1831
+ return eval_run_raw
1832
+
1833
+ return EvalRunRecord.model_validate(eval_run_raw)
1834
+
1835
+ except Exception as e:
1836
+ log_error(f"Exception getting eval run {eval_run_id}: {e}")
1837
+ raise e
1838
+
1839
+ def get_eval_runs(
1840
+ self,
1841
+ limit: Optional[int] = None,
1842
+ page: Optional[int] = None,
1843
+ sort_by: Optional[str] = None,
1844
+ sort_order: Optional[str] = None,
1845
+ agent_id: Optional[str] = None,
1846
+ team_id: Optional[str] = None,
1847
+ workflow_id: Optional[str] = None,
1848
+ model_id: Optional[str] = None,
1849
+ filter_type: Optional[EvalFilterType] = None,
1850
+ eval_type: Optional[List[EvalType]] = None,
1851
+ deserialize: Optional[bool] = True,
1852
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1853
+ """Get all eval runs from the database.
1854
+
1855
+ Args:
1856
+ limit (Optional[int]): The maximum number of eval runs to return.
1857
+ page (Optional[int]): The page number to return.
1858
+ sort_by (Optional[str]): The field to sort by.
1859
+ sort_order (Optional[str]): The order to sort by.
1860
+ agent_id (Optional[str]): The ID of the agent to filter by.
1861
+ team_id (Optional[str]): The ID of the team to filter by.
1862
+ workflow_id (Optional[str]): The ID of the workflow to filter by.
1863
+ model_id (Optional[str]): The ID of the model to filter by.
1864
+ eval_type (Optional[List[EvalType]]): The type of eval to filter by.
1865
+ filter_type (Optional[EvalFilterType]): The type of filter to apply.
1866
+ deserialize (Optional[bool]): Whether to serialize the eval runs. Defaults to True.
1867
+ create_table_if_not_found (Optional[bool]): Whether to create the collection if it doesn't exist.
1868
+
1869
+ Returns:
1870
+ Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1871
+ - When deserialize=True: List of EvalRunRecord objects
1872
+ - When deserialize=False: List of eval run dictionaries and the total count
1873
+
1874
+ Raises:
1875
+ Exception: If there is an error getting the eval runs.
1876
+ """
1877
+ try:
1878
+ collection = self._get_collection(table_type="evals")
1879
+ if collection is None:
1880
+ return [] if deserialize else ([], 0)
1881
+
1882
+ query: Dict[str, Any] = {}
1883
+ if agent_id is not None:
1884
+ query["agent_id"] = agent_id
1885
+ if team_id is not None:
1886
+ query["team_id"] = team_id
1887
+ if workflow_id is not None:
1888
+ query["workflow_id"] = workflow_id
1889
+ if model_id is not None:
1890
+ query["model_id"] = model_id
1891
+ if eval_type is not None and len(eval_type) > 0:
1892
+ query["eval_type"] = {"$in": eval_type}
1893
+ if filter_type is not None:
1894
+ if filter_type == EvalFilterType.AGENT:
1895
+ query["agent_id"] = {"$ne": None}
1896
+ elif filter_type == EvalFilterType.TEAM:
1897
+ query["team_id"] = {"$ne": None}
1898
+ elif filter_type == EvalFilterType.WORKFLOW:
1899
+ query["workflow_id"] = {"$ne": None}
1900
+
1901
+ # Get total count
1902
+ total_count = collection.count_documents(query)
1903
+
1904
+ # Apply default sorting by created_at desc if no sort parameters provided
1905
+ if sort_by is None:
1906
+ sort_criteria = [("created_at", -1)]
1907
+ else:
1908
+ sort_criteria = apply_sorting({}, sort_by, sort_order)
1909
+
1910
+ # Apply pagination
1911
+ query_args = apply_pagination({}, limit, page)
1912
+
1913
+ cursor = collection.find(query)
1914
+ if sort_criteria:
1915
+ cursor = cursor.sort(sort_criteria)
1916
+ if query_args.get("skip"):
1917
+ cursor = cursor.skip(query_args["skip"])
1918
+ if query_args.get("limit"):
1919
+ cursor = cursor.limit(query_args["limit"])
1920
+
1921
+ records = list(cursor)
1922
+ if not records:
1923
+ return [] if deserialize else ([], 0)
1924
+
1925
+ if not deserialize:
1926
+ return records, total_count
1927
+
1928
+ return [EvalRunRecord.model_validate(row) for row in records]
1929
+
1930
+ except Exception as e:
1931
+ log_error(f"Exception getting eval runs: {e}")
1932
+ raise e
1933
+
1934
+ def rename_eval_run(
1935
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1936
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1937
+ """Update the name of an eval run in the database.
1938
+
1939
+ Args:
1940
+ eval_run_id (str): The ID of the eval run to update.
1941
+ name (str): The new name of the eval run.
1942
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1943
+
1944
+ Returns:
1945
+ Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1946
+ - When deserialize=True: EvalRunRecord object
1947
+ - When deserialize=False: EvalRun dictionary
1948
+
1949
+ Raises:
1950
+ Exception: If there is an error updating the eval run.
1951
+ """
1952
+ try:
1953
+ collection = self._get_collection(table_type="evals")
1954
+ if collection is None:
1955
+ return None
1956
+
1957
+ result = collection.find_one_and_update(
1958
+ {"run_id": eval_run_id}, {"$set": {"name": name, "updated_at": int(time.time())}}
1959
+ )
1960
+
1961
+ log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
1962
+
1963
+ if not result or not deserialize:
1964
+ return result
1965
+
1966
+ return EvalRunRecord.model_validate(result)
1967
+
1968
+ except Exception as e:
1969
+ log_error(f"Error updating eval run name {eval_run_id}: {e}")
1970
+ raise e
1971
+
1972
+ def migrate_table_from_v1_to_v2(self, v1_db_schema: str, v1_table_name: str, v1_table_type: str):
1973
+ """Migrate all content in the given collection to the right v2 collection"""
1974
+
1975
+ from typing import List, Sequence, Union
1976
+
1977
+ from agno.db.migrations.v1_to_v2 import (
1978
+ get_all_table_content,
1979
+ parse_agent_sessions,
1980
+ parse_memories,
1981
+ parse_team_sessions,
1982
+ parse_workflow_sessions,
1983
+ )
1984
+
1985
+ # Get all content from the old collection
1986
+ old_content: list[dict[str, Any]] = get_all_table_content(
1987
+ db=self,
1988
+ db_schema=v1_db_schema,
1989
+ table_name=v1_table_name,
1990
+ )
1991
+ if not old_content:
1992
+ log_info(f"No content to migrate from collection {v1_table_name}")
1993
+ return
1994
+
1995
+ # Parse the content into the new format
1996
+ memories: List[UserMemory] = []
1997
+ sessions: Sequence[Union[AgentSession, TeamSession, WorkflowSession]] = []
1998
+ if v1_table_type == "agent_sessions":
1999
+ sessions = parse_agent_sessions(old_content)
2000
+ elif v1_table_type == "team_sessions":
2001
+ sessions = parse_team_sessions(old_content)
2002
+ elif v1_table_type == "workflow_sessions":
2003
+ sessions = parse_workflow_sessions(old_content)
2004
+ elif v1_table_type == "memories":
2005
+ memories = parse_memories(old_content)
2006
+ else:
2007
+ raise ValueError(f"Invalid table type: {v1_table_type}")
2008
+
2009
+ # Insert the new content into the new collection
2010
+ if v1_table_type == "agent_sessions":
2011
+ for session in sessions:
2012
+ self.upsert_session(session)
2013
+ log_info(f"Migrated {len(sessions)} Agent sessions to collection: {self.session_table_name}")
2014
+
2015
+ elif v1_table_type == "team_sessions":
2016
+ for session in sessions:
2017
+ self.upsert_session(session)
2018
+ log_info(f"Migrated {len(sessions)} Team sessions to collection: {self.session_table_name}")
2019
+
2020
+ elif v1_table_type == "workflow_sessions":
2021
+ for session in sessions:
2022
+ self.upsert_session(session)
2023
+ log_info(f"Migrated {len(sessions)} Workflow sessions to collection: {self.session_table_name}")
2024
+
2025
+ elif v1_table_type == "memories":
2026
+ for memory in memories:
2027
+ self.upsert_user_memory(memory)
2028
+ log_info(f"Migrated {len(memories)} memories to collection: {self.memory_table_name}")
2029
+
2030
+ # --- Traces ---
2031
+ def _get_component_level(
2032
+ self, workflow_id: Optional[str], team_id: Optional[str], agent_id: Optional[str], name: str
2033
+ ) -> int:
2034
+ """Get the component level for a trace based on its context.
2035
+
2036
+ Component levels (higher = more important):
2037
+ - 3: Workflow root (.run or .arun with workflow_id)
2038
+ - 2: Team root (.run or .arun with team_id)
2039
+ - 1: Agent root (.run or .arun with agent_id)
2040
+ - 0: Child span (not a root)
2041
+
2042
+ Args:
2043
+ workflow_id: The workflow ID of the trace.
2044
+ team_id: The team ID of the trace.
2045
+ agent_id: The agent ID of the trace.
2046
+ name: The name of the trace.
2047
+
2048
+ Returns:
2049
+ int: The component level (0-3).
2050
+ """
2051
+ # Check if name indicates a root span
2052
+ is_root_name = ".run" in name or ".arun" in name
2053
+
2054
+ if not is_root_name:
2055
+ return 0 # Child span (not a root)
2056
+ elif workflow_id:
2057
+ return 3 # Workflow root
2058
+ elif team_id:
2059
+ return 2 # Team root
2060
+ elif agent_id:
2061
+ return 1 # Agent root
2062
+ else:
2063
+ return 0 # Unknown
2064
+
2065
+ def upsert_trace(self, trace: "Trace") -> None:
2066
+ """Create or update a single trace record in the database.
2067
+
2068
+ Uses MongoDB's update_one with upsert=True and aggregation pipeline
2069
+ to handle concurrent inserts atomically and avoid race conditions.
2070
+
2071
+ Args:
2072
+ trace: The Trace object to store (one per trace_id).
2073
+ """
2074
+ try:
2075
+ collection = self._get_collection(table_type="traces", create_collection_if_not_found=True)
2076
+ if collection is None:
2077
+ return
2078
+
2079
+ trace_dict = trace.to_dict()
2080
+ trace_dict.pop("total_spans", None)
2081
+ trace_dict.pop("error_count", None)
2082
+
2083
+ # Calculate the component level for the new trace
2084
+ new_level = self._get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
2085
+
2086
+ # Use MongoDB aggregation pipeline update for atomic upsert
2087
+ # This allows conditional logic within a single atomic operation
2088
+ pipeline: List[Dict[str, Any]] = [
2089
+ {
2090
+ "$set": {
2091
+ # Always update these fields
2092
+ "status": trace.status,
2093
+ "created_at": {"$ifNull": ["$created_at", trace_dict.get("created_at")]},
2094
+ # Use $min for start_time (keep earliest)
2095
+ "start_time": {
2096
+ "$cond": {
2097
+ "if": {"$eq": [{"$type": "$start_time"}, "missing"]},
2098
+ "then": trace_dict.get("start_time"),
2099
+ "else": {"$min": ["$start_time", trace_dict.get("start_time")]},
2100
+ }
2101
+ },
2102
+ # Use $max for end_time (keep latest)
2103
+ "end_time": {
2104
+ "$cond": {
2105
+ "if": {"$eq": [{"$type": "$end_time"}, "missing"]},
2106
+ "then": trace_dict.get("end_time"),
2107
+ "else": {"$max": ["$end_time", trace_dict.get("end_time")]},
2108
+ }
2109
+ },
2110
+ # Preserve existing non-null context values using $ifNull
2111
+ "run_id": {"$ifNull": [trace.run_id, "$run_id"]},
2112
+ "session_id": {"$ifNull": [trace.session_id, "$session_id"]},
2113
+ "user_id": {"$ifNull": [trace.user_id, "$user_id"]},
2114
+ "agent_id": {"$ifNull": [trace.agent_id, "$agent_id"]},
2115
+ "team_id": {"$ifNull": [trace.team_id, "$team_id"]},
2116
+ "workflow_id": {"$ifNull": [trace.workflow_id, "$workflow_id"]},
2117
+ }
2118
+ },
2119
+ {
2120
+ "$set": {
2121
+ # Calculate duration_ms from the (potentially updated) start_time and end_time
2122
+ # MongoDB stores dates as strings in ISO format, so we need to parse them
2123
+ "duration_ms": {
2124
+ "$cond": {
2125
+ "if": {
2126
+ "$and": [
2127
+ {"$ne": [{"$type": "$start_time"}, "missing"]},
2128
+ {"$ne": [{"$type": "$end_time"}, "missing"]},
2129
+ ]
2130
+ },
2131
+ "then": {
2132
+ "$subtract": [
2133
+ {"$toLong": {"$toDate": "$end_time"}},
2134
+ {"$toLong": {"$toDate": "$start_time"}},
2135
+ ]
2136
+ },
2137
+ "else": trace_dict.get("duration_ms", 0),
2138
+ }
2139
+ },
2140
+ # Update name based on component level priority
2141
+ # Only update if new trace is from a higher-level component
2142
+ "name": {
2143
+ "$cond": {
2144
+ "if": {"$eq": [{"$type": "$name"}, "missing"]},
2145
+ "then": trace.name,
2146
+ "else": {
2147
+ "$cond": {
2148
+ "if": {
2149
+ "$gt": [
2150
+ new_level,
2151
+ {
2152
+ "$switch": {
2153
+ "branches": [
2154
+ # Check if existing name is a root span
2155
+ {
2156
+ "case": {
2157
+ "$not": {
2158
+ "$or": [
2159
+ {
2160
+ "$regexMatch": {
2161
+ "input": {"$ifNull": ["$name", ""]},
2162
+ "regex": "\\.run",
2163
+ }
2164
+ },
2165
+ {
2166
+ "$regexMatch": {
2167
+ "input": {"$ifNull": ["$name", ""]},
2168
+ "regex": "\\.arun",
2169
+ }
2170
+ },
2171
+ ]
2172
+ }
2173
+ },
2174
+ "then": 0,
2175
+ },
2176
+ # Workflow root (level 3)
2177
+ {
2178
+ "case": {"$ne": ["$workflow_id", None]},
2179
+ "then": 3,
2180
+ },
2181
+ # Team root (level 2)
2182
+ {
2183
+ "case": {"$ne": ["$team_id", None]},
2184
+ "then": 2,
2185
+ },
2186
+ # Agent root (level 1)
2187
+ {
2188
+ "case": {"$ne": ["$agent_id", None]},
2189
+ "then": 1,
2190
+ },
2191
+ ],
2192
+ "default": 0,
2193
+ }
2194
+ },
2195
+ ]
2196
+ },
2197
+ "then": trace.name,
2198
+ "else": "$name",
2199
+ }
2200
+ },
2201
+ }
2202
+ },
2203
+ }
2204
+ },
2205
+ ]
2206
+
2207
+ # Perform atomic upsert using aggregation pipeline
2208
+ collection.update_one(
2209
+ {"trace_id": trace.trace_id},
2210
+ pipeline,
2211
+ upsert=True,
2212
+ )
2213
+
2214
+ except Exception as e:
2215
+ log_error(f"Error creating trace: {e}")
2216
+ # Don't raise - tracing should not break the main application flow
2217
+
2218
+ def get_trace(
2219
+ self,
2220
+ trace_id: Optional[str] = None,
2221
+ run_id: Optional[str] = None,
2222
+ ):
2223
+ """Get a single trace by trace_id or other filters.
2224
+
2225
+ Args:
2226
+ trace_id: The unique trace identifier.
2227
+ run_id: Filter by run ID (returns first match).
2228
+
2229
+ Returns:
2230
+ Optional[Trace]: The trace if found, None otherwise.
2231
+
2232
+ Note:
2233
+ If multiple filters are provided, trace_id takes precedence.
2234
+ For other filters, the most recent trace is returned.
2235
+ """
2236
+ try:
2237
+ from agno.tracing.schemas import Trace as TraceSchema
2238
+
2239
+ collection = self._get_collection(table_type="traces")
2240
+ if collection is None:
2241
+ return None
2242
+
2243
+ # Get spans collection for aggregation
2244
+ spans_collection = self._get_collection(table_type="spans")
2245
+
2246
+ query: Dict[str, Any] = {}
2247
+ if trace_id:
2248
+ query["trace_id"] = trace_id
2249
+ elif run_id:
2250
+ query["run_id"] = run_id
2251
+ else:
2252
+ log_debug("get_trace called without any filter parameters")
2253
+ return None
2254
+
2255
+ # Find trace with sorting by most recent
2256
+ result = collection.find_one(query, sort=[("start_time", -1)])
2257
+
2258
+ if result:
2259
+ # Calculate total_spans and error_count from spans collection
2260
+ total_spans = 0
2261
+ error_count = 0
2262
+ if spans_collection is not None:
2263
+ total_spans = spans_collection.count_documents({"trace_id": result["trace_id"]})
2264
+ error_count = spans_collection.count_documents(
2265
+ {"trace_id": result["trace_id"], "status_code": "ERROR"}
2266
+ )
2267
+
2268
+ result["total_spans"] = total_spans
2269
+ result["error_count"] = error_count
2270
+ # Remove MongoDB's _id field
2271
+ result.pop("_id", None)
2272
+ return TraceSchema.from_dict(result)
2273
+ return None
2274
+
2275
+ except Exception as e:
2276
+ log_error(f"Error getting trace: {e}")
2277
+ return None
2278
+
2279
+ def get_traces(
2280
+ self,
2281
+ run_id: Optional[str] = None,
2282
+ session_id: Optional[str] = None,
2283
+ user_id: Optional[str] = None,
2284
+ agent_id: Optional[str] = None,
2285
+ team_id: Optional[str] = None,
2286
+ workflow_id: Optional[str] = None,
2287
+ status: Optional[str] = None,
2288
+ start_time: Optional[datetime] = None,
2289
+ end_time: Optional[datetime] = None,
2290
+ limit: Optional[int] = 20,
2291
+ page: Optional[int] = 1,
2292
+ ) -> tuple[List, int]:
2293
+ """Get traces matching the provided filters with pagination.
2294
+
2295
+ Args:
2296
+ run_id: Filter by run ID.
2297
+ session_id: Filter by session ID.
2298
+ user_id: Filter by user ID.
2299
+ agent_id: Filter by agent ID.
2300
+ team_id: Filter by team ID.
2301
+ workflow_id: Filter by workflow ID.
2302
+ status: Filter by status (OK, ERROR, UNSET).
2303
+ start_time: Filter traces starting after this datetime.
2304
+ end_time: Filter traces ending before this datetime.
2305
+ limit: Maximum number of traces to return per page.
2306
+ page: Page number (1-indexed).
2307
+
2308
+ Returns:
2309
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
2310
+ """
2311
+ try:
2312
+ from agno.tracing.schemas import Trace as TraceSchema
2313
+
2314
+ collection = self._get_collection(table_type="traces")
2315
+ if collection is None:
2316
+ log_debug("Traces collection not found")
2317
+ return [], 0
2318
+
2319
+ # Get spans collection for aggregation
2320
+ spans_collection = self._get_collection(table_type="spans")
2321
+
2322
+ # Build query
2323
+ query: Dict[str, Any] = {}
2324
+ if run_id:
2325
+ query["run_id"] = run_id
2326
+ if session_id:
2327
+ query["session_id"] = session_id
2328
+ if user_id:
2329
+ query["user_id"] = user_id
2330
+ if agent_id:
2331
+ query["agent_id"] = agent_id
2332
+ if team_id:
2333
+ query["team_id"] = team_id
2334
+ if workflow_id:
2335
+ query["workflow_id"] = workflow_id
2336
+ if status:
2337
+ query["status"] = status
2338
+ if start_time:
2339
+ query["start_time"] = {"$gte": start_time.isoformat()}
2340
+ if end_time:
2341
+ if "end_time" in query:
2342
+ query["end_time"]["$lte"] = end_time.isoformat()
2343
+ else:
2344
+ query["end_time"] = {"$lte": end_time.isoformat()}
2345
+
2346
+ # Get total count
2347
+ total_count = collection.count_documents(query)
2348
+
2349
+ # Apply pagination
2350
+ skip = ((page or 1) - 1) * (limit or 20)
2351
+ cursor = collection.find(query).sort("start_time", -1).skip(skip).limit(limit or 20)
2352
+
2353
+ results = list(cursor)
2354
+
2355
+ traces = []
2356
+ for row in results:
2357
+ # Calculate total_spans and error_count from spans collection
2358
+ total_spans = 0
2359
+ error_count = 0
2360
+ if spans_collection is not None:
2361
+ total_spans = spans_collection.count_documents({"trace_id": row["trace_id"]})
2362
+ error_count = spans_collection.count_documents(
2363
+ {"trace_id": row["trace_id"], "status_code": "ERROR"}
2364
+ )
2365
+
2366
+ row["total_spans"] = total_spans
2367
+ row["error_count"] = error_count
2368
+ # Remove MongoDB's _id field
2369
+ row.pop("_id", None)
2370
+ traces.append(TraceSchema.from_dict(row))
2371
+
2372
+ return traces, total_count
2373
+
2374
+ except Exception as e:
2375
+ log_error(f"Error getting traces: {e}")
2376
+ return [], 0
2377
+
2378
+ def get_trace_stats(
2379
+ self,
2380
+ user_id: Optional[str] = None,
2381
+ agent_id: Optional[str] = None,
2382
+ team_id: Optional[str] = None,
2383
+ workflow_id: Optional[str] = None,
2384
+ start_time: Optional[datetime] = None,
2385
+ end_time: Optional[datetime] = None,
2386
+ limit: Optional[int] = 20,
2387
+ page: Optional[int] = 1,
2388
+ ) -> tuple[List[Dict[str, Any]], int]:
2389
+ """Get trace statistics grouped by session.
2390
+
2391
+ Args:
2392
+ user_id: Filter by user ID.
2393
+ agent_id: Filter by agent ID.
2394
+ team_id: Filter by team ID.
2395
+ workflow_id: Filter by workflow ID.
2396
+ start_time: Filter sessions with traces created after this datetime.
2397
+ end_time: Filter sessions with traces created before this datetime.
2398
+ limit: Maximum number of sessions to return per page.
2399
+ page: Page number (1-indexed).
2400
+
2401
+ Returns:
2402
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
2403
+ Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
2404
+ workflow_id, first_trace_at, last_trace_at.
2405
+ """
2406
+ try:
2407
+ collection = self._get_collection(table_type="traces")
2408
+ if collection is None:
2409
+ log_debug("Traces collection not found")
2410
+ return [], 0
2411
+
2412
+ # Build match stage
2413
+ match_stage: Dict[str, Any] = {"session_id": {"$ne": None}}
2414
+ if user_id:
2415
+ match_stage["user_id"] = user_id
2416
+ if agent_id:
2417
+ match_stage["agent_id"] = agent_id
2418
+ if team_id:
2419
+ match_stage["team_id"] = team_id
2420
+ if workflow_id:
2421
+ match_stage["workflow_id"] = workflow_id
2422
+ if start_time:
2423
+ match_stage["created_at"] = {"$gte": start_time.isoformat()}
2424
+ if end_time:
2425
+ if "created_at" in match_stage:
2426
+ match_stage["created_at"]["$lte"] = end_time.isoformat()
2427
+ else:
2428
+ match_stage["created_at"] = {"$lte": end_time.isoformat()}
2429
+
2430
+ # Build aggregation pipeline
2431
+ pipeline: List[Dict[str, Any]] = [
2432
+ {"$match": match_stage},
2433
+ {
2434
+ "$group": {
2435
+ "_id": "$session_id",
2436
+ "user_id": {"$first": "$user_id"},
2437
+ "agent_id": {"$first": "$agent_id"},
2438
+ "team_id": {"$first": "$team_id"},
2439
+ "workflow_id": {"$first": "$workflow_id"},
2440
+ "total_traces": {"$sum": 1},
2441
+ "first_trace_at": {"$min": "$created_at"},
2442
+ "last_trace_at": {"$max": "$created_at"},
2443
+ }
2444
+ },
2445
+ {"$sort": {"last_trace_at": -1}},
2446
+ ]
2447
+
2448
+ # Get total count
2449
+ count_pipeline = pipeline + [{"$count": "total"}]
2450
+ count_result = list(collection.aggregate(count_pipeline))
2451
+ total_count = count_result[0]["total"] if count_result else 0
2452
+
2453
+ # Apply pagination
2454
+ skip = ((page or 1) - 1) * (limit or 20)
2455
+ pipeline.append({"$skip": skip})
2456
+ pipeline.append({"$limit": limit or 20})
2457
+
2458
+ results = list(collection.aggregate(pipeline))
2459
+
2460
+ # Convert to list of dicts with datetime objects
2461
+ stats_list = []
2462
+ for row in results:
2463
+ # Convert ISO strings to datetime objects
2464
+ first_trace_at_str = row["first_trace_at"]
2465
+ last_trace_at_str = row["last_trace_at"]
2466
+
2467
+ # Parse ISO format strings to datetime objects
2468
+ first_trace_at = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
2469
+ last_trace_at = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
2470
+
2471
+ stats_list.append(
2472
+ {
2473
+ "session_id": row["_id"],
2474
+ "user_id": row["user_id"],
2475
+ "agent_id": row["agent_id"],
2476
+ "team_id": row["team_id"],
2477
+ "workflow_id": row["workflow_id"],
2478
+ "total_traces": row["total_traces"],
2479
+ "first_trace_at": first_trace_at,
2480
+ "last_trace_at": last_trace_at,
2481
+ }
2482
+ )
2483
+
2484
+ return stats_list, total_count
2485
+
2486
+ except Exception as e:
2487
+ log_error(f"Error getting trace stats: {e}")
2488
+ return [], 0
2489
+
2490
+ # --- Spans ---
2491
+ def create_span(self, span: "Span") -> None:
2492
+ """Create a single span in the database.
2493
+
2494
+ Args:
2495
+ span: The Span object to store.
2496
+ """
2497
+ try:
2498
+ collection = self._get_collection(table_type="spans", create_collection_if_not_found=True)
2499
+ if collection is None:
2500
+ return
2501
+
2502
+ collection.insert_one(span.to_dict())
2503
+
2504
+ except Exception as e:
2505
+ log_error(f"Error creating span: {e}")
2506
+
2507
+ def create_spans(self, spans: List) -> None:
2508
+ """Create multiple spans in the database as a batch.
2509
+
2510
+ Args:
2511
+ spans: List of Span objects to store.
2512
+ """
2513
+ if not spans:
2514
+ return
2515
+
2516
+ try:
2517
+ collection = self._get_collection(table_type="spans", create_collection_if_not_found=True)
2518
+ if collection is None:
2519
+ return
2520
+
2521
+ span_dicts = [span.to_dict() for span in spans]
2522
+ collection.insert_many(span_dicts)
2523
+
2524
+ except Exception as e:
2525
+ log_error(f"Error creating spans batch: {e}")
2526
+
2527
+ def get_span(self, span_id: str):
2528
+ """Get a single span by its span_id.
2529
+
2530
+ Args:
2531
+ span_id: The unique span identifier.
2532
+
2533
+ Returns:
2534
+ Optional[Span]: The span if found, None otherwise.
2535
+ """
2536
+ try:
2537
+ from agno.tracing.schemas import Span as SpanSchema
2538
+
2539
+ collection = self._get_collection(table_type="spans")
2540
+ if collection is None:
2541
+ return None
2542
+
2543
+ result = collection.find_one({"span_id": span_id})
2544
+ if result:
2545
+ # Remove MongoDB's _id field
2546
+ result.pop("_id", None)
2547
+ return SpanSchema.from_dict(result)
2548
+ return None
2549
+
2550
+ except Exception as e:
2551
+ log_error(f"Error getting span: {e}")
2552
+ return None
2553
+
2554
+ def get_spans(
2555
+ self,
2556
+ trace_id: Optional[str] = None,
2557
+ parent_span_id: Optional[str] = None,
2558
+ limit: Optional[int] = 1000,
2559
+ ) -> List:
2560
+ """Get spans matching the provided filters.
2561
+
2562
+ Args:
2563
+ trace_id: Filter by trace ID.
2564
+ parent_span_id: Filter by parent span ID.
2565
+ limit: Maximum number of spans to return.
2566
+
2567
+ Returns:
2568
+ List[Span]: List of matching spans.
2569
+ """
2570
+ try:
2571
+ from agno.tracing.schemas import Span as SpanSchema
2572
+
2573
+ collection = self._get_collection(table_type="spans")
2574
+ if collection is None:
2575
+ return []
2576
+
2577
+ # Build query
2578
+ query: Dict[str, Any] = {}
2579
+ if trace_id:
2580
+ query["trace_id"] = trace_id
2581
+ if parent_span_id:
2582
+ query["parent_span_id"] = parent_span_id
2583
+
2584
+ cursor = collection.find(query).limit(limit or 1000)
2585
+ results = list(cursor)
2586
+
2587
+ spans = []
2588
+ for row in results:
2589
+ # Remove MongoDB's _id field
2590
+ row.pop("_id", None)
2591
+ spans.append(SpanSchema.from_dict(row))
2592
+
2593
+ return spans
2594
+
2595
+ except Exception as e:
2596
+ log_error(f"Error getting spans: {e}")
2597
+ return []