agno 2.1.2__py3-none-any.whl → 2.3.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/db/gcs_json/utils.py CHANGED
@@ -5,33 +5,10 @@ from datetime import date, datetime, timedelta, timezone
5
5
  from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
- from agno.db.base import SessionType
9
- from agno.run.agent import RunOutput
10
- from agno.run.team import TeamRunOutput
11
- from agno.session.summary import SessionSummary
8
+ from agno.db.schemas.culture import CulturalKnowledge
12
9
  from agno.utils.log import log_debug
13
10
 
14
11
 
15
- def hydrate_session(session: dict) -> dict:
16
- """Convert nested dictionaries to their corresponding object types.
17
-
18
- Args:
19
- session (dict): The session dictionary to hydrate.
20
-
21
- Returns:
22
- dict: The hydrated session dictionary.
23
- """
24
- if session.get("summary") is not None:
25
- session["summary"] = SessionSummary.from_dict(session["summary"])
26
- if session.get("runs") is not None:
27
- if session["session_type"] == SessionType.AGENT:
28
- session["runs"] = [RunOutput.from_dict(run) for run in session["runs"]]
29
- elif session["session_type"] == SessionType.TEAM:
30
- session["runs"] = [TeamRunOutput.from_dict(run) for run in session["runs"]]
31
-
32
- return session
33
-
34
-
35
12
  def apply_sorting(
36
13
  data: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
37
14
  ) -> List[Dict[str, Any]]:
@@ -101,7 +78,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
101
78
  all_user_ids = set()
102
79
 
103
80
  for session_type, sessions_count_key, runs_count_key in session_types:
104
- sessions = sessions_data.get(session_type, [])
81
+ sessions = sessions_data.get(session_type, []) or []
105
82
  metrics[sessions_count_key] = len(sessions)
106
83
 
107
84
  for session in sessions:
@@ -122,7 +99,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
122
99
 
123
100
  model_metrics = []
124
101
  for model, count in model_counts.items():
125
- model_id, model_provider = model.split(":")
102
+ model_id, model_provider = model.rsplit(":", 1)
126
103
  model_metrics.append({"model_id": model_id, "model_provider": model_provider, "count": count})
127
104
 
128
105
  metrics["users_count"] = len(all_user_ids)
@@ -192,3 +169,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
192
169
  if days_diff <= 0:
193
170
  return []
194
171
  return [starting_date + timedelta(days=x) for x in range(days_diff)]
172
+
173
+
174
+ # -- Cultural Knowledge util methods --
175
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
176
+ """Serialize a CulturalKnowledge object for database storage.
177
+
178
+ Converts the model's separate content, categories, and notes fields
179
+ into a single dict for the database content column.
180
+
181
+ Args:
182
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
183
+
184
+ Returns:
185
+ Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
186
+ """
187
+ content_dict: Dict[str, Any] = {}
188
+ if cultural_knowledge.content is not None:
189
+ content_dict["content"] = cultural_knowledge.content
190
+ if cultural_knowledge.categories is not None:
191
+ content_dict["categories"] = cultural_knowledge.categories
192
+ if cultural_knowledge.notes is not None:
193
+ content_dict["notes"] = cultural_knowledge.notes
194
+
195
+ return content_dict if content_dict else {}
196
+
197
+
198
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
199
+ """Deserialize a database row to a CulturalKnowledge object.
200
+
201
+ The database stores content as a dict containing content, categories, and notes.
202
+ This method extracts those fields and converts them back to the model format.
203
+
204
+ Args:
205
+ db_row (Dict[str, Any]): The database row as a dictionary.
206
+
207
+ Returns:
208
+ CulturalKnowledge: The cultural knowledge object.
209
+ """
210
+ # Extract content, categories, and notes from the content field
211
+ content_json = db_row.get("content", {}) or {}
212
+
213
+ return CulturalKnowledge.from_dict(
214
+ {
215
+ "id": db_row.get("id"),
216
+ "name": db_row.get("name"),
217
+ "summary": db_row.get("summary"),
218
+ "content": content_json.get("content"),
219
+ "categories": content_json.get("categories"),
220
+ "notes": content_json.get("notes"),
221
+ "metadata": db_row.get("metadata"),
222
+ "input": db_row.get("input"),
223
+ "created_at": db_row.get("created_at"),
224
+ "updated_at": db_row.get("updated_at"),
225
+ "agent_id": db_row.get("agent_id"),
226
+ "team_id": db_row.get("team_id"),
227
+ }
228
+ )
@@ -1,22 +1,28 @@
1
1
  import time
