agno 2.0.1__py3-none-any.whl → 2.3.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 (314) hide show
  1. agno/agent/agent.py +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  import time
2
+ from copy import deepcopy
2
3
  from datetime import date, datetime, timedelta, timezone
3
4
  from typing import Any, Dict, List, Optional, Tuple, Union
4
5
  from uuid import uuid4
@@ -7,9 +8,12 @@ from agno.db.base import BaseDb, SessionType
7
8
  from agno.db.in_memory.utils import (
8
9
  apply_sorting,
9
10
  calculate_date_metrics,
11
+ deserialize_cultural_knowledge_from_db,
10
12
  fetch_all_sessions_data,
11
13
  get_dates_to_calculate_metrics_for,
14
+ serialize_cultural_knowledge_for_db,
12
15
  )
16
+ from agno.db.schemas.culture import CulturalKnowledge
13
17
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
14
18
  from agno.db.schemas.knowledge import KnowledgeRow
15
19
  from agno.db.schemas.memory import UserMemory
@@ -28,9 +32,21 @@ class InMemoryDb(BaseDb):
28
32
  self._metrics: List[Dict[str, Any]] = []
29
33
  self._eval_runs: List[Dict[str, Any]] = []
30
34
  self._knowledge: List[Dict[str, Any]] = []
35
+ self._cultural_knowledge: List[Dict[str, Any]] = []
31
36
 
32
- # -- Session methods --
37
+ def table_exists(self, table_name: str) -> bool:
38
+ """In-memory implementation, always returns True."""
39
+ return True
40
+
41
+ def get_latest_schema_version(self):
42
+ """Get the latest version of the database schema."""
43
+ pass
33
44
 
45
+ def upsert_schema_version(self, version: str) -> None:
46
+ """Upsert the schema version into the database."""
47
+ pass
48
+
49
+ # -- Session methods --
34
50
  def delete_session(self, session_id: str) -> bool:
35
51
  """Delete a session from in-memory storage.
36
52
 
@@ -56,7 +72,7 @@ class InMemoryDb(BaseDb):
56
72
 
57
73
  except Exception as e:
58
74
  log_error(f"Error deleting session: {e}")
59
- return False
75
+ raise e
60
76
 
61
77
  def delete_sessions(self, session_ids: List[str]) -> None:
62
78
  """Delete multiple sessions from in-memory storage.
@@ -73,6 +89,7 @@ class InMemoryDb(BaseDb):
73
89
 
74
90
  except Exception as e:
75
91
  log_error(f"Error deleting sessions: {e}")
92
+ raise e
76
93
 
