agno 1.8.1__py3-none-any.whl → 2.0.0__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 (590) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +19 -27
  3. agno/agent/agent.py +3143 -4170
  4. agno/api/agent.py +11 -67
  5. agno/api/api.py +5 -46
  6. agno/api/evals.py +8 -19
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -41
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +5 -21
  11. agno/api/schemas/evals.py +7 -16
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +5 -21
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +11 -7
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +11 -66
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/db/__init__.py +24 -0
  25. agno/db/base.py +245 -0
  26. agno/db/dynamo/__init__.py +3 -0
  27. agno/db/dynamo/dynamo.py +1743 -0
  28. agno/db/dynamo/schemas.py +278 -0
  29. agno/db/dynamo/utils.py +684 -0
  30. agno/db/firestore/__init__.py +3 -0
  31. agno/db/firestore/firestore.py +1432 -0
  32. agno/db/firestore/schemas.py +130 -0
  33. agno/db/firestore/utils.py +278 -0
  34. agno/db/gcs_json/__init__.py +3 -0
  35. agno/db/gcs_json/gcs_json_db.py +1001 -0
  36. agno/db/gcs_json/utils.py +194 -0
  37. agno/db/in_memory/__init__.py +3 -0
  38. agno/db/in_memory/in_memory_db.py +882 -0
  39. agno/db/in_memory/utils.py +172 -0
  40. agno/db/json/__init__.py +3 -0
  41. agno/db/json/json_db.py +1045 -0
  42. agno/db/json/utils.py +196 -0
  43. agno/db/migrations/v1_to_v2.py +162 -0
  44. agno/db/mongo/__init__.py +3 -0
  45. agno/db/mongo/mongo.py +1416 -0
  46. agno/db/mongo/schemas.py +77 -0
  47. agno/db/mongo/utils.py +204 -0
  48. agno/db/mysql/__init__.py +3 -0
  49. agno/db/mysql/mysql.py +1719 -0
  50. agno/db/mysql/schemas.py +124 -0
  51. agno/db/mysql/utils.py +297 -0
  52. agno/db/postgres/__init__.py +3 -0
  53. agno/db/postgres/postgres.py +1710 -0
  54. agno/db/postgres/schemas.py +124 -0
  55. agno/db/postgres/utils.py +280 -0
  56. agno/db/redis/__init__.py +3 -0
  57. agno/db/redis/redis.py +1367 -0
  58. agno/db/redis/schemas.py +109 -0
  59. agno/db/redis/utils.py +288 -0
  60. agno/db/schemas/__init__.py +3 -0
  61. agno/db/schemas/evals.py +33 -0
  62. agno/db/schemas/knowledge.py +40 -0
  63. agno/db/schemas/memory.py +46 -0
  64. agno/db/singlestore/__init__.py +3 -0
  65. agno/db/singlestore/schemas.py +116 -0
  66. agno/db/singlestore/singlestore.py +1712 -0
  67. agno/db/singlestore/utils.py +326 -0
  68. agno/db/sqlite/__init__.py +3 -0
  69. agno/db/sqlite/schemas.py +119 -0
  70. agno/db/sqlite/sqlite.py +1676 -0
  71. agno/db/sqlite/utils.py +268 -0
  72. agno/db/utils.py +88 -0
  73. agno/eval/__init__.py +14 -0
  74. agno/eval/accuracy.py +154 -48
  75. agno/eval/performance.py +88 -23
  76. agno/eval/reliability.py +73 -20
  77. agno/eval/utils.py +23 -13
  78. agno/integrations/discord/__init__.py +3 -0
  79. agno/{app → integrations}/discord/client.py +15 -11
  80. agno/knowledge/__init__.py +2 -2
  81. agno/{document → knowledge}/chunking/agentic.py +2 -2
  82. agno/{document → knowledge}/chunking/document.py +2 -2
  83. agno/{document → knowledge}/chunking/fixed.py +3 -3
  84. agno/{document → knowledge}/chunking/markdown.py +2 -2
  85. agno/{document → knowledge}/chunking/recursive.py +2 -2
  86. agno/{document → knowledge}/chunking/row.py +2 -2
  87. agno/knowledge/chunking/semantic.py +59 -0
  88. agno/knowledge/chunking/strategy.py +121 -0
  89. agno/knowledge/content.py +74 -0
  90. agno/knowledge/document/__init__.py +5 -0
  91. agno/{document → knowledge/document}/base.py +12 -2
  92. agno/knowledge/embedder/__init__.py +5 -0
  93. agno/{embedder → knowledge/embedder}/aws_bedrock.py +127 -1
  94. agno/{embedder → knowledge/embedder}/azure_openai.py +65 -1
  95. agno/{embedder → knowledge/embedder}/base.py +6 -0
  96. agno/{embedder → knowledge/embedder}/cohere.py +72 -1
  97. agno/{embedder → knowledge/embedder}/fastembed.py +17 -1
  98. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  99. agno/{embedder → knowledge/embedder}/google.py +74 -1
  100. agno/{embedder → knowledge/embedder}/huggingface.py +36 -2
  101. agno/{embedder → knowledge/embedder}/jina.py +48 -2
  102. agno/knowledge/embedder/langdb.py +22 -0
  103. agno/knowledge/embedder/mistral.py +139 -0
  104. agno/{embedder → knowledge/embedder}/nebius.py +1 -1
  105. agno/{embedder → knowledge/embedder}/ollama.py +54 -3
  106. agno/knowledge/embedder/openai.py +223 -0
  107. agno/{embedder → knowledge/embedder}/sentence_transformer.py +16 -1
  108. agno/{embedder → knowledge/embedder}/together.py +1 -1
  109. agno/{embedder → knowledge/embedder}/voyageai.py +49 -1
  110. agno/knowledge/knowledge.py +1551 -0
  111. agno/knowledge/reader/__init__.py +7 -0
  112. agno/{document → knowledge}/reader/arxiv_reader.py +32 -4
  113. agno/knowledge/reader/base.py +88 -0
  114. agno/{document → knowledge}/reader/csv_reader.py +47 -65
  115. agno/knowledge/reader/docx_reader.py +83 -0
  116. agno/{document → knowledge}/reader/firecrawl_reader.py +42 -21
  117. agno/{document → knowledge}/reader/json_reader.py +30 -9
  118. agno/{document → knowledge}/reader/markdown_reader.py +58 -9
  119. agno/{document → knowledge}/reader/pdf_reader.py +71 -126
  120. agno/knowledge/reader/reader_factory.py +268 -0
  121. agno/knowledge/reader/s3_reader.py +101 -0
  122. agno/{document → knowledge}/reader/text_reader.py +31 -10
  123. agno/knowledge/reader/url_reader.py +128 -0
  124. agno/knowledge/reader/web_search_reader.py +366 -0
  125. agno/{document → knowledge}/reader/website_reader.py +37 -10
  126. agno/knowledge/reader/wikipedia_reader.py +59 -0
  127. agno/knowledge/reader/youtube_reader.py +78 -0
  128. agno/knowledge/remote_content/remote_content.py +88 -0
  129. agno/{reranker → knowledge/reranker}/base.py +1 -1
  130. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  131. agno/{reranker → knowledge/reranker}/infinity.py +2 -2
  132. agno/{reranker → knowledge/reranker}/sentence_transformer.py +2 -2
  133. agno/knowledge/types.py +30 -0
  134. agno/knowledge/utils.py +169 -0
  135. agno/media.py +269 -268
  136. agno/memory/__init__.py +2 -10
  137. agno/memory/manager.py +1003 -148
  138. agno/models/aimlapi/__init__.py +2 -2
  139. agno/models/aimlapi/aimlapi.py +6 -6
  140. agno/models/anthropic/claude.py +131 -131
  141. agno/models/aws/bedrock.py +110 -182
  142. agno/models/aws/claude.py +64 -18
  143. agno/models/azure/ai_foundry.py +73 -23
  144. agno/models/base.py +346 -290
  145. agno/models/cerebras/cerebras.py +84 -27
  146. agno/models/cohere/chat.py +106 -98
  147. agno/models/google/gemini.py +105 -46
  148. agno/models/groq/groq.py +97 -35
  149. agno/models/huggingface/huggingface.py +92 -27
  150. agno/models/ibm/watsonx.py +72 -13
  151. agno/models/litellm/chat.py +85 -13
  152. agno/models/message.py +46 -151
  153. agno/models/meta/llama.py +85 -49
  154. agno/models/metrics.py +120 -0
  155. agno/models/mistral/mistral.py +90 -21
  156. agno/models/ollama/__init__.py +0 -2
  157. agno/models/ollama/chat.py +85 -47
  158. agno/models/openai/chat.py +154 -37
  159. agno/models/openai/responses.py +178 -105
  160. agno/models/perplexity/perplexity.py +26 -2
  161. agno/models/portkey/portkey.py +0 -7
  162. agno/models/response.py +15 -9
  163. agno/models/utils.py +20 -0
  164. agno/models/vercel/__init__.py +2 -2
  165. agno/models/vercel/v0.py +1 -1
  166. agno/models/vllm/__init__.py +2 -2
  167. agno/models/vllm/vllm.py +3 -3
  168. agno/models/xai/xai.py +10 -10
  169. agno/os/__init__.py +3 -0
  170. agno/os/app.py +497 -0
  171. agno/os/auth.py +47 -0
  172. agno/os/config.py +103 -0
  173. agno/os/interfaces/agui/__init__.py +3 -0
  174. agno/os/interfaces/agui/agui.py +31 -0
  175. agno/{app/agui/async_router.py → os/interfaces/agui/router.py} +16 -16
  176. agno/{app → os/interfaces}/agui/utils.py +77 -33
  177. agno/os/interfaces/base.py +21 -0
  178. agno/os/interfaces/slack/__init__.py +3 -0
  179. agno/{app/slack/async_router.py → os/interfaces/slack/router.py} +3 -5
  180. agno/os/interfaces/slack/slack.py +32 -0
  181. agno/os/interfaces/whatsapp/__init__.py +3 -0
  182. agno/{app/whatsapp/async_router.py → os/interfaces/whatsapp/router.py} +4 -7
  183. agno/os/interfaces/whatsapp/whatsapp.py +29 -0
  184. agno/os/mcp.py +235 -0
  185. agno/os/router.py +1400 -0
  186. agno/os/routers/__init__.py +3 -0
  187. agno/os/routers/evals/__init__.py +3 -0
  188. agno/os/routers/evals/evals.py +393 -0
  189. agno/os/routers/evals/schemas.py +142 -0
  190. agno/os/routers/evals/utils.py +161 -0
  191. agno/os/routers/knowledge/__init__.py +3 -0
  192. agno/os/routers/knowledge/knowledge.py +850 -0
  193. agno/os/routers/knowledge/schemas.py +118 -0
  194. agno/os/routers/memory/__init__.py +3 -0
  195. agno/os/routers/memory/memory.py +410 -0
  196. agno/os/routers/memory/schemas.py +58 -0
  197. agno/os/routers/metrics/__init__.py +3 -0
  198. agno/os/routers/metrics/metrics.py +178 -0
  199. agno/os/routers/metrics/schemas.py +47 -0
  200. agno/os/routers/session/__init__.py +3 -0
  201. agno/os/routers/session/session.py +536 -0
  202. agno/os/schema.py +945 -0
  203. agno/{app/playground → os}/settings.py +7 -15
  204. agno/os/utils.py +270 -0
  205. agno/reasoning/azure_ai_foundry.py +4 -4
  206. agno/reasoning/deepseek.py +4 -4
  207. agno/reasoning/default.py +6 -11
  208. agno/reasoning/groq.py +4 -4
  209. agno/reasoning/helpers.py +4 -6
  210. agno/reasoning/ollama.py +4 -4
  211. agno/reasoning/openai.py +4 -4
  212. agno/run/agent.py +633 -0
  213. agno/run/base.py +53 -77
  214. agno/run/cancel.py +81 -0
  215. agno/run/team.py +243 -96
  216. agno/run/workflow.py +550 -12
  217. agno/session/__init__.py +10 -0
  218. agno/session/agent.py +244 -0
  219. agno/session/summary.py +225 -0
  220. agno/session/team.py +262 -0
  221. agno/{storage/session/v2 → session}/workflow.py +47 -24
  222. agno/team/__init__.py +15 -16
  223. agno/team/team.py +3260 -4824
  224. agno/tools/agentql.py +14 -5
  225. agno/tools/airflow.py +9 -4
  226. agno/tools/api.py +7 -3
  227. agno/tools/apify.py +2 -46
  228. agno/tools/arxiv.py +8 -3
  229. agno/tools/aws_lambda.py +7 -5
  230. agno/tools/aws_ses.py +7 -1
  231. agno/tools/baidusearch.py +4 -1
  232. agno/tools/bitbucket.py +4 -4
  233. agno/tools/brandfetch.py +14 -11
  234. agno/tools/bravesearch.py +4 -1
  235. agno/tools/brightdata.py +43 -23
  236. agno/tools/browserbase.py +13 -4
  237. agno/tools/calcom.py +12 -10
  238. agno/tools/calculator.py +10 -27
  239. agno/tools/cartesia.py +20 -17
  240. agno/tools/{clickup_tool.py → clickup.py} +12 -25
  241. agno/tools/confluence.py +8 -8
  242. agno/tools/crawl4ai.py +7 -1
  243. agno/tools/csv_toolkit.py +9 -8
  244. agno/tools/dalle.py +22 -12
  245. agno/tools/daytona.py +13 -16
  246. agno/tools/decorator.py +6 -3
  247. agno/tools/desi_vocal.py +17 -8
  248. agno/tools/discord.py +11 -8
  249. agno/tools/docker.py +30 -42
  250. agno/tools/duckdb.py +34 -53
  251. agno/tools/duckduckgo.py +8 -7
  252. agno/tools/e2b.py +62 -62
  253. agno/tools/eleven_labs.py +36 -29
  254. agno/tools/email.py +4 -1
  255. agno/tools/evm.py +7 -1
  256. agno/tools/exa.py +19 -14
  257. agno/tools/fal.py +30 -30
  258. agno/tools/file.py +9 -8
  259. agno/tools/financial_datasets.py +25 -44
  260. agno/tools/firecrawl.py +22 -22
  261. agno/tools/function.py +127 -18
  262. agno/tools/giphy.py +23 -11
  263. agno/tools/github.py +48 -126
  264. agno/tools/gmail.py +45 -61
  265. agno/tools/google_bigquery.py +7 -6
  266. agno/tools/google_maps.py +11 -26
  267. agno/tools/googlesearch.py +7 -2
  268. agno/tools/googlesheets.py +21 -17
  269. agno/tools/hackernews.py +9 -5
  270. agno/tools/jina.py +5 -4
  271. agno/tools/jira.py +18 -9
  272. agno/tools/knowledge.py +31 -32
  273. agno/tools/linear.py +19 -34
  274. agno/tools/linkup.py +5 -1
  275. agno/tools/local_file_system.py +8 -5
  276. agno/tools/lumalab.py +32 -20
  277. agno/tools/mcp.py +1 -2
  278. agno/tools/mem0.py +18 -12
  279. agno/tools/memori.py +14 -10
  280. agno/tools/mlx_transcribe.py +3 -2
  281. agno/tools/models/azure_openai.py +33 -15
  282. agno/tools/models/gemini.py +59 -32
  283. agno/tools/models/groq.py +30 -23
  284. agno/tools/models/nebius.py +28 -12
  285. agno/tools/models_labs.py +40 -16
  286. agno/tools/moviepy_video.py +7 -6
  287. agno/tools/neo4j.py +10 -8
  288. agno/tools/newspaper.py +7 -2
  289. agno/tools/newspaper4k.py +8 -3
  290. agno/tools/openai.py +58 -32
  291. agno/tools/openbb.py +12 -11
  292. agno/tools/opencv.py +63 -47
  293. agno/tools/openweather.py +14 -12
  294. agno/tools/pandas.py +11 -3
  295. agno/tools/postgres.py +4 -12
  296. agno/tools/pubmed.py +4 -1
  297. agno/tools/python.py +9 -22
  298. agno/tools/reasoning.py +35 -27
  299. agno/tools/reddit.py +11 -26
  300. agno/tools/replicate.py +55 -42
  301. agno/tools/resend.py +4 -1
  302. agno/tools/scrapegraph.py +15 -14
  303. agno/tools/searxng.py +10 -23
  304. agno/tools/serpapi.py +6 -3
  305. agno/tools/serper.py +13 -4
  306. agno/tools/shell.py +9 -2
  307. agno/tools/slack.py +12 -11
  308. agno/tools/sleep.py +3 -2
  309. agno/tools/spider.py +24 -4
  310. agno/tools/sql.py +7 -6
  311. agno/tools/tavily.py +6 -4
  312. agno/tools/telegram.py +12 -4
  313. agno/tools/todoist.py +11 -31
  314. agno/tools/toolkit.py +1 -1
  315. agno/tools/trafilatura.py +22 -6
  316. agno/tools/trello.py +9 -22
  317. agno/tools/twilio.py +10 -3
  318. agno/tools/user_control_flow.py +6 -1
  319. agno/tools/valyu.py +34 -5
  320. agno/tools/visualization.py +19 -28
  321. agno/tools/webbrowser.py +4 -3
  322. agno/tools/webex.py +11 -7
  323. agno/tools/website.py +15 -46
  324. agno/tools/webtools.py +12 -4
  325. agno/tools/whatsapp.py +5 -9
  326. agno/tools/wikipedia.py +20 -13
  327. agno/tools/x.py +14 -13
  328. agno/tools/yfinance.py +13 -40
  329. agno/tools/youtube.py +26 -20
  330. agno/tools/zendesk.py +7 -2
  331. agno/tools/zep.py +10 -7
  332. agno/tools/zoom.py +10 -9
  333. agno/utils/common.py +1 -19
  334. agno/utils/events.py +100 -123
  335. agno/utils/gemini.py +32 -2
  336. agno/utils/knowledge.py +29 -0
  337. agno/utils/log.py +54 -4
  338. agno/utils/mcp.py +68 -10
  339. agno/utils/media.py +39 -0
  340. agno/utils/message.py +12 -1
  341. agno/utils/models/aws_claude.py +1 -1
  342. agno/utils/models/claude.py +47 -4
  343. agno/utils/models/cohere.py +1 -1
  344. agno/utils/models/mistral.py +8 -7
  345. agno/utils/models/schema_utils.py +3 -3
  346. agno/utils/models/watsonx.py +1 -1
  347. agno/utils/openai.py +1 -1
  348. agno/utils/pprint.py +33 -32
  349. agno/utils/print_response/agent.py +779 -0
  350. agno/utils/print_response/team.py +1669 -0
  351. agno/utils/print_response/workflow.py +1451 -0
  352. agno/utils/prompts.py +14 -14
  353. agno/utils/reasoning.py +87 -0
  354. agno/utils/response.py +42 -42
  355. agno/utils/streamlit.py +481 -0
  356. agno/utils/string.py +8 -22
  357. agno/utils/team.py +50 -0
  358. agno/utils/timer.py +2 -2
  359. agno/vectordb/base.py +33 -21
  360. agno/vectordb/cassandra/cassandra.py +287 -23
  361. agno/vectordb/chroma/chromadb.py +482 -59
  362. agno/vectordb/clickhouse/clickhousedb.py +270 -63
  363. agno/vectordb/couchbase/couchbase.py +309 -29
  364. agno/vectordb/lancedb/lance_db.py +360 -21
  365. agno/vectordb/langchaindb/__init__.py +5 -0
  366. agno/vectordb/langchaindb/langchaindb.py +145 -0
  367. agno/vectordb/lightrag/__init__.py +5 -0
  368. agno/vectordb/lightrag/lightrag.py +374 -0
  369. agno/vectordb/llamaindex/llamaindexdb.py +127 -0
  370. agno/vectordb/milvus/milvus.py +242 -32
  371. agno/vectordb/mongodb/mongodb.py +200 -24
  372. agno/vectordb/pgvector/pgvector.py +319 -37
  373. agno/vectordb/pineconedb/pineconedb.py +221 -27
  374. agno/vectordb/qdrant/qdrant.py +334 -14
  375. agno/vectordb/singlestore/singlestore.py +286 -29
  376. agno/vectordb/surrealdb/surrealdb.py +187 -7
  377. agno/vectordb/upstashdb/upstashdb.py +342 -26
  378. agno/vectordb/weaviate/weaviate.py +227 -165
  379. agno/workflow/__init__.py +17 -13
  380. agno/workflow/{v2/condition.py → condition.py} +135 -32
  381. agno/workflow/{v2/loop.py → loop.py} +115 -28
  382. agno/workflow/{v2/parallel.py → parallel.py} +138 -108
  383. agno/workflow/{v2/router.py → router.py} +133 -32
  384. agno/workflow/{v2/step.py → step.py} +207 -49
  385. agno/workflow/{v2/steps.py → steps.py} +147 -66
  386. agno/workflow/types.py +482 -0
  387. agno/workflow/workflow.py +2410 -696
  388. agno-2.0.0.dist-info/METADATA +494 -0
  389. agno-2.0.0.dist-info/RECORD +515 -0
  390. agno-2.0.0.dist-info/licenses/LICENSE +201 -0
  391. agno/agent/metrics.py +0 -107
  392. agno/api/app.py +0 -35
  393. agno/api/playground.py +0 -92
  394. agno/api/schemas/app.py +0 -12
  395. agno/api/schemas/playground.py +0 -22
  396. agno/api/schemas/user.py +0 -35
  397. agno/api/schemas/workspace.py +0 -46
  398. agno/api/user.py +0 -160
  399. agno/api/workflows.py +0 -33
  400. agno/api/workspace.py +0 -175
  401. agno/app/agui/__init__.py +0 -3
  402. agno/app/agui/app.py +0 -17
  403. agno/app/agui/sync_router.py +0 -120
  404. agno/app/base.py +0 -186
  405. agno/app/discord/__init__.py +0 -3
  406. agno/app/fastapi/__init__.py +0 -3
  407. agno/app/fastapi/app.py +0 -107
  408. agno/app/fastapi/async_router.py +0 -457
  409. agno/app/fastapi/sync_router.py +0 -448
  410. agno/app/playground/app.py +0 -228
  411. agno/app/playground/async_router.py +0 -1050
  412. agno/app/playground/deploy.py +0 -249
  413. agno/app/playground/operator.py +0 -183
  414. agno/app/playground/schemas.py +0 -220
  415. agno/app/playground/serve.py +0 -55
  416. agno/app/playground/sync_router.py +0 -1042
  417. agno/app/playground/utils.py +0 -46
  418. agno/app/settings.py +0 -15
  419. agno/app/slack/__init__.py +0 -3
  420. agno/app/slack/app.py +0 -19
  421. agno/app/slack/sync_router.py +0 -92
  422. agno/app/utils.py +0 -54
  423. agno/app/whatsapp/__init__.py +0 -3
  424. agno/app/whatsapp/app.py +0 -15
  425. agno/app/whatsapp/sync_router.py +0 -197
  426. agno/cli/auth_server.py +0 -249
  427. agno/cli/config.py +0 -274
  428. agno/cli/console.py +0 -88
  429. agno/cli/credentials.py +0 -23
  430. agno/cli/entrypoint.py +0 -571
  431. agno/cli/operator.py +0 -357
  432. agno/cli/settings.py +0 -96
  433. agno/cli/ws/ws_cli.py +0 -817
  434. agno/constants.py +0 -13
  435. agno/document/__init__.py +0 -5
  436. agno/document/chunking/semantic.py +0 -45
  437. agno/document/chunking/strategy.py +0 -31
  438. agno/document/reader/__init__.py +0 -5
  439. agno/document/reader/base.py +0 -47
  440. agno/document/reader/docx_reader.py +0 -60
  441. agno/document/reader/gcs/pdf_reader.py +0 -44
  442. agno/document/reader/s3/pdf_reader.py +0 -59
  443. agno/document/reader/s3/text_reader.py +0 -63
  444. agno/document/reader/url_reader.py +0 -59
  445. agno/document/reader/youtube_reader.py +0 -58
  446. agno/embedder/__init__.py +0 -5
  447. agno/embedder/langdb.py +0 -80
  448. agno/embedder/mistral.py +0 -82
  449. agno/embedder/openai.py +0 -78
  450. agno/file/__init__.py +0 -5
  451. agno/file/file.py +0 -16
  452. agno/file/local/csv.py +0 -32
  453. agno/file/local/txt.py +0 -19
  454. agno/infra/app.py +0 -240
  455. agno/infra/base.py +0 -144
  456. agno/infra/context.py +0 -20
  457. agno/infra/db_app.py +0 -52
  458. agno/infra/resource.py +0 -205
  459. agno/infra/resources.py +0 -55
  460. agno/knowledge/agent.py +0 -702
  461. agno/knowledge/arxiv.py +0 -33
  462. agno/knowledge/combined.py +0 -36
  463. agno/knowledge/csv.py +0 -144
  464. agno/knowledge/csv_url.py +0 -124
  465. agno/knowledge/document.py +0 -223
  466. agno/knowledge/docx.py +0 -137
  467. agno/knowledge/firecrawl.py +0 -34
  468. agno/knowledge/gcs/__init__.py +0 -0
  469. agno/knowledge/gcs/base.py +0 -39
  470. agno/knowledge/gcs/pdf.py +0 -125
  471. agno/knowledge/json.py +0 -137
  472. agno/knowledge/langchain.py +0 -71
  473. agno/knowledge/light_rag.py +0 -273
  474. agno/knowledge/llamaindex.py +0 -66
  475. agno/knowledge/markdown.py +0 -154
  476. agno/knowledge/pdf.py +0 -164
  477. agno/knowledge/pdf_bytes.py +0 -42
  478. agno/knowledge/pdf_url.py +0 -148
  479. agno/knowledge/s3/__init__.py +0 -0
  480. agno/knowledge/s3/base.py +0 -64
  481. agno/knowledge/s3/pdf.py +0 -33
  482. agno/knowledge/s3/text.py +0 -34
  483. agno/knowledge/text.py +0 -141
  484. agno/knowledge/url.py +0 -46
  485. agno/knowledge/website.py +0 -179
  486. agno/knowledge/wikipedia.py +0 -32
  487. agno/knowledge/youtube.py +0 -35
  488. agno/memory/agent.py +0 -423
  489. agno/memory/classifier.py +0 -104
  490. agno/memory/db/__init__.py +0 -5
  491. agno/memory/db/base.py +0 -42
  492. agno/memory/db/mongodb.py +0 -189
  493. agno/memory/db/postgres.py +0 -203
  494. agno/memory/db/sqlite.py +0 -193
  495. agno/memory/memory.py +0 -22
  496. agno/memory/row.py +0 -36
  497. agno/memory/summarizer.py +0 -201
  498. agno/memory/summary.py +0 -19
  499. agno/memory/team.py +0 -415
  500. agno/memory/v2/__init__.py +0 -2
  501. agno/memory/v2/db/__init__.py +0 -1
  502. agno/memory/v2/db/base.py +0 -42
  503. agno/memory/v2/db/firestore.py +0 -339
  504. agno/memory/v2/db/mongodb.py +0 -196
  505. agno/memory/v2/db/postgres.py +0 -214
  506. agno/memory/v2/db/redis.py +0 -187
  507. agno/memory/v2/db/schema.py +0 -54
  508. agno/memory/v2/db/sqlite.py +0 -209
  509. agno/memory/v2/manager.py +0 -437
  510. agno/memory/v2/memory.py +0 -1097
  511. agno/memory/v2/schema.py +0 -55
  512. agno/memory/v2/summarizer.py +0 -215
  513. agno/memory/workflow.py +0 -38
  514. agno/models/ollama/tools.py +0 -430
  515. agno/models/qwen/__init__.py +0 -5
  516. agno/playground/__init__.py +0 -10
  517. agno/playground/deploy.py +0 -3
  518. agno/playground/playground.py +0 -3
  519. agno/playground/serve.py +0 -3
  520. agno/playground/settings.py +0 -3
  521. agno/reranker/__init__.py +0 -0
  522. agno/run/response.py +0 -467
  523. agno/run/v2/__init__.py +0 -0
  524. agno/run/v2/workflow.py +0 -567
  525. agno/storage/__init__.py +0 -0
  526. agno/storage/agent/__init__.py +0 -0
  527. agno/storage/agent/dynamodb.py +0 -1
  528. agno/storage/agent/json.py +0 -1
  529. agno/storage/agent/mongodb.py +0 -1
  530. agno/storage/agent/postgres.py +0 -1
  531. agno/storage/agent/singlestore.py +0 -1
  532. agno/storage/agent/sqlite.py +0 -1
  533. agno/storage/agent/yaml.py +0 -1
  534. agno/storage/base.py +0 -60
  535. agno/storage/dynamodb.py +0 -673
  536. agno/storage/firestore.py +0 -297
  537. agno/storage/gcs_json.py +0 -261
  538. agno/storage/in_memory.py +0 -234
  539. agno/storage/json.py +0 -237
  540. agno/storage/mongodb.py +0 -328
  541. agno/storage/mysql.py +0 -685
  542. agno/storage/postgres.py +0 -682
  543. agno/storage/redis.py +0 -336
  544. agno/storage/session/__init__.py +0 -16
  545. agno/storage/session/agent.py +0 -64
  546. agno/storage/session/team.py +0 -63
  547. agno/storage/session/v2/__init__.py +0 -5
  548. agno/storage/session/workflow.py +0 -61
  549. agno/storage/singlestore.py +0 -606
  550. agno/storage/sqlite.py +0 -646
  551. agno/storage/workflow/__init__.py +0 -0
  552. agno/storage/workflow/mongodb.py +0 -1
  553. agno/storage/workflow/postgres.py +0 -1
  554. agno/storage/workflow/sqlite.py +0 -1
  555. agno/storage/yaml.py +0 -241
  556. agno/tools/thinking.py +0 -73
  557. agno/utils/defaults.py +0 -57
  558. agno/utils/filesystem.py +0 -39
  559. agno/utils/git.py +0 -52
  560. agno/utils/json_io.py +0 -30
  561. agno/utils/load_env.py +0 -19
  562. agno/utils/py_io.py +0 -19
  563. agno/utils/pyproject.py +0 -18
  564. agno/utils/resource_filter.py +0 -31
  565. agno/workflow/v2/__init__.py +0 -21
  566. agno/workflow/v2/types.py +0 -357
  567. agno/workflow/v2/workflow.py +0 -3312
  568. agno/workspace/__init__.py +0 -0
  569. agno/workspace/config.py +0 -325
  570. agno/workspace/enums.py +0 -6
  571. agno/workspace/helpers.py +0 -52
  572. agno/workspace/operator.py +0 -757
  573. agno/workspace/settings.py +0 -158
  574. agno-1.8.1.dist-info/METADATA +0 -982
  575. agno-1.8.1.dist-info/RECORD +0 -566
  576. agno-1.8.1.dist-info/entry_points.txt +0 -3
  577. agno-1.8.1.dist-info/licenses/LICENSE +0 -375
  578. /agno/{app → db/migrations}/__init__.py +0 -0
  579. /agno/{app/playground/__init__.py → db/schemas/metrics.py} +0 -0
  580. /agno/{cli → integrations}/__init__.py +0 -0
  581. /agno/{cli/ws → knowledge/chunking}/__init__.py +0 -0
  582. /agno/{document/chunking → knowledge/remote_content}/__init__.py +0 -0
  583. /agno/{document/reader/gcs → knowledge/reranker}/__init__.py +0 -0
  584. /agno/{document/reader/s3 → os/interfaces}/__init__.py +0 -0
  585. /agno/{app → os/interfaces}/slack/security.py +0 -0
  586. /agno/{app → os/interfaces}/whatsapp/security.py +0 -0
  587. /agno/{file/local → utils/print_response}/__init__.py +0 -0
  588. /agno/{infra → vectordb/llamaindex}/__init__.py +0 -0
  589. {agno-1.8.1.dist-info → agno-2.0.0.dist-info}/WHEEL +0 -0
  590. {agno-1.8.1.dist-info → agno-2.0.0.dist-info}/top_level.txt +0 -0