2
2
  from copy import deepcopy
3
3
  from datetime import date, datetime, timedelta, timezone
4
- from typing import Any, Dict, List, Optional, Tuple, Union
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
7
  from agno.db.base import BaseDb, SessionType
8
8
  from agno.db.in_memory.utils import (
9
9
  apply_sorting,
10
10
  calculate_date_metrics,
11
+ deserialize_cultural_knowledge_from_db,
11
12
  fetch_all_sessions_data,
12
13
  get_dates_to_calculate_metrics_for,
14
+ serialize_cultural_knowledge_for_db,
13
15
  )
16
+ from agno.db.schemas.culture import CulturalKnowledge
14
17
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
15
18
  from agno.db.schemas.knowledge import KnowledgeRow
16
19
  from agno.db.schemas.memory import UserMemory
17
20
  from agno.session import AgentSession, Session, TeamSession, WorkflowSession
18
21
  from agno.utils.log import log_debug, log_error, log_info, log_warning
19
22
 
23
+ if TYPE_CHECKING:
24
+ from agno.tracing.schemas import Span, Trace
25
+
20
26
 
21
27
  class InMemoryDb(BaseDb):
22
28
  def __init__(self):
@@ -29,9 +35,21 @@ class InMemoryDb(BaseDb):
29
35
  self._metrics: List[Dict[str, Any]] = []
30
36
  self._eval_runs: List[Dict[str, Any]] = []
31
37
  self._knowledge: List[Dict[str, Any]] = []
38
+ self._cultural_knowledge: List[Dict[str, Any]] = []
32
39
 
33
- # -- Session methods --
40
+ def table_exists(self, table_name: str) -> bool:
41
+ """In-memory implementation, always returns True."""
42
+ return True
43
+
44
+ def get_latest_schema_version(self):
45
+ """Get the latest version of the database schema."""
46
+ pass
47
+
48
+ def upsert_schema_version(self, version: str) -> None:
49
+ """Upsert the schema version into the database."""
50
+ pass
34
51
 
52
+ # -- Session methods --
35
53
  def delete_session(self, session_id: str) -> bool:
36
54
  """Delete a session from in-memory storage.
37
55
 
@@ -104,9 +122,6 @@ class InMemoryDb(BaseDb):
104
122
  if session_data.get("session_id") == session_id:
105
123
  if user_id is not None and session_data.get("user_id") != user_id:
106
124
  continue
107
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
108
- if session_data.get("session_type") != session_type_value:
109
- continue
110
125
 
111
126
  session_data_copy = deepcopy(session_data)
112
127
 
@@ -309,7 +324,7 @@ class InMemoryDb(BaseDb):
309
324
  return False
310
325
 
311
326
  def upsert_sessions(
312
- self, sessions: List[Session], deserialize: Optional[bool] = True
327
+ self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
313
328
  ) -> List[Union[Session, Dict[str, Any]]]:
314
329
  """
315
330
  Bulk upsert multiple sessions for improved performance on large datasets.
@@ -359,8 +374,7 @@ class InMemoryDb(BaseDb):
359
374
  # If user_id is provided, verify ownership before deleting
360
375
  if user_id is not None:
361
376
  self._memories = [
362
- m for m in self._memories
363
- if not (m.get("memory_id") == memory_id and m.get("user_id") == user_id)
377
+ m for m in self._memories if not (m.get("memory_id") == memory_id and m.get("user_id") == user_id)
364
378
  ]
365
379
  else:
366
380
  self._memories = [m for m in self._memories if m.get("memory_id") != memory_id]
@@ -388,8 +402,7 @@ class InMemoryDb(BaseDb):
388
402
  # If user_id is provided, verify ownership before deleting
