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
@@ -4,30 +4,35 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
4
  from uuid import uuid4
5
5
 
6
6
  from agno.db.base import BaseDb, SessionType
7
+ from agno.db.migrations.manager import MigrationManager
7
8
  from agno.db.postgres.schemas import get_table_schema_definition
8
9
  from agno.db.postgres.utils import (
9
10
  apply_sorting,
10
11
  bulk_upsert_metrics,
11
12
  calculate_date_metrics,
12
13
  create_schema,
14
+ deserialize_cultural_knowledge,
13
15
  fetch_all_sessions_data,
14
16
  get_dates_to_calculate_metrics_for,
15
17
  is_table_available,
16
18
  is_valid_table,
19
+ serialize_cultural_knowledge,
17
20
  )
21
+ from agno.db.schemas.culture import CulturalKnowledge
18
22
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
19
23
  from agno.db.schemas.knowledge import KnowledgeRow
20
24
  from agno.db.schemas.memory import UserMemory
21
25
  from agno.session import AgentSession, Session, TeamSession, WorkflowSession
22
26
  from agno.utils.log import log_debug, log_error, log_info, log_warning
27
+ from agno.utils.string import generate_id
23
28
 
24
29
  try:
25
- from sqlalchemy import Index, String, UniqueConstraint, func, update
30
+ from sqlalchemy import Index, String, UniqueConstraint, func, select, update
26
31
  from sqlalchemy.dialects import postgresql
27
32
  from sqlalchemy.engine import Engine, create_engine
28
33
  from sqlalchemy.orm import scoped_session, sessionmaker
29
34
  from sqlalchemy.schema import Column, MetaData, Table
30
- from sqlalchemy.sql.expression import select, text
35
+ from sqlalchemy.sql.expression import text
31
36
  except ImportError:
32
37
  raise ImportError("`sqlalchemy` not installed. Please install it using `pip install sqlalchemy`")
33
38
 
@@ -39,10 +44,12 @@ class PostgresDb(BaseDb):
39
44
  db_engine: Optional[Engine] = None,
40
45
  db_schema: Optional[str] = None,
41
46
  session_table: Optional[str] = None,
47
+ culture_table: Optional[str] = None,
42
48
  memory_table: Optional[str] = None,
43
49
  metrics_table: Optional[str] = None,
44
50
  eval_table: Optional[str] = None,
45
51
  knowledge_table: Optional[str] = None,
52
+ versions_table: Optional[str] = None,
46
53
  id: Optional[str] = None,
47
54
  ):
48
55
  """
@@ -62,12 +69,29 @@ class PostgresDb(BaseDb):
62
69
  metrics_table (Optional[str]): Name of the table to store metrics.
63
70
  eval_table (Optional[str]): Name of the table to store evaluation runs data.
64
71
  knowledge_table (Optional[str]): Name of the table to store knowledge content.
72
+ culture_table (Optional[str]): Name of the table to store cultural knowledge.
73
+ versions_table (Optional[str]): Name of the table to store schema versions.
65
74
  id (Optional[str]): ID of the database.
66
75
 
67
76
  Raises:
68
77
  ValueError: If neither db_url nor db_engine is provided.
69
78
  ValueError: If none of the tables are provided.
70
79
  """
80
+ _engine: Optional[Engine] = db_engine
81
+ if _engine is None and db_url is not None:
82
+ _engine = create_engine(db_url)
83
+ if _engine is None:
84
+ raise ValueError("One of db_url or db_engine must be provided")
85
+
86
+ self.db_url: Optional[str] = db_url
87
+ self.db_engine: Engine = _engine
88
+
89
+ if id is None:
90
+ base_seed = db_url or str(db_engine.url) # type: ignore
91
+ schema_suffix = db_schema if db_schema is not None else "ai"
92
+ seed = f"{base_seed}#{schema_suffix}"
93
+ id = generate_id(seed)
94
+
71
95
  super().__init__(
72
96
  id=id,
73
97
  session_table=session_table,
@@ -75,16 +99,10 @@ class PostgresDb(BaseDb):
75
99
  metrics_table=metrics_table,
76
100
  eval_table=eval_table,
77
101
  knowledge_table=knowledge_table,
102
+ culture_table=culture_table,
103
+ versions_table=versions_table,
78
104
  )
79
105
 
80
- _engine: Optional[Engine] = db_engine
81
- if _engine is None and db_url is not None:
82
- _engine = create_engine(db_url)
83
- if _engine is None:
84
- raise ValueError("One of db_url or db_engine must be provided")
85
-
86
- self.db_url: Optional[str] = db_url
87
- self.db_engine: Engine = _engine
88
106
  self.db_schema: str = db_schema if db_schema is not None else "ai"
89
107
  self.metadata: MetaData = MetaData()
90
108
 
@@ -92,6 +110,37 @@ class PostgresDb(BaseDb):
92
110
  self.Session: scoped_session = scoped_session(sessionmaker(bind=self.db_engine))
93
111
 
94
112
  # -- DB methods --
113
+ def table_exists(self, table_name: str) -> bool:
114
+ """Check if a table with the given name exists in the Postgres database.
115
+
116
+ Args:
117
+ table_name: Name of the table to check
118
+
119
+ Returns:
120
+ bool: True if the table exists in the database, False otherwise
121
+ """
122
+ with self.Session() as sess:
123
+ return is_table_available(session=sess, table_name=table_name, db_schema=self.db_schema)
124
+
125
+ def _create_all_tables(self):
126
+ """Create all tables for the database."""
127
+ tables_to_create = [
128
+ (self.session_table_name, "sessions"),
129
+ (self.memory_table_name, "memories"),
130
+ (self.metrics_table_name, "metrics"),
131
+ (self.eval_table_name, "evals"),
132
+ (self.knowledge_table_name, "knowledge"),
133
+ (self.versions_table_name, "versions"),
134
+ ]
135
+
136
+ for table_name, table_type in tables_to_create:
137
+ if table_name != self.versions_table_name:
138
+ # Also store the schema version for the created table
139
+ latest_schema_version = MigrationManager(self).latest_schema_version
140
+ self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
141
+
142
+ self._create_table(table_name=table_name, table_type=table_type, db_schema=self.db_schema)
143
+
95
144
  def _create_table(self, table_name: str, table_type: str, db_schema: str) -> Table:
96
145
  """
97
146
  Create a table with the appropriate schema based on the table type.
@@ -170,7 +219,7 @@ class PostgresDb(BaseDb):
170
219
  except Exception as e:
171
220
  log_error(f"Error creating index {idx.name}: {e}")
172
221
 
173
- log_info(f"Successfully created table {table_name} in schema {db_schema}")
222
+ log_debug(f"Successfully created table {table_name} in schema {db_schema}")
174
223
  return table
175
224
 
176
225
  except Exception as e:
@@ -223,6 +272,24 @@ class PostgresDb(BaseDb):
223
272
  )
224
273
  return self.knowledge_table
225
274
 
275
+ if table_type == "culture":
276
+ self.culture_table = self._get_or_create_table(
277
+ table_name=self.culture_table_name,
278
+ table_type="culture",
279
+ db_schema=self.db_schema,
280
+ create_table_if_not_found=create_table_if_not_found,
281
+ )
282
+ return self.culture_table
283
+
284
+ if table_type == "versions":
285
+ self.versions_table = self._get_or_create_table(
286
+ table_name=self.versions_table_name,
287
+ table_type="versions",
288
+ db_schema=self.db_schema,
289
+ create_table_if_not_found=create_table_if_not_found,
290
+ )
291
+ return self.versions_table
292
+
226
293
  raise ValueError(f"Unknown table type: {table_type}")
227
294
 
228
295
  def _get_or_create_table(
@@ -247,6 +314,11 @@ class PostgresDb(BaseDb):
247
314
  if not create_table_if_not_found:
248
315
  return None
249
316
 
317
+ if table_name != self.versions_table_name:
318
+ # Also store the schema version for the created table
319
+ latest_schema_version = MigrationManager(self).latest_schema_version
320
+ self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
321
+
250
322
  return self._create_table(table_name=table_name, table_type=table_type, db_schema=db_schema)
251
323
 
252
324
  if not is_valid_table(
@@ -265,8 +337,43 @@ class PostgresDb(BaseDb):
265
337
  log_error(f"Error loading existing table {db_schema}.{table_name}: {e}")
266
338
  raise
267
339
 
268
- # -- Session methods --
340
+ def get_latest_schema_version(self, table_name: str):
341
+ """Get the latest version of the database schema."""
342
+ table = self._get_table(table_type="versions", create_table_if_not_found=True)
343
+ if table is None:
344
+ return "2.0.0"
345
+ with self.Session() as sess:
346
+ stmt = select(table)
347
+ # Latest version for the given table
348
+ stmt = stmt.where(table.c.table_name == table_name)
349
+ stmt = stmt.order_by(table.c.version.desc()).limit(1)
350
+ result = sess.execute(stmt).fetchone()
351
+ if result is None:
352
+ return "2.0.0"
353
+ version_dict = dict(result._mapping)
354
+ return version_dict.get("version") or "2.0.0"
355
+
356
+ def upsert_schema_version(self, table_name: str, version: str) -> None:
357
+ """Upsert the schema version into the database."""
358
+ table = self._get_table(table_type="versions", create_table_if_not_found=True)
359
+ if table is None:
360
+ return
361
+ current_datetime = datetime.now().isoformat()
362
+ with self.Session() as sess, sess.begin():
363
+ stmt = postgresql.insert(table).values(
364
+ table_name=table_name,
365
+ version=version,
366
+ created_at=current_datetime, # Store as ISO format string
367
+ updated_at=current_datetime,
368
+ )
369
+ # Update version if table_name already exists
370
+ stmt = stmt.on_conflict_do_update(
371
+ index_elements=["table_name"],
372
+ set_=dict(version=version, updated_at=current_datetime),
373
+ )
374
+ sess.execute(stmt)
269
375
 
376
+ # -- Session methods --
270
377
  def delete_session(self, session_id: str) -> bool:
271
378
  """
272
379
  Delete a session from the database.
@@ -299,7 +406,7 @@ class PostgresDb(BaseDb):
299
406
 
300
407
  except Exception as e:
301
408
  log_error(f"Error deleting session: {e}")
302
- return False
409
+ raise e
303
410
 
304
411
  def delete_sessions(self, session_ids: List[str]) -> None:
