agno 1.8.2__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 (589) hide show
  1. agno/agent/__init__.py +19 -27
  2. agno/agent/agent.py +3143 -4170
  3. agno/api/agent.py +11 -67
  4. agno/api/api.py +5 -46
  5. agno/api/evals.py +8 -19
  6. agno/api/os.py +17 -0
  7. agno/api/routes.py +6 -41
  8. agno/api/schemas/__init__.py +9 -0
  9. agno/api/schemas/agent.py +5 -21
  10. agno/api/schemas/evals.py +7 -16
  11. agno/api/schemas/os.py +14 -0
  12. agno/api/schemas/team.py +5 -21
  13. agno/api/schemas/utils.py +21 -0
  14. agno/api/schemas/workflows.py +11 -7
  15. agno/api/settings.py +53 -0
  16. agno/api/team.py +11 -66
  17. agno/api/workflow.py +28 -0
  18. agno/cloud/aws/base.py +214 -0
  19. agno/cloud/aws/s3/__init__.py +2 -0
  20. agno/cloud/aws/s3/api_client.py +43 -0
  21. agno/cloud/aws/s3/bucket.py +195 -0
  22. agno/cloud/aws/s3/object.py +57 -0
  23. agno/db/__init__.py +24 -0
  24. agno/db/base.py +245 -0
  25. agno/db/dynamo/__init__.py +3 -0
  26. agno/db/dynamo/dynamo.py +1743 -0
  27. agno/db/dynamo/schemas.py +278 -0
  28. agno/db/dynamo/utils.py +684 -0
  29. agno/db/firestore/__init__.py +3 -0
  30. agno/db/firestore/firestore.py +1432 -0
  31. agno/db/firestore/schemas.py +130 -0
  32. agno/db/firestore/utils.py +278 -0
  33. agno/db/gcs_json/__init__.py +3 -0
  34. agno/db/gcs_json/gcs_json_db.py +1001 -0
  35. agno/db/gcs_json/utils.py +194 -0
  36. agno/db/in_memory/__init__.py +3 -0
  37. agno/db/in_memory/in_memory_db.py +882 -0
  38. agno/db/in_memory/utils.py +172 -0
  39. agno/db/json/__init__.py +3 -0
  40. agno/db/json/json_db.py +1045 -0
  41. agno/db/json/utils.py +196 -0
  42. agno/db/migrations/v1_to_v2.py +162 -0
  43. agno/db/mongo/__init__.py +3 -0
  44. agno/db/mongo/mongo.py +1416 -0
  45. agno/db/mongo/schemas.py +77 -0
  46. agno/db/mongo/utils.py +204 -0
  47. agno/db/mysql/__init__.py +3 -0
  48. agno/db/mysql/mysql.py +1719 -0
  49. agno/db/mysql/schemas.py +124 -0
  50. agno/db/mysql/utils.py +297 -0
  51. agno/db/postgres/__init__.py +3 -0
  52. agno/db/postgres/postgres.py +1710 -0
  53. agno/db/postgres/schemas.py +124 -0
  54. agno/db/postgres/utils.py +280 -0
  55. agno/db/redis/__init__.py +3 -0
  56. agno/db/redis/redis.py +1367 -0
  57. agno/db/redis/schemas.py +109 -0
  58. agno/db/redis/utils.py +288 -0
  59. agno/db/schemas/__init__.py +3 -0
  60. agno/db/schemas/evals.py +33 -0
  61. agno/db/schemas/knowledge.py +40 -0
  62. agno/db/schemas/memory.py +46 -0
  63. agno/db/singlestore/__init__.py +3 -0
  64. agno/db/singlestore/schemas.py +116 -0
  65. agno/db/singlestore/singlestore.py +1712 -0
  66. agno/db/singlestore/utils.py +326 -0
  67. agno/db/sqlite/__init__.py +3 -0
  68. agno/db/sqlite/schemas.py +119 -0
  69. agno/db/sqlite/sqlite.py +1676 -0
  70. agno/db/sqlite/utils.py +268 -0
  71. agno/db/utils.py +88 -0
  72. agno/eval/__init__.py +14 -0
  73. agno/eval/accuracy.py +154 -48
  74. agno/eval/performance.py +88 -23
  75. agno/eval/reliability.py +73 -20
  76. agno/eval/utils.py +23 -13
  77. agno/integrations/discord/__init__.py +3 -0
  78. agno/{app → integrations}/discord/client.py +10 -10
  79. agno/knowledge/__init__.py +2 -2
  80. agno/{document → knowledge}/chunking/agentic.py +2 -2
  81. agno/{document → knowledge}/chunking/document.py +2 -2
  82. agno/{document → knowledge}/chunking/fixed.py +3 -3
  83. agno/{document → knowledge}/chunking/markdown.py +2 -2
  84. agno/{document → knowledge}/chunking/recursive.py +2 -2
  85. agno/{document → knowledge}/chunking/row.py +2 -2
  86. agno/knowledge/chunking/semantic.py +59 -0
  87. agno/knowledge/chunking/strategy.py +121 -0
  88. agno/knowledge/content.py +74 -0
  89. agno/knowledge/document/__init__.py +5 -0
  90. agno/{document → knowledge/document}/base.py +12 -2
  91. agno/knowledge/embedder/__init__.py +5 -0
  92. agno/{embedder → knowledge/embedder}/aws_bedrock.py +127 -1
  93. agno/{embedder → knowledge/embedder}/azure_openai.py +65 -1
  94. agno/{embedder → knowledge/embedder}/base.py +6 -0
  95. agno/{embedder → knowledge/embedder}/cohere.py +72 -1
  96. agno/{embedder → knowledge/embedder}/fastembed.py +17 -1
  97. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  98. agno/{embedder → knowledge/embedder}/google.py +74 -1
  99. agno/{embedder → knowledge/embedder}/huggingface.py +36 -2
  100. agno/{embedder → knowledge/embedder}/jina.py +48 -2
  101. agno/knowledge/embedder/langdb.py +22 -0
  102. agno/knowledge/embedder/mistral.py +139 -0
  103. agno/{embedder → knowledge/embedder}/nebius.py +1 -1
  104. agno/{embedder → knowledge/embedder}/ollama.py +54 -3
  105. agno/knowledge/embedder/openai.py +223 -0
  106. agno/{embedder → knowledge/embedder}/sentence_transformer.py +16 -1
  107. agno/{embedder → knowledge/embedder}/together.py +1 -1
  108. agno/{embedder → knowledge/embedder}/voyageai.py +49 -1
  109. agno/knowledge/knowledge.py +1551 -0
  110. agno/knowledge/reader/__init__.py +7 -0
  111. agno/{document → knowledge}/reader/arxiv_reader.py +32 -4
  112. agno/knowledge/reader/base.py +88 -0
  113. agno/{document → knowledge}/reader/csv_reader.py +47 -65
  114. agno/knowledge/reader/docx_reader.py +83 -0
  115. agno/{document → knowledge}/reader/firecrawl_reader.py +42 -21
  116. agno/{document → knowledge}/reader/json_reader.py +30 -9
  117. agno/{document → knowledge}/reader/markdown_reader.py +58 -9
  118. agno/{document → knowledge}/reader/pdf_reader.py +71 -126
  119. agno/knowledge/reader/reader_factory.py +268 -0
  120. agno/knowledge/reader/s3_reader.py +101 -0
  121. agno/{document → knowledge}/reader/text_reader.py +31 -10
  122. agno/knowledge/reader/url_reader.py +128 -0
  123. agno/knowledge/reader/web_search_reader.py +366 -0
  124. agno/{document → knowledge}/reader/website_reader.py +37 -10
  125. agno/knowledge/reader/wikipedia_reader.py +59 -0
  126. agno/knowledge/reader/youtube_reader.py +78 -0
  127. agno/knowledge/remote_content/remote_content.py +88 -0
  128. agno/{reranker → knowledge/reranker}/base.py +1 -1
  129. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  130. agno/{reranker → knowledge/reranker}/infinity.py +2 -2
  131. agno/{reranker → knowledge/reranker}/sentence_transformer.py +2 -2
  132. agno/knowledge/types.py +30 -0
  133. agno/knowledge/utils.py +169 -0
  134. agno/media.py +269 -268
  135. agno/memory/__init__.py +2 -10
  136. agno/memory/manager.py +1003 -148
  137. agno/models/aimlapi/__init__.py +2 -2
  138. agno/models/aimlapi/aimlapi.py +6 -6
  139. agno/models/anthropic/claude.py +128 -72
  140. agno/models/aws/bedrock.py +107 -175
  141. agno/models/aws/claude.py +64 -18
  142. agno/models/azure/ai_foundry.py +73 -23
  143. agno/models/base.py +346 -290
  144. agno/models/cerebras/cerebras.py +84 -27
  145. agno/models/cohere/chat.py +106 -98
  146. agno/models/google/gemini.py +105 -46
  147. agno/models/groq/groq.py +97 -35
  148. agno/models/huggingface/huggingface.py +92 -27
  149. agno/models/ibm/watsonx.py +72 -13
  150. agno/models/litellm/chat.py +85 -13
  151. agno/models/message.py +46 -151
  152. agno/models/meta/llama.py +85 -49
  153. agno/models/metrics.py +120 -0
  154. agno/models/mistral/mistral.py +90 -21
  155. agno/models/ollama/__init__.py +0 -2
  156. agno/models/ollama/chat.py +85 -47
  157. agno/models/openai/chat.py +154 -37
  158. agno/models/openai/responses.py +178 -105
  159. agno/models/perplexity/perplexity.py +26 -2
  160. agno/models/portkey/portkey.py +0 -7
  161. agno/models/response.py +15 -9
  162. agno/models/utils.py +20 -0
  163. agno/models/vercel/__init__.py +2 -2
  164. agno/models/vercel/v0.py +1 -1
  165. agno/models/vllm/__init__.py +2 -2
  166. agno/models/vllm/vllm.py +3 -3
  167. agno/models/xai/xai.py +10 -10
  168. agno/os/__init__.py +3 -0
  169. agno/os/app.py +497 -0
  170. agno/os/auth.py +47 -0
  171. agno/os/config.py +103 -0
  172. agno/os/interfaces/agui/__init__.py +3 -0
  173. agno/os/interfaces/agui/agui.py +31 -0
  174. agno/{app/agui/async_router.py → os/interfaces/agui/router.py} +16 -16
  175. agno/{app → os/interfaces}/agui/utils.py +65 -28
  176. agno/os/interfaces/base.py +21 -0
  177. agno/os/interfaces/slack/__init__.py +3 -0
  178. agno/{app/slack/async_router.py → os/interfaces/slack/router.py} +3 -5
  179. agno/os/interfaces/slack/slack.py +32 -0
  180. agno/os/interfaces/whatsapp/__init__.py +3 -0
  181. agno/{app/whatsapp/async_router.py → os/interfaces/whatsapp/router.py} +4 -7
  182. agno/os/interfaces/whatsapp/whatsapp.py +29 -0
  183. agno/os/mcp.py +235 -0
  184. agno/os/router.py +1400 -0
  185. agno/os/routers/__init__.py +3 -0
  186. agno/os/routers/evals/__init__.py +3 -0
  187. agno/os/routers/evals/evals.py +393 -0
  188. agno/os/routers/evals/schemas.py +142 -0
  189. agno/os/routers/evals/utils.py +161 -0
  190. agno/os/routers/knowledge/__init__.py +3 -0
  191. agno/os/routers/knowledge/knowledge.py +850 -0
  192. agno/os/routers/knowledge/schemas.py +118 -0
  193. agno/os/routers/memory/__init__.py +3 -0
  194. agno/os/routers/memory/memory.py +410 -0
  195. agno/os/routers/memory/schemas.py +58 -0
  196. agno/os/routers/metrics/__init__.py +3 -0
  197. agno/os/routers/metrics/metrics.py +178 -0
  198. agno/os/routers/metrics/schemas.py +47 -0
  199. agno/os/routers/session/__init__.py +3 -0
  200. agno/os/routers/session/session.py +536 -0
  201. agno/os/schema.py +945 -0
  202. agno/{app/playground → os}/settings.py +7 -15
  203. agno/os/utils.py +270 -0
  204. agno/reasoning/azure_ai_foundry.py +4 -4
  205. agno/reasoning/deepseek.py +4 -4
  206. agno/reasoning/default.py +6 -11
  207. agno/reasoning/groq.py +4 -4
  208. agno/reasoning/helpers.py +4 -6
  209. agno/reasoning/ollama.py +4 -4
  210. agno/reasoning/openai.py +4 -4
  211. agno/run/agent.py +633 -0
  212. agno/run/base.py +53 -77
  213. agno/run/cancel.py +81 -0
  214. agno/run/team.py +243 -96
  215. agno/run/workflow.py +550 -12
  216. agno/session/__init__.py +10 -0
  217. agno/session/agent.py +244 -0
  218. agno/session/summary.py +225 -0
  219. agno/session/team.py +262 -0
  220. agno/{storage/session/v2 → session}/workflow.py +47 -24
  221. agno/team/__init__.py +15 -16
  222. agno/team/team.py +3260 -4824
  223. agno/tools/agentql.py +14 -5
  224. agno/tools/airflow.py +9 -4
  225. agno/tools/api.py +7 -3
  226. agno/tools/apify.py +2 -46
  227. agno/tools/arxiv.py +8 -3
  228. agno/tools/aws_lambda.py +7 -5
  229. agno/tools/aws_ses.py +7 -1
  230. agno/tools/baidusearch.py +4 -1
  231. agno/tools/bitbucket.py +4 -4
  232. agno/tools/brandfetch.py +14 -11
  233. agno/tools/bravesearch.py +4 -1
  234. agno/tools/brightdata.py +43 -23
  235. agno/tools/browserbase.py +13 -4
  236. agno/tools/calcom.py +12 -10
  237. agno/tools/calculator.py +10 -27
  238. agno/tools/cartesia.py +20 -17
  239. agno/tools/{clickup_tool.py → clickup.py} +12 -25
  240. agno/tools/confluence.py +8 -8
  241. agno/tools/crawl4ai.py +7 -1
  242. agno/tools/csv_toolkit.py +9 -8
  243. agno/tools/dalle.py +22 -12
  244. agno/tools/daytona.py +13 -16
  245. agno/tools/decorator.py +6 -3
  246. agno/tools/desi_vocal.py +17 -8
  247. agno/tools/discord.py +11 -8
  248. agno/tools/docker.py +30 -42
  249. agno/tools/duckdb.py +34 -53
  250. agno/tools/duckduckgo.py +8 -7
  251. agno/tools/e2b.py +62 -62
  252. agno/tools/eleven_labs.py +36 -29
  253. agno/tools/email.py +4 -1
  254. agno/tools/evm.py +7 -1
  255. agno/tools/exa.py +19 -14
  256. agno/tools/fal.py +30 -30
  257. agno/tools/file.py +9 -8
  258. agno/tools/financial_datasets.py +25 -44
  259. agno/tools/firecrawl.py +17 -18
  260. agno/tools/function.py +127 -18
  261. agno/tools/giphy.py +23 -11
  262. agno/tools/github.py +48 -126
  263. agno/tools/gmail.py +45 -61
  264. agno/tools/google_bigquery.py +7 -6
  265. agno/tools/google_maps.py +11 -26
  266. agno/tools/googlesearch.py +7 -2
  267. agno/tools/googlesheets.py +21 -17
  268. agno/tools/hackernews.py +9 -5
  269. agno/tools/jina.py +5 -4
  270. agno/tools/jira.py +18 -9
  271. agno/tools/knowledge.py +31 -32
  272. agno/tools/linear.py +18 -33
  273. agno/tools/linkup.py +5 -1
  274. agno/tools/local_file_system.py +8 -5
  275. agno/tools/lumalab.py +32 -20
  276. agno/tools/mcp.py +1 -2
  277. agno/tools/mem0.py +18 -12
  278. agno/tools/memori.py +14 -10
  279. agno/tools/mlx_transcribe.py +3 -2
  280. agno/tools/models/azure_openai.py +33 -15
  281. agno/tools/models/gemini.py +59 -32
  282. agno/tools/models/groq.py +30 -23
  283. agno/tools/models/nebius.py +28 -12
  284. agno/tools/models_labs.py +40 -16
  285. agno/tools/moviepy_video.py +7 -6
  286. agno/tools/neo4j.py +10 -8
  287. agno/tools/newspaper.py +7 -2
  288. agno/tools/newspaper4k.py +8 -3
  289. agno/tools/openai.py +58 -32
  290. agno/tools/openbb.py +12 -11
  291. agno/tools/opencv.py +63 -47
  292. agno/tools/openweather.py +14 -12
  293. agno/tools/pandas.py +11 -3
  294. agno/tools/postgres.py +4 -12
  295. agno/tools/pubmed.py +4 -1
  296. agno/tools/python.py +9 -22
  297. agno/tools/reasoning.py +35 -27
  298. agno/tools/reddit.py +11 -26
  299. agno/tools/replicate.py +55 -42
  300. agno/tools/resend.py +4 -1
  301. agno/tools/scrapegraph.py +15 -14
  302. agno/tools/searxng.py +10 -23
  303. agno/tools/serpapi.py +6 -3
  304. agno/tools/serper.py +13 -4
  305. agno/tools/shell.py +9 -2
  306. agno/tools/slack.py +12 -11
  307. agno/tools/sleep.py +3 -2
  308. agno/tools/spider.py +24 -4
  309. agno/tools/sql.py +7 -6
  310. agno/tools/tavily.py +6 -4
  311. agno/tools/telegram.py +12 -4
  312. agno/tools/todoist.py +11 -31
  313. agno/tools/toolkit.py +1 -1
  314. agno/tools/trafilatura.py +22 -6
  315. agno/tools/trello.py +9 -22
  316. agno/tools/twilio.py +10 -3
  317. agno/tools/user_control_flow.py +6 -1
  318. agno/tools/valyu.py +34 -5
  319. agno/tools/visualization.py +19 -28
  320. agno/tools/webbrowser.py +4 -3
  321. agno/tools/webex.py +11 -7
  322. agno/tools/website.py +15 -46
  323. agno/tools/webtools.py +12 -4
  324. agno/tools/whatsapp.py +5 -9
  325. agno/tools/wikipedia.py +20 -13
  326. agno/tools/x.py +14 -13
  327. agno/tools/yfinance.py +13 -40
  328. agno/tools/youtube.py +26 -20
  329. agno/tools/zendesk.py +7 -2
  330. agno/tools/zep.py +10 -7
  331. agno/tools/zoom.py +10 -9
  332. agno/utils/common.py +1 -19
  333. agno/utils/events.py +100 -123
  334. agno/utils/gemini.py +1 -1
  335. agno/utils/knowledge.py +29 -0
  336. agno/utils/log.py +54 -4
  337. agno/utils/mcp.py +68 -10
  338. agno/utils/media.py +39 -0
  339. agno/utils/message.py +12 -1
  340. agno/utils/models/aws_claude.py +1 -1
  341. agno/utils/models/claude.py +6 -12
  342. agno/utils/models/cohere.py +1 -1
  343. agno/utils/models/mistral.py +8 -7
  344. agno/utils/models/schema_utils.py +3 -3
  345. agno/utils/models/watsonx.py +1 -1
  346. agno/utils/openai.py +1 -1
  347. agno/utils/pprint.py +33 -32
  348. agno/utils/print_response/agent.py +779 -0
  349. agno/utils/print_response/team.py +1669 -0
  350. agno/utils/print_response/workflow.py +1451 -0
  351. agno/utils/prompts.py +14 -14
  352. agno/utils/reasoning.py +87 -0
  353. agno/utils/response.py +42 -42
  354. agno/utils/streamlit.py +481 -0
  355. agno/utils/string.py +8 -22
  356. agno/utils/team.py +50 -0
  357. agno/utils/timer.py +2 -2
  358. agno/vectordb/base.py +33 -21
  359. agno/vectordb/cassandra/cassandra.py +287 -23
  360. agno/vectordb/chroma/chromadb.py +482 -59
  361. agno/vectordb/clickhouse/clickhousedb.py +270 -63
  362. agno/vectordb/couchbase/couchbase.py +309 -29
  363. agno/vectordb/lancedb/lance_db.py +360 -21
  364. agno/vectordb/langchaindb/__init__.py +5 -0
  365. agno/vectordb/langchaindb/langchaindb.py +145 -0
  366. agno/vectordb/lightrag/__init__.py +5 -0
  367. agno/vectordb/lightrag/lightrag.py +374 -0
  368. agno/vectordb/llamaindex/llamaindexdb.py +127 -0
  369. agno/vectordb/milvus/milvus.py +242 -32
  370. agno/vectordb/mongodb/mongodb.py +200 -24
  371. agno/vectordb/pgvector/pgvector.py +319 -37
  372. agno/vectordb/pineconedb/pineconedb.py +221 -27
  373. agno/vectordb/qdrant/qdrant.py +334 -14
  374. agno/vectordb/singlestore/singlestore.py +286 -29
  375. agno/vectordb/surrealdb/surrealdb.py +187 -7
  376. agno/vectordb/upstashdb/upstashdb.py +342 -26
  377. agno/vectordb/weaviate/weaviate.py +227 -165
  378. agno/workflow/__init__.py +17 -13
  379. agno/workflow/{v2/condition.py → condition.py} +135 -32
  380. agno/workflow/{v2/loop.py → loop.py} +115 -28
  381. agno/workflow/{v2/parallel.py → parallel.py} +138 -108
  382. agno/workflow/{v2/router.py → router.py} +133 -32
  383. agno/workflow/{v2/step.py → step.py} +207 -49
  384. agno/workflow/{v2/steps.py → steps.py} +147 -66
  385. agno/workflow/types.py +482 -0
  386. agno/workflow/workflow.py +2410 -696
  387. agno-2.0.0.dist-info/METADATA +494 -0
  388. agno-2.0.0.dist-info/RECORD +515 -0
  389. agno-2.0.0.dist-info/licenses/LICENSE +201 -0
  390. agno/agent/metrics.py +0 -110
  391. agno/api/app.py +0 -35
  392. agno/api/playground.py +0 -92
  393. agno/api/schemas/app.py +0 -12
  394. agno/api/schemas/playground.py +0 -22
  395. agno/api/schemas/user.py +0 -35
  396. agno/api/schemas/workspace.py +0 -46
  397. agno/api/user.py +0 -160
  398. agno/api/workflows.py +0 -33
  399. agno/api/workspace.py +0 -175
  400. agno/app/agui/__init__.py +0 -3
  401. agno/app/agui/app.py +0 -17
  402. agno/app/agui/sync_router.py +0 -120
  403. agno/app/base.py +0 -186
  404. agno/app/discord/__init__.py +0 -3
  405. agno/app/fastapi/__init__.py +0 -3
  406. agno/app/fastapi/app.py +0 -107
  407. agno/app/fastapi/async_router.py +0 -457
  408. agno/app/fastapi/sync_router.py +0 -448
  409. agno/app/playground/app.py +0 -228
  410. agno/app/playground/async_router.py +0 -1053
  411. agno/app/playground/deploy.py +0 -249
  412. agno/app/playground/operator.py +0 -183
  413. agno/app/playground/schemas.py +0 -223
  414. agno/app/playground/serve.py +0 -55
  415. agno/app/playground/sync_router.py +0 -1045
  416. agno/app/playground/utils.py +0 -46
  417. agno/app/settings.py +0 -15
  418. agno/app/slack/__init__.py +0 -3
  419. agno/app/slack/app.py +0 -19
  420. agno/app/slack/sync_router.py +0 -92
  421. agno/app/utils.py +0 -54
  422. agno/app/whatsapp/__init__.py +0 -3
  423. agno/app/whatsapp/app.py +0 -15
  424. agno/app/whatsapp/sync_router.py +0 -197
  425. agno/cli/auth_server.py +0 -249
  426. agno/cli/config.py +0 -274
  427. agno/cli/console.py +0 -88
  428. agno/cli/credentials.py +0 -23
  429. agno/cli/entrypoint.py +0 -571
  430. agno/cli/operator.py +0 -357
  431. agno/cli/settings.py +0 -96
  432. agno/cli/ws/ws_cli.py +0 -817
  433. agno/constants.py +0 -13
  434. agno/document/__init__.py +0 -5
  435. agno/document/chunking/semantic.py +0 -45
  436. agno/document/chunking/strategy.py +0 -31
  437. agno/document/reader/__init__.py +0 -5
  438. agno/document/reader/base.py +0 -47
  439. agno/document/reader/docx_reader.py +0 -60
  440. agno/document/reader/gcs/pdf_reader.py +0 -44
  441. agno/document/reader/s3/pdf_reader.py +0 -59
  442. agno/document/reader/s3/text_reader.py +0 -63
  443. agno/document/reader/url_reader.py +0 -59
  444. agno/document/reader/youtube_reader.py +0 -58
  445. agno/embedder/__init__.py +0 -5
  446. agno/embedder/langdb.py +0 -80
  447. agno/embedder/mistral.py +0 -82
  448. agno/embedder/openai.py +0 -78
  449. agno/file/__init__.py +0 -5
  450. agno/file/file.py +0 -16
  451. agno/file/local/csv.py +0 -32
  452. agno/file/local/txt.py +0 -19
  453. agno/infra/app.py +0 -240
  454. agno/infra/base.py +0 -144
  455. agno/infra/context.py +0 -20
  456. agno/infra/db_app.py +0 -52
  457. agno/infra/resource.py +0 -205
  458. agno/infra/resources.py +0 -55
  459. agno/knowledge/agent.py +0 -702
  460. agno/knowledge/arxiv.py +0 -33
  461. agno/knowledge/combined.py +0 -36
  462. agno/knowledge/csv.py +0 -144
  463. agno/knowledge/csv_url.py +0 -124
  464. agno/knowledge/document.py +0 -223
  465. agno/knowledge/docx.py +0 -137
  466. agno/knowledge/firecrawl.py +0 -34
  467. agno/knowledge/gcs/__init__.py +0 -0
  468. agno/knowledge/gcs/base.py +0 -39
  469. agno/knowledge/gcs/pdf.py +0 -125
  470. agno/knowledge/json.py +0 -137
  471. agno/knowledge/langchain.py +0 -71
  472. agno/knowledge/light_rag.py +0 -273
  473. agno/knowledge/llamaindex.py +0 -66
  474. agno/knowledge/markdown.py +0 -154
  475. agno/knowledge/pdf.py +0 -164
  476. agno/knowledge/pdf_bytes.py +0 -42
  477. agno/knowledge/pdf_url.py +0 -148
  478. agno/knowledge/s3/__init__.py +0 -0
  479. agno/knowledge/s3/base.py +0 -64
  480. agno/knowledge/s3/pdf.py +0 -33
  481. agno/knowledge/s3/text.py +0 -34
  482. agno/knowledge/text.py +0 -141
  483. agno/knowledge/url.py +0 -46
  484. agno/knowledge/website.py +0 -179
  485. agno/knowledge/wikipedia.py +0 -32
  486. agno/knowledge/youtube.py +0 -35
  487. agno/memory/agent.py +0 -423
  488. agno/memory/classifier.py +0 -104
  489. agno/memory/db/__init__.py +0 -5
  490. agno/memory/db/base.py +0 -42
  491. agno/memory/db/mongodb.py +0 -189
  492. agno/memory/db/postgres.py +0 -203
  493. agno/memory/db/sqlite.py +0 -193
  494. agno/memory/memory.py +0 -22
  495. agno/memory/row.py +0 -36
  496. agno/memory/summarizer.py +0 -201
  497. agno/memory/summary.py +0 -19
  498. agno/memory/team.py +0 -415
  499. agno/memory/v2/__init__.py +0 -2
  500. agno/memory/v2/db/__init__.py +0 -1
  501. agno/memory/v2/db/base.py +0 -42
  502. agno/memory/v2/db/firestore.py +0 -339
  503. agno/memory/v2/db/mongodb.py +0 -196
  504. agno/memory/v2/db/postgres.py +0 -214
  505. agno/memory/v2/db/redis.py +0 -187
  506. agno/memory/v2/db/schema.py +0 -54
  507. agno/memory/v2/db/sqlite.py +0 -209
  508. agno/memory/v2/manager.py +0 -437
  509. agno/memory/v2/memory.py +0 -1097
  510. agno/memory/v2/schema.py +0 -55
  511. agno/memory/v2/summarizer.py +0 -215
  512. agno/memory/workflow.py +0 -38
  513. agno/models/ollama/tools.py +0 -430
  514. agno/models/qwen/__init__.py +0 -5
  515. agno/playground/__init__.py +0 -10
  516. agno/playground/deploy.py +0 -3
  517. agno/playground/playground.py +0 -3
  518. agno/playground/serve.py +0 -3
  519. agno/playground/settings.py +0 -3
  520. agno/reranker/__init__.py +0 -0
  521. agno/run/response.py +0 -467
  522. agno/run/v2/__init__.py +0 -0
  523. agno/run/v2/workflow.py +0 -567
  524. agno/storage/__init__.py +0 -0
  525. agno/storage/agent/__init__.py +0 -0
  526. agno/storage/agent/dynamodb.py +0 -1
  527. agno/storage/agent/json.py +0 -1
  528. agno/storage/agent/mongodb.py +0 -1
  529. agno/storage/agent/postgres.py +0 -1
  530. agno/storage/agent/singlestore.py +0 -1
  531. agno/storage/agent/sqlite.py +0 -1
  532. agno/storage/agent/yaml.py +0 -1
  533. agno/storage/base.py +0 -60
  534. agno/storage/dynamodb.py +0 -673
  535. agno/storage/firestore.py +0 -297
  536. agno/storage/gcs_json.py +0 -261
  537. agno/storage/in_memory.py +0 -234
  538. agno/storage/json.py +0 -237
  539. agno/storage/mongodb.py +0 -328
  540. agno/storage/mysql.py +0 -685
  541. agno/storage/postgres.py +0 -682
  542. agno/storage/redis.py +0 -336
  543. agno/storage/session/__init__.py +0 -16
  544. agno/storage/session/agent.py +0 -64
  545. agno/storage/session/team.py +0 -63
  546. agno/storage/session/v2/__init__.py +0 -5
  547. agno/storage/session/workflow.py +0 -61
  548. agno/storage/singlestore.py +0 -606
  549. agno/storage/sqlite.py +0 -646
  550. agno/storage/workflow/__init__.py +0 -0
  551. agno/storage/workflow/mongodb.py +0 -1
  552. agno/storage/workflow/postgres.py +0 -1
  553. agno/storage/workflow/sqlite.py +0 -1
  554. agno/storage/yaml.py +0 -241
  555. agno/tools/thinking.py +0 -73
  556. agno/utils/defaults.py +0 -57
  557. agno/utils/filesystem.py +0 -39
  558. agno/utils/git.py +0 -52
  559. agno/utils/json_io.py +0 -30
  560. agno/utils/load_env.py +0 -19
  561. agno/utils/py_io.py +0 -19
  562. agno/utils/pyproject.py +0 -18
  563. agno/utils/resource_filter.py +0 -31
  564. agno/workflow/v2/__init__.py +0 -21
  565. agno/workflow/v2/types.py +0 -357
  566. agno/workflow/v2/workflow.py +0 -3313
  567. agno/workspace/__init__.py +0 -0
  568. agno/workspace/config.py +0 -325
  569. agno/workspace/enums.py +0 -6
  570. agno/workspace/helpers.py +0 -52
  571. agno/workspace/operator.py +0 -757
  572. agno/workspace/settings.py +0 -158
  573. agno-1.8.2.dist-info/METADATA +0 -982
  574. agno-1.8.2.dist-info/RECORD +0 -566
  575. agno-1.8.2.dist-info/entry_points.txt +0 -3
  576. agno-1.8.2.dist-info/licenses/LICENSE +0 -375
  577. /agno/{app → db/migrations}/__init__.py +0 -0
  578. /agno/{app/playground/__init__.py → db/schemas/metrics.py} +0 -0
  579. /agno/{cli → integrations}/__init__.py +0 -0
  580. /agno/{cli/ws → knowledge/chunking}/__init__.py +0 -0
  581. /agno/{document/chunking → knowledge/remote_content}/__init__.py +0 -0
  582. /agno/{document/reader/gcs → knowledge/reranker}/__init__.py +0 -0
  583. /agno/{document/reader/s3 → os/interfaces}/__init__.py +0 -0
  584. /agno/{app → os/interfaces}/slack/security.py +0 -0
  585. /agno/{app → os/interfaces}/whatsapp/security.py +0 -0
  586. /agno/{file/local → utils/print_response}/__init__.py +0 -0
  587. /agno/{infra → vectordb/llamaindex}/__init__.py +0 -0
  588. {agno-1.8.2.dist-info → agno-2.0.0.dist-info}/WHEEL +0 -0
  589. {agno-1.8.2.dist-info → agno-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1045 @@
1
+ import json
2
+ import os
3
+ import time
4
+ from datetime import date, datetime, timedelta, timezone
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Tuple, Union
7
+ from uuid import uuid4
8
+
9
+ from agno.db.base import BaseDb, SessionType
10
+ from agno.db.json.utils import (
11
+ apply_sorting,
12
+ calculate_date_metrics,
13
+ fetch_all_sessions_data,
14
+ get_dates_to_calculate_metrics_for,
15
+ hydrate_session,
16
+ )
17
+ from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
18
+ from agno.db.schemas.knowledge import KnowledgeRow
19
+ from agno.db.schemas.memory import UserMemory
20
+ from agno.session import AgentSession, Session, TeamSession, WorkflowSession
21
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
22
+
23
+
24
+ class JsonDb(BaseDb):
25
+ def __init__(
26
+ self,
27
+ db_path: Optional[str] = None,
28
+ session_table: Optional[str] = None,
29
+ memory_table: Optional[str] = None,
30
+ metrics_table: Optional[str] = None,
31
+ eval_table: Optional[str] = None,
32
+ knowledge_table: Optional[str] = None,
33
+ ):
34
+ """
35
+ Interface for interacting with JSON files as database.
36
+
37
+ Args:
38
+ db_path (Optional[str]): Path to the directory where JSON files will be stored.
39
+ session_table (Optional[str]): Name of the JSON file to store sessions (without .json extension).
40
+ memory_table (Optional[str]): Name of the JSON file to store memories.
41
+ metrics_table (Optional[str]): Name of the JSON file to store metrics.
42
+ eval_table (Optional[str]): Name of the JSON file to store evaluation runs.
43
+ knowledge_table (Optional[str]): Name of the JSON file to store knowledge content.
44
+ """
45
+ super().__init__(
46
+ session_table=session_table,
47
+ memory_table=memory_table,
48
+ metrics_table=metrics_table,
49
+ eval_table=eval_table,
50
+ knowledge_table=knowledge_table,
51
+ )
52
+
53
+ # Create the directory where the JSON files will be stored, if it doesn't exist
54
+ self.db_path = Path(db_path or os.path.join(os.getcwd(), "agno_json_db"))
55
+
56
+ def _read_json_file(self, filename: str, create_table_if_not_found: Optional[bool] = True) -> List[Dict[str, Any]]:
57
+ """Read data from a JSON file, creating it if it doesn't exist.
58
+
59
+ Args:
60
+ filename (str): The name of the JSON file to read.
61
+
62
+ Returns:
63
+ List[Dict[str, Any]]: The data from the JSON file.
64
+
65
+ Raises:
66
+ json.JSONDecodeError: If the JSON file is not valid.
67
+ """
68
+ file_path = self.db_path / f"{filename}.json"
69
+
70
+ # Create directory if it doesn't exist
71
+ self.db_path.mkdir(parents=True, exist_ok=True)
72
+
73
+ try:
74
+ with open(file_path, "r") as f:
75
+ return json.load(f)
76
+
77
+ except FileNotFoundError:
78
+ if create_table_if_not_found:
79
+ with open(file_path, "w") as f:
80
+ json.dump([], f)
81
+ return []
82
+
83
+ except json.JSONDecodeError as e:
84
+ log_error(f"Error reading the {file_path} JSON file")
85
+ raise e
86
+
87
+ def _write_json_file(self, filename: str, data: List[Dict[str, Any]]) -> None:
88
+ """Write data to a JSON file.
89
+
90
+ Args:
91
+ filename (str): The name of the JSON file to write.
92
+ data (List[Dict[str, Any]]): The data to write to the JSON file.
93
+
94
+ Raises:
95
+ Exception: If an error occurs while writing to the JSON file.
96
+ """
97
+ file_path = self.db_path / f"{filename}.json"
98
+
99
+ # Create directory if it doesn't exist
100
+ self.db_path.mkdir(parents=True, exist_ok=True)
101
+
102
+ try:
103
+ with open(file_path, "w") as f:
104
+ json.dump(data, f, indent=2, default=str)
105
+
106
+ except Exception as e:
107
+ log_error(f"Error writing to the {file_path} JSON file: {e}")
108
+ return
109
+
110
+ # -- Session methods --
111
+
112
+ def delete_session(self, session_id: str) -> bool:
113
+ """Delete a session from the JSON file.
114
+
115
+ Args:
116
+ session_id (str): The ID of the session to delete.
117
+
118
+ Returns:
119
+ bool: True if the session was deleted, False otherwise.
120
+
121
+ Raises:
122
+ Exception: If an error occurs during deletion.
123
+ """
124
+ try:
125
+ sessions = self._read_json_file(self.session_table_name)
126
+ original_count = len(sessions)
127
+ sessions = [s for s in sessions if s.get("session_id") != session_id]
128
+
129
+ if len(sessions) < original_count:
130
+ self._write_json_file(self.session_table_name, sessions)
131
+ log_debug(f"Successfully deleted session with session_id: {session_id}")
132
+ return True
133
+
134
+ else:
135
+ log_debug(f"No session found to delete with session_id: {session_id}")
136
+ return False
137
+
138
+ except Exception as e:
139
+ log_error(f"Error deleting session: {e}")
140
+ return False
141
+
142
+ def delete_sessions(self, session_ids: List[str]) -> None:
143
+ """Delete multiple sessions from the JSON file.
144
+
145
+ Args:
146
+ session_ids (List[str]): The IDs of the sessions to delete.
147
+
148
+ Raises:
149
+ Exception: If an error occurs during deletion.
150
+ """
151
+ try:
152
+ sessions = self._read_json_file(self.session_table_name)
153
+ sessions = [s for s in sessions if s.get("session_id") not in session_ids]
154
+ self._write_json_file(self.session_table_name, sessions)
155
+ log_debug(f"Successfully deleted sessions with ids: {session_ids}")
156
+
157
+ except Exception as e:
158
+ log_error(f"Error deleting sessions: {e}")
159
+
160
+ def get_session(
161
+ self,
162
+ session_id: str,
163
+ session_type: Optional[SessionType] = None,
164
+ user_id: Optional[str] = None,
165
+ deserialize: Optional[bool] = True,
166
+ ) -> Optional[Union[AgentSession, TeamSession, WorkflowSession, Dict[str, Any]]]:
167
+ """Read a session from the JSON file.
168
+
169
+ Args:
170
+ session_id (str): The ID of the session to read.
171
+ session_type (Optional[SessionType]): The type of the session to read.
172
+ user_id (Optional[str]): The ID of the user to read the session for.
173
+ deserialize (Optional[bool]): Whether to deserialize the session.
174
+
175
+ Returns:
176
+ Union[Session, Dict[str, Any], None]:
177
+ - When deserialize=True: Session object
178
+ - When deserialize=False: Session dictionary
179
+
180
+ Raises:
181
+ Exception: If an error occurs while reading the session.
182
+ """
183
+ try:
184
+ sessions = self._read_json_file(self.session_table_name)
185
+
186
+ for session_data in sessions:
187
+ if session_data.get("session_id") == session_id:
188
+ if user_id is not None and session_data.get("user_id") != user_id:
189
+ continue
190
+ session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
191
+ if session_data.get("session_type") != session_type_value:
192
+ continue
193
+
194
+ session = hydrate_session(session_data)
195
+
196
+ if not deserialize:
197
+ return session
198
+
199
+ if session_type == SessionType.AGENT:
200
+ return AgentSession.from_dict(session)
201
+ elif session_type == SessionType.TEAM:
202
+ return TeamSession.from_dict(session)
203
+ else:
204
+ return WorkflowSession.from_dict(session)
205
+
206
+ return None
207
+
208
+ except Exception as e:
209
+ log_error(f"Exception reading from session file: {e}")
210
+ return None
211
+
212
+ def get_sessions(
213
+ self,
214
+ session_type: Optional[SessionType] = None,
215
+ user_id: Optional[str] = None,
216
+ component_id: Optional[str] = None,
217
+ session_name: Optional[str] = None,
218
+ start_timestamp: Optional[int] = None,
219
+ end_timestamp: Optional[int] = None,
220
+ limit: Optional[int] = None,
221
+ page: Optional[int] = None,
222
+ sort_by: Optional[str] = None,
223
+ sort_order: Optional[str] = None,
224
+ deserialize: Optional[bool] = True,
225
+ ) -> Union[List[Session], Tuple[List[Dict[str, Any]], int]]:
226
+ """Get all sessions from the JSON file with filtering and pagination.
227
+
228
+ Args:
229
+ session_type (Optional[SessionType]): The type of the sessions to read.
230
+ user_id (Optional[str]): The ID of the user to read the sessions for.
231
+ component_id (Optional[str]): The ID of the component to read the sessions for.
232
+ session_name (Optional[str]): The name of the session to read.
233
+ start_timestamp (Optional[int]): The start timestamp of the sessions to read.
234
+ end_timestamp (Optional[int]): The end timestamp of the sessions to read.
235
+ limit (Optional[int]): The limit of the sessions to read.
236
+ page (Optional[int]): The page of the sessions to read.
237
+ sort_by (Optional[str]): The field to sort the sessions by.
238
+ sort_order (Optional[str]): The order to sort the sessions by.
239
+ deserialize (Optional[bool]): Whether to deserialize the sessions.
240
+ create_table_if_not_found (Optional[bool]): Whether to create a json file to track sessions if it doesn't exist.
241
+
242
+ Returns:
243
+ Union[List[AgentSession], List[TeamSession], List[WorkflowSession], Tuple[List[Dict[str, Any]], int]]:
244
+ - When deserialize=True: List of sessions
245
+ - When deserialize=False: Tuple with list of sessions and total count
246
+
247
+ Raises:
248
+ Exception: If an error occurs while reading the sessions.
249
+ """
250
+ try:
251
+ sessions = self._read_json_file(self.session_table_name)
252
+
253
+ # Apply filters
254
+ filtered_sessions = []
255
+ for session_data in sessions:
256
+ if user_id is not None and session_data.get("user_id") != user_id:
257
+ continue
258
+ if component_id is not None:
259
+ if session_type == SessionType.AGENT and session_data.get("agent_id") != component_id:
260
+ continue
261
+ elif session_type == SessionType.TEAM and session_data.get("team_id") != component_id:
262
+ continue
263
+ elif session_type == SessionType.WORKFLOW and session_data.get("workflow_id") != component_id:
264
+ continue
265
+ if start_timestamp is not None and session_data.get("created_at", 0) < start_timestamp:
266
+ continue
267
+ if end_timestamp is not None and session_data.get("created_at", 0) > end_timestamp:
268
+ continue
269
+ if session_name is not None:
270
+ stored_name = session_data.get("session_data", {}).get("session_name", "")
271
+ if session_name.lower() not in stored_name.lower():
272
+ continue
273
+ session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
274
+ if session_data.get("session_type") != session_type_value:
275
+ continue
276
+
277
+ filtered_sessions.append(session_data)
278
+
279
+ total_count = len(filtered_sessions)
280
+
281
+ # Apply sorting
282
+ filtered_sessions = apply_sorting(filtered_sessions, sort_by, sort_order)
283
+
284
+ # Apply pagination
285
+ if limit is not None:
286
+ start_idx = 0
287
+ if page is not None:
288
+ start_idx = (page - 1) * limit
289
+ filtered_sessions = filtered_sessions[start_idx : start_idx + limit]
290
+
291
+ if not deserialize:
292
+ return filtered_sessions, total_count
293
+
294
+ if session_type == SessionType.AGENT:
295
+ return [AgentSession.from_dict(session) for session in filtered_sessions] # type: ignore
296
+ elif session_type == SessionType.TEAM:
297
+ return [TeamSession.from_dict(session) for session in filtered_sessions] # type: ignore
298
+ elif session_type == SessionType.WORKFLOW:
299
+ return [WorkflowSession.from_dict(session) for session in filtered_sessions] # type: ignore
300
+ else:
301
+ raise ValueError(f"Invalid session type: {session_type}")
302
+
303
+ except Exception as e:
304
+ log_error(f"Exception reading from session file: {e}")
305
+ return [] if deserialize else ([], 0)
306
+
307
+ def rename_session(
308
+ self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
309
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
310
+ """Rename a session in the JSON file."""
311
+ try:
312
+ sessions = self._read_json_file(self.session_table_name)
313
+
314
+ for i, session in enumerate(sessions):
315
+ if session.get("session_id") == session_id and session.get("session_type") == session_type.value:
316
+ # Update session name in session_data
317
+ if "session_data" not in session:
318
+ session["session_data"] = {}
319
+ session["session_data"]["session_name"] = session_name
320
+
321
+ sessions[i] = session
322
+ self._write_json_file(self.session_table_name, sessions)
323
+
324
+ log_debug(f"Renamed session with id '{session_id}' to '{session_name}'")
325
+
326
+ if not deserialize:
327
+ return session
328
+
329
+ if session_type == SessionType.AGENT:
330
+ return AgentSession.from_dict(session)
331
+ elif session_type == SessionType.TEAM:
332
+ return TeamSession.from_dict(session)
333
+ else:
334
+ return WorkflowSession.from_dict(session)
335
+
336
+ return None
337
+
338
+ except Exception as e:
339
+ log_error(f"Exception renaming session: {e}")
340
+ return None
341
+
342
+ def upsert_session(
343
+ self, session: Session, deserialize: Optional[bool] = True
344
+ ) -> Optional[Union[Session, Dict[str, Any]]]:
345
+ """Insert or update a session in the JSON file."""
346
+ try:
347
+ sessions = self._read_json_file(self.session_table_name, create_table_if_not_found=True)
348
+ session_dict = session.to_dict()
349
+
350
+ # Add session_type based on session instance type
351
+ if isinstance(session, AgentSession):
352
+ session_dict["session_type"] = SessionType.AGENT.value
353
+ elif isinstance(session, TeamSession):
354
+ session_dict["session_type"] = SessionType.TEAM.value
355
+ elif isinstance(session, WorkflowSession):
356
+ session_dict["session_type"] = SessionType.WORKFLOW.value
357
+
358
+ # Find existing session to update
359
+ session_updated = False
360
+ for i, existing_session in enumerate(sessions):
361
+ if existing_session.get("session_id") == session_dict.get("session_id") and self._matches_session_key(
362
+ existing_session, session
363
+ ):
364
+ # Update existing session
365
+ session_dict["updated_at"] = int(time.time())
366
+ sessions[i] = session_dict
367
+ session_updated = True
368
+ break
369
+
370
+ if not session_updated:
371
+ # Add new session
372
+ session_dict["created_at"] = session_dict.get("created_at", int(time.time()))
373
+ session_dict["updated_at"] = session_dict.get("created_at")
374
+ sessions.append(session_dict)
375
+
376
+ self._write_json_file(self.session_table_name, sessions)
377
+
378
+ if not deserialize:
379
+ return session_dict
380
+
381
+ return session
382
+
383
+ except Exception as e:
384
+ log_error(f"Exception upserting session: {e}")
385
+ return None
386
+
387
+ def _matches_session_key(self, existing_session: Dict[str, Any], session: Session) -> bool:
388
+ """Check if existing session matches the key for the session type."""
389
+ if isinstance(session, AgentSession):
390
+ return existing_session.get("agent_id") == session.agent_id
391
+ elif isinstance(session, TeamSession):
392
+ return existing_session.get("team_id") == session.team_id
393
+ elif isinstance(session, WorkflowSession):
394
+ return existing_session.get("workflow_id") == session.workflow_id
395
+ return False
396
+
397
+ # -- Memory methods --
398
+ def delete_user_memory(self, memory_id: str):
399
+ """Delete a user memory from the JSON file."""
400
+ try:
401
+ memories = self._read_json_file(self.memory_table_name)
402
+ original_count = len(memories)
403
+ memories = [m for m in memories if m.get("memory_id") != memory_id]
404
+
405
+ if len(memories) < original_count:
406
+ self._write_json_file(self.memory_table_name, memories)
407
+ log_debug(f"Successfully deleted user memory id: {memory_id}")
408
+ else:
409
+ log_debug(f"No memory found with id: {memory_id}")
410
+
411
+ except Exception as e:
412
+ log_error(f"Error deleting memory: {e}")
413
+
414
+ def delete_user_memories(self, memory_ids: List[str]) -> None:
415
+ """Delete multiple user memories from the JSON file."""
416
+ try:
417
+ memories = self._read_json_file(self.memory_table_name)
418
+ memories = [m for m in memories if m.get("memory_id") not in memory_ids]
419
+ self._write_json_file(self.memory_table_name, memories)
420
+
421
+ log_debug(f"Successfully deleted {len(memory_ids)} user memories")
422
+
423
+ except Exception as e:
424
+ log_error(f"Error deleting memories: {e}")
425
+
426
+ def get_all_memory_topics(self) -> List[str]:
427
+ """Get all memory topics from the JSON file."""
428
+ try:
429
+ memories = self._read_json_file(self.memory_table_name)
430
+
431
+ topics = set()
432
+ for memory in memories:
433
+ memory_topics = memory.get("topics", [])
434
+ if isinstance(memory_topics, list):
435
+ topics.update(memory_topics)
436
+ return list(topics)
437
+
438
+ except Exception as e:
439
+ log_error(f"Exception reading from memory file: {e}")
440
+ return []
441
+
442
+ def get_user_memory(
443
+ self, memory_id: str, deserialize: Optional[bool] = True
444
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
445
+ """Get a memory from the JSON file."""
446
+ try:
447
+ memories = self._read_json_file(self.memory_table_name)
448
+
449
+ for memory_data in memories:
450
+ if memory_data.get("memory_id") == memory_id:
451
+ if not deserialize:
452
+ return memory_data
453
+ return UserMemory.from_dict(memory_data)
454
+
455
+ return None
456
+
457
+ except Exception as e:
458
+ log_error(f"Exception reading from memory file: {e}")
459
+ return None
460
+
461
+ def get_user_memories(
462
+ self,
463
+ user_id: Optional[str] = None,
464
+ agent_id: Optional[str] = None,
465
+ team_id: Optional[str] = None,
466
+ topics: Optional[List[str]] = None,
467
+ search_content: Optional[str] = None,
468
+ limit: Optional[int] = None,
469
+ page: Optional[int] = None,
470
+ sort_by: Optional[str] = None,
471
+ sort_order: Optional[str] = None,
472
+ deserialize: Optional[bool] = True,
473
+ ) -> Union[List[UserMemory], Tuple[List[Dict[str, Any]], int]]:
474
+ """Get all memories from the JSON file with filtering and pagination."""
475
+ try:
476
+ memories = self._read_json_file(self.memory_table_name)
477
+
478
+ # Apply filters
479
+ filtered_memories = []
480
+ for memory_data in memories:
481
+ if user_id is not None and memory_data.get("user_id") != user_id:
482
+ continue
483
+ if agent_id is not None and memory_data.get("agent_id") != agent_id:
484
+ continue
485
+ if team_id is not None and memory_data.get("team_id") != team_id:
486
+ continue
487
+ if topics is not None:
488
+ memory_topics = memory_data.get("topics", [])
489
+ if not any(topic in memory_topics for topic in topics):
490
+ continue
491
+ if search_content is not None:
492
+ memory_content = str(memory_data.get("memory", ""))
493
+ if search_content.lower() not in memory_content.lower():
494
+ continue
495
+
496
+ filtered_memories.append(memory_data)
497
+
498
+ total_count = len(filtered_memories)
499
+
500
+ # Apply sorting
501
+ filtered_memories = apply_sorting(filtered_memories, sort_by, sort_order)
502
+
503
+ # Apply pagination
504
+ if limit is not None:
505
+ start_idx = 0
506
+ if page is not None:
507
+ start_idx = (page - 1) * limit
508
+ filtered_memories = filtered_memories[start_idx : start_idx + limit]
509
+
510
+ if not deserialize:
511
+ return filtered_memories, total_count
512
+
513
+ return [UserMemory.from_dict(memory) for memory in filtered_memories]
514
+
515
+ except Exception as e:
516
+ log_error(f"Exception reading from memory file: {e}")
517
+ return [] if deserialize else ([], 0)
518
+
519
+ def get_user_memory_stats(
520
+ self, limit: Optional[int] = None, page: Optional[int] = None
521
+ ) -> Tuple[List[Dict[str, Any]], int]:
522
+ """Get user memory statistics."""
523
+ try:
524
+ memories = self._read_json_file(self.memory_table_name)
525
+ user_stats = {}
526
+
527
+ for memory in memories:
528
+ user_id = memory.get("user_id")
529
+ if user_id:
530
+ if user_id not in user_stats:
531
+ user_stats[user_id] = {"user_id": user_id, "total_memories": 0, "last_memory_updated_at": 0}
532
+ user_stats[user_id]["total_memories"] += 1
533
+ updated_at = memory.get("updated_at", 0)
534
+ if updated_at > user_stats[user_id]["last_memory_updated_at"]:
535
+ user_stats[user_id]["last_memory_updated_at"] = updated_at
536
+
537
+ stats_list = list(user_stats.values())
538
+ stats_list.sort(key=lambda x: x["last_memory_updated_at"], reverse=True)
539
+
540
+ total_count = len(stats_list)
541
+
542
+ # Apply pagination
543
+ if limit is not None:
544
+ start_idx = 0
545
+ if page is not None:
546
+ start_idx = (page - 1) * limit
547
+ stats_list = stats_list[start_idx : start_idx + limit]
548
+
549
+ return stats_list, total_count
550
+
551
+ except Exception as e:
552
+ log_error(f"Exception getting user memory stats: {e}")
553
+ return [], 0
554
+
555
+ def upsert_user_memory(
556
+ self, memory: UserMemory, deserialize: Optional[bool] = True
557
+ ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
558
+ """Upsert a user memory in the JSON file."""
559
+ try:
560
+ memories = self._read_json_file(self.memory_table_name, create_table_if_not_found=True)
561
+
562
+ if memory.memory_id is None:
563
+ memory.memory_id = str(uuid4())
564
+
565
+ memory_dict = memory.to_dict() if hasattr(memory, "to_dict") else memory.__dict__
566
+ memory_dict["updated_at"] = int(time.time())
567
+
568
+ # Find existing memory to update
569
+ memory_updated = False
570
+ for i, existing_memory in enumerate(memories):
571
+ if existing_memory.get("memory_id") == memory.memory_id:
572
+ memories[i] = memory_dict
573
+ memory_updated = True
574
+ break
575
+
576
+ if not memory_updated:
577
+ memories.append(memory_dict)
578
+
579
+ self._write_json_file(self.memory_table_name, memories)
580
+
581
+ if not deserialize:
582
+ return memory_dict
583
+ return UserMemory.from_dict(memory_dict)
584
+
585
+ except Exception as e:
586
+ log_warning(f"Exception upserting user memory: {e}")
587
+ return None
588
+
589
+ def clear_memories(self) -> None:
590
+ """Delete all memories from the database.
591
+
592
+ Raises:
593
+ Exception: If an error occurs during deletion.
594
+ """
595
+ try:
596
+ # Simply write an empty list to the memory JSON file
597
+ self._write_json_file(self.memory_table_name, [])
598
+
599
+ except Exception as e:
600
+ log_warning(f"Exception deleting all memories: {e}")
601
+
602
+ # -- Metrics methods --
603
+ def calculate_metrics(self) -> Optional[list[dict]]:
604
+ """Calculate metrics for all dates without complete metrics."""
605
+ try:
606
+ metrics = self._read_json_file(self.metrics_table_name, create_table_if_not_found=True)
607
+
608
+ starting_date = self._get_metrics_calculation_starting_date(metrics)
609
+ if starting_date is None:
610
+ log_info("No session data found. Won't calculate metrics.")
611
+ return None
612
+
613
+ dates_to_process = get_dates_to_calculate_metrics_for(starting_date)
614
+ if not dates_to_process:
615
+ log_info("Metrics already calculated for all relevant dates.")
616
+ return None
617
+
618
+ start_timestamp = int(datetime.combine(dates_to_process[0], datetime.min.time()).timestamp())
619
+ end_timestamp = int(
620
+ datetime.combine(dates_to_process[-1] + timedelta(days=1), datetime.min.time()).timestamp()
621
+ )
622
+
623
+ sessions = self._get_all_sessions_for_metrics_calculation(start_timestamp, end_timestamp)
624
+ all_sessions_data = fetch_all_sessions_data(
625
+ sessions=sessions, dates_to_process=dates_to_process, start_timestamp=start_timestamp
626
+ )
627
+ if not all_sessions_data:
628
+ log_info("No new session data found. Won't calculate metrics.")
629
+ return None
630
+
631
+ results = []
632
+
633
+ for date_to_process in dates_to_process:
634
+ date_key = date_to_process.isoformat()
635
+ sessions_for_date = all_sessions_data.get(date_key, {})
636
+
637
+ # Skip dates with no sessions
638
+ if not any(len(sessions) > 0 for sessions in sessions_for_date.values()):
639
+ continue
640
+
641
+ metrics_record = calculate_date_metrics(date_to_process, sessions_for_date)
642
+
643
+ # Upsert metrics record
644
+ existing_record_idx = None
645
+ for i, existing_metric in enumerate(metrics):
646
+ if (
647
+ existing_metric.get("date") == str(date_to_process)
648
+ and existing_metric.get("aggregation_period") == "daily"
649
+ ):
650
+ existing_record_idx = i
651
+ break
652
+
653
+ if existing_record_idx is not None:
654
+ metrics[existing_record_idx] = metrics_record
655
+ else:
656
+ metrics.append(metrics_record)
657
+
658
+ results.append(metrics_record)
659
+
660
+ if results:
661
+ self._write_json_file(self.metrics_table_name, metrics)
662
+
663
+ log_debug("Updated metrics calculations")
664
+
665
+ return results
666
+
667
+ except Exception as e:
668
+ log_warning(f"Exception refreshing metrics: {e}")
669
+ return None
670
+
671
+ def _get_metrics_calculation_starting_date(self, metrics: List[Dict[str, Any]]) -> Optional[date]:
672
+ """Get the first date for which metrics calculation is needed."""
673
+ if metrics:
674
+ # Sort by date in descending order
675
+ sorted_metrics = sorted(metrics, key=lambda x: x.get("date", ""), reverse=True)
676
+ latest_metric = sorted_metrics[0]
677
+
678
+ if latest_metric.get("completed", False):
679
+ latest_date = datetime.strptime(latest_metric["date"], "%Y-%m-%d").date()
680
+ return latest_date + timedelta(days=1)
681
+ else:
682
+ return datetime.strptime(latest_metric["date"], "%Y-%m-%d").date()
683
+
684
+ # No metrics records. Return the date of the first recorded session.
685
+ # We need to get sessions of all types, so we'll read directly from the file
686
+ all_sessions = self._read_json_file(self.session_table_name)
687
+ if all_sessions:
688
+ # Sort by created_at
689
+ all_sessions.sort(key=lambda x: x.get("created_at", 0))
690
+ first_session_date = all_sessions[0]["created_at"]
691
+ return datetime.fromtimestamp(first_session_date, tz=timezone.utc).date()
692
+
693
+ return None
694
+
695
+ def _get_all_sessions_for_metrics_calculation(
696
+ self, start_timestamp: Optional[int] = None, end_timestamp: Optional[int] = None
697
+ ) -> List[Dict[str, Any]]:
698
+ """Get all sessions for metrics calculation."""
699
+ try:
700
+ sessions = self._read_json_file(self.session_table_name)
701
+
702
+ filtered_sessions = []
703
+ for session in sessions:
704
+ created_at = session.get("created_at", 0)
705
+ if start_timestamp is not None and created_at < start_timestamp:
706
+ continue
707
+ if end_timestamp is not None and created_at >= end_timestamp:
708
+ continue
709
+
710
+ # Only include necessary fields for metrics
711
+ filtered_session = {
712
+ "user_id": session.get("user_id"),
713
+ "session_data": session.get("session_data"),
714
+ "runs": session.get("runs"),
715
+ "created_at": session.get("created_at"),
716
+ "session_type": session.get("session_type"),
717
+ }
718
+ filtered_sessions.append(filtered_session)
719
+
720
+ return filtered_sessions
721
+
722
+ except Exception as e:
723
+ log_error(f"Exception reading sessions for metrics: {e}")
724
+ return []
725
+
726
+ def get_metrics(
727
+ self,
728
+ starting_date: Optional[date] = None,
729
+ ending_date: Optional[date] = None,
730
+ ) -> Tuple[List[dict], Optional[int]]:
731
+ """Get all metrics matching the given date range."""
732
+ try:
733
+ metrics = self._read_json_file(self.metrics_table_name)
734
+
735
+ filtered_metrics = []
736
+ latest_updated_at = None
737
+
738
+ for metric in metrics:
739
+ metric_date = datetime.strptime(metric.get("date", ""), "%Y-%m-%d").date()
740
+
741
+ if starting_date and metric_date < starting_date:
742
+ continue
743
+ if ending_date and metric_date > ending_date:
744
+ continue
745
+
746
+ filtered_metrics.append(metric)
747
+
748
+ updated_at = metric.get("updated_at")
749
+ if updated_at and (latest_updated_at is None or updated_at > latest_updated_at):
750
+ latest_updated_at = updated_at
751
+
752
+ return filtered_metrics, latest_updated_at
753
+
754
+ except Exception as e:
755
+ log_error(f"Exception getting metrics: {e}")
756
+ return [], None
757
+
758
+ # -- Knowledge methods --
759
+
760
+ def delete_knowledge_content(self, id: str):
761
+ """Delete a knowledge row from the database.
762
+
763
+ Args:
764
+ id (str): The ID of the knowledge row to delete.
765
+
766
+ Raises:
767
+ Exception: If an error occurs during deletion.
768
+ """
769
+ try:
770
+ knowledge_items = self._read_json_file(self.knowledge_table_name)
771
+ knowledge_items = [item for item in knowledge_items if item.get("id") != id]
772
+ self._write_json_file(self.knowledge_table_name, knowledge_items)
773
+
774
+ except Exception as e:
775
+ log_error(f"Error deleting knowledge content: {e}")
776
+
777
+ def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
778
+ """Get a knowledge row from the database.
779
+
780
+ Args:
781
+ id (str): The ID of the knowledge row to get.
782
+
783
+ Returns:
784
+ Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
785
+
786
+ Raises:
787
+ Exception: If an error occurs during retrieval.
788
+ """
789
+ try:
790
+ knowledge_items = self._read_json_file(self.knowledge_table_name)
791
+
792
+ for item in knowledge_items:
793
+ if item.get("id") == id:
794
+ return KnowledgeRow.model_validate(item)
795
+
796
+ return None
797
+
798
+ except Exception as e:
799
+ log_error(f"Error getting knowledge content: {e}")
800
+ return None
801
+
802
+ def get_knowledge_contents(
803
+ self,
804
+ limit: Optional[int] = None,
805
+ page: Optional[int] = None,
806
+ sort_by: Optional[str] = None,
807
+ sort_order: Optional[str] = None,
808
+ ) -> Tuple[List[KnowledgeRow], int]:
809
+ """Get all knowledge contents from the database.
810
+
811
+ Args:
812
+ limit (Optional[int]): The maximum number of knowledge contents to return.
813
+ page (Optional[int]): The page number.
814
+ sort_by (Optional[str]): The column to sort by.
815
+ sort_order (Optional[str]): The order to sort by.
816
+
817
+ Returns:
818
+ Tuple[List[KnowledgeRow], int]: The knowledge contents and total count.
819
+
820
+ Raises:
821
+ Exception: If an error occurs during retrieval.
822
+ """
823
+ try:
824
+ knowledge_items = self._read_json_file(self.knowledge_table_name)
825
+
826
+ total_count = len(knowledge_items)
827
+
828
+ # Apply sorting
829
+ knowledge_items = apply_sorting(knowledge_items, sort_by, sort_order)
830
+
831
+ # Apply pagination
832
+ if limit is not None:
833
+ start_idx = 0
834
+ if page is not None:
835
+ start_idx = (page - 1) * limit
836
+ knowledge_items = knowledge_items[start_idx : start_idx + limit]
837
+
838
+ return [KnowledgeRow.model_validate(item) for item in knowledge_items], total_count
839
+
840
+ except Exception as e:
841
+ log_error(f"Error getting knowledge contents: {e}")
842
+ return [], 0
843
+
844
+ def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
845
+ """Upsert knowledge content in the database.
846
+
847
+ Args:
848
+ knowledge_row (KnowledgeRow): The knowledge row to upsert.
849
+
850
+ Returns:
851
+ Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
852
+
853
+ Raises:
854
+ Exception: If an error occurs during upsert.
855
+ """
856
+ try:
857
+ knowledge_items = self._read_json_file(self.knowledge_table_name, create_table_if_not_found=True)
858
+ knowledge_dict = knowledge_row.model_dump()
859
+
860
+ # Find existing item to update
861
+ item_updated = False
862
+ for i, existing_item in enumerate(knowledge_items):
863
+ if existing_item.get("id") == knowledge_row.id:
864
+ knowledge_items[i] = knowledge_dict
865
+ item_updated = True
866
+ break
867
+
868
+ if not item_updated:
869
+ knowledge_items.append(knowledge_dict)
870
+
871
+ self._write_json_file(self.knowledge_table_name, knowledge_items)
872
+
873
+ return knowledge_row
874
+
875
+ except Exception as e:
876
+ log_error(f"Error upserting knowledge row: {e}")
877
+ return None
878
+
879
+ # -- Eval methods --
880
+
881
+ def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
882
+ """Create an EvalRunRecord in the JSON file."""
883
+ try:
884
+ eval_runs = self._read_json_file(self.eval_table_name, create_table_if_not_found=True)
885
+
886
+ current_time = int(time.time())
887
+ eval_dict = eval_run.model_dump()
888
+ eval_dict["created_at"] = current_time
889
+ eval_dict["updated_at"] = current_time
890
+
891
+ eval_runs.append(eval_dict)
892
+ self._write_json_file(self.eval_table_name, eval_runs)
893
+
894
+ log_debug(f"Created eval run with id '{eval_run.run_id}'")
895
+
896
+ return eval_run
897
+
898
+ except Exception as e:
899
+ log_error(f"Error creating eval run: {e}")
900
+ return None
901
+
902
+ def delete_eval_run(self, eval_run_id: str) -> None:
903
+ """Delete an eval run from the JSON file."""
904
+ try:
905
+ eval_runs = self._read_json_file(self.eval_table_name)
906
+ original_count = len(eval_runs)
907
+ eval_runs = [run for run in eval_runs if run.get("run_id") != eval_run_id]
908
+
909
+ if len(eval_runs) < original_count:
910
+ self._write_json_file(self.eval_table_name, eval_runs)
911
+ log_debug(f"Deleted eval run with ID: {eval_run_id}")
912
+ else:
913
+ log_debug(f"No eval run found with ID: {eval_run_id}")
914
+
915
+ except Exception as e:
916
+ log_error(f"Error deleting eval run {eval_run_id}: {e}")
917
+
918
+ def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
919
+ """Delete multiple eval runs from the JSON file."""
920
+ try:
921
+ eval_runs = self._read_json_file(self.eval_table_name)
922
+ original_count = len(eval_runs)
923
+ eval_runs = [run for run in eval_runs if run.get("run_id") not in eval_run_ids]
924
+
925
+ deleted_count = original_count - len(eval_runs)
926
+ if deleted_count > 0:
927
+ self._write_json_file(self.eval_table_name, eval_runs)
928
+ log_debug(f"Deleted {deleted_count} eval runs")
929
+ else:
930
+ log_debug(f"No eval runs found with IDs: {eval_run_ids}")
931
+
932
+ except Exception as e:
933
+ log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
934
+
935
+ def get_eval_run(
936
+ self, eval_run_id: str, deserialize: Optional[bool] = True
937
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
938
+ """Get an eval run from the JSON file."""
939
+ try:
940
+ eval_runs = self._read_json_file(self.eval_table_name)
941
+
942
+ for run_data in eval_runs:
943
+ if run_data.get("run_id") == eval_run_id:
944
+ if not deserialize:
945
+ return run_data
946
+ return EvalRunRecord.model_validate(run_data)
947
+
948
+ return None
949
+
950
+ except Exception as e:
951
+ log_error(f"Exception getting eval run {eval_run_id}: {e}")
952
+ return None
953
+
954
+ def get_eval_runs(
955
+ self,
956
+ limit: Optional[int] = None,
957
+ page: Optional[int] = None,
958
+ sort_by: Optional[str] = None,
959
+ sort_order: Optional[str] = None,
960
+ agent_id: Optional[str] = None,
961
+ team_id: Optional[str] = None,
962
+ workflow_id: Optional[str] = None,
963
+ model_id: Optional[str] = None,
964
+ filter_type: Optional[EvalFilterType] = None,
965
+ eval_type: Optional[List[EvalType]] = None,
966
+ deserialize: Optional[bool] = True,
967
+ ) -> Union[List[EvalRunRecord], Tuple[List[Dict[str, Any]], int]]:
968
+ """Get all eval runs from the JSON file with filtering and pagination."""
969
+ try:
970
+ eval_runs = self._read_json_file(self.eval_table_name)
971
+
972
+ # Apply filters
973
+ filtered_runs = []
974
+ for run_data in eval_runs:
975
+ if agent_id is not None and run_data.get("agent_id") != agent_id:
976
+ continue
977
+ if team_id is not None and run_data.get("team_id") != team_id:
978
+ continue
979
+ if workflow_id is not None and run_data.get("workflow_id") != workflow_id:
980
+ continue
981
+ if model_id is not None and run_data.get("model_id") != model_id:
982
+ continue
983
+ if eval_type is not None and len(eval_type) > 0:
984
+ if run_data.get("eval_type") not in eval_type:
985
+ continue
986
+ if filter_type is not None:
987
+ if filter_type == EvalFilterType.AGENT and run_data.get("agent_id") is None:
988
+ continue
989
+ elif filter_type == EvalFilterType.TEAM and run_data.get("team_id") is None:
990
+ continue
991
+ elif filter_type == EvalFilterType.WORKFLOW and run_data.get("workflow_id") is None:
992
+ continue
993
+
994
+ filtered_runs.append(run_data)
995
+
996
+ total_count = len(filtered_runs)
997
+
998
+ # Apply sorting (default by created_at desc)
999
+ if sort_by is None:
1000
+ filtered_runs.sort(key=lambda x: x.get("created_at", 0), reverse=True)
1001
+ else:
1002
+ filtered_runs = apply_sorting(filtered_runs, sort_by, sort_order)
1003
+
1004
+ # Apply pagination
1005
+ if limit is not None:
1006
+ start_idx = 0
1007
+ if page is not None:
1008
+ start_idx = (page - 1) * limit
1009
+ filtered_runs = filtered_runs[start_idx : start_idx + limit]
1010
+
1011
+ if not deserialize:
1012
+ return filtered_runs, total_count
1013
+
1014
+ return [EvalRunRecord.model_validate(run) for run in filtered_runs]
1015
+
1016
+ except Exception as e:
1017
+ log_error(f"Exception getting eval runs: {e}")
1018
+ return [] if deserialize else ([], 0)
1019
+
1020
+ def rename_eval_run(
1021
+ self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
1022
+ ) -> Optional[Union[EvalRunRecord, Dict[str, Any]]]:
1023
+ """Rename an eval run in the JSON file."""
1024
+ try:
1025
+ eval_runs = self._read_json_file(self.eval_table_name)
1026
+
1027
+ for i, run_data in enumerate(eval_runs):
1028
+ if run_data.get("run_id") == eval_run_id:
1029
+ run_data["name"] = name
1030
+ run_data["updated_at"] = int(time.time())
1031
+ eval_runs[i] = run_data
1032
+ self._write_json_file(self.eval_table_name, eval_runs)
1033
+
1034
+ log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
1035
+
1036
+ if not deserialize:
1037
+ return run_data
1038
+
1039
+ return EvalRunRecord.model_validate(run_data)
1040
+
1041
+ return None
1042
+
1043
+ except Exception as e:
1044
+ log_error(f"Error renaming eval run {eval_run_id}: {e}")
1045
+ return None