389
403
  if user_id is not None:
390
404
  self._memories = [
391
- m for m in self._memories
392
- if not (m.get("memory_id") in memory_ids and m.get("user_id") == user_id)
405
+ m for m in self._memories if not (m.get("memory_id") in memory_ids and m.get("user_id") == user_id)
393
406
  ]
394
407
  else:
395
408
  self._memories = [m for m in self._memories if m.get("memory_id") not in memory_ids]
@@ -510,13 +523,14 @@ class InMemoryDb(BaseDb):
510
523
  raise e
511
524
 
512
525
  def get_user_memory_stats(
513
- self, limit: Optional[int] = None, page: Optional[int] = None
526
+ self, limit: Optional[int] = None, page: Optional[int] = None, user_id: Optional[str] = None
514
527
  ) -> Tuple[List[Dict[str, Any]], int]:
515
528
  """Get user memory statistics.
516
529
 
517
530
  Args:
518
531
  limit (Optional[int]): Maximum number of stats to return.
519
532
  page (Optional[int]): Page number for pagination.
533
+ user_id (Optional[str]): User ID for filtering.
520
534
 
521
535
  Returns:
522
536
  Tuple[List[Dict[str, Any]], int]: List of user memory statistics and total count.
@@ -529,10 +543,16 @@ class InMemoryDb(BaseDb):
529
543
 
530
544
  for memory in self._memories:
531
545
  memory_user_id = memory.get("user_id")
532
-
546
+ # filter by user_id if provided
547
+ if user_id is not None and memory_user_id != user_id:
548
+ continue
533
549
  if memory_user_id:
534
550
  if memory_user_id not in user_stats:
535
- user_stats[memory_user_id] = {"user_id": memory_user_id, "total_memories": 0, "last_memory_updated_at": 0}
551
+ user_stats[memory_user_id] = {
552
+ "user_id": memory_user_id,
553
+ "total_memories": 0,
554
+ "last_memory_updated_at": 0,
555
+ }
536
556
  user_stats[memory_user_id]["total_memories"] += 1
537
557
  updated_at = memory.get("updated_at", 0)
538
558
  if updated_at > user_stats[memory_user_id]["last_memory_updated_at"]:
@@ -588,7 +608,7 @@ class InMemoryDb(BaseDb):
588
608
  raise e
589
609
 
590
610
  def upsert_memories(
591
- self, memories: List[UserMemory], deserialize: Optional[bool] = True
611
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
592
612
  ) -> List[Union[UserMemory, Dict[str, Any]]]:
593
613
  """
594
614
  Bulk upsert multiple user memories for improved performance on large datasets.
@@ -1037,3 +1057,256 @@ class InMemoryDb(BaseDb):
1037
1057
  except Exception as e:
1038
1058
  log_error(f"Error renaming eval run {eval_run_id}: {e}")
1039
1059
  raise e
