agno 2.2.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 (575) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +51 -0
  3. agno/agent/agent.py +10405 -0
  4. agno/api/__init__.py +0 -0
  5. agno/api/agent.py +28 -0
  6. agno/api/api.py +40 -0
  7. agno/api/evals.py +22 -0
  8. agno/api/os.py +17 -0
  9. agno/api/routes.py +13 -0
  10. agno/api/schemas/__init__.py +9 -0
  11. agno/api/schemas/agent.py +16 -0
  12. agno/api/schemas/evals.py +16 -0
  13. agno/api/schemas/os.py +14 -0
  14. agno/api/schemas/response.py +6 -0
  15. agno/api/schemas/team.py +16 -0
  16. agno/api/schemas/utils.py +21 -0
  17. agno/api/schemas/workflows.py +16 -0
  18. agno/api/settings.py +53 -0
  19. agno/api/team.py +30 -0
  20. agno/api/workflow.py +28 -0
  21. agno/cloud/aws/base.py +214 -0
  22. agno/cloud/aws/s3/__init__.py +2 -0
  23. agno/cloud/aws/s3/api_client.py +43 -0
  24. agno/cloud/aws/s3/bucket.py +195 -0
  25. agno/cloud/aws/s3/object.py +57 -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 +598 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2042 -0
  33. agno/db/dynamo/schemas.py +314 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +1795 -0
  37. agno/db/firestore/schemas.py +140 -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 +1335 -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 +1160 -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 +1328 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/__init__.py +0 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/mongo/__init__.py +17 -0
  51. agno/db/mongo/async_mongo.py +2026 -0
  52. agno/db/mongo/mongo.py +1982 -0
  53. agno/db/mongo/schemas.py +87 -0
  54. agno/db/mongo/utils.py +259 -0
  55. agno/db/mysql/__init__.py +3 -0
  56. agno/db/mysql/mysql.py +2308 -0
  57. agno/db/mysql/schemas.py +138 -0
  58. agno/db/mysql/utils.py +355 -0
  59. agno/db/postgres/__init__.py +4 -0
  60. agno/db/postgres/async_postgres.py +1927 -0
  61. agno/db/postgres/postgres.py +2260 -0
  62. agno/db/postgres/schemas.py +139 -0
  63. agno/db/postgres/utils.py +442 -0
  64. agno/db/redis/__init__.py +3 -0
  65. agno/db/redis/redis.py +1660 -0
  66. agno/db/redis/schemas.py +123 -0
  67. agno/db/redis/utils.py +346 -0
  68. agno/db/schemas/__init__.py +4 -0
  69. agno/db/schemas/culture.py +120 -0
  70. agno/db/schemas/evals.py +33 -0
  71. agno/db/schemas/knowledge.py +40 -0
  72. agno/db/schemas/memory.py +46 -0
  73. agno/db/schemas/metrics.py +0 -0
  74. agno/db/singlestore/__init__.py +3 -0
  75. agno/db/singlestore/schemas.py +130 -0
  76. agno/db/singlestore/singlestore.py +2272 -0
  77. agno/db/singlestore/utils.py +384 -0
  78. agno/db/sqlite/__init__.py +4 -0
  79. agno/db/sqlite/async_sqlite.py +2293 -0
  80. agno/db/sqlite/schemas.py +133 -0
  81. agno/db/sqlite/sqlite.py +2288 -0
  82. agno/db/sqlite/utils.py +431 -0
  83. agno/db/surrealdb/__init__.py +3 -0
  84. agno/db/surrealdb/metrics.py +292 -0
  85. agno/db/surrealdb/models.py +309 -0
  86. agno/db/surrealdb/queries.py +71 -0
  87. agno/db/surrealdb/surrealdb.py +1353 -0
  88. agno/db/surrealdb/utils.py +147 -0
  89. agno/db/utils.py +116 -0
  90. agno/debug.py +18 -0
  91. agno/eval/__init__.py +14 -0
  92. agno/eval/accuracy.py +834 -0
  93. agno/eval/performance.py +773 -0
  94. agno/eval/reliability.py +306 -0
  95. agno/eval/utils.py +119 -0
  96. agno/exceptions.py +161 -0
  97. agno/filters.py +354 -0
  98. agno/guardrails/__init__.py +6 -0
  99. agno/guardrails/base.py +19 -0
  100. agno/guardrails/openai.py +144 -0
  101. agno/guardrails/pii.py +94 -0
  102. agno/guardrails/prompt_injection.py +52 -0
  103. agno/integrations/__init__.py +0 -0
  104. agno/integrations/discord/__init__.py +3 -0
  105. agno/integrations/discord/client.py +203 -0
  106. agno/knowledge/__init__.py +5 -0
  107. agno/knowledge/chunking/__init__.py +0 -0
  108. agno/knowledge/chunking/agentic.py +79 -0
  109. agno/knowledge/chunking/document.py +91 -0
  110. agno/knowledge/chunking/fixed.py +57 -0
  111. agno/knowledge/chunking/markdown.py +151 -0
  112. agno/knowledge/chunking/recursive.py +63 -0
  113. agno/knowledge/chunking/row.py +39 -0
  114. agno/knowledge/chunking/semantic.py +86 -0
  115. agno/knowledge/chunking/strategy.py +165 -0
  116. agno/knowledge/content.py +74 -0
  117. agno/knowledge/document/__init__.py +5 -0
  118. agno/knowledge/document/base.py +58 -0
  119. agno/knowledge/embedder/__init__.py +5 -0
  120. agno/knowledge/embedder/aws_bedrock.py +343 -0
  121. agno/knowledge/embedder/azure_openai.py +210 -0
  122. agno/knowledge/embedder/base.py +23 -0
  123. agno/knowledge/embedder/cohere.py +323 -0
  124. agno/knowledge/embedder/fastembed.py +62 -0
  125. agno/knowledge/embedder/fireworks.py +13 -0
  126. agno/knowledge/embedder/google.py +258 -0
  127. agno/knowledge/embedder/huggingface.py +94 -0
  128. agno/knowledge/embedder/jina.py +182 -0
  129. agno/knowledge/embedder/langdb.py +22 -0
  130. agno/knowledge/embedder/mistral.py +206 -0
  131. agno/knowledge/embedder/nebius.py +13 -0
  132. agno/knowledge/embedder/ollama.py +154 -0
  133. agno/knowledge/embedder/openai.py +195 -0
  134. agno/knowledge/embedder/sentence_transformer.py +63 -0
  135. agno/knowledge/embedder/together.py +13 -0
  136. agno/knowledge/embedder/vllm.py +262 -0
  137. agno/knowledge/embedder/voyageai.py +165 -0
  138. agno/knowledge/knowledge.py +1988 -0
  139. agno/knowledge/reader/__init__.py +7 -0
  140. agno/knowledge/reader/arxiv_reader.py +81 -0
  141. agno/knowledge/reader/base.py +95 -0
  142. agno/knowledge/reader/csv_reader.py +166 -0
  143. agno/knowledge/reader/docx_reader.py +82 -0
  144. agno/knowledge/reader/field_labeled_csv_reader.py +292 -0
  145. agno/knowledge/reader/firecrawl_reader.py +201 -0
  146. agno/knowledge/reader/json_reader.py +87 -0
  147. agno/knowledge/reader/markdown_reader.py +137 -0
  148. agno/knowledge/reader/pdf_reader.py +431 -0
  149. agno/knowledge/reader/pptx_reader.py +101 -0
  150. agno/knowledge/reader/reader_factory.py +313 -0
  151. agno/knowledge/reader/s3_reader.py +89 -0
  152. agno/knowledge/reader/tavily_reader.py +194 -0
  153. agno/knowledge/reader/text_reader.py +115 -0
  154. agno/knowledge/reader/web_search_reader.py +372 -0
  155. agno/knowledge/reader/website_reader.py +455 -0
  156. agno/knowledge/reader/wikipedia_reader.py +59 -0
  157. agno/knowledge/reader/youtube_reader.py +78 -0
  158. agno/knowledge/remote_content/__init__.py +0 -0
  159. agno/knowledge/remote_content/remote_content.py +88 -0
  160. agno/knowledge/reranker/__init__.py +3 -0
  161. agno/knowledge/reranker/base.py +14 -0
  162. agno/knowledge/reranker/cohere.py +64 -0
  163. agno/knowledge/reranker/infinity.py +195 -0
  164. agno/knowledge/reranker/sentence_transformer.py +54 -0
  165. agno/knowledge/types.py +39 -0
  166. agno/knowledge/utils.py +189 -0
  167. agno/media.py +462 -0
  168. agno/memory/__init__.py +3 -0
  169. agno/memory/manager.py +1327 -0
  170. agno/models/__init__.py +0 -0
  171. agno/models/aimlapi/__init__.py +5 -0
  172. agno/models/aimlapi/aimlapi.py +45 -0
  173. agno/models/anthropic/__init__.py +5 -0
  174. agno/models/anthropic/claude.py +757 -0
  175. agno/models/aws/__init__.py +15 -0
  176. agno/models/aws/bedrock.py +701 -0
  177. agno/models/aws/claude.py +378 -0
  178. agno/models/azure/__init__.py +18 -0
  179. agno/models/azure/ai_foundry.py +485 -0
  180. agno/models/azure/openai_chat.py +131 -0
  181. agno/models/base.py +2175 -0
  182. agno/models/cerebras/__init__.py +12 -0
  183. agno/models/cerebras/cerebras.py +501 -0
  184. agno/models/cerebras/cerebras_openai.py +112 -0
  185. agno/models/cohere/__init__.py +5 -0
  186. agno/models/cohere/chat.py +389 -0
  187. agno/models/cometapi/__init__.py +5 -0
  188. agno/models/cometapi/cometapi.py +57 -0
  189. agno/models/dashscope/__init__.py +5 -0
  190. agno/models/dashscope/dashscope.py +91 -0
  191. agno/models/deepinfra/__init__.py +5 -0
  192. agno/models/deepinfra/deepinfra.py +28 -0
  193. agno/models/deepseek/__init__.py +5 -0
  194. agno/models/deepseek/deepseek.py +61 -0
  195. agno/models/defaults.py +1 -0
  196. agno/models/fireworks/__init__.py +5 -0
  197. agno/models/fireworks/fireworks.py +26 -0
  198. agno/models/google/__init__.py +5 -0
  199. agno/models/google/gemini.py +1085 -0
  200. agno/models/groq/__init__.py +5 -0
  201. agno/models/groq/groq.py +556 -0
  202. agno/models/huggingface/__init__.py +5 -0
  203. agno/models/huggingface/huggingface.py +491 -0
  204. agno/models/ibm/__init__.py +5 -0
  205. agno/models/ibm/watsonx.py +422 -0
  206. agno/models/internlm/__init__.py +3 -0
  207. agno/models/internlm/internlm.py +26 -0
  208. agno/models/langdb/__init__.py +1 -0
  209. agno/models/langdb/langdb.py +48 -0
  210. agno/models/litellm/__init__.py +14 -0
  211. agno/models/litellm/chat.py +468 -0
  212. agno/models/litellm/litellm_openai.py +25 -0
  213. agno/models/llama_cpp/__init__.py +5 -0
  214. agno/models/llama_cpp/llama_cpp.py +22 -0
  215. agno/models/lmstudio/__init__.py +5 -0
  216. agno/models/lmstudio/lmstudio.py +25 -0
  217. agno/models/message.py +434 -0
  218. agno/models/meta/__init__.py +12 -0
  219. agno/models/meta/llama.py +475 -0
  220. agno/models/meta/llama_openai.py +78 -0
  221. agno/models/metrics.py +120 -0
  222. agno/models/mistral/__init__.py +5 -0
  223. agno/models/mistral/mistral.py +432 -0
  224. agno/models/nebius/__init__.py +3 -0
  225. agno/models/nebius/nebius.py +54 -0
  226. agno/models/nexus/__init__.py +3 -0
  227. agno/models/nexus/nexus.py +22 -0
  228. agno/models/nvidia/__init__.py +5 -0
  229. agno/models/nvidia/nvidia.py +28 -0
  230. agno/models/ollama/__init__.py +5 -0
  231. agno/models/ollama/chat.py +441 -0
  232. agno/models/openai/__init__.py +9 -0
  233. agno/models/openai/chat.py +883 -0
  234. agno/models/openai/like.py +27 -0
  235. agno/models/openai/responses.py +1050 -0
  236. agno/models/openrouter/__init__.py +5 -0
  237. agno/models/openrouter/openrouter.py +66 -0
  238. agno/models/perplexity/__init__.py +5 -0
  239. agno/models/perplexity/perplexity.py +187 -0
  240. agno/models/portkey/__init__.py +3 -0
  241. agno/models/portkey/portkey.py +81 -0
  242. agno/models/requesty/__init__.py +5 -0
  243. agno/models/requesty/requesty.py +52 -0
  244. agno/models/response.py +199 -0
  245. agno/models/sambanova/__init__.py +5 -0
  246. agno/models/sambanova/sambanova.py +28 -0
  247. agno/models/siliconflow/__init__.py +5 -0
  248. agno/models/siliconflow/siliconflow.py +25 -0
  249. agno/models/together/__init__.py +5 -0
  250. agno/models/together/together.py +25 -0
  251. agno/models/utils.py +266 -0
  252. agno/models/vercel/__init__.py +3 -0
  253. agno/models/vercel/v0.py +26 -0
  254. agno/models/vertexai/__init__.py +0 -0
  255. agno/models/vertexai/claude.py +70 -0
  256. agno/models/vllm/__init__.py +3 -0
  257. agno/models/vllm/vllm.py +78 -0
  258. agno/models/xai/__init__.py +3 -0
  259. agno/models/xai/xai.py +113 -0
  260. agno/os/__init__.py +3 -0
  261. agno/os/app.py +876 -0
  262. agno/os/auth.py +57 -0
  263. agno/os/config.py +104 -0
  264. agno/os/interfaces/__init__.py +1 -0
  265. agno/os/interfaces/a2a/__init__.py +3 -0
  266. agno/os/interfaces/a2a/a2a.py +42 -0
  267. agno/os/interfaces/a2a/router.py +250 -0
  268. agno/os/interfaces/a2a/utils.py +924 -0
  269. agno/os/interfaces/agui/__init__.py +3 -0
  270. agno/os/interfaces/agui/agui.py +47 -0
  271. agno/os/interfaces/agui/router.py +144 -0
  272. agno/os/interfaces/agui/utils.py +534 -0
  273. agno/os/interfaces/base.py +25 -0
  274. agno/os/interfaces/slack/__init__.py +3 -0
  275. agno/os/interfaces/slack/router.py +148 -0
  276. agno/os/interfaces/slack/security.py +30 -0
  277. agno/os/interfaces/slack/slack.py +47 -0
  278. agno/os/interfaces/whatsapp/__init__.py +3 -0
  279. agno/os/interfaces/whatsapp/router.py +211 -0
  280. agno/os/interfaces/whatsapp/security.py +53 -0
  281. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  282. agno/os/mcp.py +292 -0
  283. agno/os/middleware/__init__.py +7 -0
  284. agno/os/middleware/jwt.py +233 -0
  285. agno/os/router.py +1763 -0
  286. agno/os/routers/__init__.py +3 -0
  287. agno/os/routers/evals/__init__.py +3 -0
  288. agno/os/routers/evals/evals.py +430 -0
  289. agno/os/routers/evals/schemas.py +142 -0
  290. agno/os/routers/evals/utils.py +162 -0
  291. agno/os/routers/health.py +31 -0
  292. agno/os/routers/home.py +52 -0
  293. agno/os/routers/knowledge/__init__.py +3 -0
  294. agno/os/routers/knowledge/knowledge.py +997 -0
  295. agno/os/routers/knowledge/schemas.py +178 -0
  296. agno/os/routers/memory/__init__.py +3 -0
  297. agno/os/routers/memory/memory.py +515 -0
  298. agno/os/routers/memory/schemas.py +62 -0
  299. agno/os/routers/metrics/__init__.py +3 -0
  300. agno/os/routers/metrics/metrics.py +190 -0
  301. agno/os/routers/metrics/schemas.py +47 -0
  302. agno/os/routers/session/__init__.py +3 -0
  303. agno/os/routers/session/session.py +997 -0
  304. agno/os/schema.py +1055 -0
  305. agno/os/settings.py +43 -0
  306. agno/os/utils.py +630 -0
  307. agno/py.typed +0 -0
  308. agno/reasoning/__init__.py +0 -0
  309. agno/reasoning/anthropic.py +80 -0
  310. agno/reasoning/azure_ai_foundry.py +67 -0
  311. agno/reasoning/deepseek.py +63 -0
  312. agno/reasoning/default.py +97 -0
  313. agno/reasoning/gemini.py +73 -0
  314. agno/reasoning/groq.py +71 -0
  315. agno/reasoning/helpers.py +63 -0
  316. agno/reasoning/ollama.py +67 -0
  317. agno/reasoning/openai.py +86 -0
  318. agno/reasoning/step.py +31 -0
  319. agno/reasoning/vertexai.py +76 -0
  320. agno/run/__init__.py +6 -0
  321. agno/run/agent.py +787 -0
  322. agno/run/base.py +229 -0
  323. agno/run/cancel.py +81 -0
  324. agno/run/messages.py +32 -0
  325. agno/run/team.py +753 -0
  326. agno/run/workflow.py +708 -0
  327. agno/session/__init__.py +10 -0
  328. agno/session/agent.py +295 -0
  329. agno/session/summary.py +265 -0
  330. agno/session/team.py +392 -0
  331. agno/session/workflow.py +205 -0
  332. agno/team/__init__.py +37 -0
  333. agno/team/team.py +8793 -0
  334. agno/tools/__init__.py +10 -0
  335. agno/tools/agentql.py +120 -0
  336. agno/tools/airflow.py +69 -0
  337. agno/tools/api.py +122 -0
  338. agno/tools/apify.py +314 -0
  339. agno/tools/arxiv.py +127 -0
  340. agno/tools/aws_lambda.py +53 -0
  341. agno/tools/aws_ses.py +66 -0
  342. agno/tools/baidusearch.py +89 -0
  343. agno/tools/bitbucket.py +292 -0
  344. agno/tools/brandfetch.py +213 -0
  345. agno/tools/bravesearch.py +106 -0
  346. agno/tools/brightdata.py +367 -0
  347. agno/tools/browserbase.py +209 -0
  348. agno/tools/calcom.py +255 -0
  349. agno/tools/calculator.py +151 -0
  350. agno/tools/cartesia.py +187 -0
  351. agno/tools/clickup.py +244 -0
  352. agno/tools/confluence.py +240 -0
  353. agno/tools/crawl4ai.py +158 -0
  354. agno/tools/csv_toolkit.py +185 -0
  355. agno/tools/dalle.py +110 -0
  356. agno/tools/daytona.py +475 -0
  357. agno/tools/decorator.py +262 -0
  358. agno/tools/desi_vocal.py +108 -0
  359. agno/tools/discord.py +161 -0
  360. agno/tools/docker.py +716 -0
  361. agno/tools/duckdb.py +379 -0
  362. agno/tools/duckduckgo.py +91 -0
  363. agno/tools/e2b.py +703 -0
  364. agno/tools/eleven_labs.py +196 -0
  365. agno/tools/email.py +67 -0
  366. agno/tools/evm.py +129 -0
  367. agno/tools/exa.py +396 -0
  368. agno/tools/fal.py +127 -0
  369. agno/tools/file.py +240 -0
  370. agno/tools/file_generation.py +350 -0
  371. agno/tools/financial_datasets.py +288 -0
  372. agno/tools/firecrawl.py +143 -0
  373. agno/tools/function.py +1187 -0
  374. agno/tools/giphy.py +93 -0
  375. agno/tools/github.py +1760 -0
  376. agno/tools/gmail.py +922 -0
  377. agno/tools/google_bigquery.py +117 -0
  378. agno/tools/google_drive.py +270 -0
  379. agno/tools/google_maps.py +253 -0
  380. agno/tools/googlecalendar.py +674 -0
  381. agno/tools/googlesearch.py +98 -0
  382. agno/tools/googlesheets.py +377 -0
  383. agno/tools/hackernews.py +77 -0
  384. agno/tools/jina.py +101 -0
  385. agno/tools/jira.py +170 -0
  386. agno/tools/knowledge.py +218 -0
  387. agno/tools/linear.py +426 -0
  388. agno/tools/linkup.py +58 -0
  389. agno/tools/local_file_system.py +90 -0
  390. agno/tools/lumalab.py +183 -0
  391. agno/tools/mcp/__init__.py +10 -0
  392. agno/tools/mcp/mcp.py +331 -0
  393. agno/tools/mcp/multi_mcp.py +347 -0
  394. agno/tools/mcp/params.py +24 -0
  395. agno/tools/mcp_toolbox.py +284 -0
  396. agno/tools/mem0.py +193 -0
  397. agno/tools/memori.py +339 -0
  398. agno/tools/memory.py +419 -0
  399. agno/tools/mlx_transcribe.py +139 -0
  400. agno/tools/models/__init__.py +0 -0
  401. agno/tools/models/azure_openai.py +190 -0
  402. agno/tools/models/gemini.py +203 -0
  403. agno/tools/models/groq.py +158 -0
  404. agno/tools/models/morph.py +186 -0
  405. agno/tools/models/nebius.py +124 -0
  406. agno/tools/models_labs.py +195 -0
  407. agno/tools/moviepy_video.py +349 -0
  408. agno/tools/neo4j.py +134 -0
  409. agno/tools/newspaper.py +46 -0
  410. agno/tools/newspaper4k.py +93 -0
  411. agno/tools/notion.py +204 -0
  412. agno/tools/openai.py +202 -0
  413. agno/tools/openbb.py +160 -0
  414. agno/tools/opencv.py +321 -0
  415. agno/tools/openweather.py +233 -0
  416. agno/tools/oxylabs.py +385 -0
  417. agno/tools/pandas.py +102 -0
  418. agno/tools/parallel.py +314 -0
  419. agno/tools/postgres.py +257 -0
  420. agno/tools/pubmed.py +188 -0
  421. agno/tools/python.py +205 -0
  422. agno/tools/reasoning.py +283 -0
  423. agno/tools/reddit.py +467 -0
  424. agno/tools/replicate.py +117 -0
  425. agno/tools/resend.py +62 -0
  426. agno/tools/scrapegraph.py +222 -0
  427. agno/tools/searxng.py +152 -0
  428. agno/tools/serpapi.py +116 -0
  429. agno/tools/serper.py +255 -0
  430. agno/tools/shell.py +53 -0
  431. agno/tools/slack.py +136 -0
  432. agno/tools/sleep.py +20 -0
  433. agno/tools/spider.py +116 -0
  434. agno/tools/sql.py +154 -0
  435. agno/tools/streamlit/__init__.py +0 -0
  436. agno/tools/streamlit/components.py +113 -0
  437. agno/tools/tavily.py +254 -0
  438. agno/tools/telegram.py +48 -0
  439. agno/tools/todoist.py +218 -0
  440. agno/tools/tool_registry.py +1 -0
  441. agno/tools/toolkit.py +146 -0
  442. agno/tools/trafilatura.py +388 -0
  443. agno/tools/trello.py +274 -0
  444. agno/tools/twilio.py +186 -0
  445. agno/tools/user_control_flow.py +78 -0
  446. agno/tools/valyu.py +228 -0
  447. agno/tools/visualization.py +467 -0
  448. agno/tools/webbrowser.py +28 -0
  449. agno/tools/webex.py +76 -0
  450. agno/tools/website.py +54 -0
  451. agno/tools/webtools.py +45 -0
  452. agno/tools/whatsapp.py +286 -0
  453. agno/tools/wikipedia.py +63 -0
  454. agno/tools/workflow.py +278 -0
  455. agno/tools/x.py +335 -0
  456. agno/tools/yfinance.py +257 -0
  457. agno/tools/youtube.py +184 -0
  458. agno/tools/zendesk.py +82 -0
  459. agno/tools/zep.py +454 -0
  460. agno/tools/zoom.py +382 -0
  461. agno/utils/__init__.py +0 -0
  462. agno/utils/agent.py +820 -0
  463. agno/utils/audio.py +49 -0
  464. agno/utils/certs.py +27 -0
  465. agno/utils/code_execution.py +11 -0
  466. agno/utils/common.py +132 -0
  467. agno/utils/dttm.py +13 -0
  468. agno/utils/enum.py +22 -0
  469. agno/utils/env.py +11 -0
  470. agno/utils/events.py +696 -0
  471. agno/utils/format_str.py +16 -0
  472. agno/utils/functions.py +166 -0
  473. agno/utils/gemini.py +426 -0
  474. agno/utils/hooks.py +57 -0
  475. agno/utils/http.py +74 -0
  476. agno/utils/json_schema.py +234 -0
  477. agno/utils/knowledge.py +36 -0
  478. agno/utils/location.py +19 -0
  479. agno/utils/log.py +255 -0
  480. agno/utils/mcp.py +214 -0
  481. agno/utils/media.py +352 -0
  482. agno/utils/merge_dict.py +41 -0
  483. agno/utils/message.py +118 -0
  484. agno/utils/models/__init__.py +0 -0
  485. agno/utils/models/ai_foundry.py +43 -0
  486. agno/utils/models/claude.py +358 -0
  487. agno/utils/models/cohere.py +87 -0
  488. agno/utils/models/llama.py +78 -0
  489. agno/utils/models/mistral.py +98 -0
  490. agno/utils/models/openai_responses.py +140 -0
  491. agno/utils/models/schema_utils.py +153 -0
  492. agno/utils/models/watsonx.py +41 -0
  493. agno/utils/openai.py +257 -0
  494. agno/utils/pickle.py +32 -0
  495. agno/utils/pprint.py +178 -0
  496. agno/utils/print_response/__init__.py +0 -0
  497. agno/utils/print_response/agent.py +842 -0
  498. agno/utils/print_response/team.py +1724 -0
  499. agno/utils/print_response/workflow.py +1668 -0
  500. agno/utils/prompts.py +111 -0
  501. agno/utils/reasoning.py +108 -0
  502. agno/utils/response.py +163 -0
  503. agno/utils/response_iterator.py +17 -0
  504. agno/utils/safe_formatter.py +24 -0
  505. agno/utils/serialize.py +32 -0
  506. agno/utils/shell.py +22 -0
  507. agno/utils/streamlit.py +487 -0
  508. agno/utils/string.py +231 -0
  509. agno/utils/team.py +139 -0
  510. agno/utils/timer.py +41 -0
  511. agno/utils/tools.py +102 -0
  512. agno/utils/web.py +23 -0
  513. agno/utils/whatsapp.py +305 -0
  514. agno/utils/yaml_io.py +25 -0
  515. agno/vectordb/__init__.py +3 -0
  516. agno/vectordb/base.py +127 -0
  517. agno/vectordb/cassandra/__init__.py +5 -0
  518. agno/vectordb/cassandra/cassandra.py +501 -0
  519. agno/vectordb/cassandra/extra_param_mixin.py +11 -0
  520. agno/vectordb/cassandra/index.py +13 -0
  521. agno/vectordb/chroma/__init__.py +5 -0
  522. agno/vectordb/chroma/chromadb.py +929 -0
  523. agno/vectordb/clickhouse/__init__.py +9 -0
  524. agno/vectordb/clickhouse/clickhousedb.py +835 -0
  525. agno/vectordb/clickhouse/index.py +9 -0
  526. agno/vectordb/couchbase/__init__.py +3 -0
  527. agno/vectordb/couchbase/couchbase.py +1442 -0
  528. agno/vectordb/distance.py +7 -0
  529. agno/vectordb/lancedb/__init__.py +6 -0
  530. agno/vectordb/lancedb/lance_db.py +995 -0
  531. agno/vectordb/langchaindb/__init__.py +5 -0
  532. agno/vectordb/langchaindb/langchaindb.py +163 -0
  533. agno/vectordb/lightrag/__init__.py +5 -0
  534. agno/vectordb/lightrag/lightrag.py +388 -0
  535. agno/vectordb/llamaindex/__init__.py +3 -0
  536. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  537. agno/vectordb/milvus/__init__.py +4 -0
  538. agno/vectordb/milvus/milvus.py +1182 -0
  539. agno/vectordb/mongodb/__init__.py +9 -0
  540. agno/vectordb/mongodb/mongodb.py +1417 -0
  541. agno/vectordb/pgvector/__init__.py +12 -0
  542. agno/vectordb/pgvector/index.py +23 -0
  543. agno/vectordb/pgvector/pgvector.py +1462 -0
  544. agno/vectordb/pineconedb/__init__.py +5 -0
  545. agno/vectordb/pineconedb/pineconedb.py +747 -0
  546. agno/vectordb/qdrant/__init__.py +5 -0
  547. agno/vectordb/qdrant/qdrant.py +1134 -0
  548. agno/vectordb/redis/__init__.py +9 -0
  549. agno/vectordb/redis/redisdb.py +694 -0
  550. agno/vectordb/search.py +7 -0
  551. agno/vectordb/singlestore/__init__.py +10 -0
  552. agno/vectordb/singlestore/index.py +41 -0
  553. agno/vectordb/singlestore/singlestore.py +763 -0
  554. agno/vectordb/surrealdb/__init__.py +3 -0
  555. agno/vectordb/surrealdb/surrealdb.py +699 -0
  556. agno/vectordb/upstashdb/__init__.py +5 -0
  557. agno/vectordb/upstashdb/upstashdb.py +718 -0
  558. agno/vectordb/weaviate/__init__.py +8 -0
  559. agno/vectordb/weaviate/index.py +15 -0
  560. agno/vectordb/weaviate/weaviate.py +1005 -0
  561. agno/workflow/__init__.py +23 -0
  562. agno/workflow/agent.py +299 -0
  563. agno/workflow/condition.py +738 -0
  564. agno/workflow/loop.py +735 -0
  565. agno/workflow/parallel.py +824 -0
  566. agno/workflow/router.py +702 -0
  567. agno/workflow/step.py +1432 -0
  568. agno/workflow/steps.py +592 -0
  569. agno/workflow/types.py +520 -0
  570. agno/workflow/workflow.py +4321 -0
  571. agno-2.2.13.dist-info/METADATA +614 -0
  572. agno-2.2.13.dist-info/RECORD +575 -0
  573. agno-2.2.13.dist-info/WHEEL +5 -0
  574. agno-2.2.13.dist-info/licenses/LICENSE +201 -0
  575. agno-2.2.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2293 @@
1
+ import time
2
+ from datetime import date, datetime, timedelta, timezone
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
5
+ from uuid import uuid4
6
+
7
+ from agno.db.base import AsyncBaseDb, SessionType
8
+ from agno.db.schemas.culture import CulturalKnowledge
9
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
10
+ from agno.db.schemas.knowledge import KnowledgeRow
11
+ from agno.db.schemas.memory import UserMemory
12
+ from agno.db.sqlite.schemas import get_table_schema_definition
13
+ from agno.db.sqlite.utils import (
14
+ abulk_upsert_metrics,
15
+ ais_table_available,
16
+ ais_valid_table,
17
+ apply_sorting,
18
+ calculate_date_metrics,
19
+ deserialize_cultural_knowledge_from_db,
20
+ fetch_all_sessions_data,
21
+ get_dates_to_calculate_metrics_for,
22
+ serialize_cultural_knowledge_for_db,
23
+ )
24
+ from agno.db.utils import deserialize_session_json_fields, serialize_session_json_fields
25
+ from agno.session import AgentSession, Session, TeamSession, WorkflowSession
26
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
27
+ from agno.utils.string import generate_id
28
+
29
+ try:
30
+ from sqlalchemy import Column, MetaData, Table, and_, func, select, text
31
+ from sqlalchemy.dialects import sqlite
32
+ from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
33
+ from sqlalchemy.schema import Index, UniqueConstraint
34
+ except ImportError:
35
+ raise ImportError("`sqlalchemy` not installed. Please install it using `pip install sqlalchemy`")
36
+
37
+
38
+ class AsyncSqliteDb(AsyncBaseDb):
39
+ def __init__(
40
+ self,
41
+ db_file: Optional[str] = None,
42
+ db_engine: Optional[AsyncEngine] = None,
43
+ db_url: Optional[str] = None,
44
+ session_table: Optional[str] = None,
45
+ culture_table: Optional[str] = None,
46
+ memory_table: Optional[str] = None,
47
+ metrics_table: Optional[str] = None,
48
+ eval_table: Optional[str] = None,
49
+ knowledge_table: Optional[str] = None,
50
+ id: Optional[str] = None,
51
+ ):
52
+ """
53
+ Async interface for interacting with a SQLite database.
54
+
55
+ The following order is used to determine the database connection:
56
+ 1. Use the db_engine
57
+ 2. Use the db_url
58
+ 3. Use the db_file
59
+ 4. Create a new database in the current directory
60
+
61
+ Args:
62
+ db_file (Optional[str]): The database file to connect to.
63
+ db_engine (Optional[AsyncEngine]): The SQLAlchemy async database engine to use.
64
+ db_url (Optional[str]): The database URL to connect to.
65
+ session_table (Optional[str]): Name of the table to store Agent, Team and Workflow sessions.
66
+ culture_table (Optional[str]): Name of the table to store cultural notions.
67
+ memory_table (Optional[str]): Name of the table to store user memories.
68
+ metrics_table (Optional[str]): Name of the table to store metrics.
69
+ eval_table (Optional[str]): Name of the table to store evaluation runs data.
70
+ knowledge_table (Optional[str]): Name of the table to store knowledge documents data.
71
+ id (Optional[str]): ID of the database.
72
+
73
+ Raises:
74
+ ValueError: If none of the tables are provided.
75
+ """
76
+ if id is None:
77
+ seed = db_url or db_file or str(db_engine.url) if db_engine else "sqlite+aiosqlite:///agno.db"
78
+ id = generate_id(seed)
79
+
80
+ super().__init__(
81
+ id=id,
82
+ session_table=session_table,
83
+ culture_table=culture_table,
84
+ memory_table=memory_table,
85
+ metrics_table=metrics_table,
86
+ eval_table=eval_table,
87
+ knowledge_table=knowledge_table,
88
+ )
89
+
90
+ _engine: Optional[AsyncEngine] = db_engine
91
+ if _engine is None:
92
+ if db_url is not None:
93
+ _engine = create_async_engine(db_url)
94
+ elif db_file is not None:
95
+ db_path = Path(db_file).resolve()
96
+ db_path.parent.mkdir(parents=True, exist_ok=True)
97
+ db_file = str(db_path)
98
+ _engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}")
99
+ else:
100
+ # If none of db_engine, db_url, or db_file are provided, create a db in the current directory
101
+ default_db_path = Path("./agno.db").resolve()
102
+ _engine = create_async_engine(f"sqlite+aiosqlite:///{default_db_path}")
103
+ db_file = str(default_db_path)
104
+ log_debug(f"Created SQLite database: {default_db_path}")
105
+
106
+ self.db_engine: AsyncEngine = _engine
107
+ self.db_url: Optional[str] = db_url
108
+ self.db_file: Optional[str] = db_file
109
+ self.metadata: MetaData = MetaData()
110
+
111
+ # Initialize database session factory
112
+ self.async_session_factory = async_sessionmaker(bind=self.db_engine, expire_on_commit=False)
113
+
114
+ # -- DB methods --
115
+ async def table_exists(self, table_name: str) -> bool:
116
+ """Check if a table with the given name exists in the SQLite database.
117
+
118
+ Args:
119
+ table_name: Name of the table to check
120
+
121
+ Returns:
122
+ bool: True if the table exists in the database, False otherwise
123
+ """
124
+ async with self.async_session_factory() as sess:
125
+ return await ais_table_available(session=sess, table_name=table_name)
126
+
127
+ async def _create_all_tables(self):
128
+ """Create all tables for the database."""
129
+ tables_to_create = [
130
+ (self.session_table_name, "sessions"),
131
+ (self.memory_table_name, "memories"),
132
+ (self.metrics_table_name, "metrics"),
133
+ (self.eval_table_name, "evals"),
134
+ (self.knowledge_table_name, "knowledge"),
135
+ ]
136
+
137
+ for table_name, table_type in tables_to_create:
138
+ await self._create_table(table_name=table_name, table_type=table_type)
139
+
140
+ async def _create_table(self, table_name: str, table_type: str) -> Table:
141
+ """
142
+ Create a table with the appropriate schema based on the table type.
143
+
144
+ Args:
145
+ table_name (str): Name of the table to create
146
+ table_type (str): Type of table (used to get schema definition)
147
+
148
+ Returns:
149
+ Table: SQLAlchemy Table object
150
+ """
151
+ try:
152
+ table_schema = get_table_schema_definition(table_type)
153
+ log_debug(f"Creating table {table_name}")
154
+
155
+ columns: List[Column] = []
156
+ indexes: List[str] = []
157
+ unique_constraints: List[str] = []
158
+ schema_unique_constraints = table_schema.pop("_unique_constraints", [])
159
+
160
+ # Get the columns, indexes, and unique constraints from the table schema
161
+ for col_name, col_config in table_schema.items():
162
+ column_args = [col_name, col_config["type"]()]
163
+ column_kwargs = {}
164
+
165
+ if col_config.get("primary_key", False):
166
+ column_kwargs["primary_key"] = True
167
+ if "nullable" in col_config:
168
+ column_kwargs["nullable"] = col_config["nullable"]
169
+ if col_config.get("index", False):
170
+ indexes.append(col_name)
171
+ if col_config.get("unique", False):
172
+ column_kwargs["unique"] = True
173
+ unique_constraints.append(col_name)
174
+
175
+ columns.append(Column(*column_args, **column_kwargs)) # type: ignore
176
+
177
+ # Create the table object
178
+ table_metadata = MetaData()
179
+ table = Table(table_name, table_metadata, *columns)
180
+
181
+ # Add multi-column unique constraints with table-specific names
182
+ for constraint in schema_unique_constraints:
183
+ constraint_name = f"{table_name}_{constraint['name']}"
184
+ constraint_columns = constraint["columns"]
185
+ table.append_constraint(UniqueConstraint(*constraint_columns, name=constraint_name))
186
+
187
+ # Add indexes to the table definition
188
+ for idx_col in indexes:
189
+ idx_name = f"idx_{table_name}_{idx_col}"
190
+ table.append_constraint(Index(idx_name, idx_col))
191
+
192
+ # Create table
193
+ async with self.db_engine.begin() as conn:
194
+ await conn.run_sync(table.create, checkfirst=True)
195
+
196
+ # Create indexes
197
+ for idx in table.indexes:
198
+ try:
199
+ log_debug(f"Creating index: {idx.name}")
200
+ # Check if index already exists
201
+ async with self.async_session_factory() as sess:
202
+ exists_query = text("SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = :index_name")
203
+ result = await sess.execute(exists_query, {"index_name": idx.name})
204
+ exists = result.scalar() is not None
205
+ if exists:
206
+ log_debug(f"Index {idx.name} already exists in table {table_name}, skipping creation")
207
+ continue
208
+
209
+ async with self.db_engine.begin() as conn:
210
+ await conn.run_sync(idx.create)
211
+
212
+ except Exception as e:
213
+ log_warning(f"Error creating index {idx.name}: {e}")
214
+
215
+ log_debug(f"Successfully created table '{table_name}'")
216
+ return table
217
+
218
+ except Exception as e:
219
+ log_error(f"Could not create table '{table_name}': {e}")
220
+ raise e
221
+
222
+ async def _get_table(self, table_type: str) -> Optional[Table]:
223
+ if table_type == "sessions":
224
+ if not hasattr(self, "session_table"):
225
+ self.session_table = await self._get_or_create_table(
226
+ table_name=self.session_table_name,
227
+ table_type=table_type,
228
+ )
229
+ return self.session_table
230
+
231
+ elif table_type == "memories":
232
+ if not hasattr(self, "memory_table"):
233
+ self.memory_table = await self._get_or_create_table(
234
+ table_name=self.memory_table_name,
235
+ table_type="memories",
236
+ )
237
+ return self.memory_table
238
+
239
+ elif table_type == "metrics":
240
+ if not hasattr(self, "metrics_table"):
241
+ self.metrics_table = await self._get_or_create_table(
242
+ table_name=self.metrics_table_name,
243
+ table_type="metrics",
244
+ )
245
+ return self.metrics_table
246
+
247
+ elif table_type == "evals":
248
+ if not hasattr(self, "eval_table"):
249
+ self.eval_table = await self._get_or_create_table(
250
+ table_name=self.eval_table_name,
251
+ table_type="evals",
252
+ )
253
+ return self.eval_table
254
+
255
+ elif table_type == "knowledge":
256
+ if not hasattr(self, "knowledge_table"):
257
+ self.knowledge_table = await self._get_or_create_table(
258
+ table_name=self.knowledge_table_name,
259
+ table_type="knowledge",
260
+ )
261
+ return self.knowledge_table
262
+
263
+ elif table_type == "culture":
264
+ if not hasattr(self, "culture_table"):
265
+ self.culture_table = await self._get_or_create_table(
266
+ table_name=self.culture_table_name,
267
+ table_type="culture",
268
+ )
269
+ return self.culture_table
270
+
271
+ else:
272
+ raise ValueError(f"Unknown table type: '{table_type}'")
273
+
274
+ async def _get_or_create_table(
275
+ self,
276
+ table_name: str,
277
+ table_type: str,
278
+ ) -> Table:
279
+ """
280
+ Check if the table exists and is valid, else create it.
281
+
282
+ Args:
283
+ table_name (str): Name of the table to get or create
284
+ table_type (str): Type of table (used to get schema definition)
285
+
286
+ Returns:
287
+ Table: SQLAlchemy Table object
288
+ """
289
+ async with self.async_session_factory() as sess, sess.begin():
290
+ table_is_available = await ais_table_available(session=sess, table_name=table_name)
291
+
292
+ if not table_is_available:
293
+ return await self._create_table(table_name=table_name, table_type=table_type)
294
+
295
+ # SQLite version of table validation (no schema)
296
+ if not await ais_valid_table(db_engine=self.db_engine, table_name=table_name, table_type=table_type):
297
+ raise ValueError(f"Table {table_name} has an invalid schema")
298
+
299
+ try:
300
+ async with self.db_engine.connect() as conn:
301
+
302
+ def load_table(connection):
303
+ return Table(table_name, self.metadata, autoload_with=connection)
304
+
305
+ table = await conn.run_sync(load_table)
306
+ log_debug(f"Loaded existing table {table_name}")
307
+ return table
308
+
309
+ except Exception as e:
310
+ log_error(f"Error loading existing table {table_name}: {e}")
311
+ raise e
312
+
313
+ # -- Session methods --
314
+
315
+ async def delete_session(self, session_id: str) -> bool:
316
+ """
317
+ Delete a session from the database.
318
+
319
+ Args:
320
+ session_id (str): ID of the session to delete
321
+
322
+ Returns:
323
+ bool: True if the session was deleted, False otherwise.
324
+
325
+ Raises:
326
+ Exception: If an error occurs during deletion.
327
+ """
328
+ try:
329
+ table = await self._get_table(table_type="sessions")
330
+ if table is None:
331
+ return False
332
+
333
+ async with self.async_session_factory() as sess, sess.begin():
334
+ delete_stmt = table.delete().where(table.c.session_id == session_id)
335
+ result = await sess.execute(delete_stmt)
336
+ if result.rowcount == 0: # type: ignore
337
+ log_debug(f"No session found to delete with session_id: {session_id}")
338
+ return False
339
+ else:
340
+ log_debug(f"Successfully deleted session with session_id: {session_id}")
341
+ return True
342
+
343
+ except Exception as e:
344
+ log_error(f"Error deleting session: {e}")
345
+ return False
346
+
347
+ async def delete_sessions(self, session_ids: List[str]) -> None:
348
+ """Delete all given sessions from the database.
349
+ Can handle multiple session types in the same run.
350
+
351
+ Args:
352
+ session_ids (List[str]): The IDs of the sessions to delete.
353
+
354
+ Raises:
355
+ Exception: If an error occurs during deletion.
356
+ """
357
+ try:
358
+ table = await self._get_table(table_type="sessions")
359
+ if table is None:
360
+ return
361
+
362
+ async with self.async_session_factory() as sess, sess.begin():
363
+ delete_stmt = table.delete().where(table.c.session_id.in_(session_ids))
364
+ result = await sess.execute(delete_stmt)
365
+
366
+ log_debug(f"Successfully deleted {result.rowcount} sessions") # type: ignore
367
+
368
+ except Exception as e:
369
+ log_error(f"Error deleting sessions: {e}")
370
+
371
+ async def get_session(
372
+ self,
373
+ session_id: str,
374
+ session_type: SessionType,
375
+ user_id: Optional[str] = None,
376
+ deserialize: Optional[bool] = True,
377
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
378
+ """
379
+ Read a session from the database.
380
+
381
+ Args:
382
+ session_id (str): ID of the session to read.
383
+ session_type (SessionType): Type of session to get.
384
+ user_id (Optional[str]): User ID to filter by. Defaults to None.
385
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
386
+
387
+ Returns:
388
+ Optional[Union[Session, Dict[str, Any]]]:
389
+ - When deserialize=True: Session object
390
+ - When deserialize=False: Session dictionary
391
+
392
+ Raises:
393
+ Exception: If an error occurs during retrieval.
394
+ """
395
+ try:
396
+ table = await self._get_table(table_type="sessions")
397
+ if table is None:
398
+ return None
399
+
400
+ async with self.async_session_factory() as sess, sess.begin():
401
+ stmt = select(table).where(table.c.session_id == session_id)
402
+
403
+ # Filtering
404
+ if user_id is not None:
405
+ stmt = stmt.where(table.c.user_id == user_id)
406
+
407
+ result = await sess.execute(stmt)
408
+ row = result.fetchone()
409
+ if row is None:
410
+ return None
411
+
412
+ session_raw = deserialize_session_json_fields(dict(row._mapping))
413
+ if not session_raw or not deserialize:
414
+ return session_raw
415
+
416
+ if session_type == SessionType.AGENT:
417
+ return AgentSession.from_dict(session_raw)
418
+ elif session_type == SessionType.TEAM:
419
+ return TeamSession.from_dict(session_raw)
420
+ elif session_type == SessionType.WORKFLOW:
421
+ return WorkflowSession.from_dict(session_raw)
422
+ else:
423
+ raise ValueError(f"Invalid session type: {session_type}")
424
+
425
+ except Exception as e:
426
+ log_debug(f"Exception reading from sessions table: {e}")
427
+ raise e
428
+
429
+ async def get_sessions(
430
+ self,
431
+ session_type: Optional[SessionType] = None,
432
+ user_id: Optional[str] = None,
433
+ component_id: Optional[str] = None,
434
+ session_name: Optional[str] = None,
435
+ start_timestamp: Optional[int] = None,
436
+ end_timestamp: Optional[int] = None,
437
+ limit: Optional[int] = None,
438
+ page: Optional[int] = None,
439
+ sort_by: Optional[str] = None,
440
+ sort_order: Optional[str] = None,
441
+ deserialize: Optional[bool] = True,
442
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
443
+ """
444
+ Get all sessions in the given table. Can filter by user_id and entity_id.
445
+ Args:
446
+ session_type (Optional[SessionType]): The type of session to get.
447
+ user_id (Optional[str]): The ID of the user to filter by.
448
+ component_id (Optional[str]): The ID of the agent / workflow to filter by.
449
+ session_name (Optional[str]): The name of the session to filter by.
450
+ start_timestamp (Optional[int]): The start timestamp to filter by.
451
+ end_timestamp (Optional[int]): The end timestamp to filter by.
452
+ limit (Optional[int]): The maximum number of sessions to return. Defaults to None.
453
+ page (Optional[int]): The page number to return. Defaults to None.
454
+ sort_by (Optional[str]): The field to sort by. Defaults to None.
455
+ sort_order (Optional[str]): The sort order. Defaults to None.
456
+ deserialize (Optional[bool]): Whether to serialize the sessions. Defaults to True.
457
+
458
+ Returns:
459
+ List[Session]:
460
+ - When deserialize=True: List of Session objects matching the criteria.
461
+ - When deserialize=False: List of Session dictionaries matching the criteria.
462
+
463
+ Raises:
464
+ Exception: If an error occurs during retrieval.
465
+ """
466
+ try:
467
+ table = await self._get_table(table_type="sessions")
468
+ if table is None:
469
+ return [] if deserialize else ([], 0)
470
+
471
+ async with self.async_session_factory() as sess, sess.begin():
472
+ stmt = select(table)
473
+
474
+ # Filtering
475
+ if user_id is not None:
476
+ stmt = stmt.where(table.c.user_id == user_id)
477
+ if component_id is not None:
478
+ if session_type == SessionType.AGENT:
479
+ stmt = stmt.where(table.c.agent_id == component_id)
480
+ elif session_type == SessionType.TEAM:
481
+ stmt = stmt.where(table.c.team_id == component_id)
482
+ elif session_type == SessionType.WORKFLOW:
483
+ stmt = stmt.where(table.c.workflow_id == component_id)
484
+ if start_timestamp is not None:
485
+ stmt = stmt.where(table.c.created_at >= start_timestamp)
486
+ if end_timestamp is not None:
487
+ stmt = stmt.where(table.c.created_at <= end_timestamp)
488
+ if session_name is not None:
489
+ stmt = stmt.where(table.c.session_data.like(f"%{session_name}%"))
490
+ if session_type is not None:
491
+ stmt = stmt.where(table.c.session_type == session_type.value)
492
+
493
+ # Getting total count
494
+ count_stmt = select(func.count()).select_from(stmt.alias())
495
+ count_result = await sess.execute(count_stmt)
496
+ total_count = count_result.scalar() or 0
497
+
498
+ # Sorting
499
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
500
+
501
+ # Paginating
502
+ if limit is not None:
503
+ stmt = stmt.limit(limit)
504
+ if page is not None:
505
+ stmt = stmt.offset((page - 1) * limit)
506
+
507
+ result = await sess.execute(stmt)
508
+ records = result.fetchall()
509
+ if records is None:
510
+ return [] if deserialize else ([], 0)
511
+
512
+ sessions_raw = [deserialize_session_json_fields(dict(record._mapping)) for record in records]
513
+ if not deserialize:
514
+ return sessions_raw, total_count
515
+ if not sessions_raw:
516
+ return []
517
+
518
+ if session_type == SessionType.AGENT:
519
+ return [AgentSession.from_dict(record) for record in sessions_raw] # type: ignore
520
+ elif session_type == SessionType.TEAM:
521
+ return [TeamSession.from_dict(record) for record in sessions_raw] # type: ignore
522
+ elif session_type == SessionType.WORKFLOW:
523
+ return [WorkflowSession.from_dict(record) for record in sessions_raw] # type: ignore
524
+ else:
525
+ raise ValueError(f"Invalid session type: {session_type}")
526
+
527
+ except Exception as e:
528
+ log_debug(f"Exception reading from sessions table: {e}")
529
+ raise e
530
+
531
+ async def rename_session(
532
+ self,
533
+ session_id: str,
534
+ session_type: SessionType,
535
+ session_name: str,
536
+ deserialize: Optional[bool] = True,
537
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
538
+ """
539
+ Rename a session in the database.
540
+
541
+ Args:
542
+ session_id (str): The ID of the session to rename.
543
+ session_type (SessionType): The type of session to rename.
544
+ session_name (str): The new name for the session.
545
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
546
+
547
+ Returns:
548
+ Optional[Union[Session, Dict[str, Any]]]:
549
+ - When deserialize=True: Session object
550
+ - When deserialize=False: Session dictionary
551
+
552
+ Raises:
553
+ Exception: If an error occurs during renaming.
554
+ """
555
+ try:
556
+ # Get the current session as a deserialized object
557
+ session = await self.get_session(session_id, session_type, deserialize=True)
558
+ if session is None:
559
+ return None
560
+
561
+ session = cast(Session, session)
562
+ # Update the session name
563
+ if session.session_data is None:
564
+ session.session_data = {}
565
+ session.session_data["session_name"] = session_name
566
+
567
+ # Upsert the updated session back to the database
568
+ return await self.upsert_session(session, deserialize=deserialize)
569
+
570
+ except Exception as e:
571
+ log_error(f"Exception renaming session: {e}")
572
+ raise e
573
+
574
+ async def upsert_session(
575
+ self, session: Session, deserialize: Optional[bool] = True
576
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
577
+ """
578
+ Insert or update a session in the database.
579
+
580
+ Args:
581
+ session (Session): The session data to upsert.
582
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
583
+
584
+ Returns:
585
+ Optional[Session]:
586
+ - When deserialize=True: Session object
587
+ - When deserialize=False: Session dictionary
588
+
589
+ Raises:
590
+ Exception: If an error occurs during upserting.
591
+ """
592
+ try:
593
+ table = await self._get_table(table_type="sessions")
594
+ if table is None:
595
+ return None
596
+
597
+ serialized_session = serialize_session_json_fields(session.to_dict())
598
+
599
+ if isinstance(session, AgentSession):
600
+ async with self.async_session_factory() as sess, sess.begin():
601
+ stmt = sqlite.insert(table).values(
602
+ session_id=serialized_session.get("session_id"),
603
+ session_type=SessionType.AGENT.value,
604
+ agent_id=serialized_session.get("agent_id"),
605
+ user_id=serialized_session.get("user_id"),
606
+ agent_data=serialized_session.get("agent_data"),
607
+ session_data=serialized_session.get("session_data"),
608
+ metadata=serialized_session.get("metadata"),
609
+ runs=serialized_session.get("runs"),
610
+ summary=serialized_session.get("summary"),
611
+ created_at=serialized_session.get("created_at"),
612
+ updated_at=serialized_session.get("created_at"),
613
+ )
614
+ stmt = stmt.on_conflict_do_update(
615
+ index_elements=["session_id"],
616
+ set_=dict(
617
+ agent_id=serialized_session.get("agent_id"),
618
+ user_id=serialized_session.get("user_id"),
619
+ runs=serialized_session.get("runs"),
620
+ summary=serialized_session.get("summary"),
621
+ agent_data=serialized_session.get("agent_data"),
622
+ session_data=serialized_session.get("session_data"),
623
+ metadata=serialized_session.get("metadata"),
624
+ updated_at=int(time.time()),
625
+ ),
626
+ )
627
+ stmt = stmt.returning(*table.columns) # type: ignore
628
+ result = await sess.execute(stmt)
629
+ row = result.fetchone()
630
+
631
+ session_raw = deserialize_session_json_fields(dict(row._mapping)) if row else None
632
+ if session_raw is None or not deserialize:
633
+ return session_raw
634
+ return AgentSession.from_dict(session_raw)
635
+
636
+ elif isinstance(session, TeamSession):
637
+ async with self.async_session_factory() as sess, sess.begin():
638
+ stmt = sqlite.insert(table).values(
639
+ session_id=serialized_session.get("session_id"),
640
+ session_type=SessionType.TEAM.value,
641
+ team_id=serialized_session.get("team_id"),
642
+ user_id=serialized_session.get("user_id"),
643
+ runs=serialized_session.get("runs"),
644
+ summary=serialized_session.get("summary"),
645
+ created_at=serialized_session.get("created_at"),
646
+ updated_at=serialized_session.get("created_at"),
647
+ team_data=serialized_session.get("team_data"),
648
+ session_data=serialized_session.get("session_data"),
649
+ metadata=serialized_session.get("metadata"),
650
+ )
651
+
652
+ stmt = stmt.on_conflict_do_update(
653
+ index_elements=["session_id"],
654
+ set_=dict(
655
+ team_id=serialized_session.get("team_id"),
656
+ user_id=serialized_session.get("user_id"),
657
+ summary=serialized_session.get("summary"),
658
+ runs=serialized_session.get("runs"),
659
+ team_data=serialized_session.get("team_data"),
660
+ session_data=serialized_session.get("session_data"),
661
+ metadata=serialized_session.get("metadata"),
662
+ updated_at=int(time.time()),
663
+ ),
664
+ )
665
+ stmt = stmt.returning(*table.columns) # type: ignore
666
+ result = await sess.execute(stmt)
667
+ row = result.fetchone()
668
+
669
+ session_raw = deserialize_session_json_fields(dict(row._mapping)) if row else None
670
+ if session_raw is None or not deserialize:
671
+ return session_raw
672
+ return TeamSession.from_dict(session_raw)
673
+
674
+ else:
675
+ async with self.async_session_factory() as sess, sess.begin():
676
+ stmt = sqlite.insert(table).values(
677
+ session_id=serialized_session.get("session_id"),
678
+ session_type=SessionType.WORKFLOW.value,
679
+ workflow_id=serialized_session.get("workflow_id"),
680
+ user_id=serialized_session.get("user_id"),
681
+ runs=serialized_session.get("runs"),
682
+ summary=serialized_session.get("summary"),
683
+ created_at=serialized_session.get("created_at") or int(time.time()),
684
+ updated_at=serialized_session.get("updated_at") or int(time.time()),
685
+ workflow_data=serialized_session.get("workflow_data"),
686
+ session_data=serialized_session.get("session_data"),
687
+ metadata=serialized_session.get("metadata"),
688
+ )
689
+ stmt = stmt.on_conflict_do_update(
690
+ index_elements=["session_id"],
691
+ set_=dict(
692
+ workflow_id=serialized_session.get("workflow_id"),
693
+ user_id=serialized_session.get("user_id"),
694
+ summary=serialized_session.get("summary"),
695
+ runs=serialized_session.get("runs"),
696
+ workflow_data=serialized_session.get("workflow_data"),
697
+ session_data=serialized_session.get("session_data"),
698
+ metadata=serialized_session.get("metadata"),
699
+ updated_at=int(time.time()),
700
+ ),
701
+ )
702
+ stmt = stmt.returning(*table.columns) # type: ignore
703
+ result = await sess.execute(stmt)
704
+ row = result.fetchone()
705
+
706
+ session_raw = deserialize_session_json_fields(dict(row._mapping)) if row else None
707
+ if session_raw is None or not deserialize:
708
+ return session_raw
709
+ return WorkflowSession.from_dict(session_raw)
710
+
711
+ except Exception as e:
712
+ log_warning(f"Exception upserting into table: {e}")
713
+ raise e
714
+
715
+ async def upsert_sessions(
716
+ self,
717
+ sessions: List[Session],
718
+ deserialize: Optional[bool] = True,
719
+ preserve_updated_at: bool = False,
720
+ ) -> List[Union[Session, Dict[str, Any]]]:
721
+ """
722
+ Bulk upsert multiple sessions for improved performance on large datasets.
723
+
724
+ Args:
725
+ sessions (List[Session]): List of sessions to upsert.
726
+ deserialize (Optional[bool]): Whether to deserialize the sessions. Defaults to True.
727
+ preserve_updated_at (bool): If True, preserve the updated_at from the session object.
728
+
729
+ Returns:
730
+ List[Union[Session, Dict[str, Any]]]: List of upserted sessions.
731
+
732
+ Raises:
733
+ Exception: If an error occurs during bulk upsert.
734
+ """
735
+ if not sessions:
736
+ return []
737
+
738
+ try:
739
+ table = await self._get_table(table_type="sessions")
740
+ if table is None:
741
+ log_info("Sessions table not available, falling back to individual upserts")
742
+ return [
743
+ result
744
+ for session in sessions
745
+ if session is not None
746
+ for result in [await self.upsert_session(session, deserialize=deserialize)]
747
+ if result is not None
748
+ ]
749
+
750
+ # Group sessions by type for batch processing
751
+ agent_sessions = []
752
+ team_sessions = []
753
+ workflow_sessions = []
754
+
755
+ for session in sessions:
756
+ if isinstance(session, AgentSession):
757
+ agent_sessions.append(session)
758
+ elif isinstance(session, TeamSession):
759
+ team_sessions.append(session)
760
+ elif isinstance(session, WorkflowSession):
761
+ workflow_sessions.append(session)
762
+
763
+ results: List[Union[Session, Dict[str, Any]]] = []
764
+
765
+ async with self.async_session_factory() as sess, sess.begin():
766
+ # Bulk upsert agent sessions
767
+ if agent_sessions:
768
+ agent_data = []
769
+ for session in agent_sessions:
770
+ serialized_session = serialize_session_json_fields(session.to_dict())
771
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
772
+ updated_at = serialized_session.get("updated_at") if preserve_updated_at else int(time.time())
773
+ agent_data.append(
774
+ {
775
+ "session_id": serialized_session.get("session_id"),
776
+ "session_type": SessionType.AGENT.value,
777
+ "agent_id": serialized_session.get("agent_id"),
778
+ "user_id": serialized_session.get("user_id"),
779
+ "agent_data": serialized_session.get("agent_data"),
780
+ "session_data": serialized_session.get("session_data"),
781
+ "metadata": serialized_session.get("metadata"),
782
+ "runs": serialized_session.get("runs"),
783
+ "summary": serialized_session.get("summary"),
784
+ "created_at": serialized_session.get("created_at"),
785
+ "updated_at": updated_at,
786
+ }
787
+ )
788
+
789
+ if agent_data:
790
+ stmt = sqlite.insert(table)
791
+ stmt = stmt.on_conflict_do_update(
792
+ index_elements=["session_id"],
793
+ set_=dict(
794
+ agent_id=stmt.excluded.agent_id,
795
+ user_id=stmt.excluded.user_id,
796
+ agent_data=stmt.excluded.agent_data,
797
+ session_data=stmt.excluded.session_data,
798
+ metadata=stmt.excluded.metadata,
799
+ runs=stmt.excluded.runs,
800
+ summary=stmt.excluded.summary,
801
+ updated_at=stmt.excluded.updated_at,
802
+ ),
803
+ )
804
+ await sess.execute(stmt, agent_data)
805
+
806
+ # Fetch the results for agent sessions
807
+ agent_ids = [session.session_id for session in agent_sessions]
808
+ select_stmt = select(table).where(table.c.session_id.in_(agent_ids))
809
+ result = (await sess.execute(select_stmt)).fetchall()
810
+
811
+ for row in result:
812
+ session_dict = deserialize_session_json_fields(dict(row._mapping))
813
+ if deserialize:
814
+ deserialized_agent_session = AgentSession.from_dict(session_dict)
815
+ if deserialized_agent_session is None:
816
+ continue
817
+ results.append(deserialized_agent_session)
818
+ else:
819
+ results.append(session_dict)
820
+
821
+ # Bulk upsert team sessions
822
+ if team_sessions:
823
+ team_data = []
824
+ for session in team_sessions:
825
+ serialized_session = serialize_session_json_fields(session.to_dict())
826
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
827
+ updated_at = serialized_session.get("updated_at") if preserve_updated_at else int(time.time())
828
+ team_data.append(
829
+ {
830
+ "session_id": serialized_session.get("session_id"),
831
+ "session_type": SessionType.TEAM.value,
832
+ "team_id": serialized_session.get("team_id"),
833
+ "user_id": serialized_session.get("user_id"),
834
+ "runs": serialized_session.get("runs"),
835
+ "summary": serialized_session.get("summary"),
836
+ "created_at": serialized_session.get("created_at"),
837
+ "updated_at": updated_at,
838
+ "team_data": serialized_session.get("team_data"),
839
+ "session_data": serialized_session.get("session_data"),
840
+ "metadata": serialized_session.get("metadata"),
841
+ }
842
+ )
843
+
844
+ if team_data:
845
+ stmt = sqlite.insert(table)
846
+ stmt = stmt.on_conflict_do_update(
847
+ index_elements=["session_id"],
848
+ set_=dict(
849
+ team_id=stmt.excluded.team_id,
850
+ user_id=stmt.excluded.user_id,
851
+ team_data=stmt.excluded.team_data,
852
+ session_data=stmt.excluded.session_data,
853
+ metadata=stmt.excluded.metadata,
854
+ runs=stmt.excluded.runs,
855
+ summary=stmt.excluded.summary,
856
+ updated_at=stmt.excluded.updated_at,
857
+ ),
858
+ )
859
+ await sess.execute(stmt, team_data)
860
+
861
+ # Fetch the results for team sessions
862
+ team_ids = [session.session_id for session in team_sessions]
863
+ select_stmt = select(table).where(table.c.session_id.in_(team_ids))
864
+ result = (await sess.execute(select_stmt)).fetchall()
865
+
866
+ for row in result:
867
+ session_dict = deserialize_session_json_fields(dict(row._mapping))
868
+ if deserialize:
869
+ deserialized_team_session = TeamSession.from_dict(session_dict)
870
+ if deserialized_team_session is None:
871
+ continue
872
+ results.append(deserialized_team_session)
873
+ else:
874
+ results.append(session_dict)
875
+
876
+ # Bulk upsert workflow sessions
877
+ if workflow_sessions:
878
+ workflow_data = []
879
+ for session in workflow_sessions:
880
+ serialized_session = serialize_session_json_fields(session.to_dict())
881
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
882
+ updated_at = serialized_session.get("updated_at") if preserve_updated_at else int(time.time())
883
+ workflow_data.append(
884
+ {
885
+ "session_id": serialized_session.get("session_id"),
886
+ "session_type": SessionType.WORKFLOW.value,
887
+ "workflow_id": serialized_session.get("workflow_id"),
888
+ "user_id": serialized_session.get("user_id"),
889
+ "runs": serialized_session.get("runs"),
890
+ "summary": serialized_session.get("summary"),
891
+ "created_at": serialized_session.get("created_at"),
892
+ "updated_at": updated_at,
893
+ "workflow_data": serialized_session.get("workflow_data"),
894
+ "session_data": serialized_session.get("session_data"),
895
+ "metadata": serialized_session.get("metadata"),
896
+ }
897
+ )
898
+
899
+ if workflow_data:
900
+ stmt = sqlite.insert(table)
901
+ stmt = stmt.on_conflict_do_update(
902
+ index_elements=["session_id"],
903
+ set_=dict(
904
+ workflow_id=stmt.excluded.workflow_id,
905
+ user_id=stmt.excluded.user_id,
906
+ workflow_data=stmt.excluded.workflow_data,
907
+ session_data=stmt.excluded.session_data,
908
+ metadata=stmt.excluded.metadata,
909
+ runs=stmt.excluded.runs,
910
+ summary=stmt.excluded.summary,
911
+ updated_at=stmt.excluded.updated_at,
912
+ ),
913
+ )
914
+ await sess.execute(stmt, workflow_data)
915
+
916
+ # Fetch the results for workflow sessions
917
+ workflow_ids = [session.session_id for session in workflow_sessions]
918
+ select_stmt = select(table).where(table.c.session_id.in_(workflow_ids))
919
+ result = (await sess.execute(select_stmt)).fetchall()
920
+
921
+ for row in result:
922
+ session_dict = deserialize_session_json_fields(dict(row._mapping))
923
+ if deserialize:
924
+ deserialized_workflow_session = WorkflowSession.from_dict(session_dict)
925
+ if deserialized_workflow_session is None:
926
+ continue
927
+ results.append(deserialized_workflow_session)
928
+ else:
929
+ results.append(session_dict)
930
+
931
+ return results
932
+
933
+ except Exception as e:
934
+ log_error(f"Exception during bulk session upsert, falling back to individual upserts: {e}")
935
+ # Fallback to individual upserts
936
+ return [
937
+ result
938
+ for session in sessions
939
+ if session is not None
940
+ for result in [await self.upsert_session(session, deserialize=deserialize)]
941
+ if result is not None
942
+ ]
943
+
944
+ # -- Memory methods --
945
+
946
+ async def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None):
947
+ """Delete a user memory from the database.
948
+
949
+ Args:
950
+ memory_id (str): The ID of the memory to delete.
951
+ user_id (Optional[str]): The user ID to filter by. Defaults to None.
952
+
953
+ Returns:
954
+ bool: True if deletion was successful, False otherwise.
955
+
956
+ Raises:
957
+ Exception: If an error occurs during deletion.
958
+ """
959
+ try:
960
+ table = await self._get_table(table_type="memories")
961
+ if table is None:
962
+ return
963
+
964
+ async with self.async_session_factory() as sess, sess.begin():
965
+ delete_stmt = table.delete().where(table.c.memory_id == memory_id)
966
+ if user_id is not None:
967
+ delete_stmt = delete_stmt.where(table.c.user_id == user_id)
968
+ result = await sess.execute(delete_stmt)
969
+
970
+ success = result.rowcount > 0 # type: ignore
971
+ if success:
972
+ log_debug(f"Successfully deleted user memory id: {memory_id}")
973
+ else:
974
+ log_debug(f"No user memory found with id: {memory_id}")
975
+
976
+ except Exception as e:
977
+ log_error(f"Error deleting user memory: {e}")
978
+ raise e
979
+
980
+ async def delete_user_memories(self, memory_ids: List[str], user_id: Optional[str] = None) -> None:
981
+ """Delete user memories from the database.
982
+
983
+ Args:
984
+ memory_ids (List[str]): The IDs of the memories to delete.
985
+ user_id (Optional[str]): The user ID to filter by. Defaults to None.
986
+
987
+ Raises:
988
+ Exception: If an error occurs during deletion.
989
+ """
990
+ try:
991
+ table = await self._get_table(table_type="memories")
992
+ if table is None:
993
+ return
994
+
995
+ async with self.async_session_factory() as sess, sess.begin():
996
+ delete_stmt = table.delete().where(table.c.memory_id.in_(memory_ids))
997
+ if user_id is not None:
998
+ delete_stmt = delete_stmt.where(table.c.user_id == user_id)
999
+ result = await sess.execute(delete_stmt)
1000
+ if result.rowcount == 0: # type: ignore
1001
+ log_debug(f"No user memories found with ids: {memory_ids}")
1002
+
1003
+ except Exception as e:
1004
+ log_error(f"Error deleting user memories: {e}")
1005
+ raise e
1006
+
1007
+ async def get_all_memory_topics(self) -> List[str]:
1008
+ """Get all memory topics from the database.
1009
+
1010
+ Returns:
1011
+ List[str]: List of memory topics.
1012
+ """
1013
+ try:
1014
+ table = await self._get_table(table_type="memories")
1015
+ if table is None:
1016
+ return []
1017
+
1018
+ async with self.async_session_factory() as sess, sess.begin():
1019
+ # Select topics from all results
1020
+ stmt = select(func.json_array_elements_text(table.c.topics)).select_from(table)
1021
+ result = (await sess.execute(stmt)).fetchall()
1022
+
1023
+ return list(set([record[0] for record in result]))
1024
+
1025
+ except Exception as e:
1026
+ log_debug(f"Exception reading from memory table: {e}")
1027
+ raise e
1028
+
1029
+ async def get_user_memory(
1030
+ self,
1031
+ memory_id: str,
1032
+ deserialize: Optional[bool] = True,
1033
+ user_id: Optional[str] = None,
1034
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
1035
+ """Get a memory from the database.
1036
+
1037
+ Args:
1038
+ memory_id (str): The ID of the memory to get.
1039
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
1040
+ user_id (Optional[str]): The user ID to filter by. Defaults to None.
1041
+
1042
+ Returns:
1043
+ Optional[Union[UserMemory, Dict[str, Any]]]:
1044
+ - When deserialize=True: UserMemory object
1045
+ - When deserialize=False: Memory dictionary
1046
+
1047
+ Raises:
1048
+ Exception: If an error occurs during retrieval.
1049
+ """
1050
+ try:
1051
+ table = await self._get_table(table_type="memories")
1052
+ if table is None:
1053
+ return None
1054
+
1055
+ async with self.async_session_factory() as sess, sess.begin():
1056
+ stmt = select(table).where(table.c.memory_id == memory_id)
1057
+ if user_id is not None:
1058
+ stmt = stmt.where(table.c.user_id == user_id)
1059
+ result = (await sess.execute(stmt)).fetchone()
1060
+ if result is None:
1061
+ return None
1062
+
1063
+ memory_raw = dict(result._mapping)
1064
+ if not memory_raw or not deserialize:
1065
+ return memory_raw
1066
+
1067
+ return UserMemory.from_dict(memory_raw)
1068
+
1069
+ except Exception as e:
1070
+ log_debug(f"Exception reading from memorytable: {e}")
1071
+ raise e
1072
+
1073
+ async def get_user_memories(
1074
+ self,
1075
+ user_id: Optional[str] = None,
1076
+ agent_id: Optional[str] = None,
1077
+ team_id: Optional[str] = None,
1078
+ topics: Optional[List[str]] = None,
1079
+ search_content: Optional[str] = None,
1080
+ limit: Optional[int] = None,
1081
+ page: Optional[int] = None,
1082
+ sort_by: Optional[str] = None,
1083
+ sort_order: Optional[str] = None,
1084
+ deserialize: Optional[bool] = True,
1085
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
1086
+ """Get all memories from the database as UserMemory objects.
1087
+
1088
+ Args:
1089
+ user_id (Optional[str]): The ID of the user to filter by.
1090
+ agent_id (Optional[str]): The ID of the agent to filter by.
1091
+ team_id (Optional[str]): The ID of the team to filter by.
1092
+ topics (Optional[List[str]]): The topics to filter by.
1093
+ search_content (Optional[str]): The content to search for.
1094
+ limit (Optional[int]): The maximum number of memories to return.
1095
+ page (Optional[int]): The page number.
1096
+ sort_by (Optional[str]): The column to sort by.
1097
+ sort_order (Optional[str]): The order to sort by.
1098
+ deserialize (Optional[bool]): Whether to serialize the memories. Defaults to True.
1099
+
1100
+
1101
+ Returns:
1102
+ Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
1103
+ - When deserialize=True: List of UserMemory objects
1104
+ - When deserialize=False: List of UserMemory dictionaries and total count
1105
+
1106
+ Raises:
1107
+ Exception: If an error occurs during retrieval.
1108
+ """
1109
+ try:
1110
+ table = await self._get_table(table_type="memories")
1111
+ if table is None:
1112
+ return [] if deserialize else ([], 0)
1113
+
1114
+ async with self.async_session_factory() as sess, sess.begin():
1115
+ stmt = select(table)
1116
+
1117
+ # Filtering
1118
+ if user_id is not None:
1119
+ stmt = stmt.where(table.c.user_id == user_id)
1120
+ if agent_id is not None:
1121
+ stmt = stmt.where(table.c.agent_id == agent_id)
1122
+ if team_id is not None:
1123
+ stmt = stmt.where(table.c.team_id == team_id)
1124
+ if topics is not None:
1125
+ topic_conditions = [text(f"topics::text LIKE '%\"{topic}\"%'") for topic in topics]
1126
+ stmt = stmt.where(and_(*topic_conditions))
1127
+ if search_content is not None:
1128
+ stmt = stmt.where(table.c.memory.ilike(f"%{search_content}%"))
1129
+
1130
+ # Get total count after applying filtering
1131
+ count_stmt = select(func.count()).select_from(stmt.alias())
1132
+ total_count = (await sess.execute(count_stmt)).scalar() or 0
1133
+
1134
+ # Sorting
1135
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
1136
+ # Paginating
1137
+ if limit is not None:
1138
+ stmt = stmt.limit(limit)
1139
+ if page is not None:
1140
+ stmt = stmt.offset((page - 1) * limit)
1141
+
1142
+ result = (await sess.execute(stmt)).fetchall()
1143
+ if not result:
1144
+ return [] if deserialize else ([], 0)
1145
+
1146
+ memories_raw = [dict(record._mapping) for record in result]
1147
+
1148
+ if not deserialize:
1149
+ return memories_raw, total_count
1150
+
1151
+ return [UserMemory.from_dict(record) for record in memories_raw]
1152
+
1153
+ except Exception as e:
1154
+ log_error(f"Error reading from memory table: {e}")
1155
+ raise e
1156
+
1157
+ async def get_user_memory_stats(
1158
+ self,
1159
+ limit: Optional[int] = None,
1160
+ page: Optional[int] = None,
1161
+ ) -> Tuple[List[Dict[str, Any]], int]:
1162
+ """Get user memories stats.
1163
+
1164
+ Args:
1165
+ limit (Optional[int]): The maximum number of user stats to return.
1166
+ page (Optional[int]): The page number.
1167
+
1168
+ Returns:
1169
+ Tuple[List[Dict[str, Any]], int]: A list of dictionaries containing user stats and total count.
1170
+
1171
+ Example:
1172
+ (
1173
+ [
1174
+ {
1175
+ "user_id": "123",
1176
+ "total_memories": 10,
1177
+ "last_memory_updated_at": 1714560000,
1178
+ },
1179
+ ],
1180
+ total_count: 1,
1181
+ )
1182
+ """
1183
+ try:
1184
+ table = await self._get_table(table_type="memories")
1185
+ if table is None:
1186
+ return [], 0
1187
+
1188
+ async with self.async_session_factory() as sess, sess.begin():
1189
+ stmt = (
1190
+ select(
1191
+ table.c.user_id,
1192
+ func.count(table.c.memory_id).label("total_memories"),
1193
+ func.max(table.c.updated_at).label("last_memory_updated_at"),
1194
+ )
1195
+ .where(table.c.user_id.is_not(None))
1196
+ .group_by(table.c.user_id)
1197
+ .order_by(func.max(table.c.updated_at).desc())
1198
+ )
1199
+
1200
+ count_stmt = select(func.count()).select_from(stmt.alias())
1201
+ total_count = (await sess.execute(count_stmt)).scalar() or 0
1202
+
1203
+ # Pagination
1204
+ if limit is not None:
1205
+ stmt = stmt.limit(limit)
1206
+ if page is not None:
1207
+ stmt = stmt.offset((page - 1) * limit)
1208
+
1209
+ result = (await sess.execute(stmt)).fetchall()
1210
+ if not result:
1211
+ return [], 0
1212
+
1213
+ return [
1214
+ {
1215
+ "user_id": record.user_id, # type: ignore
1216
+ "total_memories": record.total_memories,
1217
+ "last_memory_updated_at": record.last_memory_updated_at,
1218
+ }
1219
+ for record in result
1220
+ ], total_count
1221
+
1222
+ except Exception as e:
1223
+ log_error(f"Error getting user memory stats: {e}")
1224
+ raise e
1225
+
1226
+ async def upsert_user_memory(
1227
+ self, memory: UserMemory, deserialize: Optional[bool] = True
1228
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
1229
+ """Upsert a user memory in the database.
1230
+
1231
+ Args:
1232
+ memory (UserMemory): The user memory to upsert.
1233
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
1234
+
1235
+ Returns:
1236
+ Optional[Union[UserMemory, Dict[str, Any]]]:
1237
+ - When deserialize=True: UserMemory object
1238
+ - When deserialize=False: UserMemory dictionary
1239
+
1240
+ Raises:
1241
+ Exception: If an error occurs during upsert.
1242
+ """
1243
+ try:
1244
+ table = await self._get_table(table_type="memories")
1245
+ if table is None:
1246
+ return None
1247
+
1248
+ if memory.memory_id is None:
1249
+ memory.memory_id = str(uuid4())
1250
+
1251
+ async with self.async_session_factory() as sess, sess.begin():
1252
+ stmt = sqlite.insert(table).values(
1253
+ user_id=memory.user_id,
1254
+ agent_id=memory.agent_id,
1255
+ team_id=memory.team_id,
1256
+ memory_id=memory.memory_id,
1257
+ memory=memory.memory,
1258
+ topics=memory.topics,
1259
+ input=memory.input,
1260
+ updated_at=int(time.time()),
1261
+ )
1262
+ stmt = stmt.on_conflict_do_update( # type: ignore
1263
+ index_elements=["memory_id"],
1264
+ set_=dict(
1265
+ memory=memory.memory,
1266
+ topics=memory.topics,
1267
+ input=memory.input,
1268
+ updated_at=int(time.time()),
1269
+ ),
1270
+ ).returning(table)
1271
+
1272
+ result = await sess.execute(stmt)
1273
+ row = result.fetchone()
1274
+
1275
+ if row is None:
1276
+ return None
1277
+
1278
+ memory_raw = dict(row._mapping)
1279
+ if not memory_raw or not deserialize:
1280
+ return memory_raw
1281
+
1282
+ return UserMemory.from_dict(memory_raw)
1283
+
1284
+ except Exception as e:
1285
+ log_error(f"Error upserting user memory: {e}")
1286
+ raise e
1287
+
1288
+ async def upsert_memories(
1289
+ self,
1290
+ memories: List[UserMemory],
1291
+ deserialize: Optional[bool] = True,
1292
+ preserve_updated_at: bool = False,
1293
+ ) -> List[Union[UserMemory, Dict[str, Any]]]:
1294
+ """
1295
+ Bulk upsert multiple user memories for improved performance on large datasets.
1296
+
1297
+ Args:
1298
+ memories (List[UserMemory]): List of memories to upsert.
1299
+ deserialize (Optional[bool]): Whether to deserialize the memories. Defaults to True.
1300
+
1301
+ Returns:
1302
+ List[Union[UserMemory, Dict[str, Any]]]: List of upserted memories.
1303
+
1304
+ Raises:
1305
+ Exception: If an error occurs during bulk upsert.
1306
+ """
1307
+ if not memories:
1308
+ return []
1309
+
1310
+ try:
1311
+ table = await self._get_table(table_type="memories")
1312
+ if table is None:
1313
+ log_info("Memories table not available, falling back to individual upserts")
1314
+ return [
1315
+ result
1316
+ for memory in memories
1317
+ if memory is not None
1318
+ for result in [await self.upsert_user_memory(memory, deserialize=deserialize)]
1319
+ if result is not None
1320
+ ]
1321
+ # Prepare bulk data
1322
+ bulk_data = []
1323
+ current_time = int(time.time())
1324
+ for memory in memories:
1325
+ if memory.memory_id is None:
1326
+ memory.memory_id = str(uuid4())
1327
+
1328
+ # Use preserved updated_at if flag is set and value exists, otherwise use current time
1329
+ updated_at = memory.updated_at if preserve_updated_at else current_time
1330
+ bulk_data.append(
1331
+ {
1332
+ "user_id": memory.user_id,
1333
+ "agent_id": memory.agent_id,
1334
+ "team_id": memory.team_id,
1335
+ "memory_id": memory.memory_id,
1336
+ "memory": memory.memory,
1337
+ "topics": memory.topics,
1338
+ "updated_at": updated_at,
1339
+ }
1340
+ )
1341
+
1342
+ results: List[Union[UserMemory, Dict[str, Any]]] = []
1343
+
1344
+ async with self.async_session_factory() as sess, sess.begin():
1345
+ # Bulk upsert memories using SQLite ON CONFLICT DO UPDATE
1346
+ stmt = sqlite.insert(table)
1347
+ stmt = stmt.on_conflict_do_update(
1348
+ index_elements=["memory_id"],
1349
+ set_=dict(
1350
+ memory=stmt.excluded.memory,
1351
+ topics=stmt.excluded.topics,
1352
+ input=stmt.excluded.input,
1353
+ agent_id=stmt.excluded.agent_id,
1354
+ team_id=stmt.excluded.team_id,
1355
+ updated_at=stmt.excluded.updated_at,
1356
+ ),
1357
+ )
1358
+ await sess.execute(stmt, bulk_data)
1359
+
1360
+ # Fetch results
1361
+ memory_ids = [memory.memory_id for memory in memories if memory.memory_id]
1362
+ select_stmt = select(table).where(table.c.memory_id.in_(memory_ids))
1363
+ result = (await sess.execute(select_stmt)).fetchall()
1364
+
1365
+ for row in result:
1366
+ memory_dict = dict(row._mapping)
1367
+ if deserialize:
1368
+ results.append(UserMemory.from_dict(memory_dict))
1369
+ else:
1370
+ results.append(memory_dict)
1371
+
1372
+ return results
1373
+
1374
+ except Exception as e:
1375
+ log_error(f"Exception during bulk memory upsert, falling back to individual upserts: {e}")
1376
+
1377
+ # Fallback to individual upserts
1378
+ return [
1379
+ result
1380
+ for memory in memories
1381
+ if memory is not None
1382
+ for result in [await self.upsert_user_memory(memory, deserialize=deserialize)]
1383
+ if result is not None
1384
+ ]
1385
+
1386
+ async def clear_memories(self) -> None:
1387
+ """Delete all memories from the database.
1388
+
1389
+ Raises:
1390
+ Exception: If an error occurs during deletion.
1391
+ """
1392
+ try:
1393
+ table = await self._get_table(table_type="memories")
1394
+ if table is None:
1395
+ return
1396
+
1397
+ async with self.async_session_factory() as sess, sess.begin():
1398
+ await sess.execute(table.delete())
1399
+
1400
+ except Exception as e:
1401
+ from agno.utils.log import log_warning
1402
+
1403
+ log_warning(f"Exception deleting all memories: {e}")
1404
+ raise e
1405
+
1406
+ # -- Metrics methods --
1407
+
1408
+ async def _get_all_sessions_for_metrics_calculation(
1409
+ self, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None
1410
+ ) -> List[Dict[str, Any]]:
1411
+ """
1412
+ Get all sessions of all types (agent, team, workflow) as raw dictionaries.
1413
+
1414
+ Args:
1415
+ start_timestamp (Optional[int]): The start timestamp to filter by. Defaults to None.
1416
+ end_timestamp (Optional[int]): The end timestamp to filter by. Defaults to None.
1417
+
1418
+ Returns:
1419
+ List[Dict[str, Any]]: List of session dictionaries with session_type field.
1420
+
1421
+ Raises:
1422
+ Exception: If an error occurs during retrieval.
1423
+ """
1424
+ try:
1425
+ table = await self._get_table(table_type="sessions")
1426
+ if table is None:
1427
+ return []
1428
+
1429
+ stmt = select(
1430
+ table.c.user_id,
1431
+ table.c.session_data,
1432
+ table.c.runs,
1433
+ table.c.created_at,
1434
+ table.c.session_type,
1435
+ )
1436
+
1437
+ if start_timestamp is not None:
1438
+ stmt = stmt.where(table.c.created_at >= start_timestamp)
1439
+ if end_timestamp is not None:
1440
+ stmt = stmt.where(table.c.created_at <= end_timestamp)
1441
+
1442
+ async with self.async_session_factory() as sess:
1443
+ result = (await sess.execute(stmt)).fetchall()
1444
+ return [dict(record._mapping) for record in result]
1445
+
1446
+ except Exception as e:
1447
+ log_error(f"Error reading from sessions table: {e}")
1448
+ raise e
1449
+
1450
+ async def _get_metrics_calculation_starting_date(self, table: Table) -> Optional[date]:
1451
+ """Get the first date for which metrics calculation is needed:
1452
+
1453
+ 1. If there are metrics records, return the date of the first day without a complete metrics record.
1454
+ 2. If there are no metrics records, return the date of the first recorded session.
1455
+ 3. If there are no metrics records and no sessions records, return None.
1456
+
1457
+ Args:
1458
+ table (Table): The table to get the starting date for.
1459
+
1460
+ Returns:
1461
+ Optional[date]: The starting date for which metrics calculation is needed.
1462
+ """
1463
+ async with self.async_session_factory() as sess:
1464
+ stmt = select(table).order_by(table.c.date.desc()).limit(1)
1465
+ result = (await sess.execute(stmt)).fetchone()
1466
+
1467
+ # 1. Return the date of the first day without a complete metrics record.
1468
+ if result is not None:
1469
+ if result.completed:
1470
+ return result._mapping["date"] + timedelta(days=1)
1471
+ else:
1472
+ return result._mapping["date"]
1473
+
1474
+ # 2. No metrics records. Return the date of the first recorded session.
1475
+ first_session, _ = await self.get_sessions(sort_by="created_at", sort_order="asc", limit=1, deserialize=False)
1476
+ first_session_date = first_session[0]["created_at"] if first_session else None # type: ignore
1477
+
1478
+ # 3. No metrics records and no sessions records. Return None.
1479
+ if not first_session_date:
1480
+ return None
1481
+
1482
+ return datetime.fromtimestamp(first_session_date, tz=timezone.utc).date()
1483
+
1484
+ async def calculate_metrics(self) -> Optional[list[dict]]:
1485
+ """Calculate metrics for all dates without complete metrics.
1486
+
1487
+ Returns:
1488
+ Optional[list[dict]]: The calculated metrics.
1489
+
1490
+ Raises:
1491
+ Exception: If an error occurs during metrics calculation.
1492
+ """
1493
+ try:
1494
+ table = await self._get_table(table_type="metrics")
1495
+ if table is None:
1496
+ return None
1497
+
1498
+ starting_date = await self._get_metrics_calculation_starting_date(table)
1499
+ if starting_date is None:
1500
+ log_info("No session data found. Won't calculate metrics.")
1501
+ return None
1502
+
1503
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
1504
+ if not dates_to_process:
1505
+ log_info("Metrics already calculated for all relevant dates.")
1506
+ return None
1507
+
1508
+ start_timestamp = int(
1509
+ datetime.combine(dates_to_process[0], datetime.min.time()).replace(tzinfo=timezone.utc).timestamp()
1510
+ )
1511
+ end_timestamp = int(
1512
+ datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time())
1513
+ .replace(tzinfo=timezone.utc)
1514
+ .timestamp()
1515
+ )
1516
+
1517
+ sessions = await self._get_all_sessions_for_metrics_calculation(
1518
+ start_timestamp=start_timestamp, end_timestamp=end_timestamp
1519
+ )
1520
+ all_sessions_data = fetch_all_sessions_data(
1521
+ sessions=sessions,
1522
+ dates_to_process=dates_to_process,
1523
+ start_timestamp=start_timestamp,
1524
+ )
1525
+ if not all_sessions_data:
1526
+ log_info("No new session data found. Won't calculate metrics.")
1527
+ return None
1528
+
1529
+ results = []
1530
+ metrics_records = []
1531
+
1532
+ for date_to_process in dates_to_process:
1533
+ date_key = date_to_process.isoformat()
1534
+ sessions_for_date = all_sessions_data.get(date_key, {})
1535
+
1536
+ # Skip dates with no sessions
1537
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
1538
+ continue
1539
+
1540
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
1541
+ metrics_records.append(metrics_record)
1542
+
1543
+ if metrics_records:
1544
+ async with self.async_session_factory() as sess, sess.begin():
1545
+ results = await abulk_upsert_metrics(session=sess, table=table, metrics_records=metrics_records)
1546
+
1547
+ log_debug("Updated metrics calculations")
1548
+
1549
+ return results
1550
+
1551
+ except Exception as e:
1552
+ log_error(f"Error refreshing metrics: {e}")
1553
+ raise e
1554
+
1555
+ async def get_metrics(
1556
+ self,
1557
+ starting_date: Optional[date] = None,
1558
+ ending_date: Optional[date] = None,
1559
+ ) -> Tuple[List[dict], Optional[int]]:
1560
+ """Get all metrics matching the given date range.
1561
+
1562
+ Args:
1563
+ starting_date (Optional[date]): The starting date to filter metrics by.
1564
+ ending_date (Optional[date]): The ending date to filter metrics by.
1565
+
1566
+ Returns:
1567
+ Tuple[List[dict], Optional[int]]: A tuple containing the metrics and the timestamp of the latest update.
1568
+
1569
+ Raises:
1570
+ Exception: If an error occurs during retrieval.
1571
+ """
1572
+ try:
1573
+ table = await self._get_table(table_type="metrics")
1574
+ if table is None:
1575
+ return [], None
1576
+
1577
+ async with self.async_session_factory() as sess, sess.begin():
1578
+ stmt = select(table)
1579
+ if starting_date:
1580
+ stmt = stmt.where(table.c.date >= starting_date)
1581
+ if ending_date:
1582
+ stmt = stmt.where(table.c.date <= ending_date)
1583
+ result = (await sess.execute(stmt)).fetchall()
1584
+ if not result:
1585
+ return [], None
1586
+
1587
+ # Get the latest updated_at
1588
+ latest_stmt = select(func.max(table.c.updated_at))
1589
+ latest_updated_at = (await sess.execute(latest_stmt)).scalar()
1590
+
1591
+ return [dict(row._mapping) for row in result], latest_updated_at
1592
+
1593
+ except Exception as e:
1594
+ log_error(f"Error getting metrics: {e}")
1595
+ raise e
1596
+
1597
+ # -- Knowledge methods --
1598
+
1599
+ async def delete_knowledge_content(self, id: str):
1600
+ """Delete a knowledge row from the database.
1601
+
1602
+ Args:
1603
+ id (str): The ID of the knowledge row to delete.
1604
+
1605
+ Raises:
1606
+ Exception: If an error occurs during deletion.
1607
+ """
1608
+ table = await self._get_table(table_type="knowledge")
1609
+ if table is None:
1610
+ return
1611
+
1612
+ try:
1613
+ async with self.async_session_factory() as sess, sess.begin():
1614
+ stmt = table.delete().where(table.c.id == id)
1615
+ await sess.execute(stmt)
1616
+
1617
+ except Exception as e:
1618
+ log_error(f"Error deleting knowledge content: {e}")
1619
+ raise e
1620
+
1621
+ async def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1622
+ """Get a knowledge row from the database.
1623
+
1624
+ Args:
1625
+ id (str): The ID of the knowledge row to get.
1626
+
1627
+ Returns:
1628
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1629
+
1630
+ Raises:
1631
+ Exception: If an error occurs during retrieval.
1632
+ """
1633
+ table = await self._get_table(table_type="knowledge")
1634
+ if table is None:
1635
+ return None
1636
+
1637
+ try:
1638
+ async with self.async_session_factory() as sess, sess.begin():
1639
+ stmt = select(table).where(table.c.id == id)
1640
+ result = (await sess.execute(stmt)).fetchone()
1641
+ if result is None:
1642
+ return None
1643
+
1644
+ return KnowledgeRow.model_validate(result._mapping)
1645
+
1646
+ except Exception as e:
1647
+ log_error(f"Error getting knowledge content: {e}")
1648
+ raise e
1649
+
1650
+ async 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
+
1665
+ Returns:
1666
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
1667
+
1668
+ Raises:
1669
+ Exception: If an error occurs during retrieval.
1670
+ """
1671
+ table = await self._get_table(table_type="knowledge")
1672
+ if table is None:
1673
+ return [], 0
1674
+
1675
+ try:
1676
+ async with self.async_session_factory() as sess, sess.begin():
1677
+ stmt = select(table)
1678
+
1679
+ # Apply sorting
1680
+ if sort_by is not None:
1681
+ stmt = stmt.order_by(getattr(table.c, sort_by) * (1 if sort_order == "asc" else -1))
1682
+
1683
+ # Get total count before applying limit and pagination
1684
+ count_stmt = select(func.count()).select_from(stmt.alias())
1685
+ total_count = (await sess.execute(count_stmt)).scalar() or 0
1686
+
1687
+ # Apply pagination after count
1688
+ if limit is not None:
1689
+ stmt = stmt.limit(limit)
1690
+ if page is not None:
1691
+ stmt = stmt.offset((page - 1) * limit)
1692
+
1693
+ result = (await sess.execute(stmt)).fetchall()
1694
+ return [KnowledgeRow.model_validate(record._mapping) for record in result], total_count
1695
+
1696
+ except Exception as e:
1697
+ log_error(f"Error getting knowledge contents: {e}")
1698
+ raise e
1699
+
1700
+ async def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
1701
+ """Upsert knowledge content in the database.
1702
+
1703
+ Args:
1704
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
1705
+
1706
+ Returns:
1707
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
1708
+ """
1709
+ try:
1710
+ table = await self._get_table(table_type="knowledge")
1711
+ if table is None:
1712
+ return None
1713
+
1714
+ async with self.async_session_factory() as sess, sess.begin():
1715
+ update_fields = {
1716
+ k: v
1717
+ for k, v in {
1718
+ "name": knowledge_row.name,
1719
+ "description": knowledge_row.description,
1720
+ "metadata": knowledge_row.metadata,
1721
+ "type": knowledge_row.type,
1722
+ "size": knowledge_row.size,
1723
+ "linked_to": knowledge_row.linked_to,
1724
+ "access_count": knowledge_row.access_count,
1725
+ "status": knowledge_row.status,
1726
+ "status_message": knowledge_row.status_message,
1727
+ "created_at": knowledge_row.created_at,
1728
+ "updated_at": knowledge_row.updated_at,
1729
+ "external_id": knowledge_row.external_id,
1730
+ }.items()
1731
+ # Filtering out None fields if updating
1732
+ if v is not None
1733
+ }
1734
+
1735
+ stmt = (
1736
+ sqlite.insert(table)
1737
+ .values(knowledge_row.model_dump())
1738
+ .on_conflict_do_update(index_elements=["id"], set_=update_fields)
1739
+ )
1740
+ await sess.execute(stmt)
1741
+
1742
+ return knowledge_row
1743
+
1744
+ except Exception as e:
1745
+ log_error(f"Error upserting knowledge content: {e}")
1746
+ raise e
1747
+
1748
+ # -- Eval methods --
1749
+
1750
+ async def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
1751
+ """Create an EvalRunRecord in the database.
1752
+
1753
+ Args:
1754
+ eval_run (EvalRunRecord): The eval run to create.
1755
+
1756
+ Returns:
1757
+ Optional[EvalRunRecord]: The created eval run, or None if the operation fails.
1758
+
1759
+ Raises:
1760
+ Exception: If an error occurs during creation.
1761
+ """
1762
+ try:
1763
+ table = await self._get_table(table_type="evals")
1764
+ if table is None:
1765
+ return None
1766
+
1767
+ async with self.async_session_factory() as sess, sess.begin():
1768
+ current_time = int(time.time())
1769
+ stmt = sqlite.insert(table).values(
1770
+ {
1771
+ "created_at": current_time,
1772
+ "updated_at": current_time,
1773
+ **eval_run.model_dump(),
1774
+ }
1775
+ )
1776
+ await sess.execute(stmt)
1777
+
1778
+ log_debug(f"Created eval run with id '{eval_run.run_id}'")
1779
+
1780
+ return eval_run
1781
+
1782
+ except Exception as e:
1783
+ log_error(f"Error creating eval run: {e}")
1784
+ raise e
1785
+
1786
+ async def delete_eval_run(self, eval_run_id: str) -> None:
1787
+ """Delete an eval run from the database.
1788
+
1789
+ Args:
1790
+ eval_run_id (str): The ID of the eval run to delete.
1791
+ """
1792
+ try:
1793
+ table = await self._get_table(table_type="evals")
1794
+ if table is None:
1795
+ return
1796
+
1797
+ async with self.async_session_factory() as sess, sess.begin():
1798
+ stmt = table.delete().where(table.c.run_id == eval_run_id)
1799
+ result = await sess.execute(stmt)
1800
+ if result.rowcount == 0: # type: ignore
1801
+ log_warning(f"No eval run found with ID: {eval_run_id}")
1802
+ else:
1803
+ log_debug(f"Deleted eval run with ID: {eval_run_id}")
1804
+
1805
+ except Exception as e:
1806
+ log_error(f"Error deleting eval run {eval_run_id}: {e}")
1807
+ raise e
1808
+
1809
+ async def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1810
+ """Delete multiple eval runs from the database.
1811
+
1812
+ Args:
1813
+ eval_run_ids (List[str]): List of eval run IDs to delete.
1814
+ """
1815
+ try:
1816
+ table = await self._get_table(table_type="evals")
1817
+ if table is None:
1818
+ return
1819
+
1820
+ async with self.async_session_factory() as sess, sess.begin():
1821
+ stmt = table.delete().where(table.c.run_id.in_(eval_run_ids))
1822
+ result = await sess.execute(stmt)
1823
+ if result.rowcount == 0: # type: ignore
1824
+ log_debug(f"No eval runs found with IDs: {eval_run_ids}")
1825
+ else:
1826
+ log_debug(f"Deleted {result.rowcount} eval runs") # type: ignore
1827
+
1828
+ except Exception as e:
1829
+ log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
1830
+ raise e
1831
+
1832
+ async def get_eval_run(
1833
+ self, eval_run_id: str, deserialize: Optional[bool] = True
1834
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1835
+ """Get an eval run from the database.
1836
+
1837
+ Args:
1838
+ eval_run_id (str): The ID of the eval run to get.
1839
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1840
+
1841
+ Returns:
1842
+ Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1843
+ - When deserialize=True: EvalRunRecord object
1844
+ - When deserialize=False: EvalRun dictionary
1845
+
1846
+ Raises:
1847
+ Exception: If an error occurs during retrieval.
1848
+ """
1849
+ try:
1850
+ table = await self._get_table(table_type="evals")
1851
+ if table is None:
1852
+ return None
1853
+
1854
+ async with self.async_session_factory() as sess, sess.begin():
1855
+ stmt = select(table).where(table.c.run_id == eval_run_id)
1856
+ result = (await sess.execute(stmt)).fetchone()
1857
+ if result is None:
1858
+ return None
1859
+
1860
+ eval_run_raw = dict(result._mapping)
1861
+ if not eval_run_raw or not deserialize:
1862
+ return eval_run_raw
1863
+
1864
+ return EvalRunRecord.model_validate(eval_run_raw)
1865
+
1866
+ except Exception as e:
1867
+ log_error(f"Exception getting eval run {eval_run_id}: {e}")
1868
+ raise e
1869
+
1870
+ async def get_eval_runs(
1871
+ self,
1872
+ limit: Optional[int] = None,
1873
+ page: Optional[int] = None,
1874
+ sort_by: Optional[str] = None,
1875
+ sort_order: Optional[str] = None,
1876
+ agent_id: Optional[str] = None,
1877
+ team_id: Optional[str] = None,
1878
+ workflow_id: Optional[str] = None,
1879
+ model_id: Optional[str] = None,
1880
+ filter_type: Optional[EvalFilterType] = None,
1881
+ eval_type: Optional[List[EvalType]] = None,
1882
+ deserialize: Optional[bool] = True,
1883
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1884
+ """Get all eval runs from the database.
1885
+
1886
+ Args:
1887
+ limit (Optional[int]): The maximum number of eval runs to return.
1888
+ page (Optional[int]): The page number.
1889
+ sort_by (Optional[str]): The column to sort by.
1890
+ sort_order (Optional[str]): The order to sort by.
1891
+ agent_id (Optional[str]): The ID of the agent to filter by.
1892
+ team_id (Optional[str]): The ID of the team to filter by.
1893
+ workflow_id (Optional[str]): The ID of the workflow to filter by.
1894
+ model_id (Optional[str]): The ID of the model to filter by.
1895
+ eval_type (Optional[List[EvalType]]): The type(s) of eval to filter by.
1896
+ filter_type (Optional[EvalFilterType]): Filter by component type (agent, team, workflow).
1897
+ deserialize (Optional[bool]): Whether to serialize the eval runs. Defaults to True.
1898
+ create_table_if_not_found (Optional[bool]): Whether to create the table if it doesn't exist.
1899
+
1900
+ Returns:
1901
+ Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1902
+ - When deserialize=True: List of EvalRunRecord objects
1903
+ - When deserialize=False: List of EvalRun dictionaries and total count
1904
+
1905
+ Raises:
1906
+ Exception: If an error occurs during retrieval.
1907
+ """
1908
+ try:
1909
+ table = await self._get_table(table_type="evals")
1910
+ if table is None:
1911
+ return [] if deserialize else ([], 0)
1912
+
1913
+ async with self.async_session_factory() as sess, sess.begin():
1914
+ stmt = select(table)
1915
+
1916
+ # Filtering
1917
+ if agent_id is not None:
1918
+ stmt = stmt.where(table.c.agent_id == agent_id)
1919
+ if team_id is not None:
1920
+ stmt = stmt.where(table.c.team_id == team_id)
1921
+ if workflow_id is not None:
1922
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
1923
+ if model_id is not None:
1924
+ stmt = stmt.where(table.c.model_id == model_id)
1925
+ if eval_type is not None and len(eval_type) > 0:
1926
+ stmt = stmt.where(table.c.eval_type.in_(eval_type))
1927
+ if filter_type is not None:
1928
+ if filter_type == EvalFilterType.AGENT:
1929
+ stmt = stmt.where(table.c.agent_id.is_not(None))
1930
+ elif filter_type == EvalFilterType.TEAM:
1931
+ stmt = stmt.where(table.c.team_id.is_not(None))
1932
+ elif filter_type == EvalFilterType.WORKFLOW:
1933
+ stmt = stmt.where(table.c.workflow_id.is_not(None))
1934
+
1935
+ # Get total count after applying filtering
1936
+ count_stmt = select(func.count()).select_from(stmt.alias())
1937
+ total_count = (await sess.execute(count_stmt)).scalar() or 0
1938
+
1939
+ # Sorting - apply default sort by created_at desc if no sort parameters provided
1940
+ if sort_by is None:
1941
+ stmt = stmt.order_by(table.c.created_at.desc())
1942
+ else:
1943
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
1944
+ # Paginating
1945
+ if limit is not None:
1946
+ stmt = stmt.limit(limit)
1947
+ if page is not None:
1948
+ stmt = stmt.offset((page - 1) * limit)
1949
+
1950
+ result = (await sess.execute(stmt)).fetchall()
1951
+ if not result:
1952
+ return [] if deserialize else ([], 0)
1953
+
1954
+ eval_runs_raw = [dict(row._mapping) for row in result]
1955
+ if not deserialize:
1956
+ return eval_runs_raw, total_count
1957
+
1958
+ return [EvalRunRecord.model_validate(row) for row in eval_runs_raw]
1959
+
1960
+ except Exception as e:
1961
+ log_error(f"Exception getting eval runs: {e}")
1962
+ raise e
1963
+
1964
+ async def rename_eval_run(
1965
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1966
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1967
+ """Upsert the name of an eval run in the database, returning raw dictionary.
1968
+
1969
+ Args:
1970
+ eval_run_id (str): The ID of the eval run to update.
1971
+ name (str): The new name of the eval run.
1972
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1973
+
1974
+ Returns:
1975
+ Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1976
+ - When deserialize=True: EvalRunRecord object
1977
+ - When deserialize=False: EvalRun dictionary
1978
+
1979
+ Raises:
1980
+ Exception: If an error occurs during update.
1981
+ """
1982
+ try:
1983
+ table = await self._get_table(table_type="evals")
1984
+ if table is None:
1985
+ return None
1986
+
1987
+ async with self.async_session_factory() as sess, sess.begin():
1988
+ stmt = (
1989
+ table.update().where(table.c.run_id == eval_run_id).values(name=name, updated_at=int(time.time()))
1990
+ )
1991
+ await sess.execute(stmt)
1992
+
1993
+ eval_run_raw = await self.get_eval_run(eval_run_id=eval_run_id, deserialize=deserialize)
1994
+
1995
+ log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
1996
+
1997
+ if not eval_run_raw or not deserialize:
1998
+ return eval_run_raw
1999
+
2000
+ return EvalRunRecord.model_validate(eval_run_raw)
2001
+
2002
+ except Exception as e:
2003
+ log_error(f"Error renaming eval run {eval_run_id}: {e}")
2004
+ raise e
2005
+
2006
+ # -- Migrations --
2007
+
2008
+ async def migrate_table_from_v1_to_v2(self, v1_db_schema: str, v1_table_name: str, v1_table_type: str):
2009
+ """Migrate all content in the given table to the right v2 table"""
2010
+
2011
+ from agno.db.migrations.v1_to_v2 import (
2012
+ get_all_table_content,
2013
+ parse_agent_sessions,
2014
+ parse_memories,
2015
+ parse_team_sessions,
2016
+ parse_workflow_sessions,
2017
+ )
2018
+
2019
+ # Get all content from the old table
2020
+ old_content: list[dict[str, Any]] = get_all_table_content(
2021
+ db=self,
2022
+ db_schema=v1_db_schema,
2023
+ table_name=v1_table_name,
2024
+ )
2025
+ if not old_content:
2026
+ log_info(f"No content to migrate from table {v1_table_name}")
2027
+ return
2028
+
2029
+ # Parse the content into the new format
2030
+ memories: List[UserMemory] = []
2031
+ sessions: Sequence[Union[AgentSession, TeamSession, WorkflowSession]] = []
2032
+ if v1_table_type == "agent_sessions":
2033
+ sessions = parse_agent_sessions(old_content)
2034
+ elif v1_table_type == "team_sessions":
2035
+ sessions = parse_team_sessions(old_content)
2036
+ elif v1_table_type == "workflow_sessions":
2037
+ sessions = parse_workflow_sessions(old_content)
2038
+ elif v1_table_type == "memories":
2039
+ memories = parse_memories(old_content)
2040
+ else:
2041
+ raise ValueError(f"Invalid table type: {v1_table_type}")
2042
+
2043
+ # Insert the new content into the new table
2044
+ if v1_table_type == "agent_sessions":
2045
+ for session in sessions:
2046
+ await self.upsert_session(session)
2047
+ log_info(f"Migrated {len(sessions)} Agent sessions to table: {self.session_table_name}")
2048
+
2049
+ elif v1_table_type == "team_sessions":
2050
+ for session in sessions:
2051
+ await self.upsert_session(session)
2052
+ log_info(f"Migrated {len(sessions)} Team sessions to table: {self.session_table_name}")
2053
+
2054
+ elif v1_table_type == "workflow_sessions":
2055
+ for session in sessions:
2056
+ await self.upsert_session(session)
2057
+ log_info(f"Migrated {len(sessions)} Workflow sessions to table: {self.session_table_name}")
2058
+
2059
+ elif v1_table_type == "memories":
2060
+ for memory in memories:
2061
+ await self.upsert_user_memory(memory)
2062
+ log_info(f"Migrated {len(memories)} memories to table: {self.memory_table}")
2063
+
2064
+ # -- Culture methods --
2065
+
2066
+ async def clear_cultural_knowledge(self) -> None:
2067
+ """Delete all cultural artifacts from the database.
2068
+
2069
+ Raises:
2070
+ Exception: If an error occurs during deletion.
2071
+ """
2072
+ try:
2073
+ table = await self._get_table(table_type="culture")
2074
+ if table is None:
2075
+ return
2076
+
2077
+ async with self.async_session_factory() as sess, sess.begin():
2078
+ await sess.execute(table.delete())
2079
+
2080
+ except Exception as e:
2081
+ from agno.utils.log import log_warning
2082
+
2083
+ log_warning(f"Exception deleting all cultural artifacts: {e}")
2084
+ raise e
2085
+
2086
+ async def delete_cultural_knowledge(self, id: str) -> None:
2087
+ """Delete a cultural artifact from the database.
2088
+
2089
+ Args:
2090
+ id (str): The ID of the cultural artifact to delete.
2091
+
2092
+ Raises:
2093
+ Exception: If an error occurs during deletion.
2094
+ """
2095
+ try:
2096
+ table = await self._get_table(table_type="culture")
2097
+ if table is None:
2098
+ return
2099
+
2100
+ async with self.async_session_factory() as sess, sess.begin():
2101
+ delete_stmt = table.delete().where(table.c.id == id)
2102
+ result = await sess.execute(delete_stmt)
2103
+
2104
+ success = result.rowcount > 0 # type: ignore
2105
+ if success:
2106
+ log_debug(f"Successfully deleted cultural artifact id: {id}")
2107
+ else:
2108
+ log_debug(f"No cultural artifact found with id: {id}")
2109
+
2110
+ except Exception as e:
2111
+ log_error(f"Error deleting cultural artifact: {e}")
2112
+ raise e
2113
+
2114
+ async def get_cultural_knowledge(
2115
+ self, id: str, deserialize: Optional[bool] = True
2116
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2117
+ """Get a cultural artifact from the database.
2118
+
2119
+ Args:
2120
+ id (str): The ID of the cultural artifact to get.
2121
+ deserialize (Optional[bool]): Whether to serialize the cultural artifact. Defaults to True.
2122
+
2123
+ Returns:
2124
+ Optional[CulturalKnowledge]: The cultural artifact, or None if it doesn't exist.
2125
+
2126
+ Raises:
2127
+ Exception: If an error occurs during retrieval.
2128
+ """
2129
+ try:
2130
+ table = await self._get_table(table_type="culture")
2131
+ if table is None:
2132
+ return None
2133
+
2134
+ async with self.async_session_factory() as sess, sess.begin():
2135
+ stmt = select(table).where(table.c.id == id)
2136
+ result = (await sess.execute(stmt)).fetchone()
2137
+ if result is None:
2138
+ return None
2139
+
2140
+ db_row = dict(result._mapping)
2141
+ if not db_row or not deserialize:
2142
+ return db_row
2143
+
2144
+ return deserialize_cultural_knowledge_from_db(db_row)
2145
+
2146
+ except Exception as e:
2147
+ log_error(f"Exception reading from cultural artifacts table: {e}")
2148
+ raise e
2149
+
2150
+ async def get_all_cultural_knowledge(
2151
+ self,
2152
+ name: Optional[str] = None,
2153
+ agent_id: Optional[str] = None,
2154
+ team_id: Optional[str] = None,
2155
+ limit: Optional[int] = None,
2156
+ page: Optional[int] = None,
2157
+ sort_by: Optional[str] = None,
2158
+ sort_order: Optional[str] = None,
2159
+ deserialize: Optional[bool] = True,
2160
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2161
+ """Get all cultural artifacts from the database as CulturalNotion objects.
2162
+
2163
+ Args:
2164
+ name (Optional[str]): The name of the cultural artifact to filter by.
2165
+ agent_id (Optional[str]): The ID of the agent to filter by.
2166
+ team_id (Optional[str]): The ID of the team to filter by.
2167
+ limit (Optional[int]): The maximum number of cultural artifacts to return.
2168
+ page (Optional[int]): The page number.
2169
+ sort_by (Optional[str]): The column to sort by.
2170
+ sort_order (Optional[str]): The order to sort by.
2171
+ deserialize (Optional[bool]): Whether to serialize the cultural artifacts. Defaults to True.
2172
+
2173
+ Returns:
2174
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2175
+ - When deserialize=True: List of CulturalNotion objects
2176
+ - When deserialize=False: List of CulturalNotion dictionaries and total count
2177
+
2178
+ Raises:
2179
+ Exception: If an error occurs during retrieval.
2180
+ """
2181
+ try:
2182
+ table = await self._get_table(table_type="culture")
2183
+ if table is None:
2184
+ return [] if deserialize else ([], 0)
2185
+
2186
+ async with self.async_session_factory() as sess, sess.begin():
2187
+ stmt = select(table)
2188
+
2189
+ # Filtering
2190
+ if name is not None:
2191
+ stmt = stmt.where(table.c.name == name)
2192
+ if agent_id is not None:
2193
+ stmt = stmt.where(table.c.agent_id == agent_id)
2194
+ if team_id is not None:
2195
+ stmt = stmt.where(table.c.team_id == team_id)
2196
+
2197
+ # Get total count after applying filtering
2198
+ count_stmt = select(func.count()).select_from(stmt.alias())
2199
+ total_count = (await sess.execute(count_stmt)).scalar() or 0
2200
+
2201
+ # Sorting
2202
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
2203
+ # Paginating
2204
+ if limit is not None:
2205
+ stmt = stmt.limit(limit)
2206
+ if page is not None:
2207
+ stmt = stmt.offset((page - 1) * limit)
2208
+
2209
+ result = (await sess.execute(stmt)).fetchall()
2210
+ if not result:
2211
+ return [] if deserialize else ([], 0)
2212
+
2213
+ db_rows = [dict(record._mapping) for record in result]
2214
+
2215
+ if not deserialize:
2216
+ return db_rows, total_count
2217
+
2218
+ return [deserialize_cultural_knowledge_from_db(row) for row in db_rows]
2219
+
2220
+ except Exception as e:
2221
+ log_error(f"Error reading from cultural artifacts table: {e}")
2222
+ raise e
2223
+
2224
+ async def upsert_cultural_knowledge(
2225
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
2226
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2227
+ """Upsert a cultural artifact into the database.
2228
+
2229
+ Args:
2230
+ cultural_knowledge (CulturalKnowledge): The cultural artifact to upsert.
2231
+ deserialize (Optional[bool]): Whether to serialize the cultural artifact. Defaults to True.
2232
+
2233
+ Returns:
2234
+ Optional[Union[CulturalNotion, Dict[str, Any]]]:
2235
+ - When deserialize=True: CulturalNotion object
2236
+ - When deserialize=False: CulturalNotion dictionary
2237
+
2238
+ Raises:
2239
+ Exception: If an error occurs during upsert.
2240
+ """
2241
+ try:
2242
+ table = await self._get_table(table_type="culture")
2243
+ if table is None:
2244
+ return None
2245
+
2246
+ if cultural_knowledge.id is None:
2247
+ cultural_knowledge.id = str(uuid4())
2248
+
2249
+ # Serialize content, categories, and notes into a JSON string for DB storage (SQLite requires strings)
2250
+ content_json_str = serialize_cultural_knowledge_for_db(cultural_knowledge)
2251
+
2252
+ async with self.async_session_factory() as sess, sess.begin():
2253
+ stmt = sqlite.insert(table).values(
2254
+ id=cultural_knowledge.id,
2255
+ name=cultural_knowledge.name,
2256
+ summary=cultural_knowledge.summary,
2257
+ content=content_json_str,
2258
+ metadata=cultural_knowledge.metadata,
2259
+ input=cultural_knowledge.input,
2260
+ created_at=cultural_knowledge.created_at,
2261
+ updated_at=int(time.time()),
2262
+ agent_id=cultural_knowledge.agent_id,
2263
+ team_id=cultural_knowledge.team_id,
2264
+ )
2265
+ stmt = stmt.on_conflict_do_update( # type: ignore
2266
+ index_elements=["id"],
2267
+ set_=dict(
2268
+ name=cultural_knowledge.name,
2269
+ summary=cultural_knowledge.summary,
2270
+ content=content_json_str,
2271
+ metadata=cultural_knowledge.metadata,
2272
+ input=cultural_knowledge.input,
2273
+ updated_at=int(time.time()),
2274
+ agent_id=cultural_knowledge.agent_id,
2275
+ team_id=cultural_knowledge.team_id,
2276
+ ),
2277
+ ).returning(table)
2278
+
2279
+ result = await sess.execute(stmt)
2280
+ row = result.fetchone()
2281
+
2282
+ if row is None:
2283
+ return None
2284
+
2285
+ db_row: Dict[str, Any] = dict(row._mapping)
2286
+ if not db_row or not deserialize:
2287
+ return db_row
2288
+
2289
+ return deserialize_cultural_knowledge_from_db(db_row)
2290
+
2291
+ except Exception as e:
2292
+ log_error(f"Error upserting cultural knowledge: {e}")
2293
+ raise e