agno/db/redis/redis.py ADDED
@@ -0,0 +1,1367 @@
1
+ import time
2
+ from datetime import date, datetime, timedelta, timezone
3
+ from typing import Any, Dict, List, Optional, Tuple, Union
4
+ from uuid import uuid4
5
+
6
+ from agno.db.base import BaseDb, SessionType
7
+ from agno.db.redis.utils import (
8
+ apply_filters,
9
+ apply_pagination,
10
+ apply_sorting,
11
+ calculate_date_metrics,
12
+ create_index_entries,
13
+ deserialize_data,
14
+ fetch_all_sessions_data,
15
+ generate_redis_key,
16
+ get_all_keys_for_table,
17
+ get_dates_to_calculate_metrics_for,
18
+ remove_index_entries,
19
+ serialize_data,
20
+ )
21
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
22
+ from agno.db.schemas.knowledge import KnowledgeRow
23
+ from agno.db.schemas.memory import UserMemory
24
+ from agno.session import AgentSession, Session, TeamSession, WorkflowSession
25
+ from agno.utils.log import log_debug, log_error, log_info
26
+
27
+ try:
28
+ from redis import Redis
29
+ except ImportError:
30
+ raise ImportError("`redis` not installed. Please install it using `pip install redis`")
31
+
32
+
33
+ class RedisDb(BaseDb):
34
+ def __init__(
35
+ self,
36
+ id: Optional[str] = None,
37
+ redis_client: Optional[Redis] = None,
38
+ db_url: Optional[str] = None,
39
+ db_prefix: str = "agno",
40
+ expire: Optional[int] = None,
41
+ session_table: Optional[str] = None,
42
+ memory_table: Optional[str] = None,
43
+ metrics_table: Optional[str] = None,
44
+ eval_table: Optional[str] = None,
45
+ knowledge_table: Optional[str] = None,
46
+ ):
47
+ """
48
+ Interface for interacting with a Redis database.
49
+
50
+ The following order is used to determine the database connection:
51
+ 1. Use the redis_client if provided
52
+ 2. Use the db_url
53
+ 3. Raise an error if neither is provided
54
+
55
+ Args:
56
+ id (Optional[str]): The ID of the database.
57
+ redis_client (Optional[Redis]): Redis client instance to use. If not provided a new client will be created.
58
+ db_url (Optional[str]): Redis connection URL (e.g., "redis://localhost:6379/0" or "rediss://user:pass@host:port/db")
59
+ db_prefix (str): Prefix for all Redis keys
60
+ expire (Optional[int]): TTL for Redis keys in seconds
61
+ session_table (Optional[str]): Name of the table to store sessions
62
+ memory_table (Optional[str]): Name of the table to store memories
63
+ metrics_table (Optional[str]): Name of the table to store metrics
64
+ eval_table (Optional[str]): Name of the table to store evaluation runs
65
+ knowledge_table (Optional[str]): Name of the table to store knowledge documents
66
+
67
+ Raises:
68
+ ValueError: If neither redis_client nor db_url is provided.
69
+ """
70
+ super().__init__(
71
+ id=id,
72
+ session_table=session_table,
73
+ memory_table=memory_table,
74
+ metrics_table=metrics_table,
75
+ eval_table=eval_table,
76
+ knowledge_table=knowledge_table,
77
+ )
78
+
79
+ self.db_prefix = db_prefix
80
+ self.expire = expire
81
+
82
+ if redis_client is not None:
83
+ self.redis_client = redis_client
84
+ elif db_url is not None:
85
+ self.redis_client = Redis.from_url(db_url, decode_responses=True)
86
+ else:
87
+ raise ValueError("One of redis_client or db_url must be provided")
88
+
89
+ # -- DB methods --
90
+
91
+ def _get_table_name(self, table_type: str) -> str:
92
+ """Get the active table name for the given table type."""
93
+ if table_type == "sessions":
94
+ return self.session_table_name
95
+
96
+ elif table_type == "memories":
97
+ return self.memory_table_name
98
+
99
+ elif table_type == "metrics":
100
+ return self.metrics_table_name
101
+
102
+ elif table_type == "evals":
103
+ return self.eval_table_name
104
+
105
+ elif table_type == "knowledge":
106
+ return self.knowledge_table_name
107
+
108
+ else:
109
+ raise ValueError(f"Unknown table type: {table_type}")
110
+
111
+ def _store_record(
112
+ self, table_type: str, record_id: str, data: Dict[str, Any], index_fields: Optional[List[str]] = None
113
+ ) -> bool:
114
+ """Generic method to store a record in Redis, considering optional indexing.
115
+
116
+ Args:
117
+ table_type (str): The type of table to store the record in.
118
+ record_id (str): The ID of the record to store.
119
+ data (Dict[str, Any]): The data to store in the record.
120
+ index_fields (Optional[List[str]]): The fields to index the record by.
121
+
122
+ Returns:
123
+ bool: True if the record was stored successfully, False otherwise.
124
+ """
125
+ try:
126
+ key = generate_redis_key(prefix=self.db_prefix, table_type=table_type, key_id=record_id)
127
+ serialized_data = serialize_data(data)
128
+
129
+ self.redis_client.set(key, serialized_data, ex=self.expire)
130
+
131
+ if index_fields:
132
+ create_index_entries(
133
+ redis_client=self.redis_client,
134
+ prefix=self.db_prefix,
135
+ table_type=table_type,
136
+ record_id=record_id,
137
+ record_data=data,
138
+ index_fields=index_fields,
139
+ )
140
+
141
+ return True
142
+
143
+ except Exception as e:
144
+ log_error(f"Error storing Redis record: {e}")
145
+ return False
146
+
147
+ def _get_record(self, table_type: str, record_id: str) -> Optional[Dict[str, Any]]:
148
+ """Generic method to get a record from Redis.
149
+
150
+ Args:
151
+ table_type (str): The type of table to get the record from.
152
+ record_id (str): The ID of the record to get.
153
+
154
+ Returns:
155
+ Optional[Dict[str, Any]]: The record data if found, None otherwise.
156
+ """
157
+ try:
158
+ key = generate_redis_key(prefix=self.db_prefix, table_type=table_type, key_id=record_id)
159
+
160
+ data = self.redis_client.get(key)
161
+ if data is None:
162
+ return None
163
+
164
+ return deserialize_data(data) # type: ignore
165
+
166
+ except Exception as e:
167
+ log_error(f"Error getting record {record_id}: {e}")
168
+ return None
169
+
170
+ def _delete_record(self, table_type: str, record_id: str, index_fields: Optional[List[str]] = None) -> bool:
171
+ """Generic method to delete a record from Redis.
172
+
173
+ Args:
174
+ table_type (str): The type of table to delete the record from.
175
+ record_id (str): The ID of the record to delete.
176
+ index_fields (Optional[List[str]]): The fields to index the record by.
177
+
178
+ Returns:
179
+ bool: True if the record was deleted successfully, False otherwise.
180
+
181
+ Raises:
182
+ Exception: If any error occurs while deleting the record.
183
+ """
184
+ try:
185
+ # Handle index deletion first
186
+ if index_fields:
187
+ record_data = self._get_record(table_type, record_id)
188
+ if record_data:
189
+ remove_index_entries(
190
+ redis_client=self.redis_client,
191
+ prefix=self.db_prefix,
192
+ table_type=table_type,
193
+ record_id=record_id,
194
+ record_data=record_data,
195
+ index_fields=index_fields,
196
+ )
197
+
198
+ key = generate_redis_key(prefix=self.db_prefix, table_type=table_type, key_id=record_id)
199
+ result = self.redis_client.delete(key)
200
+ if result is None or result == 0:
201
+ return False
202
+
203
+ return True
204
+
205
+ except Exception as e:
206
+ log_error(f"Error deleting record {record_id}: {e}")
207
+ return False
208
+
209
+ def _get_all_records(self, table_type: str) -> List[Dict[str, Any]]:
210
+ """Generic method to get all records for a table type.
211
+
212
+ Args:
213
+ table_type (str): The type of table to get the records from.
214
+
215
+ Returns:
216
+ List[Dict[str, Any]]: The records data if found, None otherwise.
217
+
218
+ Raises:
219
+ Exception: If any error occurs while getting the records.
220
+ """
221
+ try:
222
+ keys = get_all_keys_for_table(redis_client=self.redis_client, prefix=self.db_prefix, table_type=table_type)
223
+
224
+ records = []
225
+ for key in keys:
226
+ data = self.redis_client.get(key)
227
+ if data:
228
+ records.append(deserialize_data(data)) # type: ignore
229
+
230
+ return records
231
+
232
+ except Exception as e:
233
+ log_error(f"Error getting all records for {table_type}: {e}")
234
+ return []
235
+
236
+ # -- Session methods --
237
+
238
+ def delete_session(self, session_id: str) -> bool:
239
+ """Delete a session from Redis.
240
+
241
+ Args:
242
+ session_id (str): The ID of the session to delete.
243
+
244
+ Raises:
245
+ Exception: If any error occurs while deleting the session.
246
+ """
247
+ try:
248
+ if self._delete_record(
249
+ table_type="sessions",
250
+ record_id=session_id,
251
+ index_fields=["user_id", "agent_id", "team_id", "workflow_id", "session_type"],
252
+ ):
253
+ log_debug(f"Successfully deleted session: {session_id}")
254
+ return True
255
+ else:
256
+ log_debug(f"No session found to delete with session_id: {session_id}")
257
+ return False
258
+
259
+ except Exception as e:
260
+ log_error(f"Error deleting session: {e}")
261
+ return False
262
+
263
+ def delete_sessions(self, session_ids: List[str]) -> None:
264
+ """Delete multiple sessions from Redis.
265
+
266
+ Args:
267
+ session_ids (List[str]): The IDs of the sessions to delete.
268
+
269
+ Raises:
270
+ Exception: If any error occurs while deleting the sessions.
271
+ """
272
+ try:
273
+ deleted_count = 0
274
+ for session_id in session_ids:
275
+ if self._delete_record(
276
+ "sessions",
277
+ session_id,
278
+ index_fields=["user_id", "agent_id", "team_id", "workflow_id", "session_type"],
279
+ ):
280
+ deleted_count += 1
281
+ log_debug(f"Successfully deleted {deleted_count} sessions")
282
+
283
+ except Exception as e:
284
+ log_error(f"Error deleting sessions: {e}")
285
+
286
+ def get_session(
287
+ self,
288
+ session_id: str,
289
+ session_type: SessionType,
290
+ user_id: Optional[str] = None,
291
+ deserialize: Optional[bool] = True,
292
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
293
+ """Read a session from Redis.
294
+
295
+ Args:
296
+ session_id (str): The ID of the session to get.
297
+ user_id (Optional[str]): The ID of the user to filter by.
298
+ session_type (Optional[SessionType]): The type of session to filter by.
299
+
300
+ Returns:
301
+ Optional[Union[AgentSession, TeamSession, WorkflowSession]]: The session if found, None otherwise.
302
+
303
+ Raises:
304
+ Exception: If any error occurs while getting the session.
305
+ """
306
+ try:
307
+ session = self._get_record("sessions", session_id)
308
+ if session is None:
309
+ return None
310
+
311
+ # Apply filters
312
+ if user_id is not None and session.get("user_id") != user_id:
313
+ return None
314
+ if session_type is not None and session.get("session_type") != session_type:
315
+ return None
316
+
317
+ if not deserialize:
318
+ return session
319
+
320
+ if session_type == SessionType.AGENT.value:
321
+ return AgentSession.from_dict(session)
322
+ elif session_type == SessionType.TEAM.value:
323
+ return TeamSession.from_dict(session)
324
+ elif session_type == SessionType.WORKFLOW.value:
325
+ return WorkflowSession.from_dict(session)
326
+ else:
327
+ raise ValueError(f"Invalid session type: {session_type}")
328
+
329
+ except Exception as e:
330
+ log_error(f"Exception reading session: {e}")
331
+ return None
332
+
333
+ # TODO: optimizable
334
+ def get_sessions(
335
+ self,
336
+ session_type: Optional[SessionType] = None,
337
+ user_id: Optional[str] = None,
338
+ component_id: Optional[str] = None,
339
+ session_name: Optional[str] = None,
340
+ start_timestamp: Optional[int] = None,
341
+ end_timestamp: Optional[int] = None,
342
+ limit: Optional[int] = None,
343
+ page: Optional[int] = None,
344
+ sort_by: Optional[str] = None,
345
+ sort_order: Optional[str] = None,
346
+ deserialize: Optional[bool] = True,
347
+ create_index_if_not_found: Optional[bool] = True,
348
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
349
+ """Get all sessions matching the given filters.
350
+
351
+ Args:
352
+ session_type (Optional[SessionType]): The type of session to filter by.
353
+ user_id (Optional[str]): The ID of the user to filter by.
354
+ component_id (Optional[str]): The ID of the component to filter by.
355
+ session_name (Optional[str]): The name of the session to filter by.
356
+ limit (Optional[int]): The maximum number of sessions to return.
357
+ page (Optional[int]): The page number to return.
358
+ sort_by (Optional[str]): The field to sort by.
359
+ sort_order (Optional[str]): The order to sort by.
360
+
361
+ Returns:
362
+ List[Union[AgentSession, TeamSession, WorkflowSession]]: The list of sessions.
363
+ """
364
+ try:
365
+ all_sessions = self._get_all_records("sessions")
366
+
367
+ conditions: Dict[str, Any] = {}
368
+ if session_type is not None:
369
+ conditions["session_type"] = session_type
370
+ if user_id is not None:
371
+ conditions["user_id"] = user_id
372
+
373
+ filtered_sessions = apply_filters(records=all_sessions, conditions=conditions)
374
+
375
+ if component_id is not None:
376
+ if session_type == SessionType.AGENT:
377
+ filtered_sessions = [s for s in filtered_sessions if s.get("agent_id") == component_id]
378
+ elif session_type == SessionType.TEAM:
379
+ filtered_sessions = [s for s in filtered_sessions if s.get("team_id") == component_id]
380
+ elif session_type == SessionType.WORKFLOW:
381
+ filtered_sessions = [s for s in filtered_sessions if s.get("workflow_id") == component_id]
382
+ if start_timestamp is not None:
383
+ filtered_sessions = [s for s in filtered_sessions if s.get("created_at", 0) >= start_timestamp]
384
+ if end_timestamp is not None:
385
+ filtered_sessions = [s for s in filtered_sessions if s.get("created_at", 0) <= end_timestamp]
386
+
387
+ if session_name is not None:
388
+ filtered_sessions = [
389
+ s
390
+ for s in filtered_sessions
391
+ if session_name.lower() in s.get("session_data", {}).get("session_name", "").lower()
392
+ ]
393
+
394
+ sorted_sessions = apply_sorting(records=filtered_sessions, sort_by=sort_by, sort_order=sort_order)
395
+ sessions = apply_pagination(records=sorted_sessions, limit=limit, page=page)
396
+ sessions = [record for record in sessions]
397
+
398
+ if not deserialize:
399
+ return sessions, len(filtered_sessions)
400
+
401
+ if session_type == SessionType.AGENT:
402
+ return [AgentSession.from_dict(record) for record in sessions] # type: ignore
403
+ elif session_type == SessionType.TEAM:
404
+ return [TeamSession.from_dict(record) for record in sessions] # type: ignore
405
+ elif session_type == SessionType.WORKFLOW:
406
+ return [WorkflowSession.from_dict(record) for record in sessions] # type: ignore
407
+ else:
408
+ raise ValueError(f"Invalid session type: {session_type}")
409
+
410
+ except Exception as e:
411
+ log_error(f"Exception reading sessions: {e}")
412
+ return [], 0
413
+
414
+ def rename_session(
415
+ self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
416
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
417
+ """Rename a session in Redis.
418
+
419
+ Args:
420
+ session_id (str): The ID of the session to rename.
421
+ session_type (SessionType): The type of session to rename.
422
+ session_name (str): The new name of the session.
423
+
424
+ Returns:
425
+ Optional[Session]: The renamed session if successful, None otherwise.
426
+
427
+ Raises:
428
+ Exception: If any error occurs while renaming the session.
429
+ """
430
+ try:
431
+ session = self._get_record("sessions", session_id)
432
+ if session is None:
433
+ return None
434
+
435
+ # Update session_name, in session_data
436
+ if "session_data" not in session:
437
+ session["session_data"] = {}
438
+ session["session_data"]["session_name"] = session_name
439
+ session["updated_at"] = int(time.time())
440
+
441
+ # Store updated session
442
+ success = self._store_record("sessions", session_id, session)
443
+ if not success:
444
+ return None
445
+
446
+ log_debug(f"Renamed session with id '{session_id}' to '{session_name}'")
447
+
448
+ if not deserialize:
449
+ return session
450
+
451
+ if session_type == SessionType.AGENT:
452
+ return AgentSession.from_dict(session)
453
+ elif session_type == SessionType.TEAM:
454
+ return TeamSession.from_dict(session)
455
+ elif session_type == SessionType.WORKFLOW:
456
+ return WorkflowSession.from_dict(session)
457
+ else:
458
+ raise ValueError(f"Invalid session type: {session_type}")
459
+
460
+ except Exception as e:
461
+ log_error(f"Error renaming session: {e}")
462
+ return None
463
+
464
+ def upsert_session(
465
+ self, session: Session, deserialize: Optional[bool] = True
466
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
467
+ """Insert or update a session in Redis.
468
+
469
+ Args:
470
+ session (Session): The session to upsert.
471
+
472
+ Returns:
473
+ Optional[Session]: The upserted session if successful, None otherwise.
474
+
475
+ Raises:
476
+ Exception: If any error occurs while upserting the session.
477
+ """
478
+ try:
479
+ session_dict = session.to_dict()
480
+
481
+ if isinstance(session, AgentSession):
482
+ data = {
483
+ "session_id": session_dict.get("session_id"),
484
+ "session_type": SessionType.AGENT.value,
485
+ "agent_id": session_dict.get("agent_id"),
486
+ "team_id": session_dict.get("team_id"),
487
+ "workflow_id": session_dict.get("workflow_id"),
488
+ "user_id": session_dict.get("user_id"),
489
+ "runs": session_dict.get("runs"),
490
+ "agent_data": session_dict.get("agent_data"),
491
+ "team_data": session_dict.get("team_data"),
492
+ "workflow_data": session_dict.get("workflow_data"),
493
+ "session_data": session_dict.get("session_data"),
494
+ "summary": session_dict.get("summary"),
495
+ "metadata": session_dict.get("metadata"),
496
+ "created_at": session_dict.get("created_at") or int(time.time()),
497
+ "updated_at": int(time.time()),
498
+ }
499
+
500
+ success = self._store_record(
501
+ table_type="sessions",
502
+ record_id=session.session_id,
503
+ data=data,
504
+ index_fields=["user_id", "agent_id", "session_type"],
505
+ )
506
+ if not success:
507
+ return None
508
+
509
+ if not deserialize:
510
+ return data
511
+
512
+ return AgentSession.from_dict(data)
513
+
514
+ elif isinstance(session, TeamSession):
515
+ data = {
516
+ "session_id": session_dict.get("session_id"),
517
+ "session_type": SessionType.TEAM.value,
518
+ "agent_id": None,
519
+ "team_id": session_dict.get("team_id"),
520
+ "workflow_id": None,
521
+ "user_id": session_dict.get("user_id"),
522
+ "runs": session_dict.get("runs"),
523
+ "team_data": session_dict.get("team_data"),
524
+ "agent_data": None,
525
+ "workflow_data": None,
526
+ "session_data": session_dict.get("session_data"),
527
+ "summary": session_dict.get("summary"),
528
+ "metadata": session_dict.get("metadata"),
529
+ "created_at": session_dict.get("created_at") or int(time.time()),
530
+ "updated_at": int(time.time()),
531
+ }
532
+
533
+ success = self._store_record(
534
+ table_type="sessions",
535
+ record_id=session.session_id,
536
+ data=data,
537
+ index_fields=["user_id", "team_id", "session_type"],
538
+ )
539
+ if not success:
540
+ return None
541
+
542
+ if not deserialize:
543
+ return data
544
+
545
+ return TeamSession.from_dict(data)
546
+
547
+ else:
548
+ data = {
549
+ "session_id": session_dict.get("session_id"),
550
+ "session_type": SessionType.WORKFLOW.value,
551
+ "workflow_id": session_dict.get("workflow_id"),
552
+ "user_id": session_dict.get("user_id"),
553
+ "runs": session_dict.get("runs"),
554
+ "workflow_data": session_dict.get("workflow_data"),
555
+ "session_data": session_dict.get("session_data"),
556
+ "metadata": session_dict.get("metadata"),
557
+ "created_at": session_dict.get("created_at") or int(time.time()),
558
+ "updated_at": int(time.time()),
559
+ "agent_id": None,
560
+ "team_id": None,
561
+ "agent_data": None,
562
+ "team_data": None,
563
+ "summary": None,
564
+ }
565
+
566
+ success = self._store_record(
567
+ table_type="sessions",
568
+ record_id=session.session_id,
569
+ data=data,
570
+ index_fields=["user_id", "workflow_id", "session_type"],
571
+ )
572
+ if not success:
573
+ return None
574
+
575
+ if not deserialize:
576
+ return data
577
+
578
+ return WorkflowSession.from_dict(data)
579
+
580
+ except Exception as e:
581
+ log_error(f"Error upserting session: {e}")
582
+ return None
583
+
584
+ # -- Memory methods --
585
+
586
+ def delete_user_memory(self, memory_id: str):
587
+ """Delete a user memory from Redis.
588
+
589
+ Args:
590
+ memory_id (str): The ID of the memory to delete.
591
+
592
+ Returns:
593
+ bool: True if the memory was deleted, False otherwise.
594
+
595
+ Raises:
596
+ Exception: If any error occurs while deleting the memory.
597
+ """
598
+ try:
599
+ if self._delete_record(
600
+ "memories", memory_id, index_fields=["user_id", "agent_id", "team_id", "workflow_id"]
601
+ ):
602
+ log_debug(f"Successfully deleted user memory id: {memory_id}")
603
+ else:
604
+ log_debug(f"No user memory found with id: {memory_id}")
605
+
606
+ except Exception as e:
607
+ log_error(f"Error deleting user memory: {e}")
608
+
609
+ def delete_user_memories(self, memory_ids: List[str]) -> None:
610
+ """Delete user memories from Redis.
611
+
612
+ Args:
613
+ memory_ids (List[str]): The IDs of the memories to delete.
614
+ """
615
+ try:
616
+ # TODO: cant we optimize this?
617
+ for memory_id in memory_ids:
618
+ self._delete_record(
619
+ "memories",
620
+ memory_id,
621
+ index_fields=["user_id", "agent_id", "team_id", "workflow_id"],
622
+ )
623
+
624
+ except Exception as e:
625
+ log_error(f"Error deleting user memories: {e}")
626
+
627
+ def get_all_memory_topics(self) -> List[str]:
628
+ """Get all memory topics from Redis.
629
+
630
+ Returns:
631
+ List[str]: The list of memory topics.
632
+ """
633
+ try:
634
+ all_memories = self._get_all_records("memories")
635
+
636
+ topics = set()
637
+ for memory in all_memories:
638
+ memory_topics = memory.get("topics", [])
639
+ if isinstance(memory_topics, list):
640
+ topics.update(memory_topics)
641
+
642
+ return list(topics)
643
+
644
+ except Exception as e:
645
+ log_error(f"Exception reading memory topics: {e}")
646
+ return []
647
+
648
+ def get_user_memory(
649
+ self, memory_id: str, deserialize: Optional[bool] = True
650
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
651
+ """Get a memory from Redis.
652
+
653
+ Args:
654
+ memory_id (str): The ID of the memory to get.
655
+
656
+ Returns:
657
+ Optional[UserMemory]: The memory data if found, None otherwise.
658
+ """
659
+ try:
660
+ memory_raw = self._get_record("memories", memory_id)
661
+ if memory_raw is None:
662
+ return None
663
+
664
+ if not deserialize:
665
+ return memory_raw
666
+
667
+ return UserMemory.from_dict(memory_raw)
668
+
669
+ except Exception as e:
670
+ log_error(f"Exception reading memory: {e}")
671
+ return None
672
+
673
+ def get_user_memories(
674
+ self,
675
+ user_id: Optional[str] = None,
676
+ agent_id: Optional[str] = None,
677
+ team_id: Optional[str] = None,
678
+ topics: Optional[List[str]] = None,
679
+ search_content: Optional[str] = None,
680
+ limit: Optional[int] = None,
681
+ page: Optional[int] = None,
682
+ sort_by: Optional[str] = None,
683
+ sort_order: Optional[str] = None,
684
+ deserialize: Optional[bool] = True,
685
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
686
+ """Get all memories from Redis as UserMemory objects.
687
+
688
+ Args:
689
+ user_id (Optional[str]): The ID of the user to filter by.
690
+ agent_id (Optional[str]): The ID of the agent to filter by.
691
+ team_id (Optional[str]): The ID of the team to filter by.
692
+ topics (Optional[List[str]]): The topics to filter by.
693
+ search_content (Optional[str]): The content to search for.
694
+ limit (Optional[int]): The maximum number of memories to return.
695
+ page (Optional[int]): The page number to return.
696
+ sort_by (Optional[str]): The field to sort by.
697
+ sort_order (Optional[str]): The order to sort by.
698
+ deserialize (Optional[bool]): Whether to deserialize the memories.
699
+
700
+ Returns:
701
+ Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
702
+ - When deserialize=True: List of UserMemory objects
703
+ - When deserialize=False: Tuple of (memory dictionaries, total count)
704
+
705
+ Raises:
706
+ Exception: If any error occurs while reading the memories.
707
+ """
708
+ try:
709
+ all_memories = self._get_all_records("memories")
710
+
711
+ # Apply filters
712
+ conditions = {}
713
+ if user_id is not None:
714
+ conditions["user_id"] = user_id
715
+ if agent_id is not None:
716
+ conditions["agent_id"] = agent_id
717
+ if team_id is not None:
718
+ conditions["team_id"] = team_id
719
+
720
+ filtered_memories = apply_filters(records=all_memories, conditions=conditions)
721
+
722
+ # Apply topic filter
723
+ if topics is not None:
724
+ filtered_memories = [
725
+ m for m in filtered_memories if any(topic in m.get("topics", []) for topic in topics)
726
+ ]
727
+
728
+ # Apply content search
729
+ if search_content is not None:
730
+ filtered_memories = [
731
+ m for m in filtered_memories if search_content.lower() in str(m.get("memory", "")).lower()
732
+ ]
733
+
734
+ sorted_memories = apply_sorting(records=filtered_memories, sort_by=sort_by, sort_order=sort_order)
735
+ paginated_memories = apply_pagination(records=sorted_memories, limit=limit, page=page)
736
+
737
+ if not deserialize:
738
+ return paginated_memories, len(filtered_memories)
739
+
740
+ return [UserMemory.from_dict(record) for record in paginated_memories]
741
+
742
+ except Exception as e:
743
+ log_error(f"Exception reading memories: {e}")
744
+ return [] if deserialize else ([], 0)
745
+
746
+ def get_user_memory_stats(
747
+ self,
748
+ limit: Optional[int] = None,
749
+ page: Optional[int] = None,
750
+ ) -> Tuple[List[Dict[str, Any]], int]:
751
+ """Get user memory stats from Redis.
752
+
753
+ Args:
754
+ limit (Optional[int]): The maximum number of stats to return.
755
+ page (Optional[int]): The page number to return.
756
+
757
+ Returns:
758
+ Tuple[List[Dict[str, Any]], int]: A tuple containing the list of stats and the total number of stats.
759
+
760
+ Raises:
761
+ Exception: If any error occurs while getting the user memory stats.
762
+ """
763
+ try:
764
+ all_memories = self._get_all_records("memories")
765
+
766
+ # Group by user_id
767
+ user_stats = {}
768
+ for memory in all_memories:
769
+ user_id = memory.get("user_id")
770
+ if user_id is None:
771
+ continue
772
+
773
+ if user_id not in user_stats:
774
+ user_stats[user_id] = {
775
+ "user_id": user_id,
776
+ "total_memories": 0,
777
+ "last_memory_updated_at": 0,
778
+ }
779
+
780
+ user_stats[user_id]["total_memories"] += 1
781
+ updated_at = memory.get("updated_at", 0)
782
+ if updated_at > user_stats[user_id]["last_memory_updated_at"]:
783
+ user_stats[user_id]["last_memory_updated_at"] = updated_at
784
+
785
+ stats_list = list(user_stats.values())
786
+
787
+ # Sorting by last_memory_updated_at descending
788
+ stats_list.sort(key=lambda x: x["last_memory_updated_at"], reverse=True)
789
+
790
+ total_count = len(stats_list)
791
+
792
+ paginated_stats = apply_pagination(records=stats_list, limit=limit, page=page)
793
+
794
+ return paginated_stats, total_count
795
+
796
+ except Exception as e:
797
+ log_error(f"Exception getting user memory stats: {e}")
798
+ return [], 0
799
+
800
+ def upsert_user_memory(
801
+ self, memory: UserMemory, deserialize: Optional[bool] = True
802
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
803
+ """Upsert a user memory in Redis.
804
+
805
+ Args:
806
+ memory (UserMemory): The memory to upsert.
807
+
808
+ Returns:
809
+ Optional[UserMemory]: The upserted memory data if successful, None otherwise.
810
+ """
811
+ try:
812
+ if memory.memory_id is None:
813
+ memory.memory_id = str(uuid4())
814
+
815
+ data = {
816
+ "user_id": memory.user_id,
817
+ "agent_id": memory.agent_id,
818
+ "team_id": memory.team_id,
819
+ "memory_id": memory.memory_id,
820
+ "memory": memory.memory,
821
+ "topics": memory.topics,
822
+ "updated_at": int(time.time()),
823
+ }
824
+
825
+ success = self._store_record(
826
+ "memories", memory.memory_id, data, index_fields=["user_id", "agent_id", "team_id", "workflow_id"]
827
+ )
828
+
829
+ if not success:
830
+ return None
831
+
832
+ if not deserialize:
833
+ return data
834
+
835
+ return UserMemory.from_dict(data)
836
+
837
+ except Exception as e:
838
+ log_error(f"Error upserting user memory: {e}")
839
+ return None
840
+
841
+ def clear_memories(self) -> None:
842
+ """Delete all memories from the database.
843
+
844
+ Raises:
845
+ Exception: If an error occurs during deletion.
846
+ """
847
+ try:
848
+ # Get all keys for memories table
849
+ keys = get_all_keys_for_table(redis_client=self.redis_client, prefix=self.db_prefix, table_type="memories")
850
+
851
+ if keys:
852
+ # Delete all memory keys in a single batch operation
853
+ self.redis_client.delete(*keys)
854
+
855
+ except Exception as e:
856
+ from agno.utils.log import log_warning
857
+
858
+ log_warning(f"Exception deleting all memories: {e}")
859
+
860
+ # -- Metrics methods --
861
+
862
+ def _get_all_sessions_for_metrics_calculation(
863
+ self, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None
864
+ ) -> List[Dict[str, Any]]:
865
+ """Get all sessions for metrics calculation.
866
+
867
+ Args:
868
+ start_timestamp (Optional[int]): The start timestamp to filter by.
869
+ end_timestamp (Optional[int]): The end timestamp to filter by.
870
+
871
+ Returns:
872
+ List[Dict[str, Any]]: The list of sessions.
873
+
874
+ Raises:
875
+ Exception: If any error occurs while getting the sessions.
876
+ """
877
+ try:
878
+ all_sessions = self._get_all_records("sessions")
879
+
880
+ # Filter by timestamp if provided
881
+ if start_timestamp is not None or end_timestamp is not None:
882
+ filtered_sessions = []
883
+ for session in all_sessions:
884
+ created_at = session.get("created_at", 0)
885
+ if start_timestamp is not None and created_at < start_timestamp:
886
+ continue
887
+ if end_timestamp is not None and created_at > end_timestamp:
888
+ continue
889
+ filtered_sessions.append(session)
890
+ return filtered_sessions
891
+
892
+ return all_sessions
893
+
894
+ except Exception as e:
895
+ log_error(f"Error reading sessions for metrics: {e}")
896
+ return []
897
+
898
+ def _get_metrics_calculation_starting_date(self) -> Optional[date]:
899
+ """Get the first date for which metrics calculation is needed.
900
+
901
+ Returns:
902
+ Optional[date]: The first date for which metrics calculation is needed.
903
+
904
+ Raises:
905
+ Exception: If any error occurs while getting the metrics calculation starting date.
906
+ """
907
+ try:
908
+ all_metrics = self._get_all_records("metrics")
909
+
910
+ if all_metrics:
911
+ # Find the latest completed metric
912
+ completed_metrics = [m for m in all_metrics if m.get("completed", False)]
913
+ if completed_metrics:
914
+ latest_completed = max(completed_metrics, key=lambda x: x.get("date", ""))
915
+ return datetime.fromisoformat(latest_completed["date"]).date() + timedelta(days=1)
916
+ else:
917
+ # Find the earliest incomplete metric
918
+ incomplete_metrics = [m for m in all_metrics if not m.get("completed", False)]
919
+ if incomplete_metrics:
920
+ earliest_incomplete = min(incomplete_metrics, key=lambda x: x.get("date", ""))
921
+ return datetime.fromisoformat(earliest_incomplete["date"]).date()
922
+
923
+ # No metrics records, find first session
924
+ sessions_raw, _ = self.get_sessions(sort_by="created_at", sort_order="asc", limit=1, deserialize=False)
925
+ if sessions_raw:
926
+ first_session_date = sessions_raw[0]["created_at"] # type: ignore
927
+ return datetime.fromtimestamp(first_session_date, tz=timezone.utc).date()
928
+
929
+ return None
930
+
931
+ except Exception as e:
932
+ log_error(f"Error getting metrics starting date: {e}")
933
+ return None
934
+
935
+ def calculate_metrics(self) -> Optional[list[dict]]:
936
+ """Calculate metrics for all dates without complete metrics.
937
+
938
+ Returns:
939
+ Optional[list[dict]]: The list of metrics.
940
+
941
+ Raises:
942
+ Exception: If any error occurs while calculating the metrics.
943
+ """
944
+ try:
945
+ starting_date = self._get_metrics_calculation_starting_date()
946
+ if starting_date is None:
947
+ log_info("No session data found. Won't calculate metrics.")
948
+ return None
949
+
950
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
951
+ if not dates_to_process:
952
+ log_info("Metrics already calculated for all relevant dates.")
953
+ return None
954
+
955
+ start_timestamp = int(datetime.combine(dates_to_process[0], datetime.min.time()).timestamp())
956
+ end_timestamp = int(
957
+ datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time()).timestamp()
958
+ )
959
+
960
+ sessions = self._get_all_sessions_for_metrics_calculation(
961
+ start_timestamp=start_timestamp, end_timestamp=end_timestamp
962
+ )
963
+ all_sessions_data = fetch_all_sessions_data(
964
+ sessions=sessions, dates_to_process=dates_to_process, start_timestamp=start_timestamp
965
+ )
966
+ if not all_sessions_data:
967
+ log_info("No new session data found. Won't calculate metrics.")
968
+ return None
969
+
970
+ results = []
971
+ for date_to_process in dates_to_process:
972
+ date_key = date_to_process.isoformat()
973
+ sessions_for_date = all_sessions_data.get(date_key, {})
974
+
975
+ # Skip dates with no sessions
976
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
977
+ continue
978
+
979
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
980
+
981
+ # Check if a record already exists for this date and aggregation period
982
+ existing_record = self._get_record("metrics", metrics_record["id"])
983
+ if existing_record:
984
+ # Update the existing record while preserving created_at
985
+ metrics_record["created_at"] = existing_record.get("created_at", metrics_record["created_at"])
986
+
987
+ success = self._store_record("metrics", metrics_record["id"], metrics_record)
988
+ if success:
989
+ results.append(metrics_record)
990
+
991
+ log_debug("Updated metrics calculations")
992
+
993
+ return results
994
+
995
+ except Exception as e:
996
+ log_error(f"Error calculating metrics: {e}")
997
+ raise e
998
+
999
+ def get_metrics(
1000
+ self,
1001
+ starting_date: Optional[date] = None,
1002
+ ending_date: Optional[date] = None,
1003
+ ) -> Tuple[List[dict], Optional[int]]:
1004
+ """Get all metrics matching the given date range.
1005
+
1006
+ Args:
1007
+ starting_date (Optional[date]): The starting date to filter by.
1008
+ ending_date (Optional[date]): The ending date to filter by.
1009
+
1010
+ Returns:
1011
+ Tuple[List[dict], Optional[int]]: A tuple containing the list of metrics and the latest updated_at.
1012
+
1013
+ Raises:
1014
+ Exception: If any error occurs while getting the metrics.
1015
+ """
1016
+ try:
1017
+ all_metrics = self._get_all_records("metrics")
1018
+
1019
+ # Filter by date range
1020
+ if starting_date is not None or ending_date is not None:
1021
+ filtered_metrics = []
1022
+ for metric in all_metrics:
1023
+ metric_date = datetime.fromisoformat(metric.get("date", "")).date()
1024
+ if starting_date is not None and metric_date < starting_date:
1025
+ continue
1026
+ if ending_date is not None and metric_date > ending_date:
1027
+ continue
1028
+ filtered_metrics.append(metric)
1029
+ all_metrics = filtered_metrics
1030
+
1031
+ # Get latest updated_at
1032
+ latest_updated_at = None
1033
+ if all_metrics:
1034
+ latest_updated_at = max(metric.get("updated_at", 0) for metric in all_metrics)
1035
+
1036
+ return all_metrics, latest_updated_at
1037
+
1038
+ except Exception as e:
1039
+ log_error(f"Error getting metrics: {e}")
1040
+ return [], None
1041
+
1042
+ # -- Knowledge methods --
1043
+
1044
+ def delete_knowledge_content(self, id: str):
1045
+ """Delete a knowledge row from the database.
1046
+
1047
+ Args:
1048
+ id (str): The ID of the knowledge row to delete.
1049
+
1050
+ Raises:
1051
+ Exception: If any error occurs while deleting the knowledge content.
1052
+ """
1053
+ try:
1054
+ self._delete_record("knowledge", id)
1055
+
1056
+ except Exception as e:
1057
+ log_error(f"Error deleting knowledge content: {e}")
1058
+
1059
+ def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1060
+ """Get a knowledge row from the database.
1061
+
1062
+ Args:
1063
+ id (str): The ID of the knowledge row to get.
1064
+
1065
+ Returns:
1066
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1067
+
1068
+ Raises:
1069
+ Exception: If any error occurs while getting the knowledge content.
1070
+ """
1071
+ try:
1072
+ document_raw = self._get_record("knowledge", id)
1073
+ if document_raw is None:
1074
+ return None
1075
+
1076
+ return KnowledgeRow.model_validate(document_raw)
1077
+
1078
+ except Exception as e:
1079
+ log_error(f"Error getting knowledge content: {e}")
1080
+ return None
1081
+
1082
+ def get_knowledge_contents(
1083
+ self,
1084
+ limit: Optional[int] = None,
1085
+ page: Optional[int] = None,
1086
+ sort_by: Optional[str] = None,
1087
+ sort_order: Optional[str] = None,
1088
+ ) -> Tuple[List[KnowledgeRow], int]:
1089
+ """Get all knowledge contents from the database.
1090
+
1091
+ Args:
1092
+ limit (Optional[int]): The maximum number of knowledge contents to return.
1093
+ page (Optional[int]): The page number.
1094
+ sort_by (Optional[str]): The column to sort by.
1095
+ sort_order (Optional[str]): The order to sort by.
1096
+
1097
+ Returns:
1098
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
1099
+
1100
+ Raises:
1101
+ Exception: If an error occurs during retrieval.
1102
+
1103
+ Raises:
1104
+ Exception: If any error occurs while getting the knowledge contents.
1105
+ """
1106
+ try:
1107
+ all_documents = self._get_all_records("knowledge")
1108
+ if len(all_documents) == 0:
1109
+ return [], 0
1110
+
1111
+ total_count = len(all_documents)
1112
+
1113
+ # Apply sorting
1114
+ sorted_documents = apply_sorting(records=all_documents, sort_by=sort_by, sort_order=sort_order)
1115
+
1116
+ # Apply pagination
1117
+ paginated_documents = apply_pagination(records=sorted_documents, limit=limit, page=page)
1118
+
1119
+ return [KnowledgeRow.model_validate(doc) for doc in paginated_documents], total_count
1120
+
1121
+ except Exception as e:
1122
+ log_error(f"Error getting knowledge contents: {e}")
1123
+ return [], 0
1124
+
1125
+ def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
1126
+ """Upsert knowledge content in the database.
1127
+
1128
+ Args:
1129
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
1130
+
1131
+ Returns:
1132
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
1133
+
1134
+ Raises:
1135
+ Exception: If any error occurs while upserting the knowledge content.
1136
+ """
1137
+ try:
1138
+ data = knowledge_row.model_dump()
1139
+ success = self._store_record("knowledge", knowledge_row.id, data) # type: ignore
1140
+
1141
+ return knowledge_row if success else None
1142
+
1143
+ except Exception as e:
1144
+ log_error(f"Error upserting knowledge content: {e}")
1145
+ return None
1146
+
1147
+ # -- Eval methods --
1148
+
1149
+ def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
1150
+ """Create an EvalRunRecord in Redis.
1151
+
1152
+ Args:
1153
+ eval_run (EvalRunRecord): The eval run to create.
1154
+
1155
+ Returns:
1156
+ Optional[EvalRunRecord]: The created eval run if successful, None otherwise.
1157
+
1158
+ Raises:
1159
+ Exception: If any error occurs while creating the eval run.
1160
+ """
1161
+ try:
1162
+ current_time = int(time.time())
1163
+ data = {"created_at": current_time, "updated_at": current_time, **eval_run.model_dump()}
1164
+
1165
+ success = self._store_record(
1166
+ "evals",
1167
+ eval_run.run_id,
1168
+ data,
1169
+ index_fields=["agent_id", "team_id", "workflow_id", "model_id", "eval_type"],
1170
+ )
1171
+
1172
+ log_debug(f"Created eval run with id '{eval_run.run_id}'")
1173
+
1174
+ return eval_run if success else None
1175
+
1176
+ except Exception as e:
1177
+ log_error(f"Error creating eval run: {e}")
1178
+ return None
1179
+
1180
+ def delete_eval_run(self, eval_run_id: str) -> None:
1181
+ """Delete an eval run from Redis.
1182
+
1183
+ Args:
1184
+ eval_run_id (str): The ID of the eval run to delete.
1185
+
1186
+ Raises:
1187
+ Exception: If any error occurs while deleting the eval run.
1188
+ """
1189
+ try:
1190
+ if self._delete_record(
1191
+ "evals", eval_run_id, index_fields=["agent_id", "team_id", "workflow_id", "model_id", "eval_type"]
1192
+ ):
1193
+ log_debug(f"Deleted eval run with ID: {eval_run_id}")
1194
+ else:
1195
+ log_debug(f"No eval run found with ID: {eval_run_id}")
1196
+
1197
+ except Exception as e:
1198
+ log_error(f"Error deleting eval run {eval_run_id}: {e}")
1199
+ raise
1200
+
1201
+ def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1202
+ """Delete multiple eval runs from Redis.
1203
+
1204
+ Args:
1205
+ eval_run_ids (List[str]): The IDs of the eval runs to delete.
1206
+
1207
+ Raises:
1208
+ Exception: If any error occurs while deleting the eval runs.
1209
+ """
1210
+ try:
1211
+ deleted_count = 0
1212
+ for eval_run_id in eval_run_ids:
1213
+ if self._delete_record(
1214
+ "evals", eval_run_id, index_fields=["agent_id", "team_id", "workflow_id", "model_id", "eval_type"]
1215
+ ):
1216
+ deleted_count += 1
1217
+
1218
+ if deleted_count == 0:
1219
+ log_debug(f"No eval runs found with IDs: {eval_run_ids}")
1220
+ else:
1221
+ log_debug(f"Deleted {deleted_count} eval runs")
1222
+
1223
+ except Exception as e:
1224
+ log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
1225
+ raise
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 Redis.
1231
+
1232
+ Args:
1233
+ eval_run_id (str): The ID of the eval run to get.
1234
+
1235
+ Returns:
1236
+ Optional[EvalRunRecord]: The eval run if found, None otherwise.
1237
+
1238
+ Raises:
1239
+ Exception: If any error occurs while getting the eval run.
1240
+ """
1241
+ try:
1242
+ eval_run_raw = self._get_record("evals", eval_run_id)
1243
+ if eval_run_raw is None:
1244
+ return None
1245
+
1246
+ if not deserialize:
1247
+ return eval_run_raw
1248
+
1249
+ return EvalRunRecord.model_validate(eval_run_raw)
1250
+
1251
+ except Exception as e:
1252
+ log_error(f"Exception getting eval run {eval_run_id}: {e}")
1253
+ return None
1254
+
1255
+ def get_eval_runs(
1256
+ self,
1257
+ limit: Optional[int] = None,
1258
+ page: Optional[int] = None,
1259
+ sort_by: Optional[str] = None,
1260
+ sort_order: Optional[str] = None,
1261
+ agent_id: Optional[str] = None,
1262
+ team_id: Optional[str] = None,
1263
+ workflow_id: Optional[str] = None,
1264
+ model_id: Optional[str] = None,
1265
+ filter_type: Optional[EvalFilterType] = None,
1266
+ eval_type: Optional[List[EvalType]] = None,
1267
+ deserialize: Optional[bool] = True,
1268
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1269
+ """Get all eval runs from Redis.
1270
+
1271
+ Args:
1272
+ limit (Optional[int]): The maximum number of eval runs to return.
1273
+ page (Optional[int]): The page number to return.
1274
+ sort_by (Optional[str]): The field to sort by.
1275
+ sort_order (Optional[str]): The order to sort by.
1276
+
1277
+ Returns:
1278
+ List[EvalRunRecord]: The list of eval runs.
1279
+
1280
+ Raises:
1281
+ Exception: If any error occurs while getting the eval runs.
1282
+ """
1283
+ try:
1284
+ all_eval_runs = self._get_all_records("evals")
1285
+
1286
+ # Apply filters
1287
+ filtered_runs = []
1288
+ for run in all_eval_runs:
1289
+ # Agent/team/workflow filters
1290
+ if agent_id is not None and run.get("agent_id") != agent_id:
1291
+ continue
1292
+ if team_id is not None and run.get("team_id") != team_id:
1293
+ continue
1294
+ if workflow_id is not None and run.get("workflow_id") != workflow_id:
1295
+ continue
1296
+ if model_id is not None and run.get("model_id") != model_id:
1297
+ continue
1298
+
1299
+ # Eval type filter
1300
+ if eval_type is not None and len(eval_type) > 0:
1301
+ if run.get("eval_type") not in eval_type:
1302
+ continue
1303
+
1304
+ # Filter type
1305
+ if filter_type is not None:
1306
+ if filter_type == EvalFilterType.AGENT and run.get("agent_id") is None:
1307
+ continue
1308
+ elif filter_type == EvalFilterType.TEAM and run.get("team_id") is None:
1309
+ continue
1310
+ elif filter_type == EvalFilterType.WORKFLOW and run.get("workflow_id") is None:
1311
+ continue
1312
+
1313
+ filtered_runs.append(run)
1314
+
1315
+ if sort_by is None:
1316
+ sort_by = "created_at"
1317
+ sort_order = "desc"
1318
+
1319
+ sorted_runs = apply_sorting(records=filtered_runs, sort_by=sort_by, sort_order=sort_order)
1320
+ paginated_runs = apply_pagination(records=sorted_runs, limit=limit, page=page)
1321
+
1322
+ if not deserialize:
1323
+ return paginated_runs, len(filtered_runs)
1324
+
1325
+ return [EvalRunRecord.model_validate(row) for row in paginated_runs]
1326
+
1327
+ except Exception as e:
1328
+ log_error(f"Exception getting eval runs: {e}")
1329
+ return []
1330
+
1331
+ def rename_eval_run(
1332
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1333
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1334
+ """Update the name of an eval run in Redis.
1335
+
1336
+ Args:
1337
+ eval_run_id (str): The ID of the eval run to rename.
1338
+ name (str): The new name of the eval run.
1339
+
1340
+ Returns:
1341
+ Optional[Dict[str, Any]]: The updated eval run data if successful, None otherwise.
1342
+
1343
+ Raises:
1344
+ Exception: If any error occurs while updating the eval run name.
1345
+ """
1346
+ try:
1347
+ eval_run_data = self._get_record("evals", eval_run_id)
1348
+ if eval_run_data is None:
1349
+ return None
1350
+
1351
+ eval_run_data["name"] = name
1352
+ eval_run_data["updated_at"] = int(time.time())
1353
+
1354
+ success = self._store_record("evals", eval_run_id, eval_run_data)
1355
+ if not success:
1356
+ return None
1357
+
1358
+ log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
1359
+
1360
+ if not deserialize:
1361
+ return eval_run_data
1362
+
1363
+ return EvalRunRecord.model_validate(eval_run_data)
1364
+
1365
+ except Exception as e:
1366
+ log_error(f"Error updating eval run name {eval_run_id}: {e}")
1367
+ raise