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,1353 @@
1
+ from datetime import date, datetime, timedelta, timezone
2
+ from textwrap import dedent
3
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
+
5
+ from agno.db.base import BaseDb, SessionType
6
+ from agno.db.postgres.utils import (
7
+ get_dates_to_calculate_metrics_for,
8
+ )
9
+ from agno.db.schemas import UserMemory
10
+ from agno.db.schemas.culture import CulturalKnowledge
11
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
12
+ from agno.db.schemas.knowledge import KnowledgeRow
13
+ from agno.db.surrealdb import utils
14
+ from agno.db.surrealdb.metrics import (
15
+ bulk_upsert_metrics,
16
+ calculate_date_metrics,
17
+ fetch_all_sessions_data,
18
+ get_all_sessions_for_metrics_calculation,
19
+ get_metrics_calculation_starting_date,
20
+ )
21
+ from agno.db.surrealdb.models import (
22
+ TableType,
23
+ deserialize_cultural_knowledge,
24
+ deserialize_eval_run_record,
25
+ deserialize_knowledge_row,
26
+ deserialize_session,
27
+ deserialize_sessions,
28
+ deserialize_user_memories,
29
+ deserialize_user_memory,
30
+ desurrealize_eval_run_record,
31
+ desurrealize_session,
32
+ desurrealize_user_memory,
33
+ get_schema,
34
+ get_session_type,
35
+ serialize_cultural_knowledge,
36
+ serialize_eval_run_record,
37
+ serialize_knowledge_row,
38
+ serialize_session,
39
+ serialize_user_memory,
40
+ )
41
+ from agno.db.surrealdb.queries import COUNT_QUERY, WhereClause, order_limit_start
42
+ from agno.db.surrealdb.utils import build_client
43
+ from agno.session import Session
44
+ from agno.utils.log import log_debug, log_error, log_info
45
+ from agno.utils.string import generate_id
46
+
47
+ try:
48
+ from surrealdb import BlockingHttpSurrealConnection, BlockingWsSurrealConnection, RecordID
49
+ except ImportError:
50
+ raise ImportError("The `surrealdb` package is not installed. Please install it via `pip install surrealdb`.")
51
+
52
+
53
+ class SurrealDb(BaseDb):
54
+ def __init__(
55
+ self,
56
+ client: Optional[Union[BlockingWsSurrealConnection, BlockingHttpSurrealConnection]],
57
+ db_url: str,
58
+ db_creds: dict[str, str],
59
+ db_ns: str,
60
+ db_db: str,
61
+ session_table: Optional[str] = None,
62
+ memory_table: Optional[str] = None,
63
+ metrics_table: Optional[str] = None,
64
+ eval_table: Optional[str] = None,
65
+ knowledge_table: Optional[str] = None,
66
+ culture_table: Optional[str] = None,
67
+ id: Optional[str] = None,
68
+ ):
69
+ """
70
+ Interface for interacting with a SurrealDB database.
71
+
72
+ Args:
73
+ client: A blocking connection, either HTTP or WS
74
+ """
75
+ if id is None:
76
+ base_seed = db_url
77
+ seed = f"{base_seed}#{db_db}"
78
+ id = generate_id(seed)
79
+
80
+ super().__init__(
81
+ id=id,
82
+ session_table=session_table,
83
+ memory_table=memory_table,
84
+ metrics_table=metrics_table,
85
+ eval_table=eval_table,
86
+ knowledge_table=knowledge_table,
87
+ culture_table=culture_table,
88
+ )
89
+ self._client = client
90
+ self._db_url = db_url
91
+ self._db_creds = db_creds
92
+ self._db_ns = db_ns
93
+ self._db_db = db_db
94
+ self._users_table_name: str = "agno_users"
95
+ self._agents_table_name: str = "agno_agents"
96
+ self._teams_table_name: str = "agno_teams"
97
+ self._workflows_table_name: str = "agno_workflows"
98
+
99
+ @property
100
+ def client(self) -> Union[BlockingWsSurrealConnection, BlockingHttpSurrealConnection]:
101
+ if self._client is None:
102
+ self._client = build_client(self._db_url, self._db_creds, self._db_ns, self._db_db)
103
+ return self._client
104
+
105
+ @property
106
+ def table_names(self) -> dict[TableType, str]:
107
+ return {
108
+ "agents": self._agents_table_name,
109
+ "culture": self.culture_table_name,
110
+ "evals": self.eval_table_name,
111
+ "knowledge": self.knowledge_table_name,
112
+ "memories": self.memory_table_name,
113
+ "sessions": self.session_table_name,
114
+ "teams": self._teams_table_name,
115
+ "users": self._users_table_name,
116
+ "workflows": self._workflows_table_name,
117
+ }
118
+
119
+ def table_exists(self, table_name: str) -> bool:
120
+ """Check if a table with the given name exists in the SurrealDB database.
121
+
122
+ Args:
123
+ table_name: Name of the table to check
124
+
125
+ Returns:
126
+ bool: True if the table exists in the database, False otherwise
127
+ """
128
+ response = self._query_one("INFO FOR DB", {}, dict)
129
+ if response is None:
130
+ raise Exception("Failed to retrieve database information")
131
+ return table_name in response.get("tables", [])
132
+
133
+ def _table_exists(self, table_name: str) -> bool:
134
+ """Deprecated: Use table_exists() instead."""
135
+ return self.table_exists(table_name)
136
+
137
+ def _create_table(self, table_type: TableType, table_name: str):
138
+ query = get_schema(table_type, table_name)
139
+ self.client.query(query)
140
+
141
+ def _get_table(self, table_type: TableType, create_table_if_not_found: bool = True):
142
+ if table_type == "sessions":
143
+ table_name = self.session_table_name
144
+ elif table_type == "memories":
145
+ table_name = self.memory_table_name
146
+ elif table_type == "knowledge":
147
+ table_name = self.knowledge_table_name
148
+ elif table_type == "culture":
149
+ table_name = self.culture_table_name
150
+ elif table_type == "users":
151
+ table_name = self._users_table_name
152
+ elif table_type == "agents":
153
+ table_name = self._agents_table_name
154
+ elif table_type == "teams":
155
+ table_name = self._teams_table_name
156
+ elif table_type == "workflows":
157
+ table_name = self._workflows_table_name
158
+ elif table_type == "evals":
159
+ table_name = self.eval_table_name
160
+ elif table_type == "metrics":
161
+ table_name = self.metrics_table_name
162
+ else:
163
+ raise NotImplementedError(f"Unknown table type: {table_type}")
164
+
165
+ if create_table_if_not_found and not self._table_exists(table_name):
166
+ self._create_table(table_type, table_name)
167
+
168
+ return table_name
169
+
170
+ def _query(
171
+ self,
172
+ query: str,
173
+ vars: dict[str, Any],
174
+ record_type: type[utils.RecordType],
175
+ ) -> Sequence[utils.RecordType]:
176
+ return utils.query(self.client, query, vars, record_type)
177
+
178
+ def _query_one(
179
+ self,
180
+ query: str,
181
+ vars: dict[str, Any],
182
+ record_type: type[utils.RecordType],
183
+ ) -> Optional[utils.RecordType]:
184
+ return utils.query_one(self.client, query, vars, record_type)
185
+
186
+ def _count(self, table: str, where_clause: str, where_vars: dict[str, Any], group_by: Optional[str] = None) -> int:
187
+ total_count_query = COUNT_QUERY.format(
188
+ table=table,
189
+ where_clause=where_clause,
190
+ group_clause="GROUP ALL" if group_by is None else f"GROUP BY {group_by}",
191
+ group_fields="" if group_by is None else f", {group_by}",
192
+ )
193
+ count_result = self._query_one(total_count_query, where_vars, dict)
194
+ total_count = count_result.get("count") if count_result else 0
195
+ assert isinstance(total_count, int), f"Expected int, got {type(total_count)}"
196
+ total_count = int(total_count)
197
+ return total_count
198
+
199
+ # --- Sessions ---
200
+ def clear_sessions(self) -> None:
201
+ """Delete all session rows from the database.
202
+
203
+ Raises:
204
+ Exception: If an error occurs during deletion.
205
+ """
206
+ table = self._get_table("sessions")
207
+ _ = self.client.delete(table)
208
+
209
+ def delete_session(self, session_id: str) -> bool:
210
+ table = self._get_table(table_type="sessions")
211
+ if table is None:
212
+ return False
213
+ res = self.client.delete(RecordID(table, session_id))
214
+ return bool(res)
215
+
216
+ def delete_sessions(self, session_ids: list[str]) -> None:
217
+ table = self._get_table(table_type="sessions")
218
+ if table is None:
219
+ return
220
+
221
+ records = [RecordID(table, id) for id in session_ids]
222
+ self.client.query(f"DELETE FROM {table} WHERE id IN $records", {"records": records})
223
+
224
+ def get_session(
225
+ self,
226
+ session_id: str,
227
+ session_type: SessionType,
228
+ user_id: Optional[str] = None,
229
+ deserialize: Optional[bool] = True,
230
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
231
+ r"""
232
+ Read a session from the database.
233
+
234
+ Args:
235
+ session_id (str): ID of the session to read.
236
+ session_type (SessionType): Type of session to get.
237
+ user_id (Optional[str]): User ID to filter by. Defaults to None.
238
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
239
+
240
+ Returns:
241
+ Optional[Union[Session, Dict[str, Any]]]:
242
+ - When deserialize=True: Session object
243
+ - When deserialize=False: Session dictionary
244
+
245
+ Raises:
246
+ Exception: If an error occurs during retrieval.
247
+ """
248
+ sessions_table = self._get_table("sessions")
249
+ record = RecordID(sessions_table, session_id)
250
+ where = WhereClause()
251
+ if user_id is not None:
252
+ where = where.and_("user_id", user_id)
253
+ where_clause, where_vars = where.build()
254
+ query = dedent(f"""
255
+ SELECT *
256
+ FROM ONLY $record
257
+ {where_clause}
258
+ """)
259
+ vars = {"record": record, **where_vars}
260
+ raw = self._query_one(query, vars, dict)
261
+ if raw is None or not deserialize:
262
+ return raw
263
+
264
+ return deserialize_session(session_type, raw)
265
+
266
+ def get_sessions(
267
+ self,
268
+ session_type: Optional[SessionType] = None,
269
+ user_id: Optional[str] = None,
270
+ component_id: Optional[str] = None,
271
+ session_name: Optional[str] = None,
272
+ start_timestamp: Optional[int] = None,
273
+ end_timestamp: Optional[int] = None,
274
+ limit: Optional[int] = None,
275
+ page: Optional[int] = None,
276
+ sort_by: Optional[str] = None,
277
+ sort_order: Optional[str] = None,
278
+ deserialize: Optional[bool] = True,
279
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
280
+ r"""
281
+ Get all sessions in the given table. Can filter by user_id and entity_id.
282
+
283
+ Args:
284
+ session_type (SessionType): The type of session to get.
285
+ user_id (Optional[str]): The ID of the user to filter by.
286
+ component_id (Optional[str]): The ID of the agent / team / workflow to filter by.
287
+ session_name (Optional[str]): The name of the session to filter by.
288
+ start_timestamp (Optional[int]): The start timestamp to filter by.
289
+ end_timestamp (Optional[int]): The end timestamp to filter by.
290
+ limit (Optional[int]): The maximum number of sessions to return. Defaults to None.
291
+ page (Optional[int]): The page number to return. Defaults to None.
292
+ sort_by (Optional[str]): The field to sort by. Defaults to None.
293
+ sort_order (Optional[str]): The sort order. Defaults to None.
294
+ deserialize (Optional[bool]): Whether to serialize the sessions. Defaults to True.
295
+
296
+ Returns:
297
+ Union[List[Session], Tuple[List[Dict], int]]:
298
+ - When deserialize=True: List of Session objects
299
+ - When deserialize=False: Tuple of (session dictionaries, total count)
300
+
301
+ Raises:
302
+ Exception: If an error occurs during retrieval.
303
+ """
304
+ table = self._get_table("sessions")
305
+ # users_table = self._get_table("users", False) # Not used, commenting out for now.
306
+ agents_table = self._get_table("agents", False)
307
+ teams_table = self._get_table("teams", False)
308
+ workflows_table = self._get_table("workflows", False)
309
+
310
+ # -- Filters
311
+ where = WhereClause()
312
+
313
+ # user_id
314
+ if user_id is not None:
315
+ where = where.and_("user_id", user_id)
316
+
317
+ # component_id
318
+ if component_id is not None:
319
+ if session_type == SessionType.AGENT:
320
+ where = where.and_("agent", RecordID(agents_table, component_id))
321
+ elif session_type == SessionType.TEAM:
322
+ where = where.and_("team", RecordID(teams_table, component_id))
323
+ elif session_type == SessionType.WORKFLOW:
324
+ where = where.and_("workflow", RecordID(workflows_table, component_id))
325
+
326
+ # session_name
327
+ if session_name is not None:
328
+ where = where.and_("session_name", session_name, "~")
329
+
330
+ # start_timestamp
331
+ if start_timestamp is not None:
332
+ where = where.and_("start_timestamp", start_timestamp, ">=")
333
+
334
+ # end_timestamp
335
+ if end_timestamp is not None:
336
+ where = where.and_("end_timestamp", end_timestamp, "<=")
337
+
338
+ where_clause, where_vars = where.build()
339
+
340
+ # Total count
341
+ total_count = self._count(table, where_clause, where_vars)
342
+
343
+ # Query
344
+ order_limit_start_clause = order_limit_start(sort_by, sort_order, limit, page)
345
+ query = dedent(f"""
346
+ SELECT *
347
+ FROM {table}
348
+ {where_clause}
349
+ {order_limit_start_clause}
350
+ """)
351
+ sessions_raw = self._query(query, where_vars, dict)
352
+ converted_sessions_raw = [desurrealize_session(session, session_type) for session in sessions_raw]
353
+
354
+ if not deserialize:
355
+ return list(converted_sessions_raw), total_count
356
+
357
+ if session_type is None:
358
+ raise ValueError("session_type is required when deserialize=True")
359
+
360
+ return deserialize_sessions(session_type, list(sessions_raw))
361
+
362
+ def rename_session(
363
+ self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
364
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
365
+ """
366
+ Rename a session in the database.
367
+
368
+ Args:
369
+ session_id (str): The ID of the session to rename.
370
+ session_type (SessionType): The type of session to rename.
371
+ session_name (str): The new name for the session.
372
+ deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
373
+
374
+ Returns:
375
+ Optional[Union[Session, Dict[str, Any]]]:
376
+ - When deserialize=True: Session object
377
+ - When deserialize=False: Session dictionary
378
+
379
+ Raises:
380
+ Exception: If an error occurs during renaming.
381
+ """
382
+ table = self._get_table("sessions")
383
+ vars = {"record": RecordID(table, session_id), "name": session_name}
384
+
385
+ # Query
386
+ query = dedent("""
387
+ UPDATE ONLY $record
388
+ SET session_name = $name
389
+ """)
390
+ session_raw = self._query_one(query, vars, dict)
391
+
392
+ if session_raw is None or not deserialize:
393
+ return session_raw
394
+ return deserialize_session(session_type, session_raw)
395
+
396
+ def upsert_session(
397
+ self, session: Session, deserialize: Optional[bool] = True
398
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
399
+ """
400
+ Insert or update a session in the database.
401
+
402
+ Args:
403
+ session (Session): The session data to upsert.
404
+ deserialize (Optional[bool]): Whether to deserialize the session. Defaults to True.
405
+
406
+ Returns:
407
+ Optional[Union[Session, Dict[str, Any]]]:
408
+ - When deserialize=True: Session object
409
+ - When deserialize=False: Session dictionary
410
+
411
+ Raises:
412
+ Exception: If an error occurs during upsert.
413
+ """
414
+ session_type = get_session_type(session)
415
+ table = self._get_table("sessions")
416
+ session_raw = self._query_one(
417
+ "UPSERT ONLY $record CONTENT $content",
418
+ {
419
+ "record": RecordID(table, session.session_id),
420
+ "content": serialize_session(session, self.table_names),
421
+ },
422
+ dict,
423
+ )
424
+ if session_raw is None or not deserialize:
425
+ return session_raw
426
+
427
+ return deserialize_session(session_type, session_raw)
428
+
429
+ def upsert_sessions(
430
+ self, sessions: List[Session], deserialize: Optional[bool] = True
431
+ ) -> List[Union[Session, Dict[str, Any]]]:
432
+ """
433
+ Bulk insert or update multiple sessions.
434
+
435
+ Args:
436
+ sessions (List[Session]): The list of session data to upsert.
437
+ deserialize (Optional[bool]): Whether to deserialize the sessions. Defaults to True.
438
+
439
+ Returns:
440
+ List[Union[Session, Dict[str, Any]]]: List of upserted sessions
441
+
442
+ Raises:
443
+ Exception: If an error occurs during bulk upsert.
444
+ """
445
+ if not sessions:
446
+ return []
447
+ session_type = get_session_type(sessions[0])
448
+ table = self._get_table("sessions")
449
+ sessions_raw: List[Dict[str, Any]] = []
450
+ for session in sessions:
451
+ # UPSERT does only work for one record at a time
452
+ session_raw = self._query_one(
453
+ "UPSERT ONLY $record CONTENT $content",
454
+ {
455
+ "record": RecordID(table, session.session_id),
456
+ "content": serialize_session(session, self.table_names),
457
+ },
458
+ dict,
459
+ )
460
+ if session_raw:
461
+ sessions_raw.append(session_raw)
462
+ if not deserialize:
463
+ return list(sessions_raw)
464
+
465
+ # wrapping with list because of:
466
+ # Type "List[Session]" is not assignable to return type "List[Session | Dict[str, Any]]"
467
+ # Consider switching from "list" to "Sequence" which is covariant
468
+ return list(deserialize_sessions(session_type, sessions_raw))
469
+
470
+ # --- Memory ---
471
+ def clear_memories(self) -> None:
472
+ """Delete all memories from the database.
473
+
474
+ Raises:
475
+ Exception: If an error occurs during deletion.
476
+ """
477
+ table = self._get_table("memories")
478
+ _ = self.client.delete(table)
479
+
480
+ # -- Cultural Knowledge methods --
481
+ def clear_cultural_knowledge(self) -> None:
482
+ """Delete all cultural knowledge from the database.
483
+
484
+ Raises:
485
+ Exception: If an error occurs during deletion.
486
+ """
487
+ table = self._get_table("culture")
488
+ _ = self.client.delete(table)
489
+
490
+ def delete_cultural_knowledge(self, id: str) -> None:
491
+ """Delete cultural knowledge by ID.
492
+
493
+ Args:
494
+ id (str): The ID of the cultural knowledge to delete.
495
+
496
+ Raises:
497
+ Exception: If an error occurs during deletion.
498
+ """
499
+ table = self._get_table("culture")
500
+ rec_id = RecordID(table, id)
501
+ self.client.delete(rec_id)
502
+
503
+ def get_cultural_knowledge(
504
+ self, id: str, deserialize: Optional[bool] = True
505
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
506
+ """Get cultural knowledge by ID.
507
+
508
+ Args:
509
+ id (str): The ID of the cultural knowledge to retrieve.
510
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
511
+
512
+ Returns:
513
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
514
+
515
+ Raises:
516
+ Exception: If an error occurs during retrieval.
517
+ """
518
+ table = self._get_table("culture")
519
+ rec_id = RecordID(table, id)
520
+ result = self.client.select(rec_id)
521
+
522
+ if result is None:
523
+ return None
524
+
525
+ if not deserialize:
526
+ return result # type: ignore
527
+
528
+ return deserialize_cultural_knowledge(result) # type: ignore
529
+
530
+ def get_all_cultural_knowledge(
531
+ self,
532
+ agent_id: Optional[str] = None,
533
+ team_id: Optional[str] = None,
534
+ name: Optional[str] = None,
535
+ limit: Optional[int] = None,
536
+ page: Optional[int] = None,
537
+ sort_by: Optional[str] = None,
538
+ sort_order: Optional[str] = None,
539
+ deserialize: Optional[bool] = True,
540
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
541
+ """Get all cultural knowledge with filtering and pagination.
542
+
543
+ Args:
544
+ agent_id (Optional[str]): Filter by agent ID.
545
+ team_id (Optional[str]): Filter by team ID.
546
+ name (Optional[str]): Filter by name (case-insensitive partial match).
547
+ limit (Optional[int]): Maximum number of results to return.
548
+ page (Optional[int]): Page number for pagination.
549
+ sort_by (Optional[str]): Field to sort by.
550
+ sort_order (Optional[str]): Sort order ('asc' or 'desc').
551
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
552
+
553
+ Returns:
554
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
555
+ - When deserialize=True: List of CulturalKnowledge objects
556
+ - When deserialize=False: Tuple with list of dictionaries and total count
557
+
558
+ Raises:
559
+ Exception: If an error occurs during retrieval.
560
+ """
561
+ table = self._get_table("culture")
562
+
563
+ # Build where clauses
564
+ where_clauses: List[WhereClause] = []
565
+ if agent_id is not None:
566
+ agent_rec_id = RecordID(self._get_table("agents"), agent_id)
567
+ where_clauses.append(("agent", "=", agent_rec_id)) # type: ignore
568
+ if team_id is not None:
569
+ team_rec_id = RecordID(self._get_table("teams"), team_id)
570
+ where_clauses.append(("team", "=", team_rec_id)) # type: ignore
571
+ if name is not None:
572
+ where_clauses.append(("string::lowercase(name)", "CONTAINS", name.lower())) # type: ignore
573
+
574
+ # Build query for total count
575
+ count_query = COUNT_QUERY.format(
576
+ table=table,
577
+ where=""
578
+ if not where_clauses
579
+ else f"WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}", # type: ignore
580
+ )
581
+ params = {chr(97 + i): w[2] for i, w in enumerate(where_clauses)} # type: ignore
582
+ total_count = self._query_one(count_query, params, int) or 0
583
+
584
+ # Build main query
585
+ order_limit = order_limit_start(sort_by, sort_order, limit, page)
586
+ query = f"SELECT * FROM {table}"
587
+ if where_clauses:
588
+ query += f" WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}" # type: ignore
589
+ query += order_limit
590
+
591
+ results = self._query(query, params, list) or []
592
+
593
+ if not deserialize:
594
+ return results, total_count # type: ignore
595
+
596
+ return [deserialize_cultural_knowledge(r) for r in results] # type: ignore
597
+
598
+ def upsert_cultural_knowledge(
599
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
600
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
601
+ """Upsert cultural knowledge in SurrealDB.
602
+
603
+ Args:
604
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
605
+ deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
606
+
607
+ Returns:
608
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
609
+
610
+ Raises:
611
+ Exception: If an error occurs during upsert.
612
+ """
613
+ table = self._get_table("culture", create_table_if_not_found=True)
614
+ serialized = serialize_cultural_knowledge(cultural_knowledge, table)
615
+
616
+ result = self.client.upsert(serialized["id"], serialized)
617
+
618
+ if result is None:
619
+ return None
620
+
621
+ if not deserialize:
622
+ return result # type: ignore
623
+
624
+ return deserialize_cultural_knowledge(result) # type: ignore
625
+
626
+ def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None) -> None:
627
+ """Delete a user memory from the database.
628
+
629
+ Args:
630
+ memory_id (str): The ID of the memory to delete.
631
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
632
+
633
+ Returns:
634
+ bool: True if deletion was successful, False otherwise.
635
+
636
+ Raises:
637
+ Exception: If an error occurs during deletion.
638
+ """
639
+ table = self._get_table("memories")
640
+ mem_rec_id = RecordID(table, memory_id)
641
+ if user_id is None:
642
+ self.client.delete(mem_rec_id)
643
+ else:
644
+ user_rec_id = RecordID(self._get_table("users"), user_id)
645
+ self.client.query(
646
+ f"DELETE FROM {table} WHERE user = $user AND id = $memory",
647
+ {"user": user_rec_id, "memory": mem_rec_id},
648
+ )
649
+
650
+ def delete_user_memories(self, memory_ids: List[str], user_id: Optional[str] = None) -> None:
651
+ """Delete user memories from the database.
652
+
653
+ Args:
654
+ memory_ids (List[str]): The IDs of the memories to delete.
655
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
656
+
657
+ Raises:
658
+ Exception: If an error occurs during deletion.
659
+ """
660
+ table = self._get_table("memories")
661
+ records = [RecordID(table, memory_id) for memory_id in memory_ids]
662
+ if user_id is None:
663
+ _ = self.client.query(f"DELETE FROM {table} WHERE id IN $records", {"records": records})
664
+ else:
665
+ user_rec_id = RecordID(self._get_table("users"), user_id)
666
+ _ = self.client.query(
667
+ f"DELETE FROM {table} WHERE id IN $records AND user = $user", {"records": records, "user": user_rec_id}
668
+ )
669
+
670
+ def get_all_memory_topics(self, user_id: Optional[str] = None) -> List[str]:
671
+ """Get all memory topics from the database.
672
+
673
+ Args:
674
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
675
+
676
+ Returns:
677
+ List[str]: List of memory topics.
678
+ """
679
+ table = self._get_table("memories")
680
+ vars: dict[str, Any] = {}
681
+
682
+ # Query
683
+ if user_id is None:
684
+ query = dedent(f"""
685
+ RETURN (
686
+ SELECT
687
+ array::flatten(topics) as topics
688
+ FROM ONLY {table}
689
+ GROUP ALL
690
+ ).topics.distinct();
691
+ """)
692
+ else:
693
+ query = dedent(f"""
694
+ RETURN (
695
+ SELECT
696
+ array::flatten(topics) as topics
697
+ FROM ONLY {table}
698
+ WHERE user = $user
699
+ GROUP ALL
700
+ ).topics.distinct();
701
+ """)
702
+ vars["user"] = RecordID(self._get_table("users"), user_id)
703
+
704
+ result = self._query(query, vars, str)
705
+ return list(result)
706
+
707
+ def get_user_memory(
708
+ self, memory_id: str, deserialize: Optional[bool] = True, user_id: Optional[str] = None
709
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
710
+ """Get a memory from the database.
711
+
712
+ Args:
713
+ memory_id (str): The ID of the memory to get.
714
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
715
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
716
+
717
+ Returns:
718
+ Optional[Union[UserMemory, Dict[str, Any]]]:
719
+ - When deserialize=True: UserMemory object
720
+ - When deserialize=False: UserMemory dictionary
721
+
722
+ Raises:
723
+ Exception: If an error occurs during retrieval.
724
+ """
725
+ table_name = self._get_table("memories")
726
+ record = RecordID(table_name, memory_id)
727
+ vars = {"record": record}
728
+
729
+ if user_id is None:
730
+ query = "SELECT * FROM ONLY $record"
731
+ else:
732
+ query = "SELECT * FROM ONLY $record WHERE user = $user"
733
+ vars["user"] = RecordID(self._get_table("users"), user_id)
734
+
735
+ result = self._query_one(query, vars, dict)
736
+ if result is None or not deserialize:
737
+ return result
738
+ return deserialize_user_memory(result)
739
+
740
+ def get_user_memories(
741
+ self,
742
+ user_id: Optional[str] = None,
743
+ agent_id: Optional[str] = None,
744
+ team_id: Optional[str] = None,
745
+ topics: Optional[List[str]] = None,
746
+ search_content: Optional[str] = None,
747
+ limit: Optional[int] = None,
748
+ page: Optional[int] = None,
749
+ sort_by: Optional[str] = None,
750
+ sort_order: Optional[str] = None,
751
+ deserialize: Optional[bool] = True,
752
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
753
+ """Get all memories from the database as UserMemory objects.
754
+
755
+ Args:
756
+ user_id (Optional[str]): The ID of the user to filter by.
757
+ agent_id (Optional[str]): The ID of the agent to filter by.
758
+ team_id (Optional[str]): The ID of the team to filter by.
759
+ topics (Optional[List[str]]): The topics to filter by.
760
+ search_content (Optional[str]): The content to search for.
761
+ limit (Optional[int]): The maximum number of memories to return.
762
+ page (Optional[int]): The page number.
763
+ sort_by (Optional[str]): The column to sort by.
764
+ sort_order (Optional[str]): The order to sort by.
765
+ deserialize (Optional[bool]): Whether to serialize the memories. Defaults to True.
766
+
767
+
768
+ Returns:
769
+ Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
770
+ - When deserialize=True: List of UserMemory objects
771
+ - When deserialize=False: Tuple of (memory dictionaries, total count)
772
+
773
+ Raises:
774
+ Exception: If an error occurs during retrieval.
775
+ """
776
+ table = self._get_table("memories")
777
+ where = WhereClause()
778
+ if user_id is not None:
779
+ rec_id = RecordID(self._get_table("users"), user_id)
780
+ where.and_("user", rec_id)
781
+ if agent_id is not None:
782
+ rec_id = RecordID(self._get_table("agents"), agent_id)
783
+ where.and_("agent", rec_id)
784
+ if team_id is not None:
785
+ rec_id = RecordID(self._get_table("teams"), team_id)
786
+ where.and_("team", rec_id)
787
+ if topics is not None:
788
+ where.and_("topics", topics, "CONTAINSANY")
789
+ if search_content is not None:
790
+ where.and_("memory", search_content, "~")
791
+ where_clause, where_vars = where.build()
792
+
793
+ # Total count
794
+ total_count = self._count(table, where_clause, where_vars)
795
+
796
+ # Query
797
+ order_limit_start_clause = order_limit_start(sort_by, sort_order, limit, page)
798
+ query = dedent(f"""
799
+ SELECT *
800
+ FROM {table}
801
+ {where_clause}
802
+ {order_limit_start_clause}
803
+ """)
804
+ result = self._query(query, where_vars, dict)
805
+ if deserialize:
806
+ return deserialize_user_memories(result)
807
+ return [desurrealize_user_memory(x) for x in result], total_count
808
+
809
+ def get_user_memory_stats(
810
+ self,
811
+ limit: Optional[int] = None,
812
+ page: Optional[int] = None,
813
+ user_id: Optional[str] = None,
814
+ ) -> Tuple[List[Dict[str, Any]], int]:
815
+ """Get user memories stats.
816
+
817
+ Args:
818
+ limit (Optional[int]): The maximum number of user stats to return.
819
+ page (Optional[int]): The page number.
820
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
821
+
822
+ Returns:
823
+ Tuple[List[Dict[str, Any]], int]: A list of dictionaries containing user stats and total count.
824
+
825
+ Example:
826
+ (
827
+ [
828
+ {
829
+ "user_id": "123",
830
+ "total_memories": 10,
831
+ "last_memory_updated_at": 1714560000,
832
+ },
833
+ ],
834
+ total_count: 1,
835
+ )
836
+ """
837
+ memories_table_name = self._get_table("memories")
838
+ where = WhereClause()
839
+
840
+ if user_id is None:
841
+ where.and_("!!user", True, "=") # this checks that user is not falsy
842
+ else:
843
+ where.and_("user", RecordID(self._get_table("users"), user_id), "=")
844
+
845
+ where_clause, where_vars = where.build()
846
+ # Group
847
+ group_clause = "GROUP BY user"
848
+ # Order
849
+ order_limit_start_clause = order_limit_start("last_memory_updated_at", "DESC", limit, page)
850
+ # Total count
851
+ total_count = (
852
+ self._query_one(f"(SELECT user FROM {memories_table_name} GROUP BY user).map(|$x| $x.user).len()", {}, int)
853
+ or 0
854
+ )
855
+ # Query
856
+ query = dedent(f"""
857
+ SELECT
858
+ user,
859
+ count(id) AS total_memories,
860
+ time::max(updated_at) AS last_memory_updated_at
861
+ FROM {memories_table_name}
862
+ {where_clause}
863
+ {group_clause}
864
+ {order_limit_start_clause}
865
+ """)
866
+ result = self._query(query, where_vars, dict)
867
+
868
+ # deserialize dates and RecordIDs
869
+ for row in result:
870
+ row["user_id"] = row["user"].id
871
+ del row["user"]
872
+ row["last_memory_updated_at"] = row["last_memory_updated_at"].timestamp()
873
+ row["last_memory_updated_at"] = int(row["last_memory_updated_at"])
874
+
875
+ return list(result), total_count
876
+
877
+ def upsert_user_memory(
878
+ self, memory: UserMemory, deserialize: Optional[bool] = True
879
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
880
+ """Upsert a user memory in the database.
881
+
882
+ Args:
883
+ memory (UserMemory): The user memory to upsert.
884
+ deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
885
+
886
+ Returns:
887
+ Optional[Union[UserMemory, Dict[str, Any]]]:
888
+ - When deserialize=True: UserMemory object
889
+ - When deserialize=False: UserMemory dictionary
890
+
891
+ Raises:
892
+ Exception: If an error occurs during upsert.
893
+ """
894
+ table = self._get_table("memories")
895
+ user_table = self._get_table("users")
896
+ if memory.memory_id:
897
+ record = RecordID(table, memory.memory_id)
898
+ query = "UPSERT ONLY $record CONTENT $content"
899
+ result = self._query_one(
900
+ query, {"record": record, "content": serialize_user_memory(memory, table, user_table)}, dict
901
+ )
902
+ else:
903
+ query = f"CREATE ONLY {table} CONTENT $content"
904
+ result = self._query_one(query, {"content": serialize_user_memory(memory, table, user_table)}, dict)
905
+ if result is None:
906
+ return None
907
+ elif not deserialize:
908
+ return desurrealize_user_memory(result)
909
+ return deserialize_user_memory(result)
910
+
911
+ def upsert_memories(
912
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True
913
+ ) -> List[Union[UserMemory, Dict[str, Any]]]:
914
+ """
915
+ Bulk insert or update multiple memories in the database for improved performance.
916
+
917
+ Args:
918
+ memories (List[UserMemory]): The list of memories to upsert.
919
+ deserialize (Optional[bool]): Whether to deserialize the memories. Defaults to True.
920
+
921
+ Returns:
922
+ List[Union[UserMemory, Dict[str, Any]]]: List of upserted memories
923
+
924
+ Raises:
925
+ Exception: If an error occurs during bulk upsert.
926
+ """
927
+ if not memories:
928
+ return []
929
+ table = self._get_table("memories")
930
+ user_table_name = self._get_table("users")
931
+ raw: list[dict] = []
932
+ for memory in memories:
933
+ if memory.memory_id:
934
+ # UPSERT does only work for one record at a time
935
+ session_raw = self._query_one(
936
+ "UPSERT ONLY $record CONTENT $content",
937
+ {
938
+ "record": RecordID(table, memory.memory_id),
939
+ "content": serialize_user_memory(memory, table, user_table_name),
940
+ },
941
+ dict,
942
+ )
943
+ else:
944
+ session_raw = self._query_one(
945
+ f"CREATE ONLY {table} CONTENT $content",
946
+ {"content": serialize_user_memory(memory, table, user_table_name)},
947
+ dict,
948
+ )
949
+ if session_raw is not None:
950
+ raw.append(session_raw)
951
+ if raw is None or not deserialize:
952
+ return [desurrealize_user_memory(x) for x in raw]
953
+ # wrapping with list because of:
954
+ # Type "List[Session]" is not assignable to return type "List[Session | Dict[str, Any]]"
955
+ # Consider switching from "list" to "Sequence" which is covariant
956
+ return list(deserialize_user_memories(raw))
957
+
958
+ # --- Metrics ---
959
+ def get_metrics(
960
+ self,
961
+ starting_date: Optional[date] = None,
962
+ ending_date: Optional[date] = None,
963
+ ) -> Tuple[List[Dict[str, Any]], Optional[int]]:
964
+ """Get all metrics matching the given date range.
965
+
966
+ Args:
967
+ starting_date (Optional[date]): The starting date to filter metrics by.
968
+ ending_date (Optional[date]): The ending date to filter metrics by.
969
+
970
+ Returns:
971
+ Tuple[List[dict], Optional[int]]: A tuple containing the metrics and the timestamp of the latest update.
972
+
973
+ Raises:
974
+ Exception: If an error occurs during retrieval.
975
+ """
976
+ table = self._get_table("metrics")
977
+
978
+ where = WhereClause()
979
+
980
+ # starting_date - need to convert date to datetime for comparison
981
+ if starting_date is not None:
982
+ starting_datetime = datetime.combine(starting_date, datetime.min.time()).replace(tzinfo=timezone.utc)
983
+ where = where.and_("date", starting_datetime, ">=")
984
+
985
+ # ending_date - need to convert date to datetime for comparison
986
+ if ending_date is not None:
987
+ ending_datetime = datetime.combine(ending_date, datetime.min.time()).replace(tzinfo=timezone.utc)
988
+ where = where.and_("date", ending_datetime, "<=")
989
+
990
+ where_clause, where_vars = where.build()
991
+
992
+ # Query
993
+ query = dedent(f"""
994
+ SELECT *
995
+ FROM {table}
996
+ {where_clause}
997
+ ORDER BY date ASC
998
+ """)
999
+
1000
+ results = self._query(query, where_vars, dict)
1001
+
1002
+ # Get the latest updated_at from all results
1003
+ latest_update = None
1004
+ if results:
1005
+ # Find the maximum updated_at timestamp
1006
+ latest_update = max(int(r["updated_at"].timestamp()) for r in results)
1007
+
1008
+ # Transform results to match expected format
1009
+ transformed_results = []
1010
+ for r in results:
1011
+ transformed = dict(r)
1012
+
1013
+ # Convert RecordID to string
1014
+ if hasattr(transformed.get("id"), "id"):
1015
+ transformed["id"] = transformed["id"].id
1016
+ elif isinstance(transformed.get("id"), RecordID):
1017
+ transformed["id"] = str(transformed["id"].id)
1018
+
1019
+ # Convert datetime objects to Unix timestamps
1020
+ if isinstance(transformed.get("created_at"), datetime):
1021
+ transformed["created_at"] = int(transformed["created_at"].timestamp())
1022
+ if isinstance(transformed.get("updated_at"), datetime):
1023
+ transformed["updated_at"] = int(transformed["updated_at"].timestamp())
1024
+ if isinstance(transformed.get("date"), datetime):
1025
+ transformed["date"] = int(transformed["date"].timestamp())
1026
+
1027
+ transformed_results.append(transformed)
1028
+
1029
+ return transformed_results, latest_update
1030
+
1031
+ return [], latest_update
1032
+
1033
+ def calculate_metrics(self) -> Optional[List[Dict[str, Any]]]: # More specific return type
1034
+ """Calculate metrics for all dates without complete metrics.
1035
+
1036
+ Returns:
1037
+ Optional[List[Dict[str, Any]]]: The calculated metrics.
1038
+
1039
+ Raises:
1040
+ Exception: If an error occurs during metrics calculation.
1041
+ """
1042
+ try:
1043
+ table = self._get_table("metrics") # Removed create_table_if_not_found parameter
1044
+
1045
+ starting_date = get_metrics_calculation_starting_date(self.client, table, self.get_sessions)
1046
+
1047
+ if starting_date is None:
1048
+ log_info("No session data found. Won't calculate metrics.")
1049
+ return None
1050
+
1051
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
1052
+ if not dates_to_process:
1053
+ log_info("Metrics already calculated for all relevant dates.")
1054
+ return None
1055
+
1056
+ start_timestamp = datetime.combine(dates_to_process[0], datetime.min.time()).replace(tzinfo=timezone.utc)
1057
+ end_timestamp = datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time()).replace(
1058
+ tzinfo=timezone.utc
1059
+ )
1060
+
1061
+ sessions = get_all_sessions_for_metrics_calculation(
1062
+ self.client, self._get_table("sessions"), start_timestamp, end_timestamp
1063
+ )
1064
+
1065
+ all_sessions_data = fetch_all_sessions_data(
1066
+ sessions=sessions, # Added parameter name for clarity
1067
+ dates_to_process=dates_to_process,
1068
+ start_timestamp=int(start_timestamp.timestamp()), # This expects int
1069
+ )
1070
+ if not all_sessions_data:
1071
+ log_info("No new session data found. Won't calculate metrics.")
1072
+ return None
1073
+
1074
+ metrics_records = []
1075
+
1076
+ for date_to_process in dates_to_process:
1077
+ date_key = date_to_process.isoformat()
1078
+ sessions_for_date = all_sessions_data.get(date_key, {})
1079
+
1080
+ # Skip dates with no sessions
1081
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
1082
+ continue
1083
+
1084
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
1085
+ metrics_records.append(metrics_record)
1086
+
1087
+ results = [] # Initialize before the if block
1088
+ if metrics_records:
1089
+ results = bulk_upsert_metrics(self.client, table, metrics_records)
1090
+
1091
+ log_debug("Updated metrics calculations")
1092
+ return results
1093
+
1094
+ except Exception as e:
1095
+ log_error(f"Exception refreshing metrics: {e}")
1096
+ raise e
1097
+
1098
+ # --- Knowledge ---
1099
+ def clear_knowledge(self) -> None:
1100
+ """Delete all knowledge rows from the database.
1101
+
1102
+ Raises:
1103
+ Exception: If an error occurs during deletion.
1104
+ """
1105
+ table = self._get_table("knowledge")
1106
+ _ = self.client.delete(table)
1107
+
1108
+ def delete_knowledge_content(self, id: str):
1109
+ """Delete a knowledge row from the database.
1110
+
1111
+ Args:
1112
+ id (str): The ID of the knowledge row to delete.
1113
+ """
1114
+ table = self._get_table("knowledge")
1115
+ self.client.delete(RecordID(table, id))
1116
+
1117
+ def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1118
+ """Get a knowledge row from the database.
1119
+
1120
+ Args:
1121
+ id (str): The ID of the knowledge row to get.
1122
+
1123
+ Returns:
1124
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1125
+ """
1126
+ table = self._get_table("knowledge")
1127
+ record_id = RecordID(table, id)
1128
+ raw = self._query_one("SELECT * FROM ONLY $record_id", {"record_id": record_id}, dict)
1129
+ return deserialize_knowledge_row(raw) if raw else None
1130
+
1131
+ def get_knowledge_contents(
1132
+ self,
1133
+ limit: Optional[int] = None,
1134
+ page: Optional[int] = None,
1135
+ sort_by: Optional[str] = None,
1136
+ sort_order: Optional[str] = None,
1137
+ ) -> Tuple[List[KnowledgeRow], int]:
1138
+ """Get all knowledge contents from the database.
1139
+
1140
+ Args:
1141
+ limit (Optional[int]): The maximum number of knowledge contents to return.
1142
+ page (Optional[int]): The page number.
1143
+ sort_by (Optional[str]): The column to sort by.
1144
+ sort_order (Optional[str]): The order to sort by.
1145
+
1146
+ Returns:
1147
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
1148
+
1149
+ Raises:
1150
+ Exception: If an error occurs during retrieval.
1151
+ """
1152
+ table = self._get_table("knowledge")
1153
+ where = WhereClause()
1154
+ where_clause, where_vars = where.build()
1155
+
1156
+ # Total count
1157
+ total_count = self._count(table, where_clause, where_vars)
1158
+
1159
+ # Query
1160
+ order_limit_start_clause = order_limit_start(sort_by, sort_order, limit, page)
1161
+ query = dedent(f"""
1162
+ SELECT *
1163
+ FROM {table}
1164
+ {where_clause}
1165
+ {order_limit_start_clause}
1166
+ """)
1167
+ result = self._query(query, where_vars, dict)
1168
+ return [deserialize_knowledge_row(row) for row in result], total_count
1169
+
1170
+ def upsert_knowledge_content(self, knowledge_row: KnowledgeRow) -> Optional[KnowledgeRow]:
1171
+ """Upsert knowledge content in the database.
1172
+
1173
+ Args:
1174
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
1175
+
1176
+ Returns:
1177
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
1178
+ """
1179
+ knowledge_table_name = self._get_table("knowledge")
1180
+ record = RecordID(knowledge_table_name, knowledge_row.id)
1181
+ query = "UPSERT ONLY $record CONTENT $content"
1182
+ result = self._query_one(
1183
+ query, {"record": record, "content": serialize_knowledge_row(knowledge_row, knowledge_table_name)}, dict
1184
+ )
1185
+ return deserialize_knowledge_row(result) if result else None
1186
+
1187
+ # --- Evals ---
1188
+ def clear_evals(self) -> None:
1189
+ """Delete all eval rows from the database.
1190
+
1191
+ Raises:
1192
+ Exception: If an error occurs during deletion.
1193
+ """
1194
+ table = self._get_table("evals")
1195
+ _ = self.client.delete(table)
1196
+
1197
+ def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
1198
+ """Create an EvalRunRecord in the database.
1199
+
1200
+ Args:
1201
+ eval_run (EvalRunRecord): The eval run to create.
1202
+
1203
+ Returns:
1204
+ Optional[EvalRunRecord]: The created eval run, or None if the operation fails.
1205
+
1206
+ Raises:
1207
+ Exception: If an error occurs during creation.
1208
+ """
1209
+ table = self._get_table("evals")
1210
+ rec_id = RecordID(table, eval_run.run_id)
1211
+ query = "CREATE ONLY $record CONTENT $content"
1212
+ result = self._query_one(
1213
+ query, {"record": rec_id, "content": serialize_eval_run_record(eval_run, self.table_names)}, dict
1214
+ )
1215
+ return deserialize_eval_run_record(result) if result else None
1216
+
1217
+ def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1218
+ """Delete multiple eval runs from the database.
1219
+
1220
+ Args:
1221
+ eval_run_ids (List[str]): List of eval run IDs to delete.
1222
+ """
1223
+ table = self._get_table("evals")
1224
+ records = [RecordID(table, id) for id in eval_run_ids]
1225
+ _ = self.client.query(f"DELETE FROM {table} WHERE id IN $records", {"records": records})
1226
+
1227
+ def get_eval_run(
1228
+ self, eval_run_id: str, deserialize: Optional[bool] = True
1229
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1230
+ """Get an eval run from the database.
1231
+
1232
+ Args:
1233
+ eval_run_id (str): The ID of the eval run to get.
1234
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1235
+
1236
+ Returns:
1237
+ Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1238
+ - When deserialize=True: EvalRunRecord object
1239
+ - When deserialize=False: EvalRun dictionary
1240
+
1241
+ Raises:
1242
+ Exception: If an error occurs during retrieval.
1243
+ """
1244
+ table = self._get_table("evals")
1245
+ record = RecordID(table, eval_run_id)
1246
+ result = self._query_one("SELECT * FROM ONLY $record", {"record": record}, dict)
1247
+ if not result or not deserialize:
1248
+ return desurrealize_eval_run_record(result) if result is not None else None
1249
+ return deserialize_eval_run_record(result)
1250
+
1251
+ def get_eval_runs(
1252
+ self,
1253
+ limit: Optional[int] = None,
1254
+ page: Optional[int] = None,
1255
+ sort_by: Optional[str] = None,
1256
+ sort_order: Optional[str] = None,
1257
+ agent_id: Optional[str] = None,
1258
+ team_id: Optional[str] = None,
1259
+ workflow_id: Optional[str] = None,
1260
+ model_id: Optional[str] = None,
1261
+ filter_type: Optional[EvalFilterType] = None,
1262
+ eval_type: Optional[List[EvalType]] = None,
1263
+ deserialize: Optional[bool] = True,
1264
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1265
+ """Get all eval runs from the database.
1266
+
1267
+ Args:
1268
+ limit (Optional[int]): The maximum number of eval runs to return.
1269
+ page (Optional[int]): The page number to return.
1270
+ sort_by (Optional[str]): The field to sort by.
1271
+ sort_order (Optional[str]): The order to sort by.
1272
+ agent_id (Optional[str]): The ID of the agent to filter by.
1273
+ team_id (Optional[str]): The ID of the team to filter by.
1274
+ workflow_id (Optional[str]): The ID of the workflow to filter by.
1275
+ model_id (Optional[str]): The ID of the model to filter by.
1276
+ eval_type (Optional[List[EvalType]]): The type of eval to filter by.
1277
+ filter_type (Optional[EvalFilterType]): The type of filter to apply.
1278
+ deserialize (Optional[bool]): Whether to serialize the eval runs. Defaults to True.
1279
+
1280
+ Returns:
1281
+ Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1282
+ - When deserialize=True: List of EvalRunRecord objects
1283
+ - When deserialize=False: List of eval run dictionaries and the total count
1284
+
1285
+ Raises:
1286
+ Exception: If there is an error getting the eval runs.
1287
+ """
1288
+ table = self._get_table("evals")
1289
+
1290
+ where = WhereClause()
1291
+ if filter_type is not None:
1292
+ if filter_type == EvalFilterType.AGENT:
1293
+ where.and_("agent", RecordID(self._get_table("agents"), agent_id))
1294
+ elif filter_type == EvalFilterType.TEAM:
1295
+ where.and_("team", RecordID(self._get_table("teams"), team_id))
1296
+ elif filter_type == EvalFilterType.WORKFLOW:
1297
+ where.and_("workflow", RecordID(self._get_table("workflows"), workflow_id))
1298
+ if model_id is not None:
1299
+ where.and_("model_id", model_id)
1300
+ if eval_type is not None:
1301
+ where.and_("eval_type", eval_type)
1302
+ where_clause, where_vars = where.build()
1303
+
1304
+ # Order
1305
+ order_limit_start_clause = order_limit_start(sort_by, sort_order, limit, page)
1306
+
1307
+ # Total count
1308
+ total_count = self._count(table, where_clause, where_vars)
1309
+
1310
+ # Query
1311
+ query = dedent(f"""
1312
+ SELECT *
1313
+ FROM {table}
1314
+ {where_clause}
1315
+ {order_limit_start_clause}
1316
+ """)
1317
+ result = self._query(query, where_vars, dict)
1318
+
1319
+ if not deserialize:
1320
+ return list(result), total_count
1321
+ return [deserialize_eval_run_record(x) for x in result]
1322
+
1323
+ def rename_eval_run(
1324
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1325
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1326
+ """Update the name of an eval run in the database.
1327
+
1328
+ Args:
1329
+ eval_run_id (str): The ID of the eval run to update.
1330
+ name (str): The new name of the eval run.
1331
+ deserialize (Optional[bool]): Whether to serialize the eval run. Defaults to True.
1332
+
1333
+ Returns:
1334
+ Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1335
+ - When deserialize=True: EvalRunRecord object
1336
+ - When deserialize=False: EvalRun dictionary
1337
+
1338
+ Raises:
1339
+ Exception: If there is an error updating the eval run.
1340
+ """
1341
+ table = self._get_table("evals")
1342
+ vars = {"record": RecordID(table, eval_run_id), "name": name}
1343
+
1344
+ # Query
1345
+ query = dedent("""
1346
+ UPDATE ONLY $record
1347
+ SET name = $name
1348
+ """)
1349
+ raw = self._query_one(query, vars, dict)
1350
+
1351
+ if not raw or not deserialize:
1352
+ return raw
1353
+ return deserialize_eval_run_record(raw)