305
412
  """Delete all given sessions from the database.
@@ -324,6 +431,7 @@ class PostgresDb(BaseDb):
324
431
 
325
432
  except Exception as e:
326
433
  log_error(f"Error deleting sessions: {e}")
434
+ raise e
327
435
 
328
436
  def get_session(
329
437
  self,
@@ -337,8 +445,8 @@ class PostgresDb(BaseDb):
337
445
 
338
446
  Args:
339
447
  session_id (str): ID of the session to read.
448
+ session_type (SessionType): Type of session to get.
340
449
  user_id (Optional[str]): User ID to filter by. Defaults to None.
341
- session_type (Optional[SessionType]): Type of session to read. Defaults to None.
342
450
  deserialize (Optional[bool]): Whether to serialize the session. Defaults to True.
343
451
 
344
452
  Returns:
@@ -359,9 +467,6 @@ class PostgresDb(BaseDb):
359
467
 
360
468
  if user_id is not None:
361
469
  stmt = stmt.where(table.c.user_id == user_id)
362
- if session_type is not None:
363
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
364
- stmt = stmt.where(table.c.session_type == session_type_value)
365
470
  result = sess.execute(stmt).fetchone()
366
471
  if result is None:
367
472
  return None
@@ -382,7 +487,7 @@ class PostgresDb(BaseDb):
382
487
 
383
488
  except Exception as e:
384
489
  log_error(f"Exception reading from session table: {e}")
385
- return None
490
+ raise e
386
491
 
387
492
  def get_sessions(
388
493
  self,
@@ -402,6 +507,7 @@ class PostgresDb(BaseDb):
402
507
  Get all sessions in the given table. Can filter by user_id and entity_id.
403
508
 
404
509
  Args:
510
+ session_type (Optional[SessionType]): The type of session to get.
405
511
  user_id (Optional[str]): The ID of the user to filter by.
406
512
  entity_id (Optional[str]): The ID of the agent / workflow to filter by.
407
513
  start_timestamp (Optional[int]): The start timestamp to filter by.
@@ -484,7 +590,7 @@ class PostgresDb(BaseDb):
484
590
 
485
591
  except Exception as e:
486
592
  log_error(f"Exception reading from session table: {e}")
487
- return [] if deserialize else ([], 0)
593
+ raise e
488
594
 
489
595
  def rename_session(
490
596
  self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
@@ -551,7 +657,7 @@ class PostgresDb(BaseDb):
551
657
 
552
658
  except Exception as e:
553
659
  log_error(f"Exception renaming session: {e}")
554
- return None
660
+ raise e
555
661
 
556
662
  def upsert_session(
557
663
  self, session: Session, deserialize: Optional[bool] = True
@@ -691,12 +797,189 @@ class PostgresDb(BaseDb):
691
797
 
692
798
  except Exception as e:
693
799
  log_error(f"Exception upserting into sessions table: {e}")
694
- return None
800
+ raise e
801
+
802
+ def upsert_sessions(
803
+ self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
804
+ ) -> List[Union[Session, Dict[str, Any]]]:
805
+ """
806
+ Bulk insert or update multiple sessions.
807
+
808
+ Args:
809
+ sessions (List[Session]): The list of session data to upsert.
810
+ deserialize (Optional[bool]): Whether to deserialize the sessions. Defaults to True.
811
+ preserve_updated_at (bool): If True, preserve the updated_at from the session object.
812
+
813
+ Returns:
814
+ List[Union[Session, Dict[str, Any]]]: List of upserted sessions
815
+
816
+ Raises:
817
+ Exception: If an error occurs during bulk upsert.
818
+ """
819
+ try:
820
+ if not sessions:
821
+ return []
822
+
823
+ table = self._get_table(table_type="sessions", create_table_if_not_found=True)
824
+ if table is None:
825
+ return []
826
+
827
+ # Group sessions by type for better handling
828
+ agent_sessions = [s for s in sessions if isinstance(s, AgentSession)]
829
+ team_sessions = [s for s in sessions if isinstance(s, TeamSession)]
830
+ workflow_sessions = [s for s in sessions if isinstance(s, WorkflowSession)]
831
+
832
+ results: List[Union[Session, Dict[str, Any]]] = []
833
+
834
+ # Bulk upsert agent sessions
835
+ if agent_sessions:
836
+ session_records = []
837
+ for agent_session in agent_sessions:
838
+ session_dict = agent_session.to_dict()
839
+ # Use preserved updated_at if flag is set (even if None), otherwise use current time
840
+ updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
841
+ session_records.append(
842
+ {
843
+ "session_id": session_dict.get("session_id"),
844
+ "session_type": SessionType.AGENT.value,
845
+ "agent_id": session_dict.get("agent_id"),
846
+ "user_id": session_dict.get("user_id"),
847
+ "agent_data": session_dict.get("agent_data"),
848
+ "session_data": session_dict.get("session_data"),
849
+ "summary": session_dict.get("summary"),
850
+ "metadata": session_dict.get("metadata"),
851
+ "runs": session_dict.get("runs"),
852
+ "created_at": session_dict.get("created_at"),
853
+ "updated_at": updated_at,
854
+ }
855
+ )
856
+
857
+ with self.Session() as sess, sess.begin():
858
+ stmt: Any = postgresql.insert(table)
859
+ update_columns = {
860
+ col.name: stmt.excluded[col.name]
861
+ for col in table.columns
862
+ if col.name not in ["id", "session_id", "created_at"]
863
+ }
864
+ stmt = stmt.on_conflict_do_update(index_elements=["session_id"], set_=update_columns).returning(
865
+ table
866
+ )
867
+
868
+ result = sess.execute(stmt, session_records)
869
+ for row in result.fetchall():
870
+ session_dict = dict(row._mapping)
871
+ if deserialize:
872
+ deserialized_agent_session = AgentSession.from_dict(session_dict)
873
+ if deserialized_agent_session is None:
874
+ continue
875
+ results.append(deserialized_agent_session)
876
+ else:
877
+ results.append(session_dict)
878
+
879
+ # Bulk upsert team sessions
880
+ if team_sessions:
881
+ session_records = []
882
+ for team_session in team_sessions:
883
+ session_dict = team_session.to_dict()
884
+ # Use preserved updated_at if flag is set (even if None), otherwise use current time
885
+ updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
886
+ session_records.append(
887
+ {
888
+ "session_id": session_dict.get("session_id"),
889
+ "session_type": SessionType.TEAM.value,
890
+ "team_id": session_dict.get("team_id"),
891
+ "user_id": session_dict.get("user_id"),
892
+ "team_data": session_dict.get("team_data"),
893
+ "session_data": session_dict.get("session_data"),
894
+ "summary": session_dict.get("summary"),
895
+ "metadata": session_dict.get("metadata"),
896
+ "runs": session_dict.get("runs"),
897
+ "created_at": session_dict.get("created_at"),
898
+ "updated_at": updated_at,
899
+ }
900
+ )
901
+
902
+ with self.Session() as sess, sess.begin():
903
+ stmt = postgresql.insert(table)
904
+ update_columns = {
905
+ col.name: stmt.excluded[col.name]
906
+ for col in table.columns
907
+ if col.name not in ["id", "session_id", "created_at"]
908
+ }
909
+ stmt = stmt.on_conflict_do_update(index_elements=["session_id"], set_=update_columns).returning(
910
+ table
911
+ )
912
+
913
+ result = sess.execute(stmt, session_records)
914
+ for row in result.fetchall():
915
+ session_dict = dict(row._mapping)
916
+ if deserialize:
917
+ deserialized_team_session = TeamSession.from_dict(session_dict)
918
+ if deserialized_team_session is None:
919
+ continue
920
+ results.append(deserialized_team_session)
921
+ else:
922
+ results.append(session_dict)
923
+
924
+ # Bulk upsert workflow sessions
925
+ if workflow_sessions:
926
+ session_records = []
927
+ for workflow_session in workflow_sessions:
928
+ session_dict = workflow_session.to_dict()
929
+ # Use preserved updated_at if flag is set (even if None), otherwise use current time
930
+ updated_at = session_dict.get("updated_at") if preserve_updated_at else int(time.time())
931
+ session_records.append(
932
+ {
933
+ "session_id": session_dict.get("session_id"),
934
+ "session_type": SessionType.WORKFLOW.value,
935
+ "workflow_id": session_dict.get("workflow_id"),
936
+ "user_id": session_dict.get("user_id"),
937
+ "workflow_data": session_dict.get("workflow_data"),
938
+ "session_data": session_dict.get("session_data"),
939
+ "summary": session_dict.get("summary"),
940
+ "metadata": session_dict.get("metadata"),
941
+ "runs": session_dict.get("runs"),
942
+ "created_at": session_dict.get("created_at"),
943
+ "updated_at": updated_at,
944
+ }
945
+ )
946
+
947
+ with self.Session() as sess, sess.begin():
948
+ stmt = postgresql.insert(table)
949
+ update_columns = {
950
+ col.name: stmt.excluded[col.name]
951
+ for col in table.columns
952
+ if col.name not in ["id", "session_id", "created_at"]
953
+ }
954
+ stmt = stmt.on_conflict_do_update(index_elements=["session_id"], set_=update_columns).returning(
955
+ table
956
+ )
957
+
958
+ result = sess.execute(stmt, session_records)
959
+ for row in result.fetchall():
960
+ session_dict = dict(row._mapping)
961
+ if deserialize:
962
+ deserialized_workflow_session = WorkflowSession.from_dict(session_dict)
963
+ if deserialized_workflow_session is None:
964
+ continue
965
+ results.append(deserialized_workflow_session)
966
+ else:
967
+ results.append(session_dict)
968
+
969
+ return results
970
+
971
+ except Exception as e:
972
+ log_error(f"Exception bulk upserting sessions: {e}")
973
+ return []
695
974
 
696
975
  # -- Memory methods --
697
- def delete_user_memory(self, memory_id: str):
976
+ def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None):
698
977
  """Delete a user memory from the database.
