agno 1.8.0__py3-none-any.whl → 2.0.0a1__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 (583) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +19 -27
  3. agno/agent/agent.py +2781 -4126
  4. agno/api/agent.py +9 -65
  5. agno/api/api.py +5 -46
  6. agno/api/evals.py +6 -17
  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 +9 -64
  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 +1749 -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 +1438 -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 +888 -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 +1051 -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 +1417 -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 +298 -0
  52. agno/db/postgres/__init__.py +3 -0
  53. agno/db/postgres/postgres.py +1720 -0
  54. agno/db/postgres/schemas.py +124 -0
  55. agno/db/postgres/utils.py +281 -0
  56. agno/db/redis/__init__.py +3 -0
  57. agno/db/redis/redis.py +1371 -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 +1722 -0
  67. agno/db/singlestore/utils.py +327 -0
  68. agno/db/sqlite/__init__.py +3 -0
  69. agno/db/sqlite/schemas.py +119 -0
  70. agno/db/sqlite/sqlite.py +1680 -0
  71. agno/db/sqlite/utils.py +269 -0
  72. agno/db/utils.py +88 -0
  73. agno/eval/__init__.py +14 -0
  74. agno/eval/accuracy.py +142 -43
  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 +10 -10
  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 +1515 -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 +68 -15
  115. agno/knowledge/reader/docx_reader.py +83 -0
  116. agno/{document → knowledge}/reader/firecrawl_reader.py +42 -21
  117. agno/knowledge/reader/gcs_reader.py +67 -0
  118. agno/{document → knowledge}/reader/json_reader.py +30 -9
  119. agno/{document → knowledge}/reader/markdown_reader.py +36 -9
  120. agno/{document → knowledge}/reader/pdf_reader.py +79 -21
  121. agno/knowledge/reader/reader_factory.py +275 -0
  122. agno/knowledge/reader/s3_reader.py +171 -0
  123. agno/{document → knowledge}/reader/text_reader.py +31 -10
  124. agno/knowledge/reader/url_reader.py +84 -0
  125. agno/knowledge/reader/web_search_reader.py +389 -0
  126. agno/{document → knowledge}/reader/website_reader.py +37 -10
  127. agno/knowledge/reader/wikipedia_reader.py +59 -0
  128. agno/knowledge/reader/youtube_reader.py +78 -0
  129. agno/knowledge/remote_content/remote_content.py +88 -0
  130. agno/{reranker → knowledge/reranker}/base.py +1 -1
  131. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  132. agno/{reranker → knowledge/reranker}/infinity.py +2 -2
  133. agno/{reranker → knowledge/reranker}/sentence_transformer.py +2 -2
  134. agno/knowledge/types.py +30 -0
  135. agno/knowledge/utils.py +169 -0
  136. agno/media.py +2 -2
  137. agno/memory/__init__.py +2 -10
  138. agno/memory/manager.py +1003 -148
  139. agno/models/aimlapi/__init__.py +2 -2
  140. agno/models/aimlapi/aimlapi.py +6 -6
  141. agno/models/anthropic/claude.py +129 -82
  142. agno/models/aws/bedrock.py +107 -175
  143. agno/models/aws/claude.py +64 -18
  144. agno/models/azure/ai_foundry.py +73 -23
  145. agno/models/base.py +347 -287
  146. agno/models/cerebras/cerebras.py +84 -27
  147. agno/models/cohere/chat.py +106 -98
  148. agno/models/dashscope/dashscope.py +14 -5
  149. agno/models/google/gemini.py +123 -53
  150. agno/models/groq/groq.py +97 -35
  151. agno/models/huggingface/huggingface.py +92 -27
  152. agno/models/ibm/watsonx.py +72 -13
  153. agno/models/litellm/chat.py +85 -13
  154. agno/models/message.py +38 -144
  155. agno/models/meta/llama.py +85 -49
  156. agno/models/metrics.py +120 -0
  157. agno/models/mistral/mistral.py +90 -21
  158. agno/models/ollama/__init__.py +0 -2
  159. agno/models/ollama/chat.py +84 -46
  160. agno/models/openai/chat.py +135 -27
  161. agno/models/openai/responses.py +233 -115
  162. agno/models/perplexity/perplexity.py +26 -2
  163. agno/models/portkey/portkey.py +0 -7
  164. agno/models/response.py +14 -8
  165. agno/models/utils.py +20 -0
  166. agno/models/vercel/__init__.py +2 -2
  167. agno/models/vercel/v0.py +1 -1
  168. agno/models/vllm/__init__.py +2 -2
  169. agno/models/vllm/vllm.py +3 -3
  170. agno/models/xai/xai.py +10 -10
  171. agno/os/__init__.py +3 -0
  172. agno/os/app.py +393 -0
  173. agno/os/auth.py +47 -0
  174. agno/os/config.py +103 -0
  175. agno/os/interfaces/agui/__init__.py +3 -0
  176. agno/os/interfaces/agui/agui.py +31 -0
  177. agno/{app/agui/async_router.py → os/interfaces/agui/router.py} +16 -16
  178. agno/{app → os/interfaces}/agui/utils.py +65 -28
  179. agno/os/interfaces/base.py +21 -0
  180. agno/os/interfaces/slack/__init__.py +3 -0
  181. agno/{app/slack/async_router.py → os/interfaces/slack/router.py} +3 -5
  182. agno/os/interfaces/slack/slack.py +33 -0
  183. agno/os/interfaces/whatsapp/__init__.py +3 -0
  184. agno/{app/whatsapp/async_router.py → os/interfaces/whatsapp/router.py} +4 -7
  185. agno/os/interfaces/whatsapp/whatsapp.py +30 -0
  186. agno/os/router.py +843 -0
  187. agno/os/routers/__init__.py +3 -0
  188. agno/os/routers/evals/__init__.py +3 -0
  189. agno/os/routers/evals/evals.py +204 -0
  190. agno/os/routers/evals/schemas.py +142 -0
  191. agno/os/routers/evals/utils.py +161 -0
  192. agno/os/routers/knowledge/__init__.py +3 -0
  193. agno/os/routers/knowledge/knowledge.py +413 -0
  194. agno/os/routers/knowledge/schemas.py +118 -0
  195. agno/os/routers/memory/__init__.py +3 -0
  196. agno/os/routers/memory/memory.py +179 -0
  197. agno/os/routers/memory/schemas.py +58 -0
  198. agno/os/routers/metrics/__init__.py +3 -0
  199. agno/os/routers/metrics/metrics.py +58 -0
  200. agno/os/routers/metrics/schemas.py +47 -0
  201. agno/os/routers/session/__init__.py +3 -0
  202. agno/os/routers/session/session.py +163 -0
  203. agno/os/schema.py +892 -0
  204. agno/{app/playground → os}/settings.py +8 -15
  205. agno/os/utils.py +270 -0
  206. agno/reasoning/azure_ai_foundry.py +4 -4
  207. agno/reasoning/deepseek.py +4 -4
  208. agno/reasoning/default.py +6 -11
  209. agno/reasoning/groq.py +4 -4
  210. agno/reasoning/helpers.py +4 -6
  211. agno/reasoning/ollama.py +4 -4
  212. agno/reasoning/openai.py +4 -4
  213. agno/run/{response.py → agent.py} +144 -72
  214. agno/run/base.py +44 -58
  215. agno/run/cancel.py +83 -0
  216. agno/run/team.py +133 -77
  217. agno/run/workflow.py +537 -12
  218. agno/session/__init__.py +10 -0
  219. agno/session/agent.py +244 -0
  220. agno/session/summary.py +225 -0
  221. agno/session/team.py +262 -0
  222. agno/{storage/session/v2 → session}/workflow.py +47 -24
  223. agno/team/__init__.py +15 -16
  224. agno/team/team.py +2967 -4243
  225. agno/tools/agentql.py +14 -5
  226. agno/tools/airflow.py +9 -4
  227. agno/tools/api.py +7 -3
  228. agno/tools/apify.py +2 -46
  229. agno/tools/arxiv.py +8 -3
  230. agno/tools/aws_lambda.py +7 -5
  231. agno/tools/aws_ses.py +7 -1
  232. agno/tools/baidusearch.py +4 -1
  233. agno/tools/bitbucket.py +4 -4
  234. agno/tools/brandfetch.py +14 -11
  235. agno/tools/bravesearch.py +4 -1
  236. agno/tools/brightdata.py +42 -22
  237. agno/tools/browserbase.py +13 -4
  238. agno/tools/calcom.py +12 -10
  239. agno/tools/calculator.py +10 -27
  240. agno/tools/cartesia.py +18 -13
  241. agno/tools/{clickup_tool.py → clickup.py} +12 -25
  242. agno/tools/confluence.py +71 -18
  243. agno/tools/crawl4ai.py +7 -1
  244. agno/tools/csv_toolkit.py +9 -8
  245. agno/tools/dalle.py +18 -11
  246. agno/tools/daytona.py +13 -16
  247. agno/tools/decorator.py +6 -3
  248. agno/tools/desi_vocal.py +16 -7
  249. agno/tools/discord.py +11 -8
  250. agno/tools/docker.py +30 -42
  251. agno/tools/duckdb.py +34 -53
  252. agno/tools/duckduckgo.py +8 -7
  253. agno/tools/e2b.py +62 -62
  254. agno/tools/eleven_labs.py +35 -28
  255. agno/tools/email.py +4 -1
  256. agno/tools/evm.py +7 -1
  257. agno/tools/exa.py +19 -14
  258. agno/tools/fal.py +29 -29
  259. agno/tools/file.py +9 -8
  260. agno/tools/financial_datasets.py +25 -44
  261. agno/tools/firecrawl.py +22 -22
  262. agno/tools/function.py +68 -17
  263. agno/tools/giphy.py +22 -10
  264. agno/tools/github.py +48 -126
  265. agno/tools/gmail.py +46 -62
  266. agno/tools/google_bigquery.py +7 -6
  267. agno/tools/google_maps.py +11 -26
  268. agno/tools/googlesearch.py +7 -2
  269. agno/tools/googlesheets.py +21 -17
  270. agno/tools/hackernews.py +9 -5
  271. agno/tools/jina.py +5 -4
  272. agno/tools/jira.py +18 -9
  273. agno/tools/knowledge.py +31 -32
  274. agno/tools/linear.py +18 -33
  275. agno/tools/linkup.py +5 -1
  276. agno/tools/local_file_system.py +8 -5
  277. agno/tools/lumalab.py +31 -19
  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 +32 -14
  282. agno/tools/models/gemini.py +58 -31
  283. agno/tools/models/groq.py +29 -20
  284. agno/tools/models/nebius.py +27 -11
  285. agno/tools/models_labs.py +39 -15
  286. agno/tools/moviepy_video.py +7 -6
  287. agno/tools/neo4j.py +134 -0
  288. agno/tools/newspaper.py +7 -2
  289. agno/tools/newspaper4k.py +8 -3
  290. agno/tools/openai.py +57 -26
  291. agno/tools/openbb.py +12 -11
  292. agno/tools/opencv.py +62 -46
  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 +54 -41
  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 +95 -118
  335. agno/utils/knowledge.py +29 -0
  336. agno/utils/location.py +2 -2
  337. agno/utils/log.py +2 -2
  338. agno/utils/mcp.py +11 -5
  339. agno/utils/media.py +39 -0
  340. agno/utils/message.py +12 -1
  341. agno/utils/models/claude.py +6 -4
  342. agno/utils/models/mistral.py +8 -7
  343. agno/utils/models/schema_utils.py +3 -3
  344. agno/utils/pprint.py +33 -32
  345. agno/utils/print_response/agent.py +779 -0
  346. agno/utils/print_response/team.py +1565 -0
  347. agno/utils/print_response/workflow.py +1451 -0
  348. agno/utils/prompts.py +14 -14
  349. agno/utils/reasoning.py +87 -0
  350. agno/utils/response.py +42 -42
  351. agno/utils/string.py +8 -22
  352. agno/utils/team.py +50 -0
  353. agno/utils/timer.py +2 -2
  354. agno/vectordb/base.py +33 -21
  355. agno/vectordb/cassandra/cassandra.py +287 -23
  356. agno/vectordb/chroma/chromadb.py +482 -59
  357. agno/vectordb/clickhouse/clickhousedb.py +270 -63
  358. agno/vectordb/couchbase/couchbase.py +309 -29
  359. agno/vectordb/lancedb/lance_db.py +360 -21
  360. agno/vectordb/langchaindb/__init__.py +5 -0
  361. agno/vectordb/langchaindb/langchaindb.py +145 -0
  362. agno/vectordb/lightrag/__init__.py +5 -0
  363. agno/vectordb/lightrag/lightrag.py +374 -0
  364. agno/vectordb/llamaindex/llamaindexdb.py +127 -0
  365. agno/vectordb/milvus/milvus.py +242 -32
  366. agno/vectordb/mongodb/mongodb.py +200 -24
  367. agno/vectordb/pgvector/pgvector.py +319 -37
  368. agno/vectordb/pineconedb/pineconedb.py +221 -27
  369. agno/vectordb/qdrant/qdrant.py +356 -14
  370. agno/vectordb/singlestore/singlestore.py +286 -29
  371. agno/vectordb/surrealdb/surrealdb.py +187 -7
  372. agno/vectordb/upstashdb/upstashdb.py +342 -26
  373. agno/vectordb/weaviate/weaviate.py +227 -165
  374. agno/workflow/__init__.py +17 -13
  375. agno/workflow/{v2/condition.py → condition.py} +135 -32
  376. agno/workflow/{v2/loop.py → loop.py} +115 -28
  377. agno/workflow/{v2/parallel.py → parallel.py} +138 -108
  378. agno/workflow/{v2/router.py → router.py} +133 -32
  379. agno/workflow/{v2/step.py → step.py} +200 -42
  380. agno/workflow/{v2/steps.py → steps.py} +147 -66
  381. agno/workflow/types.py +482 -0
  382. agno/workflow/workflow.py +2394 -696
  383. agno-2.0.0a1.dist-info/METADATA +355 -0
  384. agno-2.0.0a1.dist-info/RECORD +514 -0
  385. agno/agent/metrics.py +0 -107
  386. agno/api/app.py +0 -35
  387. agno/api/playground.py +0 -92
  388. agno/api/schemas/app.py +0 -12
  389. agno/api/schemas/playground.py +0 -22
  390. agno/api/schemas/user.py +0 -35
  391. agno/api/schemas/workspace.py +0 -46
  392. agno/api/user.py +0 -160
  393. agno/api/workflows.py +0 -33
  394. agno/api/workspace.py +0 -175
  395. agno/app/agui/__init__.py +0 -3
  396. agno/app/agui/app.py +0 -17
  397. agno/app/agui/sync_router.py +0 -120
  398. agno/app/base.py +0 -186
  399. agno/app/discord/__init__.py +0 -3
  400. agno/app/fastapi/__init__.py +0 -3
  401. agno/app/fastapi/app.py +0 -107
  402. agno/app/fastapi/async_router.py +0 -457
  403. agno/app/fastapi/sync_router.py +0 -448
  404. agno/app/playground/app.py +0 -228
  405. agno/app/playground/async_router.py +0 -1050
  406. agno/app/playground/deploy.py +0 -249
  407. agno/app/playground/operator.py +0 -183
  408. agno/app/playground/schemas.py +0 -220
  409. agno/app/playground/serve.py +0 -55
  410. agno/app/playground/sync_router.py +0 -1042
  411. agno/app/playground/utils.py +0 -46
  412. agno/app/settings.py +0 -15
  413. agno/app/slack/__init__.py +0 -3
  414. agno/app/slack/app.py +0 -19
  415. agno/app/slack/sync_router.py +0 -92
  416. agno/app/utils.py +0 -54
  417. agno/app/whatsapp/__init__.py +0 -3
  418. agno/app/whatsapp/app.py +0 -15
  419. agno/app/whatsapp/sync_router.py +0 -197
  420. agno/cli/auth_server.py +0 -249
  421. agno/cli/config.py +0 -274
  422. agno/cli/console.py +0 -88
  423. agno/cli/credentials.py +0 -23
  424. agno/cli/entrypoint.py +0 -571
  425. agno/cli/operator.py +0 -357
  426. agno/cli/settings.py +0 -96
  427. agno/cli/ws/ws_cli.py +0 -817
  428. agno/constants.py +0 -13
  429. agno/document/__init__.py +0 -5
  430. agno/document/chunking/semantic.py +0 -45
  431. agno/document/chunking/strategy.py +0 -31
  432. agno/document/reader/__init__.py +0 -5
  433. agno/document/reader/base.py +0 -47
  434. agno/document/reader/docx_reader.py +0 -60
  435. agno/document/reader/gcs/pdf_reader.py +0 -44
  436. agno/document/reader/s3/pdf_reader.py +0 -59
  437. agno/document/reader/s3/text_reader.py +0 -63
  438. agno/document/reader/url_reader.py +0 -59
  439. agno/document/reader/youtube_reader.py +0 -58
  440. agno/embedder/__init__.py +0 -5
  441. agno/embedder/langdb.py +0 -80
  442. agno/embedder/mistral.py +0 -82
  443. agno/embedder/openai.py +0 -78
  444. agno/file/__init__.py +0 -5
  445. agno/file/file.py +0 -16
  446. agno/file/local/csv.py +0 -32
  447. agno/file/local/txt.py +0 -19
  448. agno/infra/app.py +0 -240
  449. agno/infra/base.py +0 -144
  450. agno/infra/context.py +0 -20
  451. agno/infra/db_app.py +0 -52
  452. agno/infra/resource.py +0 -205
  453. agno/infra/resources.py +0 -55
  454. agno/knowledge/agent.py +0 -698
  455. agno/knowledge/arxiv.py +0 -33
  456. agno/knowledge/combined.py +0 -36
  457. agno/knowledge/csv.py +0 -144
  458. agno/knowledge/csv_url.py +0 -124
  459. agno/knowledge/document.py +0 -223
  460. agno/knowledge/docx.py +0 -137
  461. agno/knowledge/firecrawl.py +0 -34
  462. agno/knowledge/gcs/__init__.py +0 -0
  463. agno/knowledge/gcs/base.py +0 -39
  464. agno/knowledge/gcs/pdf.py +0 -125
  465. agno/knowledge/json.py +0 -137
  466. agno/knowledge/langchain.py +0 -71
  467. agno/knowledge/light_rag.py +0 -273
  468. agno/knowledge/llamaindex.py +0 -66
  469. agno/knowledge/markdown.py +0 -154
  470. agno/knowledge/pdf.py +0 -164
  471. agno/knowledge/pdf_bytes.py +0 -42
  472. agno/knowledge/pdf_url.py +0 -148
  473. agno/knowledge/s3/__init__.py +0 -0
  474. agno/knowledge/s3/base.py +0 -64
  475. agno/knowledge/s3/pdf.py +0 -33
  476. agno/knowledge/s3/text.py +0 -34
  477. agno/knowledge/text.py +0 -141
  478. agno/knowledge/url.py +0 -46
  479. agno/knowledge/website.py +0 -179
  480. agno/knowledge/wikipedia.py +0 -32
  481. agno/knowledge/youtube.py +0 -35
  482. agno/memory/agent.py +0 -423
  483. agno/memory/classifier.py +0 -104
  484. agno/memory/db/__init__.py +0 -5
  485. agno/memory/db/base.py +0 -42
  486. agno/memory/db/mongodb.py +0 -189
  487. agno/memory/db/postgres.py +0 -203
  488. agno/memory/db/sqlite.py +0 -193
  489. agno/memory/memory.py +0 -22
  490. agno/memory/row.py +0 -36
  491. agno/memory/summarizer.py +0 -201
  492. agno/memory/summary.py +0 -19
  493. agno/memory/team.py +0 -415
  494. agno/memory/v2/__init__.py +0 -2
  495. agno/memory/v2/db/__init__.py +0 -1
  496. agno/memory/v2/db/base.py +0 -42
  497. agno/memory/v2/db/firestore.py +0 -339
  498. agno/memory/v2/db/mongodb.py +0 -196
  499. agno/memory/v2/db/postgres.py +0 -214
  500. agno/memory/v2/db/redis.py +0 -187
  501. agno/memory/v2/db/schema.py +0 -54
  502. agno/memory/v2/db/sqlite.py +0 -209
  503. agno/memory/v2/manager.py +0 -437
  504. agno/memory/v2/memory.py +0 -1097
  505. agno/memory/v2/schema.py +0 -55
  506. agno/memory/v2/summarizer.py +0 -215
  507. agno/memory/workflow.py +0 -38
  508. agno/models/ollama/tools.py +0 -430
  509. agno/models/qwen/__init__.py +0 -5
  510. agno/playground/__init__.py +0 -10
  511. agno/playground/deploy.py +0 -3
  512. agno/playground/playground.py +0 -3
  513. agno/playground/serve.py +0 -3
  514. agno/playground/settings.py +0 -3
  515. agno/reranker/__init__.py +0 -0
  516. agno/run/v2/__init__.py +0 -0
  517. agno/run/v2/workflow.py +0 -567
  518. agno/storage/__init__.py +0 -0
  519. agno/storage/agent/__init__.py +0 -0
  520. agno/storage/agent/dynamodb.py +0 -1
  521. agno/storage/agent/json.py +0 -1
  522. agno/storage/agent/mongodb.py +0 -1
  523. agno/storage/agent/postgres.py +0 -1
  524. agno/storage/agent/singlestore.py +0 -1
  525. agno/storage/agent/sqlite.py +0 -1
  526. agno/storage/agent/yaml.py +0 -1
  527. agno/storage/base.py +0 -60
  528. agno/storage/dynamodb.py +0 -673
  529. agno/storage/firestore.py +0 -297
  530. agno/storage/gcs_json.py +0 -261
  531. agno/storage/in_memory.py +0 -234
  532. agno/storage/json.py +0 -237
  533. agno/storage/mongodb.py +0 -328
  534. agno/storage/mysql.py +0 -685
  535. agno/storage/postgres.py +0 -682
  536. agno/storage/redis.py +0 -336
  537. agno/storage/session/__init__.py +0 -16
  538. agno/storage/session/agent.py +0 -64
  539. agno/storage/session/team.py +0 -63
  540. agno/storage/session/v2/__init__.py +0 -5
  541. agno/storage/session/workflow.py +0 -61
  542. agno/storage/singlestore.py +0 -606
  543. agno/storage/sqlite.py +0 -646
  544. agno/storage/workflow/__init__.py +0 -0
  545. agno/storage/workflow/mongodb.py +0 -1
  546. agno/storage/workflow/postgres.py +0 -1
  547. agno/storage/workflow/sqlite.py +0 -1
  548. agno/storage/yaml.py +0 -241
  549. agno/tools/thinking.py +0 -73
  550. agno/utils/defaults.py +0 -57
  551. agno/utils/filesystem.py +0 -39
  552. agno/utils/git.py +0 -52
  553. agno/utils/json_io.py +0 -30
  554. agno/utils/load_env.py +0 -19
  555. agno/utils/py_io.py +0 -19
  556. agno/utils/pyproject.py +0 -18
  557. agno/utils/resource_filter.py +0 -31
  558. agno/workflow/v2/__init__.py +0 -21
  559. agno/workflow/v2/types.py +0 -357
  560. agno/workflow/v2/workflow.py +0 -3312
  561. agno/workspace/__init__.py +0 -0
  562. agno/workspace/config.py +0 -325
  563. agno/workspace/enums.py +0 -6
  564. agno/workspace/helpers.py +0 -52
  565. agno/workspace/operator.py +0 -757
  566. agno/workspace/settings.py +0 -158
  567. agno-1.8.0.dist-info/METADATA +0 -979
  568. agno-1.8.0.dist-info/RECORD +0 -565
  569. agno-1.8.0.dist-info/entry_points.txt +0 -3
  570. /agno/{app → db/migrations}/__init__.py +0 -0
  571. /agno/{app/playground/__init__.py → db/schemas/metrics.py} +0 -0
  572. /agno/{cli → integrations}/__init__.py +0 -0
  573. /agno/{cli/ws → knowledge/chunking}/__init__.py +0 -0
  574. /agno/{document/chunking → knowledge/remote_content}/__init__.py +0 -0
  575. /agno/{document/reader/gcs → knowledge/reranker}/__init__.py +0 -0
  576. /agno/{document/reader/s3 → os/interfaces}/__init__.py +0 -0
  577. /agno/{app → os/interfaces}/slack/security.py +0 -0
  578. /agno/{app → os/interfaces}/whatsapp/security.py +0 -0
  579. /agno/{file/local → utils/print_response}/__init__.py +0 -0
  580. /agno/{infra → vectordb/llamaindex}/__init__.py +0 -0
  581. {agno-1.8.0.dist-info → agno-2.0.0a1.dist-info}/WHEEL +0 -0
  582. {agno-1.8.0.dist-info → agno-2.0.0a1.dist-info}/licenses/LICENSE +0 -0
  583. {agno-1.8.0.dist-info → agno-2.0.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1749 @@
1
+ import json
2
+ import time
3
+ from datetime import date, datetime, timedelta, timezone
4
+ from os import getenv
5
+ from typing import Any, Dict, List, Optional, Tuple, Union
6
+
7
+ from agno.db.base import BaseDb, SessionType
8
+ from agno.db.dynamo.schemas import get_table_schema_definition
9
+ from agno.db.dynamo.utils import (
10
+ apply_pagination,
11
+ apply_sorting,
12
+ build_query_filter_expression,
13
+ build_topic_filter_expression,
14
+ calculate_date_metrics,
15
+ create_table_if_not_exists,
16
+ deserialize_eval_record,
17
+ deserialize_from_dynamodb_item,
18
+ deserialize_knowledge_row,
19
+ deserialize_session,
20
+ deserialize_session_result,
21
+ execute_query_with_pagination,
22
+ fetch_all_sessions_data,
23
+ get_dates_to_calculate_metrics_for,
24
+ merge_with_existing_session,
25
+ prepare_session_data,
26
+ serialize_eval_record,
27
+ serialize_knowledge_row,
28
+ serialize_to_dynamo_item,
29
+ )
30
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
31
+ from agno.db.schemas.knowledge import KnowledgeRow
32
+ from agno.db.schemas.memory import UserMemory
33
+ from agno.session import AgentSession, Session, TeamSession, WorkflowSession
34
+ from agno.utils.log import log_debug, log_error
35
+
36
+ try:
37
+ import boto3 # type: ignore[import-untyped]
38
+ except ImportError:
39
+ raise ImportError("`boto3` not installed. Please install it using `pip install boto3`")
40
+
41
+
42
+ # DynamoDB batch_write_item has a hard limit of 25 items per request
43
+ DYNAMO_BATCH_SIZE_LIMIT = 25
44
+
45
+
46
+ class DynamoDb(BaseDb):
47
+ def __init__(
48
+ self,
49
+ db_client=None,
50
+ region_name: Optional[str] = None,
51
+ aws_access_key_id: Optional[str] = None,
52
+ aws_secret_access_key: Optional[str] = None,
53
+ session_table: Optional[str] = None,
54
+ memory_table: Optional[str] = None,
55
+ metrics_table: Optional[str] = None,
56
+ eval_table: Optional[str] = None,
57
+ knowledge_table: Optional[str] = None,
58
+ ):
59
+ """
60
+ Interface for interacting with a DynamoDB database.
61
+
62
+ Args:
63
+ db_client: The DynamoDB client to use.
64
+ region_name: AWS region name.
65
+ aws_access_key_id: AWS access key ID.
66
+ aws_secret_access_key: AWS secret access key.
67
+ session_table: The name of the session table.
68
+ memory_table: The name of the memory table.
69
+ metrics_table: The name of the metrics table.
70
+ eval_table: The name of the eval table.
71
+ knowledge_table: The name of the knowledge table.
72
+ """
73
+ super().__init__(
74
+ session_table=session_table,
75
+ memory_table=memory_table,
76
+ metrics_table=metrics_table,
77
+ eval_table=eval_table,
78
+ knowledge_table=knowledge_table,
79
+ )
80
+
81
+ if db_client is not None:
82
+ self.client = db_client
83
+ else:
84
+ if not region_name and not getenv("AWS_REGION"):
85
+ raise ValueError("AWS_REGION is not set. Please set the AWS_REGION environment variable.")
86
+ if not aws_access_key_id and not getenv("AWS_ACCESS_KEY_ID"):
87
+ raise ValueError("AWS_ACCESS_KEY_ID is not set. Please set the AWS_ACCESS_KEY_ID environment variable.")
88
+ if not aws_secret_access_key and not getenv("AWS_SECRET_ACCESS_KEY"):
89
+ raise ValueError(
90
+ "AWS_SECRET_ACCESS_KEY is not set. Please set the AWS_SECRET_ACCESS_KEY environment variable."
91
+ )
92
+
93
+ session_kwargs = {}
94
+ session_kwargs["region_name"] = region_name or getenv("AWS_REGION")
95
+ session_kwargs["aws_access_key_id"] = aws_access_key_id or getenv("AWS_ACCESS_KEY_ID")
96
+ session_kwargs["aws_secret_access_key"] = aws_secret_access_key or getenv("AWS_SECRET_ACCESS_KEY")
97
+
98
+ session = boto3.Session(**session_kwargs)
99
+ self.client = session.client("dynamodb")
100
+
101
+ def _create_tables(self):
102
+ tables_to_create = [
103
+ (self.session_table_name, "sessions"),
104
+ (self.memory_table_name, "memories"),
105
+ (self.metrics_table_name, "metrics"),
106
+ (self.eval_table_name, "evals"),
107
+ (self.knowledge_table_name, "knowledge_sources"),
108
+ ]
109
+
110
+ for table_name, table_type in tables_to_create:
111
+ if table_name:
112
+ try:
113
+ schema = get_table_schema_definition(table_type)
114
+ schema["TableName"] = table_name
115
+ create_table_if_not_exists(self.client, table_name, schema)
116
+
117
+ except Exception as e:
118
+ log_error(f"Failed to create table {table_name}: {e}")
119
+
120
+ def _table_exists(self, table_name: str) -> bool:
121
+ """Check if a DynamoDB table with the given name exists.
122
+
123
+ Args:
124
+ table_name: The name of the table to check
125
+
126
+ Returns:
127
+ bool: True if the table exists, False otherwise
128
+ """
129
+ try:
130
+ self.client.describe_table(TableName=table_name)
131
+ return True
132
+ except self.client.exceptions.ResourceNotFoundException:
133
+ return False
134
+ except Exception as e:
135
+ log_error(f"Error checking if table {table_name} exists: {e}")
136
+ return False
137
+
138
+ def _get_table(self, table_type: str, create_table_if_not_found: Optional[bool] = True) -> Optional[str]:
139
+ """
140
+ Get table name and ensure the table exists, creating it if needed.
141
+
142
+ Args:
143
+ table_type: Type of table ("sessions", "memories", "metrics", "evals", "knowledge_sources")
144
+
145
+ Returns:
146
+ str: The table name
147
+
148
+ Raises:
149
+ ValueError: If table name is not configured or table type is unknown
150
+ """
151
+ table_name = None
152
+
153
+ if table_type == "sessions":
154
+ table_name = self.session_table_name
155
+ elif table_type == "memories":
156
+ table_name = self.memory_table_name
157
+ elif table_type == "metrics":
158
+ table_name = self.metrics_table_name
159
+ elif table_type == "evals":
160
+ table_name = self.eval_table_name
161
+ elif table_type == "knowledge":
162
+ table_name = self.knowledge_table_name
163
+ else:
164
+ raise ValueError(f"Unknown table type: {table_type}")
165
+
166
+ # Check if table exists, create if it doesn't
167
+ if not self._table_exists(table_name) and create_table_if_not_found:
168
+ schema = get_table_schema_definition(table_type)
169
+ schema["TableName"] = table_name
170
+ create_table_if_not_exists(self.client, table_name, schema)
171
+
172
+ return table_name
173
+
174
+ # --- Sessions ---
175
+
176
+ def delete_session(self, session_id: Optional[str] = None, session_type: Optional[SessionType] = None) -> bool:
177
+ """
178
+ Delete a session from the database.
179
+
180
+ Args:
181
+ session_id: The ID of the session to delete.
182
+
183
+ Raises:
184
+ Exception: If any error occurs while deleting the session.
185
+ """
186
+ if not session_id:
187
+ return False
188
+
189
+ try:
190
+ self.client.delete_item(
191
+ TableName=self.session_table_name,
192
+ Key={"session_id": {"S": session_id}},
193
+ )
194
+ return True
195
+
196
+ except Exception as e:
197
+ log_error(f"Failed to delete session {session_id}: {e}")
198
+ raise e
199
+
200
+ def delete_sessions(self, session_ids: List[str]) -> None:
201
+ """
202
+ Delete sessions from the database in batches.
203
+
204
+ Args:
205
+ session_ids: List of session IDs to delete
206
+
207
+ Raises:
208
+ Exception: If any error occurs while deleting the sessions.
209
+ """
210
+ if not session_ids or not self.session_table_name:
211
+ return
212
+
213
+ try:
214
+ # Process the items to delete in batches of the max allowed size or less
215
+ for i in range(0, len(session_ids), DYNAMO_BATCH_SIZE_LIMIT):
216
+ batch = session_ids[i : i + DYNAMO_BATCH_SIZE_LIMIT]
217
+ delete_requests = []
218
+
219
+ for session_id in batch:
220
+ delete_requests.append({"DeleteRequest": {"Key": {"session_id": {"S": session_id}}}})
221
+
222
+ if delete_requests:
223
+ self.client.batch_write_item(RequestItems={self.session_table_name: delete_requests})
224
+
225
+ except Exception as e:
226
+ log_error(f"Failed to delete sessions: {e}")
227
+
228
+ def get_session(
229
+ self,
230
+ session_id: str,
231
+ session_type: Optional[SessionType] = None,
232
+ user_id: Optional[str] = None,
233
+ deserialize: Optional[bool] = True,
234
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
235
+ """
236
+ Get a session from the database as a Session object.
237
+
238
+ Args:
239
+ session_id (str): The ID of the session to get.
240
+ session_type (Optional[SessionType]): The type of session to get.
241
+ user_id (Optional[str]): The ID of the user to get the session for.
242
+ deserialize (Optional[bool]): Whether to deserialize the session.
243
+
244
+ Returns:
245
+ Optional[Session]: The session data as a Session object.
246
+
247
+ Raises:
248
+ Exception: If any error occurs while getting the session.
249
+ """
250
+ try:
251
+ table_name = self._get_table("sessions")
252
+ response = self.client.get_item(
253
+ TableName=table_name,
254
+ Key={"session_id": {"S": session_id}},
255
+ )
256
+
257
+ item = response.get("Item")
258
+ if not item:
259
+ return None
260
+
261
+ session = deserialize_from_dynamodb_item(item)
262
+
263
+ if session_type and session.get("session_type") != session_type.value:
264
+ return None
265
+ if user_id and session.get("user_id") != user_id:
266
+ return None
267
+
268
+ if not session:
269
+ return None
270
+
271
+ if not deserialize:
272
+ return session
273
+
274
+ if session_type == SessionType.AGENT:
275
+ return AgentSession.from_dict(session)
276
+ elif session_type == SessionType.TEAM:
277
+ return TeamSession.from_dict(session)
278
+ else:
279
+ return WorkflowSession.from_dict(session)
280
+
281
+ except Exception as e:
282
+ log_error(f"Failed to get session {session_id}: {e}")
283
+ return None
284
+
285
+ def get_sessions(
286
+ self,
287
+ session_type: SessionType,
288
+ user_id: Optional[str] = None,
289
+ component_id: Optional[str] = None,
290
+ session_name: Optional[str] = None,
291
+ start_timestamp: Optional[int] = None,
292
+ end_timestamp: Optional[int] = None,
293
+ limit: Optional[int] = None,
294
+ page: Optional[int] = None,
295
+ sort_by: Optional[str] = None,
296
+ sort_order: Optional[str] = None,
297
+ deserialize: Optional[bool] = True,
298
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
299
+ try:
300
+ table_name = self._get_table("sessions")
301
+ if table_name is None:
302
+ return [] if deserialize else ([], 0)
303
+
304
+ # Build filter expression for additional filters
305
+ filter_expression = None
306
+ expression_attribute_names = {}
307
+ expression_attribute_values = {":session_type": {"S": session_type.value}}
308
+
309
+ if user_id:
310
+ filter_expression = "#user_id = :user_id"
311
+ expression_attribute_names["#user_id"] = "user_id"
312
+ expression_attribute_values[":user_id"] = {"S": user_id}
313
+
314
+ if component_id:
315
+ # Map component_id to the appropriate field based on session type
316
+ if session_type == SessionType.AGENT:
317
+ component_filter = "#agent_id = :component_id"
318
+ expression_attribute_names["#agent_id"] = "agent_id"
319
+ elif session_type == SessionType.TEAM:
320
+ component_filter = "#team_id = :component_id"
321
+ expression_attribute_names["#team_id"] = "team_id"
322
+ else:
323
+ component_filter = "#workflow_id = :component_id"
324
+ expression_attribute_names["#workflow_id"] = "workflow_id"
325
+
326
+ if component_filter:
327
+ expression_attribute_values[":component_id"] = {"S": component_id}
328
+ if filter_expression:
329
+ filter_expression += f" AND {component_filter}"
330
+ else:
331
+ filter_expression = component_filter
332
+
333
+ if session_name:
334
+ name_filter = "#session_name = :session_name"
335
+ expression_attribute_names["#session_name"] = "session_name"
336
+ expression_attribute_values[":session_name"] = {"S": session_name}
337
+ if filter_expression:
338
+ filter_expression += f" AND {name_filter}"
339
+ else:
340
+ filter_expression = name_filter
341
+
342
+ # Use GSI query for session_type
343
+ query_kwargs = {
344
+ "TableName": table_name,
345
+ "IndexName": "session_type-created_at-index",
346
+ "KeyConditionExpression": "session_type = :session_type",
347
+ "ExpressionAttributeValues": expression_attribute_values,
348
+ }
349
+ if filter_expression:
350
+ query_kwargs["FilterExpression"] = filter_expression
351
+ if expression_attribute_names:
352
+ query_kwargs["ExpressionAttributeNames"] = expression_attribute_names
353
+
354
+ # Apply sorting
355
+ if sort_by == "created_at":
356
+ query_kwargs["ScanIndexForward"] = sort_order != "desc" # type: ignore
357
+
358
+ # Apply limit at DynamoDB level
359
+ if limit and not page:
360
+ query_kwargs["Limit"] = limit # type: ignore
361
+
362
+ items = []
363
+ response = self.client.query(**query_kwargs)
364
+ items.extend(response.get("Items", []))
365
+
366
+ # Handle pagination
367
+ while "LastEvaluatedKey" in response:
368
+ query_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
369
+ response = self.client.query(**query_kwargs)
370
+ items.extend(response.get("Items", []))
371
+
372
+ # Convert DynamoDB items to session data
373
+ sessions_data = []
374
+ for item in items:
375
+ session_data = deserialize_from_dynamodb_item(item)
376
+ if session_data:
377
+ sessions_data.append(session_data)
378
+
379
+ # Apply in-memory sorting for fields not supported by DynamoDB
380
+ if sort_by and sort_by != "created_at":
381
+ sessions_data = apply_sorting(sessions_data, sort_by, sort_order)
382
+
383
+ # Get total count before pagination
384
+ total_count = len(sessions_data)
385
+
386
+ # Apply pagination
387
+ if page:
388
+ sessions_data = apply_pagination(sessions_data, limit, page)
389
+
390
+ if not deserialize:
391
+ return sessions_data, total_count
392
+
393
+ sessions = []
394
+ for session_data in sessions_data:
395
+ session = deserialize_session(session_data)
396
+ if session:
397
+ sessions.append(session)
398
+
399
+ return sessions
400
+
401
+ except Exception as e:
402
+ log_error(f"Failed to get sessions: {e}")
403
+ return []
404
+
405
+ def rename_session(
406
+ self,
407
+ session_id: str,
408
+ session_type: SessionType,
409
+ session_name: str,
410
+ deserialize: Optional[bool] = True,
411
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
412
+ """
413
+ Rename a session in the database.
414
+
415
+ Args:
416
+ session_id: The ID of the session to rename.
417
+ session_type: The type of session to rename.
418
+ session_name: The new name for the session.
419
+
420
+ Returns:
421
+ Optional[Session]: The renamed session if successful, None otherwise.
422
+
423
+ Raises:
424
+ Exception: If any error occurs while renaming the session.
425
+ """
426
+ try:
427
+ if not self.session_table_name:
428
+ raise Exception("Sessions table not found")
429
+
430
+ # Get current session_data
431
+ get_response = self.client.get_item(
432
+ TableName=self.session_table_name,
433
+ Key={"session_id": {"S": session_id}},
434
+ )
435
+ current_item = get_response.get("Item")
436
+ if not current_item:
437
+ return None
438
+
439
+ # Update session_data with the new session_name
440
+ session_data = deserialize_from_dynamodb_item(current_item).get("session_data", {})
441
+ session_data["session_name"] = session_name
442
+ response = self.client.update_item(
443
+ TableName=self.session_table_name,
444
+ Key={"session_id": {"S": session_id}},
445
+ UpdateExpression="SET session_data = :session_data, updated_at = :updated_at",
446
+ ConditionExpression="session_type = :session_type",
447
+ ExpressionAttributeValues={
448
+ ":session_data": {"S": json.dumps(session_data)},
449
+ ":session_type": {"S": session_type.value},
450
+ ":updated_at": {"N": str(int(time.time()))},
451
+ },
452
+ ReturnValues="ALL_NEW",
453
+ )
454
+ item = response.get("Attributes")
455
+ if not item:
456
+ return None
457
+
458
+ session = deserialize_from_dynamodb_item(item)
459
+ if not deserialize:
460
+ return session
461
+
462
+ if session_type == SessionType.AGENT:
463
+ return AgentSession.from_dict(session)
464
+ elif session_type == SessionType.TEAM:
465
+ return TeamSession.from_dict(session)
466
+ else:
467
+ return WorkflowSession.from_dict(session)
468
+
469
+ except Exception as e:
470
+ log_error(f"Failed to rename session {session_id}: {e}")
471
+ return None
472
+
473
+ def upsert_session(
474
+ self, session: Session, deserialize: Optional[bool] = True
475
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
476
+ """
477
+ Upsert a session into the database.
478
+
479
+ This method provides true upsert behavior: creates a new session if it doesn't exist,
480
+ or updates an existing session while preserving important fields.
481
+
482
+ Args:
483
+ session (Session): The session to upsert.
484
+ deserialize (Optional[bool]): Whether to deserialize the session.
485
+
486
+ Returns:
487
+ Optional[Session]: The upserted session if successful, None otherwise.
488
+ """
489
+ try:
490
+ table_name = self._get_table("sessions", create_table_if_not_found=True)
491
+
492
+ # Get session if it already exists in the db.
493
+ # We need to do this to handle updating nested fields.
494
+ response = self.client.get_item(TableName=table_name, Key={"session_id": {"S": session.session_id}})
495
+ existing_item = response.get("Item")
496
+
497
+ # Prepare the session to upsert, merging with existing session if it exists.
498
+ serialized_session = prepare_session_data(session)
499
+ if existing_item:
500
+ serialized_session = merge_with_existing_session(serialized_session, existing_item)
501
+ serialized_session["updated_at"] = int(time.time())
502
+ else:
503
+ serialized_session["updated_at"] = serialized_session["created_at"]
504
+
505
+ # Upsert
506
+ item = serialize_to_dynamo_item(serialized_session)
507
+ self.client.put_item(TableName=table_name, Item=item)
508
+
509
+ log_debug(f"Upserted session with id '{session.session_id}'")
510
+
511
+ return deserialize_session_result(serialized_session, session, deserialize)
512
+
513
+ except Exception as e:
514
+ log_error(f"Failed to upsert session {session.session_id}: {e}")
515
+ return None
516
+
517
+ # --- User Memory ---
518
+
519
+ def delete_user_memory(self, memory_id: str) -> None:
520
+ """
521
+ Delete a user memory from the database.
522
+
523
+ Args:
524
+ memory_id: The ID of the memory to delete.
525
+
526
+ Raises:
527
+ Exception: If any error occurs while deleting the user memory.
528
+ """
529
+ try:
530
+ self.client.delete_item(
531
+ TableName=self.memory_table_name,
532
+ Key={"memory_id": {"S": memory_id}},
533
+ )
534
+ log_debug(f"Deleted user memory {memory_id}")
535
+
536
+ except Exception as e:
537
+ log_error(f"Failed to delete user memory {memory_id}: {e}")
538
+
539
+ def delete_user_memories(self, memory_ids: List[str]) -> None:
540
+ """
541
+ Delete user memories from the database in batches.
542
+
543
+ Args:
544
+ memory_ids: List of memory IDs to delete
545
+
546
+ Raises:
547
+ Exception: If any error occurs while deleting the user memories.
548
+ """
549
+
550
+ try:
551
+ for i in range(0, len(memory_ids), DYNAMO_BATCH_SIZE_LIMIT):
552
+ batch = memory_ids[i : i + DYNAMO_BATCH_SIZE_LIMIT]
553
+
554
+ delete_requests = []
555
+ for memory_id in batch:
556
+ delete_requests.append({"DeleteRequest": {"Key": {"memory_id": {"S": memory_id}}}})
557
+
558
+ self.client.batch_write_item(RequestItems={self.memory_table_name: delete_requests})
559
+
560
+ except Exception as e:
561
+ log_error(f"Failed to delete user memories: {e}")
562
+
563
+ def get_all_memory_topics(self) -> List[str]:
564
+ """Get all memory topics from the database.
565
+
566
+ Returns:
567
+ List[str]: List of unique memory topics.
568
+ """
569
+ try:
570
+ table_name = self._get_table("memories")
571
+ if table_name is None:
572
+ return []
573
+
574
+ # Scan the entire table to get all memories
575
+ response = self.client.scan(TableName=table_name)
576
+ items = response.get("Items", [])
577
+
578
+ # Handle pagination
579
+ while "LastEvaluatedKey" in response:
580
+ response = self.client.scan(TableName=table_name, ExclusiveStartKey=response["LastEvaluatedKey"])
581
+ items.extend(response.get("Items", []))
582
+
583
+ # Extract topics from all memories
584
+ all_topics = set()
585
+ for item in items:
586
+ memory_data = deserialize_from_dynamodb_item(item)
587
+ topics = memory_data.get("memory", {}).get("topics", [])
588
+ all_topics.update(topics)
589
+
590
+ return list(all_topics)
591
+
592
+ except Exception as e:
593
+ log_error(f"Exception reading from memory table: {e}")
594
+ return []
595
+
596
+ def get_user_memory(
597
+ self, memory_id: str, deserialize: Optional[bool] = True
598
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
599
+ """
600
+ Get a user memory from the database as a UserMemory object.
601
+
602
+ Args:
603
+ memory_id: The ID of the memory to get.
604
+
605
+ Returns:
606
+ Optional[UserMemory]: The user memory data if found, None otherwise.
607
+
608
+ Raises:
609
+ Exception: If any error occurs while getting the user memory.
610
+ """
611
+ try:
612
+ table_name = self._get_table("memories")
613
+ response = self.client.get_item(TableName=table_name, Key={"memory_id": {"S": memory_id}})
614
+
615
+ item = response.get("Item")
616
+ if not item:
617
+ return None
618
+
619
+ item = deserialize_from_dynamodb_item(item)
620
+ if not deserialize:
621
+ return item
622
+
623
+ return UserMemory.from_dict(item)
624
+
625
+ except Exception as e:
626
+ log_error(f"Failed to get user memory {memory_id}: {e}")
627
+ return None
628
+
629
+ def get_user_memories(
630
+ self,
631
+ user_id: Optional[str] = None,
632
+ agent_id: Optional[str] = None,
633
+ team_id: Optional[str] = None,
634
+ topics: Optional[List[str]] = None,
635
+ search_content: Optional[str] = None,
636
+ limit: Optional[int] = None,
637
+ page: Optional[int] = None,
638
+ sort_by: Optional[str] = None,
639
+ sort_order: Optional[str] = None,
640
+ deserialize: Optional[bool] = True,
641
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
642
+ """
643
+ Get user memories from the database as a list of UserMemory objects.
644
+
645
+ Args:
646
+ user_id: The ID of the user to get the memories for.
647
+ agent_id: The ID of the agent to get the memories for.
648
+ team_id: The ID of the team to get the memories for.
649
+ workflow_id: The ID of the workflow to get the memories for.
650
+ topics: The topics to filter the memories by.
651
+ search_content: The content to search for in the memories.
652
+ limit: The maximum number of memories to return.
653
+ page: The page number to return.
654
+ sort_by: The field to sort the memories by.
655
+ sort_order: The order to sort the memories by.
656
+ deserialize: Whether to deserialize the memories.
657
+
658
+ Returns:
659
+ Union[List[UserMemory], List[Dict[str, Any]], Tuple[List[Dict[str, Any]], int]]: The user memories data.
660
+
661
+ Raises:
662
+ Exception: If any error occurs while getting the user memories.
663
+ """
664
+ try:
665
+ table_name = self._get_table("memories")
666
+ if table_name is None:
667
+ return [] if deserialize else ([], 0)
668
+
669
+ # Build filter expressions for component filters
670
+ (
671
+ filter_expression,
672
+ expression_attribute_names,
673
+ expression_attribute_values,
674
+ ) = build_query_filter_expression(filters={"agent_id": agent_id, "team_id": team_id})
675
+
676
+ # Build topic filter expression if topics provided
677
+ if topics:
678
+ topic_filter, topic_values = build_topic_filter_expression(topics)
679
+ expression_attribute_values.update(topic_values)
680
+ filter_expression = f"{filter_expression} AND {topic_filter}" if filter_expression else topic_filter
681
+
682
+ # Add search content filter if provided
683
+ if search_content:
684
+ search_filter = "contains(memory, :search_content)"
685
+ expression_attribute_values[":search_content"] = {"S": search_content}
686
+ filter_expression = f"{filter_expression} AND {search_filter}" if filter_expression else search_filter
687
+
688
+ # Determine whether to use GSI query or table scan
689
+ if user_id:
690
+ # Use GSI query when user_id is provided
691
+ key_condition_expression = "#user_id = :user_id"
692
+
693
+ # Set up expression attributes for GSI key condition
694
+ expression_attribute_names["#user_id"] = "user_id"
695
+ expression_attribute_values[":user_id"] = {"S": user_id}
696
+
697
+ # Execute query with pagination
698
+ items = execute_query_with_pagination(
699
+ self.client,
700
+ table_name,
701
+ "user_id-updated_at-index",
702
+ key_condition_expression,
703
+ expression_attribute_names,
704
+ expression_attribute_values,
705
+ filter_expression,
706
+ sort_by,
707
+ sort_order,
708
+ limit,
709
+ page,
710
+ )
711
+ else:
712
+ # Use table scan when user_id is None
713
+ scan_kwargs = {"TableName": table_name}
714
+
715
+ if filter_expression:
716
+ scan_kwargs["FilterExpression"] = filter_expression
717
+ if expression_attribute_names:
718
+ scan_kwargs["ExpressionAttributeNames"] = expression_attribute_names # type: ignore
719
+ if expression_attribute_values:
720
+ scan_kwargs["ExpressionAttributeValues"] = expression_attribute_values # type: ignore
721
+
722
+ # Execute scan
723
+ response = self.client.scan(**scan_kwargs)
724
+ items = response.get("Items", [])
725
+
726
+ # Handle pagination for scan
727
+ while "LastEvaluatedKey" in response:
728
+ scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
729
+ response = self.client.scan(**scan_kwargs)
730
+ items.extend(response.get("Items", []))
731
+
732
+ items = [deserialize_from_dynamodb_item(item) for item in items]
733
+
734
+ if sort_by and sort_by != "updated_at":
735
+ items = apply_sorting(items, sort_by, sort_order)
736
+
737
+ if page:
738
+ paginated_items = apply_pagination(items, limit, page)
739
+
740
+ if not deserialize:
741
+ return paginated_items, len(items)
742
+
743
+ return [UserMemory.from_dict(item) for item in items]
744
+
745
+ except Exception as e:
746
+ log_error(f"Failed to get user memories: {e}")
747
+ return [] if deserialize else ([], 0)
748
+
749
+ def get_user_memory_stats(
750
+ self,
751
+ limit: Optional[int] = None,
752
+ page: Optional[int] = None,
753
+ ) -> Tuple[List[Dict[str, Any]], int]:
754
+ """Get user memories stats.
755
+
756
+ Args:
757
+ limit (Optional[int]): The maximum number of user stats to return.
758
+ page (Optional[int]): The page number.
759
+
760
+ Returns:
761
+ Tuple[List[Dict[str, Any]], int]: A list of dictionaries containing user stats and total count.
762
+
763
+ Example:
764
+ (
765
+ [
766
+ {
767
+ "user_id": "123",
768
+ "total_memories": 10,
769
+ "last_memory_updated_at": 1714560000,
770
+ },
771
+ ],
772
+ total_count: 1,
773
+ )
774
+ """
775
+ try:
776
+ table_name = self._get_table("memories")
777
+
778
+ response = self.client.scan(TableName=table_name)
779
+ items = response.get("Items", [])
780
+
781
+ # Handle pagination
782
+ while "LastEvaluatedKey" in response:
783
+ response = self.client.scan(TableName=table_name, ExclusiveStartKey=response["LastEvaluatedKey"])
784
+ items.extend(response.get("Items", []))
785
+
786
+ # Aggregate stats by user_id
787
+ user_stats = {}
788
+ for item in items:
789
+ memory_data = deserialize_from_dynamodb_item(item)
790
+ user_id = memory_data.get("user_id")
791
+
792
+ if user_id:
793
+ if user_id not in user_stats:
794
+ user_stats[user_id] = {
795
+ "user_id": user_id,
796
+ "total_memories": 0,
797
+ "last_memory_updated_at": None,
798
+ }
799
+
800
+ user_stats[user_id]["total_memories"] += 1
801
+
802
+ updated_at = memory_data.get("updated_at")
803
+ if updated_at:
804
+ updated_at_dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
805
+ updated_at_timestamp = int(updated_at_dt.timestamp())
806
+
807
+ if updated_at_timestamp and (
808
+ user_stats[user_id]["last_memory_updated_at"] is None
809
+ or updated_at_timestamp > user_stats[user_id]["last_memory_updated_at"]
810
+ ):
811
+ user_stats[user_id]["last_memory_updated_at"] = updated_at_timestamp
812
+
813
+ # Convert to list and apply sorting
814
+ stats_list = list(user_stats.values())
815
+ stats_list.sort(
816
+ key=lambda x: (x["last_memory_updated_at"] if x["last_memory_updated_at"] is not None else 0),
817
+ reverse=True,
818
+ )
819
+
820
+ total_count = len(stats_list)
821
+
822
+ # Apply pagination
823
+ if limit is not None:
824
+ start_index = 0
825
+ if page is not None and page > 1:
826
+ start_index = (page - 1) * limit
827
+ stats_list = stats_list[start_index : start_index + limit]
828
+
829
+ return stats_list, total_count
830
+
831
+ except Exception as e:
832
+ log_error(f"Failed to get user memory stats: {e}")
833
+ return [], 0
834
+
835
+ def upsert_user_memory(
836
+ self, memory: UserMemory, deserialize: Optional[bool] = True
837
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
838
+ """
839
+ Upsert a user memory into the database.
840
+
841
+ Args:
842
+ memory: The memory to upsert.
843
+
844
+ Returns:
845
+ Optional[Dict[str, Any]]: The upserted memory data if successful, None otherwise.
846
+ """
847
+ try:
848
+ table_name = self._get_table("memories", create_table_if_not_found=True)
849
+ memory_dict = memory.to_dict()
850
+ memory_dict["updated_at"] = datetime.now(timezone.utc).isoformat()
851
+ item = serialize_to_dynamo_item(memory_dict)
852
+
853
+ self.client.put_item(TableName=table_name, Item=item)
854
+
855
+ log_debug(f"Upserted user memory with id '{memory.memory_id}'")
856
+
857
+ if not deserialize:
858
+ return memory_dict
859
+
860
+ return UserMemory.from_dict(memory_dict)
861
+
862
+ except Exception as e:
863
+ log_error(f"Failed to upsert user memory: {e}")
864
+ return None
865
+
866
+ def clear_memories(self) -> None:
867
+ """Delete all memories from the database.
868
+
869
+ Raises:
870
+ Exception: If an error occurs during deletion.
871
+ """
872
+ try:
873
+ table_name = self._get_table("memories")
874
+
875
+ # Scan the table to get all items
876
+ response = self.client.scan(TableName=table_name)
877
+ items = response.get("Items", [])
878
+
879
+ # Handle pagination for scan
880
+ while "LastEvaluatedKey" in response:
881
+ response = self.client.scan(TableName=table_name, ExclusiveStartKey=response["LastEvaluatedKey"])
882
+ items.extend(response.get("Items", []))
883
+
884
+ if not items:
885
+ return
886
+
887
+ # Delete items in batches
888
+ for i in range(0, len(items), DYNAMO_BATCH_SIZE_LIMIT):
889
+ batch = items[i : i + DYNAMO_BATCH_SIZE_LIMIT]
890
+
891
+ delete_requests = []
892
+ for item in batch:
893
+ # Extract the memory_id from the item
894
+ memory_id = item.get("memory_id", {}).get("S")
895
+ if memory_id:
896
+ delete_requests.append({"DeleteRequest": {"Key": {"memory_id": {"S": memory_id}}}})
897
+
898
+ if delete_requests:
899
+ self.client.batch_write_item(RequestItems={table_name: delete_requests})
900
+
901
+ except Exception as e:
902
+ from agno.utils.log import log_warning
903
+
904
+ log_warning(f"Exception deleting all memories: {e}")
905
+
906
+ # --- Metrics ---
907
+
908
+ def calculate_metrics(self) -> Optional[Any]:
909
+ """Calculate metrics for all dates without complete metrics.
910
+
911
+ Returns:
912
+ Optional[Any]: The calculated metrics or None if no metrics table.
913
+
914
+ Raises:
915
+ Exception: If an error occurs during metrics calculation.
916
+ """
917
+ if not self.metrics_table_name:
918
+ return None
919
+
920
+ try:
921
+ from agno.utils.log import log_info
922
+
923
+ # Get starting date for metrics calculation
924
+ starting_date = self._get_metrics_calculation_starting_date()
925
+ if starting_date is None:
926
+ log_info("No session data found. Won't calculate metrics.")
927
+ return None
928
+
929
+ # Get dates that need metrics calculation
930
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
931
+ if not dates_to_process:
932
+ log_info("Metrics already calculated for all relevant dates.")
933
+ return None
934
+
935
+ # Get timestamp range for session data
936
+ start_timestamp = int(datetime.combine(dates_to_process[0], datetime.min.time()).timestamp())
937
+ end_timestamp = int(
938
+ datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time()).timestamp()
939
+ )
940
+
941
+ # Get all sessions for the date range
942
+ sessions = self._get_all_sessions_for_metrics_calculation(
943
+ start_timestamp=start_timestamp, end_timestamp=end_timestamp
944
+ )
945
+
946
+ # Process session data for metrics calculation
947
+
948
+ all_sessions_data = fetch_all_sessions_data(
949
+ sessions=sessions,
950
+ dates_to_process=dates_to_process,
951
+ start_timestamp=start_timestamp,
952
+ )
953
+
954
+ if not all_sessions_data:
955
+ log_info("No new session data found. Won't calculate metrics.")
956
+ return None
957
+
958
+ # Calculate metrics for each date
959
+ results = []
960
+ metrics_records = []
961
+ for date_to_process in dates_to_process:
962
+ date_key = date_to_process.isoformat()
963
+ sessions_for_date = all_sessions_data.get(date_key, {})
964
+
965
+ # Skip dates with no sessions
966
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
967
+ continue
968
+
969
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
970
+ metrics_records.append(metrics_record)
971
+
972
+ # Store metrics in DynamoDB
973
+ if metrics_records:
974
+ results = self._bulk_upsert_metrics(metrics_records)
975
+
976
+ log_debug("Updated metrics calculations")
977
+
978
+ return results
979
+
980
+ except Exception as e:
981
+ log_error(f"Failed to calculate metrics: {e}")
982
+ return None
983
+
984
+ def _get_metrics_calculation_starting_date(self) -> Optional[date]:
985
+ """Get the first date for which metrics calculation is needed:
986
+ 1. If there are metrics records, return the date of the first day without a complete metrics record.
987
+ 2. If there are no metrics records, return the date of the first recorded session.
988
+ 3. If there are no metrics records and no sessions records, return None.
989
+
990
+ Returns:
991
+ Optional[date]: The starting date for which metrics calculation is needed.
992
+ """
993
+ try:
994
+ metrics_table_name = self._get_table("metrics")
995
+
996
+ # 1. Check for existing metrics records
997
+ response = self.client.scan(
998
+ TableName=metrics_table_name,
999
+ ProjectionExpression="#date, completed",
1000
+ ExpressionAttributeNames={"#date": "date"},
1001
+ Limit=1000, # Get reasonable number of records to find incomplete ones
1002
+ )
1003
+
1004
+ metrics_items = response.get("Items", [])
1005
+
1006
+ # Handle pagination to get all metrics records
1007
+ while "LastEvaluatedKey" in response:
1008
+ response = self.client.scan(
1009
+ TableName=metrics_table_name,
1010
+ ProjectionExpression="#date, completed",
1011
+ ExpressionAttributeNames={"#date": "date"},
1012
+ ExclusiveStartKey=response["LastEvaluatedKey"],
1013
+ Limit=1000,
1014
+ )
1015
+ metrics_items.extend(response.get("Items", []))
1016
+
1017
+ if metrics_items:
1018
+ # Find the latest date with metrics
1019
+ latest_complete_date = None
1020
+ incomplete_dates = []
1021
+
1022
+ for item in metrics_items:
1023
+ metrics_data = deserialize_from_dynamodb_item(item)
1024
+ record_date = datetime.fromisoformat(metrics_data["date"]).date()
1025
+ is_completed = metrics_data.get("completed", False)
1026
+
1027
+ if is_completed:
1028
+ if latest_complete_date is None or record_date > latest_complete_date:
1029
+ latest_complete_date = record_date
1030
+ else:
1031
+ incomplete_dates.append(record_date)
1032
+
1033
+ # Return the earliest incomplete date, or the day after the latest complete date
1034
+ if incomplete_dates:
1035
+ return min(incomplete_dates)
1036
+ elif latest_complete_date:
1037
+ return latest_complete_date + timedelta(days=1)
1038
+
1039
+ # 2. No metrics records. Return the date of the first recorded session.
1040
+ sessions_table_name = self._get_table("sessions")
1041
+
1042
+ earliest_session_date = None
1043
+ for session_type in ["agent", "team", "workflow"]:
1044
+ response = self.client.query(
1045
+ TableName=sessions_table_name,
1046
+ IndexName="session_type-created_at-index",
1047
+ KeyConditionExpression="session_type = :session_type",
1048
+ ExpressionAttributeValues={":session_type": {"S": session_type}},
1049
+ ScanIndexForward=True, # Ascending order to get earliest
1050
+ Limit=1,
1051
+ )
1052
+
1053
+ items = response.get("Items", [])
1054
+ if items:
1055
+ first_session = deserialize_from_dynamodb_item(items[0])
1056
+ first_session_timestamp = first_session.get("created_at")
1057
+
1058
+ if first_session_timestamp:
1059
+ session_date = datetime.fromtimestamp(first_session_timestamp, tz=timezone.utc).date()
1060
+ if earliest_session_date is None or session_date < earliest_session_date:
1061
+ earliest_session_date = session_date
1062
+
1063
+ # 3. Return the earliest session date or None if no sessions exist
1064
+ return earliest_session_date
1065
+
1066
+ except Exception as e:
1067
+ log_error(f"Failed to get metrics calculation starting date: {e}")
1068
+ return None
1069
+
1070
+ def _get_all_sessions_for_metrics_calculation(
1071
+ self, start_timestamp: int, end_timestamp: int
1072
+ ) -> List[Dict[str, Any]]:
1073
+ """Get all sessions within a timestamp range for metrics calculation.
1074
+
1075
+ Args:
1076
+ start_timestamp: Start timestamp (inclusive)
1077
+ end_timestamp: End timestamp (exclusive)
1078
+
1079
+ Returns:
1080
+ List[Dict[str, Any]]: List of session data dictionaries
1081
+ """
1082
+ try:
1083
+ table_name = self._get_table("sessions")
1084
+ all_sessions = []
1085
+
1086
+ # Query sessions by different types within the time range
1087
+ for session_type in ["agent", "team", "workflow"]:
1088
+ response = self.client.query(
1089
+ TableName=table_name,
1090
+ IndexName="session_type-created_at-index",
1091
+ KeyConditionExpression="session_type = :session_type AND created_at BETWEEN :start_ts AND :end_ts",
1092
+ ExpressionAttributeValues={
1093
+ ":session_type": {"S": session_type},
1094
+ ":start_ts": {"N": str(start_timestamp)},
1095
+ ":end_ts": {"N": str(end_timestamp)},
1096
+ },
1097
+ )
1098
+
1099
+ items = response.get("Items", [])
1100
+
1101
+ # Handle pagination
1102
+ while "LastEvaluatedKey" in response:
1103
+ response = self.client.query(
1104
+ TableName=table_name,
1105
+ IndexName="session_type-created_at-index",
1106
+ KeyConditionExpression="session_type = :session_type AND created_at BETWEEN :start_ts AND :end_ts",
1107
+ ExpressionAttributeValues={
1108
+ ":session_type": {"S": session_type},
1109
+ ":start_ts": {"N": str(start_timestamp)},
1110
+ ":end_ts": {"N": str(end_timestamp)},
1111
+ },
1112
+ ExclusiveStartKey=response["LastEvaluatedKey"],
1113
+ )
1114
+ items.extend(response.get("Items", []))
1115
+
1116
+ # Deserialize sessions
1117
+ for item in items:
1118
+ session_data = deserialize_from_dynamodb_item(item)
1119
+ if session_data:
1120
+ all_sessions.append(session_data)
1121
+
1122
+ return all_sessions
1123
+
1124
+ except Exception as e:
1125
+ log_error(f"Failed to get sessions for metrics calculation: {e}")
1126
+ return []
1127
+
1128
+ def _bulk_upsert_metrics(self, metrics_records: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
1129
+ """Bulk upsert metrics records into DynamoDB with proper deduplication.
1130
+
1131
+ Args:
1132
+ metrics_records: List of metrics records to upsert
1133
+
1134
+ Returns:
1135
+ List[Dict[str, Any]]: List of upserted records
1136
+ """
1137
+ try:
1138
+ table_name = self._get_table("metrics")
1139
+ if table_name is None:
1140
+ return []
1141
+
1142
+ results = []
1143
+
1144
+ # Process each record individually to handle proper upsert
1145
+ for record in metrics_records:
1146
+ upserted_record = self._upsert_single_metrics_record(table_name, record)
1147
+ if upserted_record:
1148
+ results.append(upserted_record)
1149
+
1150
+ return results
1151
+
1152
+ except Exception as e:
1153
+ log_error(f"Failed to bulk upsert metrics: {e}")
1154
+ return []
1155
+
1156
+ def _upsert_single_metrics_record(self, table_name: str, record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1157
+ """Upsert a single metrics record, checking for existing records with the same date.
1158
+
1159
+ Args:
1160
+ table_name: The DynamoDB table name
1161
+ record: The metrics record to upsert
1162
+
1163
+ Returns:
1164
+ Optional[Dict[str, Any]]: The upserted record or None if failed
1165
+ """
1166
+ try:
1167
+ date_str = record.get("date")
1168
+ if not date_str:
1169
+ log_error("Metrics record missing date field")
1170
+ return None
1171
+
1172
+ # Convert date object to string if needed
1173
+ if hasattr(date_str, "isoformat"):
1174
+ date_str = date_str.isoformat()
1175
+
1176
+ # Check if a record already exists for this date
1177
+ existing_record = self._get_existing_metrics_record(table_name, date_str)
1178
+
1179
+ if existing_record:
1180
+ return self._update_existing_metrics_record(table_name, existing_record, record)
1181
+ else:
1182
+ return self._create_new_metrics_record(table_name, record)
1183
+
1184
+ except Exception as e:
1185
+ log_error(f"Failed to upsert single metrics record: {e}")
1186
+ return None
1187
+
1188
+ def _get_existing_metrics_record(self, table_name: str, date_str: str) -> Optional[Dict[str, Any]]:
1189
+ """Get existing metrics record for a given date.
1190
+
1191
+ Args:
1192
+ table_name: The DynamoDB table name
1193
+ date_str: The date string to search for
1194
+
1195
+ Returns:
1196
+ Optional[Dict[str, Any]]: The existing record or None if not found
1197
+ """
1198
+ try:
1199
+ # Query using the date-aggregation_period-index
1200
+ response = self.client.query(
1201
+ TableName=table_name,
1202
+ IndexName="date-aggregation_period-index",
1203
+ KeyConditionExpression="#date = :date AND aggregation_period = :period",
1204
+ ExpressionAttributeNames={"#date": "date"},
1205
+ ExpressionAttributeValues={
1206
+ ":date": {"S": date_str},
1207
+ ":period": {"S": "daily"},
1208
+ },
1209
+ Limit=1,
1210
+ )
1211
+
1212
+ items = response.get("Items", [])
1213
+ if items:
1214
+ return deserialize_from_dynamodb_item(items[0])
1215
+ return None
1216
+
1217
+ except Exception as e:
1218
+ log_error(f"Failed to get existing metrics record for date {date_str}: {e}")
1219
+ return None
1220
+
1221
+ def _update_existing_metrics_record(
1222
+ self,
1223
+ table_name: str,
1224
+ existing_record: Dict[str, Any],
1225
+ new_record: Dict[str, Any],
1226
+ ) -> Optional[Dict[str, Any]]:
1227
+ """Update an existing metrics record.
1228
+
1229
+ Args:
1230
+ table_name: The DynamoDB table name
1231
+ existing_record: The existing record
1232
+ new_record: The new record data
1233
+
1234
+ Returns:
1235
+ Optional[Dict[str, Any]]: The updated record or None if failed
1236
+ """
1237
+ try:
1238
+ # Use the existing record's ID
1239
+ new_record["id"] = existing_record["id"]
1240
+ new_record["updated_at"] = int(time.time())
1241
+
1242
+ # Prepare and serialize the record
1243
+ prepared_record = self._prepare_metrics_record_for_dynamo(new_record)
1244
+ item = self._serialize_metrics_to_dynamo_item(prepared_record)
1245
+
1246
+ # Update the record
1247
+ self.client.put_item(TableName=table_name, Item=item)
1248
+
1249
+ return new_record
1250
+
1251
+ except Exception as e:
1252
+ log_error(f"Failed to update existing metrics record: {e}")
1253
+ return None
1254
+
1255
+ def _create_new_metrics_record(self, table_name: str, record: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1256
+ """Create a new metrics record.
1257
+
1258
+ Args:
1259
+ table_name: The DynamoDB table name
1260
+ record: The record to create
1261
+
1262
+ Returns:
1263
+ Optional[Dict[str, Any]]: The created record or None if failed
1264
+ """
1265
+ try:
1266
+ # Prepare and serialize the record
1267
+ prepared_record = self._prepare_metrics_record_for_dynamo(record)
1268
+ item = self._serialize_metrics_to_dynamo_item(prepared_record)
1269
+
1270
+ # Create the record
1271
+ self.client.put_item(TableName=table_name, Item=item)
1272
+
1273
+ return record
1274
+
1275
+ except Exception as e:
1276
+ log_error(f"Failed to create new metrics record: {e}")
1277
+ return None
1278
+
1279
+ def _prepare_metrics_record_for_dynamo(self, record: Dict[str, Any]) -> Dict[str, Any]:
1280
+ """Prepare a metrics record for DynamoDB serialization by converting all data types properly.
1281
+
1282
+ Args:
1283
+ record: The metrics record to prepare
1284
+
1285
+ Returns:
1286
+ Dict[str, Any]: The prepared record ready for DynamoDB serialization
1287
+ """
1288
+
1289
+ def convert_value(value):
1290
+ """Recursively convert values to DynamoDB-compatible types."""
1291
+ if value is None:
1292
+ return None
1293
+ elif isinstance(value, bool):
1294
+ return value
1295
+ elif isinstance(value, (int, float)):
1296
+ return value
1297
+ elif isinstance(value, str):
1298
+ return value
1299
+ elif hasattr(value, "isoformat"): # date/datetime objects
1300
+ return value.isoformat()
1301
+ elif isinstance(value, dict):
1302
+ return {k: convert_value(v) for k, v in value.items()}
1303
+ elif isinstance(value, list):
1304
+ return [convert_value(item) for item in value]
1305
+ else:
1306
+ return str(value)
1307
+
1308
+ return {key: convert_value(value) for key, value in record.items()}
1309
+
1310
+ def _serialize_metrics_to_dynamo_item(self, data: Dict[str, Any]) -> Dict[str, Any]:
1311
+ """Serialize metrics data to DynamoDB item format with proper boolean handling.
1312
+
1313
+ Args:
1314
+ data: The metrics data to serialize
1315
+
1316
+ Returns:
1317
+ Dict[str, Any]: DynamoDB-ready item
1318
+ """
1319
+ import json
1320
+
1321
+ item = {}
1322
+ for key, value in data.items():
1323
+ if value is not None:
1324
+ if isinstance(value, bool):
1325
+ item[key] = {"BOOL": str(value)}
1326
+ elif isinstance(value, (int, float)):
1327
+ item[key] = {"N": str(value)}
1328
+ elif isinstance(value, str):
1329
+ item[key] = {"S": str(value)}
1330
+ elif isinstance(value, (dict, list)):
1331
+ item[key] = {"S": json.dumps(str(value))}
1332
+ else:
1333
+ item[key] = {"S": str(value)}
1334
+ return item
1335
+
1336
+ def get_metrics(
1337
+ self,
1338
+ starting_date: Optional[date] = None,
1339
+ ending_date: Optional[date] = None,
1340
+ ) -> Tuple[List[Any], Optional[int]]:
1341
+ """
1342
+ Get metrics from the database.
1343
+
1344
+ Args:
1345
+ starting_date: The starting date to filter metrics by.
1346
+ ending_date: The ending date to filter metrics by.
1347
+
1348
+ Returns:
1349
+ Tuple[List[Any], Optional[int]]: A tuple containing the metrics data and the total count.
1350
+
1351
+ Raises:
1352
+ Exception: If any error occurs while getting the metrics.
1353
+ """
1354
+
1355
+ try:
1356
+ table_name = self._get_table("metrics")
1357
+ if table_name is None:
1358
+ return ([], None)
1359
+
1360
+ # Build query parameters
1361
+ scan_kwargs: Dict[str, Any] = {"TableName": table_name}
1362
+
1363
+ if starting_date or ending_date:
1364
+ filter_expressions = []
1365
+ expression_values = {}
1366
+
1367
+ if starting_date:
1368
+ filter_expressions.append("#date >= :start_date")
1369
+ expression_values[":start_date"] = {"S": starting_date.isoformat()}
1370
+
1371
+ if ending_date:
1372
+ filter_expressions.append("#date <= :end_date")
1373
+ expression_values[":end_date"] = {"S": ending_date.isoformat()}
1374
+
1375
+ scan_kwargs["FilterExpression"] = " AND ".join(filter_expressions)
1376
+ scan_kwargs["ExpressionAttributeNames"] = {"#date": "date"}
1377
+ scan_kwargs["ExpressionAttributeValues"] = expression_values
1378
+
1379
+ # Execute scan
1380
+ response = self.client.scan(**scan_kwargs)
1381
+ items = response.get("Items", [])
1382
+
1383
+ # Handle pagination
1384
+ while "LastEvaluatedKey" in response:
1385
+ scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
1386
+ response = self.client.scan(**scan_kwargs)
1387
+ items.extend(response.get("Items", []))
1388
+
1389
+ # Convert to metrics data
1390
+ metrics_data = []
1391
+ for item in items:
1392
+ metric_data = deserialize_from_dynamodb_item(item)
1393
+ if metric_data:
1394
+ metrics_data.append(metric_data)
1395
+
1396
+ return metrics_data, len(metrics_data)
1397
+
1398
+ except Exception as e:
1399
+ log_error(f"Failed to get metrics: {e}")
1400
+ return [], 0
1401
+
1402
+ # --- Knowledge methods ---
1403
+
1404
+ def delete_knowledge_content(self, id: str):
1405
+ """Delete a knowledge row from the database.
1406
+
1407
+ Args:
1408
+ id (str): The ID of the knowledge row to delete.
1409
+
1410
+ Raises:
1411
+ Exception: If an error occurs during deletion.
1412
+ """
1413
+ try:
1414
+ table_name = self._get_table("knowledge")
1415
+
1416
+ self.client.delete_item(TableName=table_name, Key={"id": {"S": id}})
1417
+
1418
+ log_debug(f"Deleted knowledge content {id}")
1419
+
1420
+ except Exception as e:
1421
+ log_error(f"Failed to delete knowledge content {id}: {e}")
1422
+
1423
+ def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1424
+ """Get a knowledge row from the database.
1425
+
1426
+ Args:
1427
+ id (str): The ID of the knowledge row to get.
1428
+
1429
+ Returns:
1430
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1431
+ """
1432
+ try:
1433
+ table_name = self._get_table("knowledge")
1434
+ response = self.client.get_item(TableName=table_name, Key={"id": {"S": id}})
1435
+
1436
+ item = response.get("Item")
1437
+ if item:
1438
+ return deserialize_knowledge_row(item)
1439
+
1440
+ return None
1441
+
1442
+ except Exception as e:
1443
+ log_error(f"Failed to get knowledge content {id}: {e}")
1444
+ return None
1445
+
1446
+ def get_knowledge_contents(
1447
+ self,
1448
+ limit: Optional[int] = None,
1449
+ page: Optional[int] = None,
1450
+ sort_by: Optional[str] = None,
1451
+ sort_order: Optional[str] = None,
1452
+ ) -> Tuple[List[KnowledgeRow], int]:
1453
+ """Get all knowledge contents from the database.
1454
+
1455
+ Args:
1456
+ limit (Optional[int]): The maximum number of knowledge contents to return.
1457
+ page (Optional[int]): The page number.
1458
+ sort_by (Optional[str]): The column to sort by.
1459
+ sort_order (Optional[str]): The order to sort by.
1460
+ create_table_if_not_found (Optional[bool]): Whether to create the table if it doesn't exist.
1461
+
1462
+ Returns:
1463
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
1464
+
1465
+ Raises:
1466
+ Exception: If an error occurs during retrieval.
1467
+ """
1468
+ try:
1469
+ table_name = self._get_table("knowledge")
1470
+ if table_name is None:
1471
+ return [], 0
1472
+
1473
+ response = self.client.scan(TableName=table_name)
1474
+ items = response.get("Items", [])
1475
+
1476
+ # Handle pagination
1477
+ while "LastEvaluatedKey" in response:
1478
+ response = self.client.scan(
1479
+ TableName=table_name,
1480
+ ExclusiveStartKey=response["LastEvaluatedKey"],
1481
+ )
1482
+ items.extend(response.get("Items", []))
1483
+
1484
+ # Convert to knowledge rows
1485
+ knowledge_rows = []
1486
+ for item in items:
1487
+ try:
1488
+ knowledge_row = deserialize_knowledge_row(item)
1489
+ knowledge_rows.append(knowledge_row)
1490
+ except Exception as e:
1491
+ log_error(f"Failed to deserialize knowledge row: {e}")
1492
+
1493
+ # Apply sorting
1494
+ if sort_by:
1495
+ reverse = sort_order == "desc"
1496
+ knowledge_rows = sorted(
1497
+ knowledge_rows,
1498
+ key=lambda x: getattr(x, sort_by, ""),
1499
+ reverse=reverse,
1500
+ )
1501
+
1502
+ # Get total count before pagination
1503
+ total_count = len(knowledge_rows)
1504
+
1505
+ # Apply pagination
1506
+ if limit:
1507
+ start_index = 0
1508
+ if page and page > 1:
1509
+ start_index = (page - 1) * limit
1510
+ knowledge_rows = knowledge_rows[start_index : start_index + limit]
1511
+
1512
+ return knowledge_rows, total_count
1513
+
1514
+ except Exception as e:
1515
+ log_error(f"Failed to get knowledge contents: {e}")
1516
+ return [], 0
1517
+
1518
+ def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
1519
+ """Upsert knowledge content in the database.
1520
+
1521
+ Args:
1522
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
1523
+
1524
+ Returns:
1525
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
1526
+ """
1527
+ try:
1528
+ table_name = self._get_table("knowledge", create_table_if_not_found=True)
1529
+ item = serialize_knowledge_row(knowledge_row)
1530
+
1531
+ self.client.put_item(TableName=table_name, Item=item)
1532
+
1533
+ log_debug(f"Upserted knowledge content with id '{knowledge_row.id}'")
1534
+
1535
+ return knowledge_row
1536
+
1537
+ except Exception as e:
1538
+ log_error(f"Failed to upsert knowledge content {knowledge_row.id}: {e}")
1539
+ return None
1540
+
1541
+ # --- Eval ---
1542
+
1543
+ def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
1544
+ """Create an eval run in the database.
1545
+
1546
+ Args:
1547
+ eval_run (EvalRunRecord): The eval run to create.
1548
+
1549
+ Returns:
1550
+ Optional[EvalRunRecord]: The created eval run, or None if the operation fails.
1551
+
1552
+ Raises:
1553
+ Exception: If an error occurs during creation.
1554
+ """
1555
+ try:
1556
+ table_name = self._get_table("evals", create_table_if_not_found=True)
1557
+
1558
+ item = serialize_eval_record(eval_run)
1559
+ current_time = int(datetime.now(timezone.utc).timestamp())
1560
+ item["created_at"] = {"N": str(current_time)}
1561
+ item["updated_at"] = {"N": str(current_time)}
1562
+
1563
+ self.client.put_item(TableName=table_name, Item=item)
1564
+
1565
+ return eval_run
1566
+
1567
+ except Exception as e:
1568
+ log_error(f"Failed to create eval run: {e}")
1569
+ return None
1570
+
1571
+ def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1572
+ if not eval_run_ids or not self.eval_table_name:
1573
+ return
1574
+
1575
+ try:
1576
+ for i in range(0, len(eval_run_ids), DYNAMO_BATCH_SIZE_LIMIT):
1577
+ batch = eval_run_ids[i : i + DYNAMO_BATCH_SIZE_LIMIT]
1578
+
1579
+ delete_requests = []
1580
+ for eval_run_id in batch:
1581
+ delete_requests.append({"DeleteRequest": {"Key": {"run_id": {"S": eval_run_id}}}})
1582
+
1583
+ self.client.batch_write_item(RequestItems={self.eval_table_name: delete_requests})
1584
+
1585
+ except Exception as e:
1586
+ log_error(f"Failed to delete eval runs: {e}")
1587
+
1588
+ def get_eval_run_raw(self, eval_run_id: str, table: Optional[Any] = None) -> Optional[Dict[str, Any]]:
1589
+ if not self.eval_table_name:
1590
+ return None
1591
+
1592
+ try:
1593
+ response = self.client.get_item(TableName=self.eval_table_name, Key={"run_id": {"S": eval_run_id}})
1594
+
1595
+ item = response.get("Item")
1596
+ if item:
1597
+ return deserialize_from_dynamodb_item(item)
1598
+ return None
1599
+
1600
+ except Exception as e:
1601
+ log_error(f"Failed to get eval run {eval_run_id}: {e}")
1602
+ return None
1603
+
1604
+ def get_eval_run(self, eval_run_id: str, table: Optional[Any] = None) -> Optional[EvalRunRecord]:
1605
+ if not self.eval_table_name:
1606
+ return None
1607
+
1608
+ try:
1609
+ response = self.client.get_item(TableName=self.eval_table_name, Key={"run_id": {"S": eval_run_id}})
1610
+
1611
+ item = response.get("Item")
1612
+ if item:
1613
+ return deserialize_eval_record(item)
1614
+ return None
1615
+
1616
+ except Exception as e:
1617
+ log_error(f"Failed to get eval run {eval_run_id}: {e}")
1618
+ return None
1619
+
1620
+ def get_eval_runs(
1621
+ self,
1622
+ limit: Optional[int] = None,
1623
+ page: Optional[int] = None,
1624
+ sort_by: Optional[str] = None,
1625
+ sort_order: Optional[str] = None,
1626
+ agent_id: Optional[str] = None,
1627
+ team_id: Optional[str] = None,
1628
+ workflow_id: Optional[str] = None,
1629
+ model_id: Optional[str] = None,
1630
+ filter_type: Optional[EvalFilterType] = None,
1631
+ eval_type: Optional[List[EvalType]] = None,
1632
+ deserialize: Optional[bool] = True,
1633
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
1634
+ try:
1635
+ table_name = self._get_table("evals")
1636
+ if table_name is None:
1637
+ return [] if deserialize else ([], 0)
1638
+
1639
+ scan_kwargs = {"TableName": table_name}
1640
+
1641
+ filter_expressions = []
1642
+ expression_values = {}
1643
+
1644
+ if agent_id:
1645
+ filter_expressions.append("agent_id = :agent_id")
1646
+ expression_values[":agent_id"] = {"S": agent_id}
1647
+
1648
+ if team_id:
1649
+ filter_expressions.append("team_id = :team_id")
1650
+ expression_values[":team_id"] = {"S": team_id}
1651
+
1652
+ if workflow_id:
1653
+ filter_expressions.append("workflow_id = :workflow_id")
1654
+ expression_values[":workflow_id"] = {"S": workflow_id}
1655
+
1656
+ if model_id:
1657
+ filter_expressions.append("model_id = :model_id")
1658
+ expression_values[":model_id"] = {"S": model_id}
1659
+
1660
+ if eval_type is not None and len(eval_type) > 0:
1661
+ eval_type_conditions = []
1662
+ for i, et in enumerate(eval_type):
1663
+ param_name = f":eval_type_{i}"
1664
+ eval_type_conditions.append(f"eval_type = {param_name}")
1665
+ expression_values[param_name] = {"S": str(et.value)}
1666
+ filter_expressions.append(f"({' OR '.join(eval_type_conditions)})")
1667
+
1668
+ if filter_type is not None:
1669
+ if filter_type == EvalFilterType.AGENT:
1670
+ filter_expressions.append("agent_id IS NOT NULL")
1671
+ elif filter_type == EvalFilterType.TEAM:
1672
+ filter_expressions.append("team_id IS NOT NULL")
1673
+ elif filter_type == EvalFilterType.WORKFLOW:
1674
+ filter_expressions.append("workflow_id IS NOT NULL")
1675
+
1676
+ if filter_expressions:
1677
+ scan_kwargs["FilterExpression"] = " AND ".join(filter_expressions)
1678
+ scan_kwargs["ExpressionAttributeValues"] = expression_values # type: ignore
1679
+
1680
+ # Execute scan
1681
+ response = self.client.scan(**scan_kwargs)
1682
+ items = response.get("Items", [])
1683
+
1684
+ # Handle pagination
1685
+ while "LastEvaluatedKey" in response:
1686
+ scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
1687
+ response = self.client.scan(**scan_kwargs)
1688
+ items.extend(response.get("Items", []))
1689
+
1690
+ # Convert to eval data
1691
+ eval_data = []
1692
+ for item in items:
1693
+ eval_item = deserialize_from_dynamodb_item(item)
1694
+ if eval_item:
1695
+ eval_data.append(eval_item)
1696
+
1697
+ # Apply sorting
1698
+ eval_data = apply_sorting(eval_data, sort_by, sort_order)
1699
+
1700
+ # Get total count before pagination
1701
+ total_count = len(eval_data)
1702
+
1703
+ # Apply pagination
1704
+ eval_data = apply_pagination(eval_data, limit, page)
1705
+
1706
+ if not deserialize:
1707
+ return eval_data, total_count
1708
+
1709
+ eval_runs = []
1710
+ for eval_item in eval_data:
1711
+ eval_run = EvalRunRecord.model_validate(eval_item)
1712
+ eval_runs.append(eval_run)
1713
+ return eval_runs
1714
+
1715
+ except Exception as e:
1716
+ log_error(f"Failed to get eval runs: {e}")
1717
+ return [] if deserialize else ([], 0)
1718
+
1719
+ def rename_eval_run(
1720
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1721
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1722
+ if not self.eval_table_name:
1723
+ return None
1724
+
1725
+ try:
1726
+ response = self.client.update_item(
1727
+ TableName=self.eval_table_name,
1728
+ Key={"run_id": {"S": eval_run_id}},
1729
+ UpdateExpression="SET #name = :name, updated_at = :updated_at",
1730
+ ExpressionAttributeNames={"#name": "name"},
1731
+ ExpressionAttributeValues={
1732
+ ":name": {"S": name},
1733
+ ":updated_at": {"N": str(int(time.time()))},
1734
+ },
1735
+ ReturnValues="ALL_NEW",
1736
+ )
1737
+
1738
+ item = response.get("Attributes")
1739
+ if item is None:
1740
+ return None
1741
+
1742
+ log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
1743
+
1744
+ item = deserialize_from_dynamodb_item(item)
1745
+ return EvalRunRecord.model_validate(item) if deserialize else item
1746
+
1747
+ except Exception as e:
1748
+ log_error(f"Failed to rename eval run {eval_run_id}: {e}")
1749
+ return None