1060
+
1061
+ # -- Culture methods --
1062
+
1063
+ def clear_cultural_knowledge(self) -> None:
1064
+ """Delete all cultural knowledge from in-memory storage."""
1065
+ try:
1066
+ self._cultural_knowledge = []
1067
+ except Exception as e:
1068
+ log_error(f"Error clearing cultural knowledge: {e}")
1069
+ raise e
1070
+
1071
+ def delete_cultural_knowledge(self, id: str) -> None:
1072
+ """Delete a cultural knowledge entry from in-memory storage."""
1073
+ try:
1074
+ self._cultural_knowledge = [ck for ck in self._cultural_knowledge if ck.get("id") != id]
1075
+ except Exception as e:
1076
+ log_error(f"Error deleting cultural knowledge: {e}")
1077
+ raise e
1078
+
1079
+ def get_cultural_knowledge(
1080
+ self, id: str, deserialize: Optional[bool] = True
1081
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1082
+ """Get a cultural knowledge entry from in-memory storage."""
1083
+ try:
1084
+ for ck_data in self._cultural_knowledge:
1085
+ if ck_data.get("id") == id:
1086
+ ck_data_copy = deepcopy(ck_data)
1087
+ if not deserialize:
1088
+ return ck_data_copy
1089
+ return deserialize_cultural_knowledge_from_db(ck_data_copy)
1090
+ return None
1091
+ except Exception as e:
1092
+ log_error(f"Error getting cultural knowledge: {e}")
1093
+ raise e
1094
+
1095
+ def get_all_cultural_knowledge(
1096
+ self,
1097
+ name: Optional[str] = None,
1098
+ agent_id: Optional[str] = None,
1099
+ team_id: Optional[str] = None,
1100
+ limit: Optional[int] = None,
1101
+ page: Optional[int] = None,
1102
+ sort_by: Optional[str] = None,
1103
+ sort_order: Optional[str] = None,
1104
+ deserialize: Optional[bool] = True,
1105
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1106
+ """Get all cultural knowledge from in-memory storage."""
1107
+ try:
1108
+ filtered_ck = []
1109
+ for ck_data in self._cultural_knowledge:
1110
+ if name and ck_data.get("name") != name:
1111
+ continue
1112
+ if agent_id and ck_data.get("agent_id") != agent_id:
1113
+ continue
1114
+ if team_id and ck_data.get("team_id") != team_id:
1115
+ continue
1116
+ filtered_ck.append(ck_data)
1117
+
1118
+ # Apply sorting
1119
+ if sort_by:
1120
+ filtered_ck = apply_sorting(filtered_ck, sort_by, sort_order)
1121
+
1122
+ total_count = len(filtered_ck)
1123
+
1124
+ # Apply pagination
1125
+ if limit and page:
1126
+ start = (page - 1) * limit
1127
+ filtered_ck = filtered_ck[start : start + limit]
1128
+ elif limit:
1129
+ filtered_ck = filtered_ck[:limit]
1130
+
1131
+ if not deserialize:
1132
+ return [deepcopy(ck) for ck in filtered_ck], total_count
1133
+
1134
+ return [deserialize_cultural_knowledge_from_db(deepcopy(ck)) for ck in filtered_ck]
1135
+ except Exception as e:
1136
+ log_error(f"Error getting all cultural knowledge: {e}")
1137
+ raise e
1138
+
1139
+ def upsert_cultural_knowledge(
1140
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
1141
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1142
+ """Upsert a cultural knowledge entry into in-memory storage."""
1143
+ try:
1144
+ if not cultural_knowledge.id:
1145
+ cultural_knowledge.id = str(uuid4())
1146
+
1147
+ # Serialize content, categories, and notes into a dict for DB storage
1148
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
1149
+
1150
+ # Create the item dict with serialized content
1151
+ ck_dict = {
1152
+ "id": cultural_knowledge.id,
1153
+ "name": cultural_knowledge.name,
1154
+ "summary": cultural_knowledge.summary,
1155
+ "content": content_dict if content_dict else None,
1156
+ "metadata": cultural_knowledge.metadata,
1157
+ "input": cultural_knowledge.input,
1158
+ "created_at": cultural_knowledge.created_at,
1159
+ "updated_at": int(time.time()),
1160
+ "agent_id": cultural_knowledge.agent_id,
1161
+ "team_id": cultural_knowledge.team_id,
1162
+ }
1163
+
1164
+ # Remove existing entry with same id
1165
+ self._cultural_knowledge = [ck for ck in self._cultural_knowledge if ck.get("id") != cultural_knowledge.id]
1166
+
1167
+ # Add new entry
1168
+ self._cultural_knowledge.append(ck_dict)
1169
+
1170
+ return self.get_cultural_knowledge(cultural_knowledge.id, deserialize=deserialize)
1171
+ except Exception as e:
1172
+ log_error(f"Error upserting cultural knowledge: {e}")
1173
+ raise e
1174
+
1175
+ # --- Traces ---
1176
+ def upsert_trace(self, trace: "Trace") -> None:
1177
+ """Create or update a single trace record in the database.
1178
+
1179
+ Args:
1180
+ trace: The Trace object to store (one per trace_id).
1181
+ """
1182
+ raise NotImplementedError
1183
+
1184
+ def get_trace(
1185
+ self,
1186
+ trace_id: Optional[str] = None,
1187
+ run_id: Optional[str] = None,
1188
+ ):
1189
+ """Get a single trace by trace_id or other filters.
1190
+
1191
+ Args:
1192
+ trace_id: The unique trace identifier.
1193
+ run_id: Filter by run ID (returns first match).
1194
+
1195
+ Returns:
1196
+ Optional[Trace]: The trace if found, None otherwise.
1197
+
1198
+ Note:
1199
+ If multiple filters are provided, trace_id takes precedence.
1200
+ For other filters, the most recent trace is returned.
1201
+ """
1202
+ raise NotImplementedError
1203
+
1204
+ def get_traces(
1205
+ self,
1206
+ run_id: Optional[str] = None,
1207
+ session_id: Optional[str] = None,
1208
+ user_id: Optional[str] = None,
1209
+ agent_id: Optional[str] = None,
1210
+ team_id: Optional[str] = None,
1211
+ workflow_id: Optional[str] = None,
1212
+ status: Optional[str] = None,
1213
+ start_time: Optional[datetime] = None,
1214
+ end_time: Optional[datetime] = None,
1215
+ limit: Optional[int] = 20,
1216
+ page: Optional[int] = 1,
1217
+ ) -> tuple[List, int]:
1218
+ """Get traces matching the provided filters.
1219
+
1220
+ Args:
1221
+ run_id: Filter by run ID.
1222
+ session_id: Filter by session ID.
1223
+ user_id: Filter by user ID.
1224
+ agent_id: Filter by agent ID.
1225
+ team_id: Filter by team ID.
1226
+ workflow_id: Filter by workflow ID.
1227
+ status: Filter by status (OK, ERROR, UNSET).
1228
+ start_time: Filter traces starting after this datetime.
1229
+ end_time: Filter traces ending before this datetime.
1230
+ limit: Maximum number of traces to return per page.
1231
+ page: Page number (1-indexed).
1232
+
1233
+ Returns:
1234
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
1235
+ """
1236
+ raise NotImplementedError
1237
+
1238
+ def get_trace_stats(
1239
+ self,
1240
+ user_id: Optional[str] = None,
1241
+ agent_id: Optional[str] = None,
1242
+ team_id: Optional[str] = None,
1243
+ workflow_id: Optional[str] = None,
1244
+ start_time: Optional[datetime] = None,
1245
+ end_time: Optional[datetime] = None,
1246
+ limit: Optional[int] = 20,
1247
+ page: Optional[int] = 1,
1248
+ ) -> tuple[List[Dict[str, Any]], int]:
1249
+ """Get trace statistics grouped by session.
1250
+
1251
+ Args:
1252
+ user_id: Filter by user ID.
1253
+ agent_id: Filter by agent ID.
1254
+ team_id: Filter by team ID.
1255
+ workflow_id: Filter by workflow ID.
1256
+ start_time: Filter sessions with traces created after this datetime.
1257
+ end_time: Filter sessions with traces created before this datetime.
1258
+ limit: Maximum number of sessions to return per page.
1259
+ page: Page number (1-indexed).
1260
+
1261
+ Returns:
1262
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
1263
+ Each dict contains: session_id, user_id, agent_id, team_id, workflow_id, total_traces,
1264
+ first_trace_at, last_trace_at.
1265
+ """
1266
+ raise NotImplementedError
1267
+
1268
+ # --- Spans ---
1269
+ def create_span(self, span: "Span") -> None:
1270
+ """Create a single span in the database.
1271
+
1272
+ Args:
1273
+ span: The Span object to store.
1274
+ """
1275
+ raise NotImplementedError
1276
+
1277
+ def create_spans(self, spans: List) -> None:
1278
+ """Create multiple spans in the database as a batch.
1279
+
1280
+ Args:
1281
+ spans: List of Span objects to store.
1282
+ """
1283
+ raise NotImplementedError
1284
+
1285
+ def get_span(self, span_id: str):
1286
+ """Get a single span by its span_id.
1287
+
1288
+ Args:
1289
+ span_id: The unique span identifier.
1290
+
1291
+ Returns:
1292
+ Optional[Span]: The span if found, None otherwise.
1293
+ """
1294
+ raise NotImplementedError
1295
+
1296
+ def get_spans(
1297
+ self,
1298
+ trace_id: Optional[str] = None,
1299
+ parent_span_id: Optional[str] = None,
1300
+ limit: Optional[int] = 1000,
1301
+ ) -> List:
1302
+ """Get spans matching the provided filters.
1303
+
1304
+ Args:
1305
+ trace_id: Filter by trace ID.
1306
+ parent_span_id: Filter by parent span ID.
1307
+ limit: Maximum number of spans to return.
1308
+
1309
+ Returns:
1310
+ List[Span]: List of matching spans.
1311
+ """
1312
+ raise NotImplementedError
@@ -5,6 +5,7 @@ from datetime import date, datetime, timedelta, timezone
5
5
  from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
+ from agno.db.schemas.culture import CulturalKnowledge
8
9
  from agno.utils.log import log_debug
9
10
 
10
11
 
@@ -77,7 +78,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
77
78
  all_user_ids = set()
78
79
 
79
80
  for session_type, sessions_count_key, runs_count_key in session_types:
80
- sessions = sessions_data.get(session_type, [])
81
+ sessions = sessions_data.get(session_type, []) or []
81
82
  metrics[sessions_count_key] = len(sessions)
82
83
 
83
84
  for session in sessions:
@@ -98,7 +99,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
98
99
 
99
100
  model_metrics = []
100
101
  for model, count in model_counts.items():
101
- model_id, model_provider = model.split(":")
102
+ model_id, model_provider = model.rsplit(":", 1)
102
103
  model_metrics.append({"model_id": model_id, "model_provider": model_provider, "count": count})
103
104
 
104
105
  metrics["users_count"] = len(all_user_ids)
@@ -170,3 +171,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
170
171
  if days_diff <= 0:
171
172
  return []
172
173
  return [starting_date + timedelta(days=x) for x in range(days_diff)]
174
+
175
+
176
+ # -- Cultural Knowledge util methods --
177
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
178
+ """Serialize a CulturalKnowledge object for database storage.
179
+
180
+ Converts the model's separate content, categories, and notes fields
181
+ into a single dict for the database content column.
182
+
183
+ Args:
184
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
185
+
186
+ Returns:
187
+ Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
188
+ """
189
+ content_dict: Dict[str, Any] = {}
190
+ if cultural_knowledge.content is not None:
191
+ content_dict["content"] = cultural_knowledge.content
192
+ if cultural_knowledge.categories is not None:
193
+ content_dict["categories"] = cultural_knowledge.categories
194
+ if cultural_knowledge.notes is not None:
195
+ content_dict["notes"] = cultural_knowledge.notes
196
+
197
+ return content_dict if content_dict else {}
198
+
199
+
200
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
201
+ """Deserialize a database row to a CulturalKnowledge object.
202
+
203
+ The database stores content as a dict containing content, categories, and notes.
204
+ This method extracts those fields and converts them back to the model format.
205
+
206
+ Args:
207
+ db_row (Dict[str, Any]): The database row as a dictionary.
208
+
209
+ Returns:
210
+ CulturalKnowledge: The cultural knowledge object.
211
+ """
212
+ # Extract content, categories, and notes from the content field
213
+ content_json = db_row.get("content", {}) or {}
214
+
215
+ return CulturalKnowledge.from_dict(
216
+ {
217
+ "id": db_row.get("id"),
218
+ "name": db_row.get("name"),
219
+ "summary": db_row.get("summary"),
220
+ "content": content_json.get("content"),
221
+ "categories": content_json.get("categories"),
222
+ "notes": content_json.get("notes"),
223
+ "metadata": db_row.get("metadata"),
224
+ "input": db_row.get("input"),
225
+ "created_at": db_row.get("created_at"),
226
+ "updated_at": db_row.get("updated_at"),
227
+ "agent_id": db_row.get("agent_id"),
228
+ "team_id": db_row.get("team_id"),
229
+ }
230
+ )