699
978
 
979
+ Args:
980
+ memory_id (str): The ID of the memory to delete.
981
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
982
+
700
983
  Returns:
701
984
  bool: True if deletion was successful, False otherwise.
702
985
 
@@ -710,6 +993,10 @@ class PostgresDb(BaseDb):
710
993
 
711
994
  with self.Session() as sess, sess.begin():
712
995
  delete_stmt = table.delete().where(table.c.memory_id == memory_id)
996
+
997
+ if user_id is not None:
998
+ delete_stmt = delete_stmt.where(table.c.user_id == user_id)
999
+
713
1000
  result = sess.execute(delete_stmt)
714
1001
 
715
1002
  success = result.rowcount > 0
@@ -720,12 +1007,14 @@ class PostgresDb(BaseDb):
720
1007
 
721
1008
  except Exception as e:
722
1009
  log_error(f"Error deleting user memory: {e}")
1010
+ raise e
723
1011
 
724
- def delete_user_memories(self, memory_ids: List[str]) -> None:
1012
+ def delete_user_memories(self, memory_ids: List[str], user_id: Optional[str] = None) -> None:
725
1013
  """Delete user memories from the database.
726
1014
 
727
1015
  Args:
728
1016
  memory_ids (List[str]): The IDs of the memories to delete.
1017
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
729
1018
 
730
1019
  Raises:
731
1020
  Exception: If an error occurs during deletion.
@@ -737,6 +1026,10 @@ class PostgresDb(BaseDb):
737
1026
 
738
1027
  with self.Session() as sess, sess.begin():
739
1028
  delete_stmt = table.delete().where(table.c.memory_id.in_(memory_ids))
1029
+
1030
+ if user_id is not None:
1031
+ delete_stmt = delete_stmt.where(table.c.user_id == user_id)
1032
+
740
1033
  result = sess.execute(delete_stmt)
741
1034
 
742
1035
  if result.rowcount == 0:
@@ -746,6 +1039,7 @@ class PostgresDb(BaseDb):
746
1039
 
747
1040
  except Exception as e:
748
1041
  log_error(f"Error deleting user memories: {e}")
1042
+ raise e
749
1043
 
750
1044
  def get_all_memory_topics(self) -> List[str]:
751
1045
  """Get all memory topics from the database.
@@ -760,6 +1054,7 @@ class PostgresDb(BaseDb):
760
1054
 
761
1055
  with self.Session() as sess, sess.begin():
762
1056
  stmt = select(func.json_array_elements_text(table.c.topics))
1057
+
763
1058
  result = sess.execute(stmt).fetchall()
764
1059
 
765
1060
  return list(set([record[0] for record in result]))
@@ -769,13 +1064,14 @@ class PostgresDb(BaseDb):
769
1064
  return []
770
1065
 