77
94
  def get_session(
78
95
  self,
@@ -102,19 +119,18 @@ class InMemoryDb(BaseDb):
102
119
  if session_data.get("session_id") == session_id:
103
120
  if user_id is not None and session_data.get("user_id") != user_id:
104
121
  continue
105
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
106
- if session_data.get("session_type") != session_type_value:
107
- continue
122
+
123
+ session_data_copy = deepcopy(session_data)
108
124
 
109
125
  if not deserialize:
110
- return session_data
126
+ return session_data_copy
111
127
 
112
128
  if session_type == SessionType.AGENT:
113
- return AgentSession.from_dict(session_data)
129
+ return AgentSession.from_dict(session_data_copy)
114
130
  elif session_type == SessionType.TEAM:
115
- return TeamSession.from_dict(session_data)
131
+ return TeamSession.from_dict(session_data_copy)
116
132
  else:
117
- return WorkflowSession.from_dict(session_data)
133
+ return WorkflowSession.from_dict(session_data_copy)
118
134
 
119
135
  return None
120
136
 
@@ -123,7 +139,7 @@ class InMemoryDb(BaseDb):
123
139
 
124
140
  traceback.print_exc()
125
141
  log_error(f"Exception reading session: {e}")
126
- return None
142
+ raise e
127
143
 
128
144
  def get_sessions(
129
145
  self,
@@ -187,7 +203,7 @@ class InMemoryDb(BaseDb):
187
203
  if session_data.get("session_type") != session_type_value:
188
204
  continue
189
205
 
190
- filtered_sessions.append(session_data)
206
+ filtered_sessions.append(deepcopy(session_data))
191
207
 
192
208
  total_count = len(filtered_sessions)
193
209
 
@@ -215,7 +231,7 @@ class InMemoryDb(BaseDb):
215
231
 
216
232
  except Exception as e:
217
233
  log_error(f"Exception reading sessions: {e}")
218
- return [] if deserialize else ([], 0)
234
+ raise e
219
235
 
220
236
  def rename_session(
221
237
  self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
@@ -232,21 +248,22 @@ class InMemoryDb(BaseDb):
232
248
 
233
249
  log_debug(f"Renamed session with id '{session_id}' to '{session_name}'")
234
250
 
251
+ session_copy = deepcopy(session)
235
252
  if not deserialize:
236
- return session
253
+ return session_copy
237
254
 
238
255
  if session_type == SessionType.AGENT:
239
- return AgentSession.from_dict(session)
256
+ return AgentSession.from_dict(session_copy)
240
257
  elif session_type == SessionType.TEAM:
241
- return TeamSession.from_dict(session)
258
+ return TeamSession.from_dict(session_copy)
242
259
  else:
243
- return WorkflowSession.from_dict(session)
260
+ return WorkflowSession.from_dict(session_copy)
244
261
 
245
262
  return None
246
263
 
247
264
  except Exception as e:
248
265
  log_error(f"Exception renaming session: {e}")
249
- return None
266
+ raise e
250
267
 
251
268
  def upsert_session(
252
269
  self, session: Session, deserialize: Optional[bool] = True
@@ -268,26 +285,30 @@ class InMemoryDb(BaseDb):
268
285
  if existing_session.get("session_id") == session_dict.get("session_id") and self._matches_session_key(
269
286
  existing_session, session
270
287
  ):
271
- # Update existing session
272
288
  session_dict["updated_at"] = int(time.time())
273
- self._sessions[i] = session_dict
289
+ self._sessions[i] = deepcopy(session_dict)
274
290
  session_updated = True
275
291
  break
276
292
 
277
293
  if not session_updated:
278
- # Add new session
279
294
  session_dict["created_at"] = session_dict.get("created_at", int(time.time()))
280
295
  session_dict["updated_at"] = session_dict.get("created_at")
281
- self._sessions.append(session_dict)
296
+ self._sessions.append(deepcopy(session_dict))
282
297
 
298
+ session_dict_copy = deepcopy(session_dict)
283
299
  if not deserialize:
284
- return session_dict
300
+ return session_dict_copy
285
301
 
286
- return session
302
+ if session_dict_copy["session_type"] == SessionType.AGENT:
303
+ return AgentSession.from_dict(session_dict_copy)
304
+ elif session_dict_copy["session_type"] == SessionType.TEAM:
305
+ return TeamSession.from_dict(session_dict_copy)
306
+ else:
307
+ return WorkflowSession.from_dict(session_dict_copy)
287
308
 
288
309
  except Exception as e:
289
310
  log_error(f"Exception upserting session: {e}")
290
- return None
311
+ raise e
291
312
 
292
313
  def _matches_session_key(self, existing_session: Dict[str, Any], session: Session) -> bool:
293
314
  """Check if existing session matches the key for the session type."""
@@ -299,11 +320,61 @@ class InMemoryDb(BaseDb):
299
320
  return existing_session.get("workflow_id") == session.workflow_id
300
321
  return False
301
322
 
323
+ def upsert_sessions(
324
+ self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
325
+ ) -> List[Union[Session, Dict[str, Any]]]:
326
+ """
327
+ Bulk upsert multiple sessions for improved performance on large datasets.
328
+
329
+ Args:
330
+ sessions (List[Session]): List of sessions to upsert.
331
+ deserialize (Optional[bool]): Whether to deserialize the sessions. Defaults to True.
332
+
333
+ Returns:
334
+ List[Union[Session, Dict[str, Any]]]: List of upserted sessions.
335
+
336
+ Raises:
337
+ Exception: If an error occurs during bulk upsert.
338
+ """
339
+ if not sessions:
340
+ return []
341
+
342
+ try:
343
+ log_info(f"In-memory database: processing {len(sessions)} sessions with individual upsert operations")
344
+
345
+ results = []
346
+ for session in sessions:
347
+ if session is not None:
348
+ result = self.upsert_session(session, deserialize=deserialize)
349
+ if result is not None:
350
+ results.append(result)
351
+ return results
352
+
353
+ except Exception as e:
354
+ log_error(f"Exception during bulk session upsert: {e}")
355
+ return []
356
+
302
357
  # -- Memory methods --
303
- def delete_user_memory(self, memory_id: str):
358
+ def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None):
359
+ """Delete a user memory from in-memory storage.
360
+
361
+ Args:
362
+ memory_id (str): The ID of the memory to delete.
363
+ user_id (Optional[str]): The ID of the user. If provided, verifies the memory belongs to this user before deletion.
364
+
365
+ Raises:
366
+ Exception: If an error occurs during deletion.
367
+ """
304
368
  try:
305
369
  original_count = len(self._memories)
306
- self._memories = [m for m in self._memories if m.get("memory_id") != memory_id]
370
+
371
+ # If user_id is provided, verify ownership before deleting
372
+ if user_id is not None:
373
+ self._memories = [
374
+ m for m in self._memories if not (m.get("memory_id") == memory_id and m.get("user_id") == user_id)
375
+ ]
376
+ else:
377
+ self._memories = [m for m in self._memories if m.get("memory_id") != memory_id]
307
378
 
308
379
  if len(self._memories) < original_count:
309
380
  log_debug(f"Successfully deleted user memory id: {memory_id}")
@@ -312,17 +383,41 @@ class InMemoryDb(BaseDb):
312
383
 
313
384
  except Exception as e:
314
385
  log_error(f"Error deleting memory: {e}")
386
+ raise e
315
387
 
316
- def delete_user_memories(self, memory_ids: List[str]) -> None:
317
- """Delete multiple user memories from in-memory storage."""
388
+ def delete_user_memories(self, memory_ids: List[str], user_id: Optional[str] = None) -> None:
389
+ """Delete multiple user memories from in-memory storage.
390
+
391
+ Args:
392
+ memory_ids (List[str]): The IDs of the memories to delete.
393
+ user_id (Optional[str]): The ID of the user. If provided, only deletes memories belonging to this user.
394
+
395
+ Raises:
396
+ Exception: If an error occurs during deletion.
397
+ """
318
398
  try:
319
- self._memories = [m for m in self._memories if m.get("memory_id") not in memory_ids]
399
+ # If user_id is provided, verify ownership before deleting
400
+ if user_id is not None:
401
+ self._memories = [
402
+ m for m in self._memories if not (m.get("memory_id") in memory_ids and m.get("user_id") == user_id)
403
+ ]
404
+ else:
405
+ self._memories = [m for m in self._memories if m.get("memory_id") not in memory_ids]
320
406
  log_debug(f"Successfully deleted {len(memory_ids)} user memories")
321
407
 
322
408
  except Exception as e:
323
409
  log_error(f"Error deleting memories: {e}")
410
+ raise e
324
411
 
325
412
  def get_all_memory_topics(self) -> List[str]:
413
+ """Get all memory topics from in-memory storage.
414
+
415
+ Returns:
416
+ List[str]: List of unique topics.
417
+
418
+ Raises:
419
+ Exception: If an error occurs while reading topics.
420
+ """
326
421
  try:
327
422
  topics = set()
328
423
  for memory in self._memories:
@@ -333,23 +428,41 @@ class InMemoryDb(BaseDb):
333
428
 
334
429
  except Exception as e:
335
430
  log_error(f"Exception reading from memory storage: {e}")
336
- return []
431
+ raise e
337
432
 
338
433
  def get_user_memory(
339
- self, memory_id: str, deserialize: Optional[bool] = True
434
+ self, memory_id: str, deserialize: Optional[bool] = True, user_id: Optional[str] = None
340
435
  ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
436
+ """Get a user memory from in-memory storage.
437
+
438
+ Args:
439
+ memory_id (str): The ID of the memory to retrieve.
440
+ deserialize (Optional[bool]): Whether to deserialize the memory. Defaults to True.
441
+ user_id (Optional[str]): The ID of the user. If provided, only returns the memory if it belongs to this user.
442
+
443
+ Returns:
444
+ Optional[Union[UserMemory, Dict[str, Any]]]: The memory object or dictionary, or None if not found.
445
+
446
+ Raises:
447
+ Exception: If an error occurs while reading the memory.
448
+ """
341
449
  try:
342
450
  for memory_data in self._memories:
343
451
  if memory_data.get("memory_id") == memory_id:
452
+ # Filter by user_id if provided
453
+ if user_id is not None and memory_data.get("user_id") != user_id:
454
+ continue
455
+
456
+ memory_data_copy = deepcopy(memory_data)
344
457
  if not deserialize:
345
- return memory_data
346
- return UserMemory.from_dict(memory_data)
458
+ return memory_data_copy
459
+ return UserMemory.from_dict(memory_data_copy)
347
460
 
348
461
  return None
349
462
 
350
463
  except Exception as e:
351
464
  log_error(f"Exception reading from memory storage: {e}")
352
- return None
465
+ raise e
353
466
 
354
467
  def get_user_memories(
355
468
  self,
@@ -383,7 +496,7 @@ class InMemoryDb(BaseDb):
383
496
  if search_content.lower() not in memory_content.lower():
384
497
  continue
385
498
 
386
- filtered_memories.append(memory_data)
499
+ filtered_memories.append(deepcopy(memory_data))
387
500
 
388
501
  total_count = len(filtered_memories)
389
502
 
@@ -404,24 +517,40 @@ class InMemoryDb(BaseDb):
404
517
 
405
518
  except Exception as e:
406
519
  log_error(f"Exception reading from memory storage: {e}")
407
- return [] if deserialize else ([], 0)
520
+ raise e
408
521
 
409
522
  def get_user_memory_stats(
410
523
  self, limit: Optional[int] = None, page: Optional[int] = None
411
524
  ) -> Tuple[List[Dict[str, Any]], int]:
412
- """Get user memory statistics."""
525
+ """Get user memory statistics.
526
+
527
+ Args:
528
+ limit (Optional[int]): Maximum number of stats to return.
529
+ page (Optional[int]): Page number for pagination.
530
+
531
+ Returns:
532
+ Tuple[List[Dict[str, Any]], int]: List of user memory statistics and total count.
533
+
534
+ Raises:
535
+ Exception: If an error occurs while getting stats.
536
+ """
413
537
  try:
414
538
  user_stats = {}
415
539
 
416
540
  for memory in self._memories:
417
- user_id = memory.get("user_id")
418
- if user_id:
419
- if user_id not in user_stats:
420
- user_stats[user_id] = {"user_id": user_id, "total_memories": 0, "last_memory_updated_at": 0}
421
- user_stats[user_id]["total_memories"] += 1
541
+ memory_user_id = memory.get("user_id")
542
+
543
+ if memory_user_id:
544
+ if memory_user_id not in user_stats:
545
+ user_stats[memory_user_id] = {
546
+ "user_id": memory_user_id,
547
+ "total_memories": 0,
548
+ "last_memory_updated_at": 0,
549
+ }
550
+ user_stats[memory_user_id]["total_memories"] += 1
422
551
  updated_at = memory.get("updated_at", 0)
423
- if updated_at > user_stats[user_id]["last_memory_updated_at"]:
424
- user_stats[user_id]["last_memory_updated_at"] = updated_at
552
+ if updated_at > user_stats[memory_user_id]["last_memory_updated_at"]:
553
+ user_stats[memory_user_id]["last_memory_updated_at"] = updated_at
425
554
 
426
555
  stats_list = list(user_stats.values())
427
556
  stats_list.sort(key=lambda x: x["last_memory_updated_at"], reverse=True)
@@ -439,7 +568,7 @@ class InMemoryDb(BaseDb):
439
568
 
440
569
  except Exception as e:
441
570
  log_error(f"Exception getting user memory stats: {e}")
442
- return [], 0
571
+ raise e
443
572
 
444
573
  def upsert_user_memory(
445
574
  self, memory: UserMemory, deserialize: Optional[bool] = True
@@ -462,13 +591,50 @@ class InMemoryDb(BaseDb):
462
591
  if not memory_updated:
463
592
  self._memories.append(memory_dict)
464
593
 
594
+ memory_dict_copy = deepcopy(memory_dict)
465
595
  if not deserialize:
466
- return memory_dict
467
- return UserMemory.from_dict(memory_dict)
596
+ return memory_dict_copy
597
+
598
+ return UserMemory.from_dict(memory_dict_copy)
468
599
 
469
600
  except Exception as e:
470
601
  log_warning(f"Exception upserting user memory: {e}")
471
- return None
602
+ raise e
603
+
604
+ def upsert_memories(
605
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
606
+ ) -> List[Union[UserMemory, Dict[str, Any]]]:
607
+ """
608
+ Bulk upsert multiple user memories for improved performance on large datasets.
609
+
610
+ Args:
611
+ memories (List[UserMemory]): List of memories to upsert.
612
+ deserialize (Optional[bool]): Whether to deserialize the memories. Defaults to True.
613
+
614
+ Returns:
615
+ List[Union[UserMemory, Dict[str, Any]]]: List of upserted memories.
616
+
617
+ Raises:
618
+ Exception: If an error occurs during bulk upsert.
619
+ """
620
+ if not memories:
621
+ return []
622
+
623
+ try:
624
+ log_info(f"In-memory database: processing {len(memories)} memories with individual upsert operations")
625
+ # For in-memory database, individual upserts are actually efficient
626
+ # since we're just manipulating Python lists and dictionaries
627
+ results = []
628
+ for memory in memories:
629
+ if memory is not None:
630
+ result = self.upsert_user_memory(memory, deserialize=deserialize)
631
+ if result is not None:
632
+ results.append(result)
633
+ return results
634
+
635
+ except Exception as e:
636
+ log_error(f"Exception during bulk memory upsert: {e}")
637
+ return []
472
638
 
473
639
  def clear_memories(self) -> None:
474
640
  """Delete all memories.
@@ -481,6 +647,7 @@ class InMemoryDb(BaseDb):
481
647
 
482
648
  except Exception as e:
483
649
  log_warning(f"Exception deleting all memories: {e}")
650
+ raise e
484
651
 
485
652
  # -- Metrics methods --
486
653
  def calculate_metrics(self) -> Optional[list[dict]]:
@@ -544,7 +711,7 @@ class InMemoryDb(BaseDb):
544
711
 
545
712
  except Exception as e:
546
713
  log_warning(f"Exception refreshing metrics: {e}")
547
- return None
714
+ raise e
548
715
 
549
716
  def _get_metrics_calculation_starting_date(self, metrics: List[Dict[str, Any]]) -> Optional[date]:
550
717
  """Get the first date for which metrics calculation is needed."""
@@ -584,8 +751,8 @@ class InMemoryDb(BaseDb):
584
751
  # Only include necessary fields for metrics
585
752
  filtered_session = {
586
753
  "user_id": session.get("user_id"),
587
- "session_data": session.get("session_data"),
588
- "runs": session.get("runs"),
754
+ "session_data": deepcopy(session.get("session_data")),
755
+ "runs": deepcopy(session.get("runs")),
589
756
  "created_at": session.get("created_at"),
590
757
  "session_type": session.get("session_type"),
591
758
  }
@@ -595,7 +762,7 @@ class InMemoryDb(BaseDb):
595
762
 
596
763
  except Exception as e:
597
764
  log_error(f"Exception reading sessions for metrics: {e}")
598
- return []
765
+ raise e
599
766
 
600
767
  def get_metrics(
601
768
  self,
@@ -615,7 +782,7 @@ class InMemoryDb(BaseDb):
615
782
  if ending_date and metric_date > ending_date:
616
783
  continue
617
784
 
618
- filtered_metrics.append(metric)
785
+ filtered_metrics.append(deepcopy(metric))
619
786
 
620
787
  updated_at = metric.get("updated_at")
621
788
  if updated_at and (latest_updated_at is None or updated_at > latest_updated_at):
@@ -625,7 +792,7 @@ class InMemoryDb(BaseDb):
625
792
 
626
793
  except Exception as e:
627
794
  log_error(f"Exception getting metrics: {e}")
628
- return [], None
795
+ raise e
629
796
 
630
797
  # -- Knowledge methods --
631
798
 
@@ -643,6 +810,7 @@ class InMemoryDb(BaseDb):
643
810
 
644
811
  except Exception as e:
645
812
  log_error(f"Error deleting knowledge content: {e}")
813
+ raise e
646
814
 
647
815
  def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
648
816
  """Get a knowledge row from in-memory storage.
@@ -665,7 +833,7 @@ class InMemoryDb(BaseDb):
665
833
 
666
834
  except Exception as e:
667
835
  log_error(f"Error getting knowledge content: {e}")
668
- return None
836
+ raise e
669
837
 
670
838
  def get_knowledge_contents(
671
839
  self,
@@ -689,7 +857,7 @@ class InMemoryDb(BaseDb):
689
857
  Exception: If an error occurs during retrieval.
690
858
  """
691
859
  try:
692
- knowledge_items = self._knowledge.copy()
860
+ knowledge_items = [deepcopy(item) for item in self._knowledge]
693
861
 
694
862
  total_count = len(knowledge_items)
695
863
 
@@ -707,7 +875,7 @@ class InMemoryDb(BaseDb):
707
875
 
708
876
  except Exception as e:
709
877
  log_error(f"Error getting knowledge contents: {e}")
710
- return [], 0
878
+ raise e
711
879
 
712
880
  def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
713
881
  """Upsert knowledge content.
@@ -739,7 +907,7 @@ class InMemoryDb(BaseDb):
739
907
 
740
908
  except Exception as e:
741
909
  log_error(f"Error upserting knowledge row: {e}")
742
- return None
910
+ raise e
743
911
 
744
912
  # -- Eval methods --
745
913
 
@@ -759,7 +927,7 @@ class InMemoryDb(BaseDb):
759
927
 
760
928
  except Exception as e:
761
929
  log_error(f"Error creating eval run: {e}")
762
- return None
930
+ raise e
763
931
 
764
932
  def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
765
933
  """Delete multiple eval runs from in-memory storage."""
@@ -775,6 +943,7 @@ class InMemoryDb(BaseDb):
775
943
 
776
944
  except Exception as e:
777
945
  log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
946
+ raise e
778
947
 
779
948
  def get_eval_run(
780
949
  self, eval_run_id: str, deserialize: Optional[bool] = True
@@ -783,15 +952,16 @@ class InMemoryDb(BaseDb):
783
952
  try:
784
953
  for run_data in self._eval_runs:
785
954
  if run_data.get("run_id") == eval_run_id:
955
+ run_data_copy = deepcopy(run_data)
786
956
  if not deserialize:
787
- return run_data
788
- return EvalRunRecord.model_validate(run_data)
957
+ return run_data_copy
958
+ return EvalRunRecord.model_validate(run_data_copy)
789
959
 
790
960
  return None
791
961
 
792
962
  except Exception as e:
793
963
  log_error(f"Exception getting eval run {eval_run_id}: {e}")
794
- return None
964
+ raise e
795
965
 
796
966
  def get_eval_runs(
797
967
  self,
@@ -831,7 +1001,7 @@ class InMemoryDb(BaseDb):
831
1001
  elif filter_type == EvalFilterType.WORKFLOW and run_data.get("workflow_id") is None:
832
1002
  continue
833
1003
 
834
- filtered_runs.append(run_data)
1004
+ filtered_runs.append(deepcopy(run_data))
835
1005
 
836
1006
  total_count = len(filtered_runs)
837
1007
 
@@ -855,7 +1025,7 @@ class InMemoryDb(BaseDb):
855
1025
 
856
1026
  except Exception as e:
857
1027
  log_error(f"Exception getting eval runs: {e}")
858
- return [] if deserialize else ([], 0)
1028
+ raise e
859
1029
 
860
1030
  def rename_eval_run(
861
1031
  self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
@@ -870,13 +1040,128 @@ class InMemoryDb(BaseDb):
870
1040
 
871
1041
  log_debug(f"Renamed eval run with id '{eval_run_id}' to '{name}'")
872
1042
 
1043
+ run_data_copy = deepcopy(run_data)
873
1044
  if not deserialize:
874
- return run_data
1045
+ return run_data_copy
875
1046
 
876
- return EvalRunRecord.model_validate(run_data)
1047
+ return EvalRunRecord.model_validate(run_data_copy)
877
1048
 
878
1049
  return None
879
1050
 
880
1051
  except Exception as e:
881
1052
  log_error(f"Error renaming eval run {eval_run_id}: {e}")
1053
+ raise e
1054
+
1055
+ # -- Culture methods --
1056
+
1057
+ def clear_cultural_knowledge(self) -> None:
1058
+ """Delete all cultural knowledge from in-memory storage."""
1059
+ try:
1060
+ self._cultural_knowledge = []
1061
+ except Exception as e:
1062
+ log_error(f"Error clearing cultural knowledge: {e}")
1063
+ raise e
1064
+
1065
+ def delete_cultural_knowledge(self, id: str) -> None:
1066
+ """Delete a cultural knowledge entry from in-memory storage."""
1067
+ try:
1068
+ self._cultural_knowledge = [ck for ck in self._cultural_knowledge if ck.get("id") != id]
1069
+ except Exception as e:
1070
+ log_error(f"Error deleting cultural knowledge: {e}")
1071
+ raise e
1072
+
1073
+ def get_cultural_knowledge(
1074
+ self, id: str, deserialize: Optional[bool] = True
1075
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1076
+ """Get a cultural knowledge entry from in-memory storage."""
1077
+ try:
1078
+ for ck_data in self._cultural_knowledge:
1079
+ if ck_data.get("id") == id:
1080
+ ck_data_copy = deepcopy(ck_data)
1081
+ if not deserialize:
1082
+ return ck_data_copy
1083
+ return deserialize_cultural_knowledge_from_db(ck_data_copy)
882
1084
  return None
1085
+ except Exception as e:
1086
+ log_error(f"Error getting cultural knowledge: {e}")
1087
+ raise e
1088
+
1089
+ def get_all_cultural_knowledge(
1090
+ self,
1091
+ name: Optional[str] = None,
1092
+ agent_id: Optional[str] = None,
1093
+ team_id: Optional[str] = None,
1094
+ limit: Optional[int] = None,
1095
+ page: Optional[int] = None,
1096
+ sort_by: Optional[str] = None,
1097
+ sort_order: Optional[str] = None,
1098
+ deserialize: Optional[bool] = True,
1099
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1100
+ """Get all cultural knowledge from in-memory storage."""
1101
+ try:
1102
+ filtered_ck = []
1103
+ for ck_data in self._cultural_knowledge:
1104
+ if name and ck_data.get("name") != name:
1105
+ continue
1106
+ if agent_id and ck_data.get("agent_id") != agent_id:
1107
+ continue
1108
+ if team_id and ck_data.get("team_id") != team_id:
1109
+ continue
1110
+ filtered_ck.append(ck_data)
1111
+
1112
+ # Apply sorting
1113
+ if sort_by:
1114
+ filtered_ck = apply_sorting(filtered_ck, sort_by, sort_order)
1115
+
1116
+ total_count = len(filtered_ck)
1117
+
1118
+ # Apply pagination
1119
+ if limit and page:
1120
+ start = (page - 1) * limit
1121
+ filtered_ck = filtered_ck[start : start + limit]
1122
+ elif limit:
1123
+ filtered_ck = filtered_ck[:limit]
1124
+
1125
+ if not deserialize:
1126
+ return [deepcopy(ck) for ck in filtered_ck], total_count
1127
+
1128
+ return [deserialize_cultural_knowledge_from_db(deepcopy(ck)) for ck in filtered_ck]
1129
+ except Exception as e:
1130
+ log_error(f"Error getting all cultural knowledge: {e}")
1131
+ raise e
1132
+
1133
+ def upsert_cultural_knowledge(
1134
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
1135
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1136
+ """Upsert a cultural knowledge entry into in-memory storage."""
1137
+ try:
1138
+ if not cultural_knowledge.id:
1139
+ cultural_knowledge.id = str(uuid4())
1140
+
1141
+ # Serialize content, categories, and notes into a dict for DB storage
1142
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
1143
+
1144
+ # Create the item dict with serialized content
1145
+ ck_dict = {
1146
+ "id": cultural_knowledge.id,
1147
+ "name": cultural_knowledge.name,
1148
+ "summary": cultural_knowledge.summary,
1149
+ "content": content_dict if content_dict else None,
1150
+ "metadata": cultural_knowledge.metadata,
1151
+ "input": cultural_knowledge.input,
1152
+ "created_at": cultural_knowledge.created_at,
1153
+ "updated_at": int(time.time()),
1154
+ "agent_id": cultural_knowledge.agent_id,
1155
+ "team_id": cultural_knowledge.team_id,
1156
+ }
1157
+
1158
+ # Remove existing entry with same id
1159
+ self._cultural_knowledge = [ck for ck in self._cultural_knowledge if ck.get("id") != cultural_knowledge.id]
1160
+
1161
+ # Add new entry
1162
+ self._cultural_knowledge.append(ck_dict)
1163
+
1164
+ return self.get_cultural_knowledge(cultural_knowledge.id, deserialize=deserialize)
1165
+ except Exception as e:
1166
+ log_error(f"Error upserting cultural knowledge: {e}")
1167
+ raise e