771
1066
  def get_user_memory(
772
- self, memory_id: str, deserialize: Optional[bool] = True
1067
+ self, memory_id: str, deserialize: Optional[bool] = True, user_id: Optional[str] = None
773
1068
  ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
774
1069
  """Get a memory from the database.
775
1070
 
776
1071
  Args:
777
1072
  memory_id (str): The ID of the memory to get.
778
1073
  deserialize (Optional[bool]): Whether to serialize the memory. Defaults to True.
1074
+ user_id (Optional[str]): The ID of the user to filter by. Defaults to None.
779
1075
 
780
1076
  Returns:
781
1077
  Union[UserMemory, Dict[str, Any], None]:
@@ -793,6 +1089,9 @@ class PostgresDb(BaseDb):
793
1089
  with self.Session() as sess, sess.begin():
794
1090
  stmt = select(table).where(table.c.memory_id == memory_id)
795
1091
 
1092
+ if user_id is not None:
1093
+ stmt = stmt.where(table.c.user_id == user_id)
1094
+
796
1095
  result = sess.execute(stmt).fetchone()
797
1096
  if not result:
798
1097
  return None
@@ -805,7 +1104,7 @@ class PostgresDb(BaseDb):
805
1104
 
806
1105
  except Exception as e:
807
1106
  log_error(f"Exception reading from memory table: {e}")
808
- return None
1107
+ raise e
809
1108
 
810
1109
  def get_user_memories(
811
1110
  self,
@@ -888,7 +1187,7 @@ class PostgresDb(BaseDb):
888
1187
 
889
1188
  except Exception as e:
890
1189
  log_error(f"Exception reading from memory table: {e}")
891
- return [] if deserialize else ([], 0)
1190
+ raise e
892
1191
 
893
1192
  def clear_memories(self) -> None:
894
1193
  """Delete all memories from the database.
@@ -905,7 +1204,8 @@ class PostgresDb(BaseDb):
905
1204
  sess.execute(table.delete())
906
1205
 
907
1206
  except Exception as e:
908
- log_warning(f"Exception deleting all memories: {e}")
1207
+ log_error(f"Exception deleting all memories: {e}")
1208
+ raise e
909
1209
 
910
1210
  def get_user_memory_stats(
911
1211
  self, limit: Optional[int] = None, page: Optional[int] = None
@@ -972,7 +1272,7 @@ class PostgresDb(BaseDb):
972
1272
 
973
1273
  except Exception as e:
974
1274
  log_error(f"Exception getting user memory stats: {e}")
975
- return [], 0
1275
+ raise e
976
1276
 
977
1277
  def upsert_user_memory(
978
1278
  self, memory: UserMemory, deserialize: Optional[bool] = True
@@ -1000,6 +1300,8 @@ class PostgresDb(BaseDb):
1000
1300
  if memory.memory_id is None:
1001
1301
  memory.memory_id = str(uuid4())
1002
1302
 
1303
+ current_time = int(time.time())
1304
+
1003
1305
  stmt = postgresql.insert(table).values(
1004
1306
  memory_id=memory.memory_id,
1005
1307
  memory=memory.memory,
@@ -1008,7 +1310,9 @@ class PostgresDb(BaseDb):
1008
1310
  agent_id=memory.agent_id,
1009
1311
  team_id=memory.team_id,
1010
1312
  topics=memory.topics,
1011
- updated_at=int(time.time()),
1313
+ feedback=memory.feedback,
1314
+ created_at=memory.created_at,
1315
+ updated_at=memory.created_at,
1012
1316
  )
1013
1317
  stmt = stmt.on_conflict_do_update( # type: ignore
1014
1318
  index_elements=["memory_id"],
@@ -1018,7 +1322,10 @@ class PostgresDb(BaseDb):
1018
1322
  input=memory.input,
1019
1323
  agent_id=memory.agent_id,
1020
1324
  team_id=memory.team_id,
1021
- updated_at=int(time.time()),
1325
+ feedback=memory.feedback,
1326
+ updated_at=current_time,
1327
+ # Preserve created_at on update - don't overwrite existing value
1328
+ created_at=table.c.created_at,
1022
1329
  ),
1023
1330
  ).returning(table)
1024
1331
 
@@ -1034,7 +1341,89 @@ class PostgresDb(BaseDb):
1034
1341
 
1035
1342
  except Exception as e:
1036
1343
  log_error(f"Exception upserting user memory: {e}")
1037
- return None
1344
+ raise e
1345
+
1346
+ def upsert_memories(
1347
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
1348
+ ) -> List[Union[UserMemory, Dict[str, Any]]]:
1349
+ """
1350
+ Bulk insert or update multiple memories in the database for improved performance.
1351
+
1352
+ Args:
1353
+ memories (List[UserMemory]): The list of memories to upsert.
1354
+ deserialize (Optional[bool]): Whether to deserialize the memories. Defaults to True.
1355
+ preserve_updated_at (bool): If True, preserve the updated_at from the memory object.
1356
+ If False (default), set updated_at to current time.
1357
+
1358
+ Returns:
1359
+ List[Union[UserMemory, Dict[str, Any]]]: List of upserted memories
1360
+
1361
+ Raises:
1362
+ Exception: If an error occurs during bulk upsert.
1363
+ """
1364
+ try:
1365
+ if not memories:
1366
+ return []
1367
+
1368
+ table = self._get_table(table_type="memories", create_table_if_not_found=True)
1369
+ if table is None:
1370
+ return []
1371
+
1372
+ # Prepare memory records for bulk insert
1373
+ memory_records = []
1374
+ current_time = int(time.time())
1375
+
1376
+ for memory in memories:
1377
+ if memory.memory_id is None:
1378
+ memory.memory_id = str(uuid4())
1379
+
1380
+ # Use preserved updated_at if flag is set (even if None), otherwise use current time
1381
+ updated_at = memory.updated_at if preserve_updated_at else current_time
1382
+
1383
+ memory_records.append(
1384
+ {
1385
+ "memory_id": memory.memory_id,
1386
+ "memory": memory.memory,
1387
+ "input": memory.input,
1388
+ "user_id": memory.user_id,
1389
+ "agent_id": memory.agent_id,
1390
+ "team_id": memory.team_id,
1391
+ "topics": memory.topics,
1392
+ "feedback": memory.feedback,
1393
+ "created_at": memory.created_at,
1394
+ "updated_at": updated_at,
1395
+ }
1396
+ )
1397
+
1398
+ results: List[Union[UserMemory, Dict[str, Any]]] = []
1399
+
1400
+ with self.Session() as sess, sess.begin():
1401
+ insert_stmt = postgresql.insert(table)
1402
+ update_columns = {
1403
+ col.name: insert_stmt.excluded[col.name]
1404
+ for col in table.columns
1405
+ if col.name not in ["memory_id", "created_at"] # Don't update primary key or created_at
1406
+ }
1407
+ stmt = insert_stmt.on_conflict_do_update(index_elements=["memory_id"], set_=update_columns).returning(
1408
+ table
1409
+ )
1410
+
1411
+ result = sess.execute(stmt, memory_records)
1412
+ for row in result.fetchall():
1413
+ memory_dict = dict(row._mapping)
1414
+ if deserialize:
1415
+ deserialized_memory = UserMemory.from_dict(memory_dict)
1416
+ if deserialized_memory is None:
1417
+ continue
1418
+ results.append(deserialized_memory)
1419
+ else:
1420
+ results.append(memory_dict)
1421
+
1422
+ return results
1423
+
1424
+ except Exception as e:
1425
+ log_error(f"Exception bulk upserting memories: {e}")
1426
+ return []
1038
1427
 
1039
1428
  # -- Metrics methods --
1040
1429
  def _get_all_sessions_for_metrics_calculation(
@@ -1078,7 +1467,7 @@ class PostgresDb(BaseDb):
1078
1467
 
1079
1468
  except Exception as e:
1080
1469
  log_error(f"Exception reading from sessions table: {e}")
1081
- return []
1470
+ raise e
1082
1471
 
1083
1472
  def _get_metrics_calculation_starting_date(self, table: Table) -> Optional[date]:
1084
1473
  """Get the first date for which metrics calculation is needed:
@@ -1185,7 +1574,7 @@ class PostgresDb(BaseDb):
1185
1574
 
1186
1575
  except Exception as e:
1187
1576
  log_error(f"Exception refreshing metrics: {e}")
1188
- return None
1577
+ raise e
1189
1578
 
1190
1579
  def get_metrics(
1191
1580
  self,
@@ -1226,8 +1615,8 @@ class PostgresDb(BaseDb):
1226
1615
  return [row._mapping for row in result], latest_updated_at
1227
1616
 
1228
1617
  except Exception as e:
1229
- log_warning(f"Exception getting metrics: {e}")
1230
- return [], None
1618
+ log_error(f"Exception getting metrics: {e}")
1619
+ raise e
1231
1620
 
1232
1621
  # -- Knowledge methods --
1233
1622
  def delete_knowledge_content(self, id: str):
@@ -1236,17 +1625,18 @@ class PostgresDb(BaseDb):
1236
1625
  Args:
1237
1626
  id (str): The ID of the knowledge row to delete.
1238
1627
  """
1239
- table = self._get_table(table_type="knowledge")
1240
- if table is None:
1241
- return
1242
-
1243
1628
  try:
1629
+ table = self._get_table(table_type="knowledge")
1630
+ if table is None:
1631
+ return
1632
+
1244
1633
  with self.Session() as sess, sess.begin():
1245
1634
  stmt = table.delete().where(table.c.id == id)
1246
1635
  sess.execute(stmt)
1247
1636
 
1248
1637
  except Exception as e:
1249
1638
  log_error(f"Exception deleting knowledge content: {e}")
1639
+ raise e
1250
1640
 
1251
1641
  def get_knowledge_content(self, id: str) -> Optional[KnowledgeRow]:
1252
1642
  """Get a knowledge row from the database.
@@ -1257,11 +1647,11 @@ class PostgresDb(BaseDb):
1257
1647
  Returns:
1258
1648
  Optional[KnowledgeRow]: The knowledge row, or None if it doesn't exist.
1259
1649
  """
1260
- table = self._get_table(table_type="knowledge")
1261
- if table is None:
1262
- return None
1263
-
1264
1650
  try:
1651
+ table = self._get_table(table_type="knowledge")
1652
+ if table is None:
1653
+ return None
1654
+
1265
1655
  with self.Session() as sess, sess.begin():
1266
1656
  stmt = select(table).where(table.c.id == id)
1267
1657
  result = sess.execute(stmt).fetchone()
@@ -1272,7 +1662,7 @@ class PostgresDb(BaseDb):
1272
1662
 
1273
1663
  except Exception as e:
1274
1664
  log_error(f"Exception getting knowledge content: {e}")
1275
- return None
1665
+ raise e
1276
1666
 
1277
1667
  def get_knowledge_contents(
1278
1668
  self,
@@ -1296,11 +1686,11 @@ class PostgresDb(BaseDb):
1296
1686
  Raises:
1297
1687
  Exception: If an error occurs during retrieval.
1298
1688
  """
1299
- table = self._get_table(table_type="knowledge")
1300
- if table is None:
1301
- return [], 0
1302
-
1303
1689
  try:
1690
+ table = self._get_table(table_type="knowledge")
1691
+ if table is None:
1692
+ return [], 0
1693
+
1304
1694
  with self.Session() as sess, sess.begin():
1305
1695
  stmt = select(table)
1306
1696
 
@@ -1323,7 +1713,7 @@ class PostgresDb(BaseDb):
1323
1713
 
1324
1714
  except Exception as e:
1325
1715
  log_error(f"Exception getting knowledge contents: {e}")
1326
- return [], 0
1716
+ raise e
1327
1717
 
1328
1718
  def upsert_knowledge_content(self, knowledge_row: KnowledgeRow):
1329
1719
  """Upsert knowledge content in the database.
@@ -1401,7 +1791,7 @@ class PostgresDb(BaseDb):
1401
1791
 
1402
1792
  except Exception as e:
1403
1793
  log_error(f"Error upserting knowledge row: {e}")
1404
- return None
1794
+ raise e
1405
1795
 
1406
1796
  # -- Eval methods --
1407
1797
  def create_eval_run(self, eval_run: EvalRunRecord) -> Optional[EvalRunRecord]:
@@ -1434,7 +1824,7 @@ class PostgresDb(BaseDb):
1434
1824
 
1435
1825
  except Exception as e:
1436
1826
  log_error(f"Error creating eval run: {e}")
1437
- return None
1827
+ raise e
1438
1828
 
1439
1829
  def delete_eval_run(self, eval_run_id: str) -> None:
1440
1830
  """Delete an eval run from the database.
@@ -1458,6 +1848,7 @@ class PostgresDb(BaseDb):
1458
1848
 
1459
1849
  except Exception as e:
1460
1850
  log_error(f"Error deleting eval run {eval_run_id}: {e}")
1851
+ raise e
1461
1852
 
1462
1853
  def delete_eval_runs(self, eval_run_ids: List[str]) -> None:
1463
1854
  """Delete multiple eval runs from the database.
@@ -1481,6 +1872,7 @@ class PostgresDb(BaseDb):
1481
1872
 
1482
1873
  except Exception as e:
1483
1874
  log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
1875
+ raise e
1484
1876
 
1485
1877
  def get_eval_run(
1486
1878
  self, eval_run_id: str, deserialize: Optional[bool] = True
@@ -1518,7 +1910,7 @@ class PostgresDb(BaseDb):
1518
1910
 
1519
1911
  except Exception as e:
1520
1912
  log_error(f"Exception getting eval run {eval_run_id}: {e}")
1521
- return None
1913
+ raise e
1522
1914
 
1523
1915
  def get_eval_runs(
1524
1916
  self,
@@ -1613,7 +2005,7 @@ class PostgresDb(BaseDb):
1613
2005
 
1614
2006
  except Exception as e:
1615
2007
  log_error(f"Exception getting eval runs: {e}")
1616
- return [] if deserialize else ([], 0)
2008
+ raise e
1617
2009
 
1618
2010
  def rename_eval_run(
1619
2011
  self, eval_run_id: str, name: str, deserialize: Optional[bool] = True
@@ -1649,7 +2041,234 @@ class PostgresDb(BaseDb):
1649
2041
 
1650
2042
  except Exception as e:
1651
2043
  log_error(f"Error upserting eval run name {eval_run_id}: {e}")
1652
- return None
2044
+ raise e
2045
+
2046
+ # -- Culture methods --
2047
+
2048
+ def clear_cultural_knowledge(self) -> None:
2049
+ """Delete all cultural knowledge from the database.
2050
+
2051
+ Raises:
2052
+ Exception: If an error occurs during deletion.
2053
+ """
2054
+ try:
2055
+ table = self._get_table(table_type="culture")
2056
+ if table is None:
2057
+ return
2058
+
2059
+ with self.Session() as sess, sess.begin():
2060
+ sess.execute(table.delete())
2061
+
2062
+ except Exception as e:
2063
+ log_warning(f"Exception deleting all cultural knowledge: {e}")
2064
+ raise e
2065
+
2066
+ def delete_cultural_knowledge(self, id: str) -> None:
2067
+ """Delete a cultural knowledge entry from the database.
2068
+
2069
+ Args:
2070
+ id (str): The ID of the cultural knowledge to delete.
2071
+
2072
+ Raises:
2073
+ Exception: If an error occurs during deletion.
2074
+ """
2075
+ try:
2076
+ table = self._get_table(table_type="culture")
2077
+ if table is None:
2078
+ return
2079
+
2080
+ with self.Session() as sess, sess.begin():
2081
+ delete_stmt = table.delete().where(table.c.id == id)
2082
+ result = sess.execute(delete_stmt)
2083
+
2084
+ success = result.rowcount > 0
2085
+ if success:
2086
+ log_debug(f"Successfully deleted cultural knowledge id: {id}")
2087
+ else:
2088
+ log_debug(f"No cultural knowledge found with id: {id}")
2089
+
2090
+ except Exception as e:
2091
+ log_error(f"Error deleting cultural knowledge: {e}")
2092
+ raise e
2093
+
2094
+ def get_cultural_knowledge(
2095
+ self, id: str, deserialize: Optional[bool] = True
2096
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2097
+ """Get a cultural knowledge entry from the database.
2098
+
2099
+ Args:
2100
+ id (str): The ID of the cultural knowledge to get.
2101
+ deserialize (Optional[bool]): Whether to deserialize the cultural knowledge. Defaults to True.
2102
+
2103
+ Returns:
2104
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge entry, or None if it doesn't exist.
2105
+
2106
+ Raises:
2107
+ Exception: If an error occurs during retrieval.
2108
+ """
2109
+ try:
2110
+ table = self._get_table(table_type="culture")
2111
+ if table is None:
2112
+ return None
2113
+
2114
+ with self.Session() as sess, sess.begin():
2115
+ stmt = select(table).where(table.c.id == id)
2116
+ result = sess.execute(stmt).fetchone()
2117
+ if result is None:
2118
+ return None
2119
+
2120
+ db_row = dict(result._mapping)
2121
+ if not db_row or not deserialize:
2122
+ return db_row
2123
+
2124
+ return deserialize_cultural_knowledge(db_row)
2125
+
2126
+ except Exception as e:
2127
+ log_error(f"Exception reading from cultural knowledge table: {e}")
2128
+ raise e
2129
+
2130
+ def get_all_cultural_knowledge(
2131
+ self,
2132
+ name: Optional[str] = None,
2133
+ agent_id: Optional[str] = None,
2134
+ team_id: Optional[str] = None,
2135
+ limit: Optional[int] = None,
2136
+ page: Optional[int] = None,
2137
+ sort_by: Optional[str] = None,
2138
+ sort_order: Optional[str] = None,
2139
+ deserialize: Optional[bool] = True,
2140
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2141
+ """Get all cultural knowledge from the database as CulturalKnowledge objects.
2142
+
2143
+ Args:
2144
+ name (Optional[str]): The name of the cultural knowledge to filter by.
2145
+ agent_id (Optional[str]): The ID of the agent to filter by.
2146
+ team_id (Optional[str]): The ID of the team to filter by.
2147
+ limit (Optional[int]): The maximum number of cultural knowledge entries to return.
2148
+ page (Optional[int]): The page number.
2149
+ sort_by (Optional[str]): The column to sort by.
2150
+ sort_order (Optional[str]): The order to sort by.
2151
+ deserialize (Optional[bool]): Whether to deserialize the cultural knowledge. Defaults to True.
2152
+
2153
+ Returns:
2154
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2155
+ - When deserialize=True: List of CulturalKnowledge objects
2156
+ - When deserialize=False: List of CulturalKnowledge dictionaries and total count
2157
+
2158
+ Raises:
2159
+ Exception: If an error occurs during retrieval.
2160
+ """
2161
+ try:
2162
+ table = self._get_table(table_type="culture")
2163
+ if table is None:
2164
+ return [] if deserialize else ([], 0)
2165
+
2166
+ with self.Session() as sess, sess.begin():
2167
+ stmt = select(table)
2168
+
2169
+ # Filtering
2170
+ if name is not None:
2171
+ stmt = stmt.where(table.c.name == name)
2172
+ if agent_id is not None:
2173
+ stmt = stmt.where(table.c.agent_id == agent_id)
2174
+ if team_id is not None:
2175
+ stmt = stmt.where(table.c.team_id == team_id)
2176
+
2177
+ # Get total count after applying filtering
2178
+ count_stmt = select(func.count()).select_from(stmt.alias())
2179
+ total_count = sess.execute(count_stmt).scalar()
2180
+
2181
+ # Sorting
2182
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
2183
+ # Paginating
2184
+ if limit is not None:
2185
+ stmt = stmt.limit(limit)
2186
+ if page is not None:
2187
+ stmt = stmt.offset((page - 1) * limit)
2188
+
2189
+ result = sess.execute(stmt).fetchall()
2190
+ if not result:
2191
+ return [] if deserialize else ([], 0)
2192
+
2193
+ db_rows = [dict(record._mapping) for record in result]
2194
+
2195
+ if not deserialize:
2196
+ return db_rows, total_count
2197
+
2198
+ return [deserialize_cultural_knowledge(row) for row in db_rows]
2199
+
2200
+ except Exception as e:
2201
+ log_error(f"Error reading from cultural knowledge table: {e}")
2202
+ raise e
2203
+
2204
+ def upsert_cultural_knowledge(
2205
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
2206
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2207
+ """Upsert a cultural knowledge entry into the database.
2208
+
2209
+ Args:
2210
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
2211
+ deserialize (Optional[bool]): Whether to deserialize the cultural knowledge. Defaults to True.
2212
+
2213
+ Returns:
2214
+ Optional[CulturalKnowledge]: The upserted cultural knowledge entry.
2215
+
2216
+ Raises:
2217
+ Exception: If an error occurs during upsert.
2218
+ """
2219
+ try:
2220
+ table = self._get_table(table_type="culture", create_table_if_not_found=True)
2221
+ if table is None:
2222
+ return None
2223
+
2224
+ if cultural_knowledge.id is None:
2225
+ cultural_knowledge.id = str(uuid4())
2226
+
2227
+ # Serialize content, categories, and notes into a JSON dict for DB storage
2228
+ content_dict = serialize_cultural_knowledge(cultural_knowledge)
2229
+
2230
+ with self.Session() as sess, sess.begin():
2231
+ stmt = postgresql.insert(table).values(
2232
+ id=cultural_knowledge.id,
2233
+ name=cultural_knowledge.name,
2234
+ summary=cultural_knowledge.summary,
2235
+ content=content_dict if content_dict else None,
2236
+ metadata=cultural_knowledge.metadata,
2237
+ input=cultural_knowledge.input,
2238
+ created_at=cultural_knowledge.created_at,
2239
+ updated_at=int(time.time()),
2240
+ agent_id=cultural_knowledge.agent_id,
2241
+ team_id=cultural_knowledge.team_id,
2242
+ )
2243
+ stmt = stmt.on_conflict_do_update( # type: ignore
2244
+ index_elements=["id"],
2245
+ set_=dict(
2246
+ name=cultural_knowledge.name,
2247
+ summary=cultural_knowledge.summary,
2248
+ content=content_dict if content_dict else None,
2249
+ metadata=cultural_knowledge.metadata,
2250
+ input=cultural_knowledge.input,
2251
+ updated_at=int(time.time()),
2252
+ agent_id=cultural_knowledge.agent_id,
2253
+ team_id=cultural_knowledge.team_id,
2254
+ ),
2255
+ ).returning(table)
2256
+
2257
+ result = sess.execute(stmt)
2258
+ row = result.fetchone()
2259
+
2260
+ if row is None:
2261
+ return None
2262
+
2263
+ db_row = dict(row._mapping)
2264
+ if not db_row or not deserialize:
2265
+ return db_row
2266
+
2267
+ return deserialize_cultural_knowledge(db_row)
2268
+
2269
+ except Exception as e:
2270
+ log_error(f"Error upserting cultural knowledge: {e}")
2271
+ raise e
1653
2272
 
1654
2273
  # -- Migrations --
1655
2274
 
@@ -1692,17 +2311,17 @@ class PostgresDb(BaseDb):
1692
2311
  if v1_table_type == "agent_sessions":
1693
2312
  for session in sessions:
1694
2313
  self.upsert_session(session)
1695
- log_info(f"Migrated {len(sessions)} Agent sessions to table: {self.session_table}")
2314
+ log_info(f"Migrated {len(sessions)} Agent sessions to table: {self.session_table_name}")
1696
2315
 
1697
2316
  elif v1_table_type == "team_sessions":
1698
2317
  for session in sessions:
1699
2318
  self.upsert_session(session)
1700
- log_info(f"Migrated {len(sessions)} Team sessions to table: {self.session_table}")
2319
+ log_info(f"Migrated {len(sessions)} Team sessions to table: {self.session_table_name}")
1701
2320
 
1702
2321
  elif v1_table_type == "workflow_sessions":
1703
2322
  for session in sessions:
1704
2323
  self.upsert_session(session)
1705
- log_info(f"Migrated {len(sessions)} Workflow sessions to table: {self.session_table}")
2324
+ log_info(f"Migrated {len(sessions)} Workflow sessions to table: {self.session_table_name}")
1706
2325
 
1707
2326
  elif v1_table_type == "memories":
1708
2327
  for memory in memories: