agno 2.1.8__py3-none-any.whl → 2.1.10__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 (57) hide show
  1. agno/agent/agent.py +646 -133
  2. agno/culture/__init__.py +3 -0
  3. agno/culture/manager.py +954 -0
  4. agno/db/async_postgres/async_postgres.py +232 -0
  5. agno/db/async_postgres/schemas.py +15 -0
  6. agno/db/async_postgres/utils.py +58 -0
  7. agno/db/base.py +83 -6
  8. agno/db/dynamo/dynamo.py +162 -0
  9. agno/db/dynamo/schemas.py +44 -0
  10. agno/db/dynamo/utils.py +59 -0
  11. agno/db/firestore/firestore.py +231 -0
  12. agno/db/firestore/schemas.py +10 -0
  13. agno/db/firestore/utils.py +96 -0
  14. agno/db/gcs_json/gcs_json_db.py +190 -0
  15. agno/db/gcs_json/utils.py +58 -0
  16. agno/db/in_memory/in_memory_db.py +118 -0
  17. agno/db/in_memory/utils.py +58 -0
  18. agno/db/json/json_db.py +129 -0
  19. agno/db/json/utils.py +58 -0
  20. agno/db/mongo/mongo.py +222 -0
  21. agno/db/mongo/schemas.py +10 -0
  22. agno/db/mongo/utils.py +59 -0
  23. agno/db/mysql/mysql.py +232 -1
  24. agno/db/mysql/schemas.py +14 -0
  25. agno/db/mysql/utils.py +58 -0
  26. agno/db/postgres/postgres.py +242 -0
  27. agno/db/postgres/schemas.py +15 -0
  28. agno/db/postgres/utils.py +58 -0
  29. agno/db/redis/redis.py +181 -0
  30. agno/db/redis/schemas.py +14 -0
  31. agno/db/redis/utils.py +58 -0
  32. agno/db/schemas/__init__.py +2 -1
  33. agno/db/schemas/culture.py +120 -0
  34. agno/db/singlestore/schemas.py +14 -0
  35. agno/db/singlestore/singlestore.py +231 -0
  36. agno/db/singlestore/utils.py +58 -0
  37. agno/db/sqlite/schemas.py +14 -0
  38. agno/db/sqlite/sqlite.py +274 -7
  39. agno/db/sqlite/utils.py +62 -0
  40. agno/db/surrealdb/models.py +51 -1
  41. agno/db/surrealdb/surrealdb.py +154 -0
  42. agno/db/surrealdb/utils.py +61 -1
  43. agno/knowledge/knowledge.py +4 -0
  44. agno/knowledge/reader/field_labeled_csv_reader.py +0 -2
  45. agno/memory/manager.py +28 -11
  46. agno/models/message.py +4 -0
  47. agno/os/app.py +28 -6
  48. agno/team/team.py +9 -9
  49. agno/tools/gmail.py +59 -14
  50. agno/tools/googlecalendar.py +13 -20
  51. agno/workflow/condition.py +31 -9
  52. agno/workflow/router.py +31 -9
  53. {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/METADATA +1 -1
  54. {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/RECORD +57 -54
  55. {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/WHEEL +0 -0
  56. {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/licenses/LICENSE +0 -0
  57. {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/top_level.txt +0 -0
agno/db/sqlite/sqlite.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
5
5
  from uuid import uuid4
6
6
 
7
7
  from agno.db.base import BaseDb, SessionType
8
+ from agno.db.schemas.culture import CulturalKnowledge
8
9
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
9
10
  from agno.db.schemas.knowledge import KnowledgeRow
10
11
  from agno.db.schemas.memory import UserMemory
@@ -13,10 +14,12 @@ from agno.db.sqlite.utils import (
13
14
  apply_sorting,
14
15
  bulk_upsert_metrics,
15
16
  calculate_date_metrics,
17
+ deserialize_cultural_knowledge_from_db,
16
18
  fetch_all_sessions_data,
17
19
  get_dates_to_calculate_metrics_for,
18
20
  is_table_available,
19
21
  is_valid_table,
22
+ serialize_cultural_knowledge_for_db,
20
23
  )
21
24
  from agno.db.utils import deserialize_session_json_fields, serialize_session_json_fields
22
25
  from agno.session import AgentSession, Session, TeamSession, WorkflowSession
@@ -40,6 +43,7 @@ class SqliteDb(BaseDb):
40
43
  db_engine: Optional[Engine] = None,
41
44
  db_url: Optional[str] = None,
42
45
  session_table: Optional[str] = None,
46
+ culture_table: Optional[str] = None,
43
47
  memory_table: Optional[str] = None,
44
48
  metrics_table: Optional[str] = None,
45
49
  eval_table: Optional[str] = None,
@@ -60,6 +64,7 @@ class SqliteDb(BaseDb):
60
64
  db_engine (Optional[Engine]): The SQLAlchemy database engine to use.
61
65
  db_url (Optional[str]): The database URL to connect to.
62
66
  session_table (Optional[str]): Name of the table to store Agent, Team and Workflow sessions.
67
+ culture_table (Optional[str]): Name of the table to store cultural notions.
63
68
  memory_table (Optional[str]): Name of the table to store user memories.
64
69
  metrics_table (Optional[str]): Name of the table to store metrics.
65
70
  eval_table (Optional[str]): Name of the table to store evaluation runs data.
@@ -76,6 +81,7 @@ class SqliteDb(BaseDb):
76
81
  super().__init__(
77
82
  id=id,
78
83
  session_table=session_table,
84
+ culture_table=culture_table,
79
85
  memory_table=memory_table,
80
86
  metrics_table=metrics_table,
81
87
  eval_table=eval_table,
@@ -229,11 +235,22 @@ class SqliteDb(BaseDb):
229
235
  )
230
236
  return self.knowledge_table
231
237
 
238
+ elif table_type == "culture":
239
+ self.culture_table = self._get_or_create_table(
240
+ table_name=self.culture_table_name,
241
+ table_type="culture",
242
+ create_table_if_not_found=create_table_if_not_found,
243
+ )
244
+ return self.culture_table
245
+
232
246
  else:
233
247
  raise ValueError(f"Unknown table type: '{table_type}'")
234
248
 
235
249
  def _get_or_create_table(
236
- self, table_name: str, table_type: str, create_table_if_not_found: Optional[bool] = False
250
+ self,
251
+ table_name: str,
252
+ table_type: str,
253
+ create_table_if_not_found: Optional[bool] = False,
237
254
  ) -> Optional[Table]:
238
255
  """
239
256
  Check if the table exists and is valid, else create it.
@@ -483,7 +500,11 @@ class SqliteDb(BaseDb):
483
500
  raise e
484
501
 
485
502
  def rename_session(
486
- self, session_id: str, session_type: SessionType, session_name: str, deserialize: Optional[bool] = True
503
+ self,
504
+ session_id: str,
505
+ session_type: SessionType,
506
+ session_name: str,
507
+ deserialize: Optional[bool] = True,
487
508
  ) -> Optional[Union[Session, Dict[str, Any]]]:
488
509
  """
489
510
  Rename a session in the database.
@@ -664,7 +685,10 @@ class SqliteDb(BaseDb):
664
685
  raise e
665
686
 
666
687
  def upsert_sessions(
667
- self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
688
+ self,
689
+ sessions: List[Session],
690
+ deserialize: Optional[bool] = True,
691
+ preserve_updated_at: bool = False,
668
692
  ) -> List[Union[Session, Dict[str, Any]]]:
669
693
  """
670
694
  Bulk upsert multiple sessions for improved performance on large datasets.
@@ -975,7 +999,10 @@ class SqliteDb(BaseDb):
975
999
  raise e
976
1000
 
977
1001
  def get_user_memory(
978
- self, memory_id: str, deserialize: Optional[bool] = True, user_id: Optional[str] = None
1002
+ self,
1003
+ memory_id: str,
1004
+ deserialize: Optional[bool] = True,
1005
+ user_id: Optional[str] = None,
979
1006
  ) -> Optional[Union[UserMemory, Dict[str, Any]]]:
980
1007
  """Get a memory from the database.
981
1008
 
@@ -1231,7 +1258,10 @@ class SqliteDb(BaseDb):
1231
1258
  raise e
1232
1259
 
1233
1260
  def upsert_memories(
1234
- self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
1261
+ self,
1262
+ memories: List[UserMemory],
1263
+ deserialize: Optional[bool] = True,
1264
+ preserve_updated_at: bool = False,
1235
1265
  ) -> List[Union[UserMemory, Dict[str, Any]]]:
1236
1266
  """
1237
1267
  Bulk upsert multiple user memories for improved performance on large datasets.
@@ -1460,7 +1490,9 @@ class SqliteDb(BaseDb):
1460
1490
  start_timestamp=start_timestamp, end_timestamp=end_timestamp
1461
1491
  )
1462
1492
  all_sessions_data = fetch_all_sessions_data(
1463
- sessions=sessions, dates_to_process=dates_to_process, start_timestamp=start_timestamp
1493
+ sessions=sessions,
1494
+ dates_to_process=dates_to_process,
1495
+ start_timestamp=start_timestamp,
1464
1496
  )
1465
1497
  if not all_sessions_data:
1466
1498
  log_info("No new session data found. Won't calculate metrics.")
@@ -1707,7 +1739,11 @@ class SqliteDb(BaseDb):
1707
1739
  with self.Session() as sess, sess.begin():
1708
1740
  current_time = int(time.time())
1709
1741
  stmt = sqlite.insert(table).values(
1710
- {"created_at": current_time, "updated_at": current_time, **eval_run.model_dump()}
1742
+ {
1743
+ "created_at": current_time,
1744
+ "updated_at": current_time,
1745
+ **eval_run.model_dump(),
1746
+ }
1711
1747
  )
1712
1748
  sess.execute(stmt)
1713
1749
  sess.commit()
@@ -1997,3 +2033,234 @@ class SqliteDb(BaseDb):
1997
2033
  for memory in memories:
1998
2034
  self.upsert_user_memory(memory)
1999
2035
  log_info(f"Migrated {len(memories)} memories to table: {self.memory_table}")
2036
+
2037
+ # -- Culture methods --
2038
+
2039
+ def clear_cultural_knowledge(self) -> None:
2040
+ """Delete all cultural artifacts from the database.
2041
+
2042
+ Raises:
2043
+ Exception: If an error occurs during deletion.
2044
+ """
2045
+ try:
2046
+ table = self._get_table(table_type="culture")
2047
+ if table is None:
2048
+ return
2049
+
2050
+ with self.Session() as sess, sess.begin():
2051
+ sess.execute(table.delete())
2052
+
2053
+ except Exception as e:
2054
+ from agno.utils.log import log_warning
2055
+
2056
+ log_warning(f"Exception deleting all cultural artifacts: {e}")
2057
+ raise e
2058
+
2059
+ def delete_cultural_knowledge(self, id: str) -> None:
2060
+ """Delete a cultural artifact from the database.
2061
+
2062
+ Args:
2063
+ id (str): The ID of the cultural artifact to delete.
2064
+
2065
+ Raises:
2066
+ Exception: If an error occurs during deletion.
2067
+ """
2068
+ try:
2069
+ table = self._get_table(table_type="culture")
2070
+ if table is None:
2071
+ return
2072
+
2073
+ with self.Session() as sess, sess.begin():
2074
+ delete_stmt = table.delete().where(table.c.id == id)
2075
+ result = sess.execute(delete_stmt)
2076
+
2077
+ success = result.rowcount > 0
2078
+ if success:
2079
+ log_debug(f"Successfully deleted cultural artifact id: {id}")
2080
+ else:
2081
+ log_debug(f"No cultural artifact found with id: {id}")
2082
+
2083
+ except Exception as e:
2084
+ log_error(f"Error deleting cultural artifact: {e}")
2085
+ raise e
2086
+
2087
+ def get_cultural_knowledge(
2088
+ self, id: str, deserialize: Optional[bool] = True
2089
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2090
+ """Get a cultural artifact from the database.
2091
+
2092
+ Args:
2093
+ id (str): The ID of the cultural artifact to get.
2094
+ deserialize (Optional[bool]): Whether to serialize the cultural artifact. Defaults to True.
2095
+
2096
+ Returns:
2097
+ Optional[CulturalKnowledge]: The cultural artifact, or None if it doesn't exist.
2098
+
2099
+ Raises:
2100
+ Exception: If an error occurs during retrieval.
2101
+ """
2102
+ try:
2103
+ table = self._get_table(table_type="culture")
2104
+ if table is None:
2105
+ return None
2106
+
2107
+ with self.Session() as sess, sess.begin():
2108
+ stmt = select(table).where(table.c.id == id)
2109
+ result = sess.execute(stmt).fetchone()
2110
+ if result is None:
2111
+ return None
2112
+
2113
+ db_row = dict(result._mapping)
2114
+ if not db_row or not deserialize:
2115
+ return db_row
2116
+
2117
+ return deserialize_cultural_knowledge_from_db(db_row)
2118
+
2119
+ except Exception as e:
2120
+ log_error(f"Exception reading from cultural artifacts table: {e}")
2121
+ raise e
2122
+
2123
+ def get_all_cultural_knowledge(
2124
+ self,
2125
+ name: Optional[str] = None,
2126
+ agent_id: Optional[str] = None,
2127
+ team_id: Optional[str] = None,
2128
+ limit: Optional[int] = None,
2129
+ page: Optional[int] = None,
2130
+ sort_by: Optional[str] = None,
2131
+ sort_order: Optional[str] = None,
2132
+ deserialize: Optional[bool] = True,
2133
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2134
+ """Get all cultural artifacts from the database as CulturalNotion objects.
2135
+
2136
+ Args:
2137
+ name (Optional[str]): The name of the cultural artifact to filter by.
2138
+ agent_id (Optional[str]): The ID of the agent to filter by.
2139
+ team_id (Optional[str]): The ID of the team to filter by.
2140
+ limit (Optional[int]): The maximum number of cultural artifacts to return.
2141
+ page (Optional[int]): The page number.
2142
+ sort_by (Optional[str]): The column to sort by.
2143
+ sort_order (Optional[str]): The order to sort by.
2144
+ deserialize (Optional[bool]): Whether to serialize the cultural artifacts. Defaults to True.
2145
+
2146
+ Returns:
2147
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
2148
+ - When deserialize=True: List of CulturalNotion objects
2149
+ - When deserialize=False: List of CulturalNotion dictionaries and total count
2150
+
2151
+ Raises:
2152
+ Exception: If an error occurs during retrieval.
2153
+ """
2154
+ try:
2155
+ table = self._get_table(table_type="culture")
2156
+ if table is None:
2157
+ return [] if deserialize else ([], 0)
2158
+
2159
+ with self.Session() as sess, sess.begin():
2160
+ stmt = select(table)
2161
+
2162
+ # Filtering
2163
+ if name is not None:
2164
+ stmt = stmt.where(table.c.name == name)
2165
+ if agent_id is not None:
2166
+ stmt = stmt.where(table.c.agent_id == agent_id)
2167
+ if team_id is not None:
2168
+ stmt = stmt.where(table.c.team_id == team_id)
2169
+
2170
+ # Get total count after applying filtering
2171
+ count_stmt = select(func.count()).select_from(stmt.alias())
2172
+ total_count = sess.execute(count_stmt).scalar()
2173
+
2174
+ # Sorting
2175
+ stmt = apply_sorting(stmt, table, sort_by, sort_order)
2176
+ # Paginating
2177
+ if limit is not None:
2178
+ stmt = stmt.limit(limit)
2179
+ if page is not None:
2180
+ stmt = stmt.offset((page - 1) * limit)
2181
+
2182
+ result = sess.execute(stmt).fetchall()
2183
+ if not result:
2184
+ return [] if deserialize else ([], 0)
2185
+
2186
+ db_rows = [dict(record._mapping) for record in result]
2187
+
2188
+ if not deserialize:
2189
+ return db_rows, total_count
2190
+
2191
+ return [deserialize_cultural_knowledge_from_db(row) for row in db_rows]
2192
+
2193
+ except Exception as e:
2194
+ log_error(f"Error reading from cultural artifacts table: {e}")
2195
+ raise e
2196
+
2197
+ def upsert_cultural_knowledge(
2198
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
2199
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2200
+ """Upsert a cultural artifact into the database.
2201
+
2202
+ Args:
2203
+ cultural_knowledge (CulturalKnowledge): The cultural artifact to upsert.
2204
+ deserialize (Optional[bool]): Whether to serialize the cultural artifact. Defaults to True.
2205
+
2206
+ Returns:
2207
+ Optional[Union[CulturalNotion, Dict[str, Any]]]:
2208
+ - When deserialize=True: CulturalNotion object
2209
+ - When deserialize=False: CulturalNotion dictionary
2210
+
2211
+ Raises:
2212
+ Exception: If an error occurs during upsert.
2213
+ """
2214
+ try:
2215
+ table = self._get_table(table_type="culture", create_table_if_not_found=True)
2216
+ if table is None:
2217
+ return None
2218
+
2219
+ if cultural_knowledge.id is None:
2220
+ cultural_knowledge.id = str(uuid4())
2221
+
2222
+ # Serialize content, categories, and notes into a JSON string for DB storage (SQLite requires strings)
2223
+ content_json_str = serialize_cultural_knowledge_for_db(cultural_knowledge)
2224
+
2225
+ with self.Session() as sess, sess.begin():
2226
+ stmt = sqlite.insert(table).values(
2227
+ id=cultural_knowledge.id,
2228
+ name=cultural_knowledge.name,
2229
+ summary=cultural_knowledge.summary,
2230
+ content=content_json_str,
2231
+ metadata=cultural_knowledge.metadata,
2232
+ input=cultural_knowledge.input,
2233
+ created_at=cultural_knowledge.created_at,
2234
+ updated_at=int(time.time()),
2235
+ agent_id=cultural_knowledge.agent_id,
2236
+ team_id=cultural_knowledge.team_id,
2237
+ )
2238
+ stmt = stmt.on_conflict_do_update( # type: ignore
2239
+ index_elements=["id"],
2240
+ set_=dict(
2241
+ name=cultural_knowledge.name,
2242
+ summary=cultural_knowledge.summary,
2243
+ content=content_json_str,
2244
+ metadata=cultural_knowledge.metadata,
2245
+ input=cultural_knowledge.input,
2246
+ updated_at=int(time.time()),
2247
+ agent_id=cultural_knowledge.agent_id,
2248
+ team_id=cultural_knowledge.team_id,
2249
+ ),
2250
+ ).returning(table)
2251
+
2252
+ result = sess.execute(stmt)
2253
+ row = result.fetchone()
2254
+
2255
+ if row is None:
2256
+ return None
2257
+
2258
+ db_row: Dict[str, Any] = dict(row._mapping)
2259
+ if not db_row or not deserialize:
2260
+ return db_row
2261
+
2262
+ return deserialize_cultural_knowledge_from_db(db_row)
2263
+
2264
+ except Exception as e:
2265
+ log_error(f"Error upserting cultural knowledge: {e}")
2266
+ raise e
agno/db/sqlite/utils.py CHANGED
@@ -4,6 +4,7 @@ from datetime import date, datetime, timedelta, timezone
4
4
  from typing import Any, Dict, List, Optional
5
5
  from uuid import uuid4
6
6
 
7
+ from agno.db.schemas.culture import CulturalKnowledge
7
8
  from agno.db.sqlite.schemas import get_table_schema_definition
8
9
  from agno.utils.log import log_debug, log_error, log_warning
9
10
 
@@ -271,3 +272,64 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
271
272
  if days_diff <= 0:
272
273
  return []
273
274
  return [starting_date + timedelta(days=x) for x in range(days_diff)]
275
+
276
+
277
+ # -- Cultural Knowledge util methods --
278
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> str:
279
+ """Serialize a CulturalKnowledge object for database storage.
280
+
281
+ Converts the model's separate content, categories, and notes fields
282
+ into a single JSON string for the database content column.
283
+ SQLite requires JSON to be stored as strings.
284
+
285
+ Args:
286
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
287
+
288
+ Returns:
289
+ str: A JSON string containing content, categories, and notes.
290
+ """
291
+ content_dict: Dict[str, Any] = {}
292
+ if cultural_knowledge.content is not None:
293
+ content_dict["content"] = cultural_knowledge.content
294
+ if cultural_knowledge.categories is not None:
295
+ content_dict["categories"] = cultural_knowledge.categories
296
+ if cultural_knowledge.notes is not None:
297
+ content_dict["notes"] = cultural_knowledge.notes
298
+
299
+ return json.dumps(content_dict) if content_dict else None # type: ignore
300
+
301
+
302
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
303
+ """Deserialize a database row to a CulturalKnowledge object.
304
+
305
+ The database stores content as a JSON dict containing content, categories, and notes.
306
+ This method extracts those fields and converts them back to the model format.
307
+
308
+ Args:
309
+ db_row (Dict[str, Any]): The database row as a dictionary.
310
+
311
+ Returns:
312
+ CulturalKnowledge: The cultural knowledge object.
313
+ """
314
+ # Extract content, categories, and notes from the JSON content field
315
+ content_json = db_row.get("content", {}) or {}
316
+
317
+ if isinstance(content_json, str):
318
+ content_json = json.loads(content_json) if content_json else {}
319
+
320
+ return CulturalKnowledge.from_dict(
321
+ {
322
+ "id": db_row.get("id"),
323
+ "name": db_row.get("name"),
324
+ "summary": db_row.get("summary"),
325
+ "content": content_json.get("content"),
326
+ "categories": content_json.get("categories"),
327
+ "notes": content_json.get("notes"),
328
+ "metadata": db_row.get("metadata"),
329
+ "input": db_row.get("input"),
330
+ "created_at": db_row.get("created_at"),
331
+ "updated_at": db_row.get("updated_at"),
332
+ "agent_id": db_row.get("agent_id"),
333
+ "team_id": db_row.get("team_id"),
334
+ }
335
+ )
@@ -1,11 +1,12 @@
1
1
  from dataclasses import asdict
2
2
  from datetime import date, datetime, timezone
3
3
  from textwrap import dedent
4
- from typing import List, Literal, Optional, Sequence
4
+ from typing import Any, Dict, List, Literal, Optional, Sequence
5
5
 
6
6
  from surrealdb import RecordID
7
7
 
8
8
  from agno.db.base import SessionType
9
+ from agno.db.schemas.culture import CulturalKnowledge
9
10
  from agno.db.schemas.evals import EvalRunRecord
10
11
  from agno.db.schemas.knowledge import KnowledgeRow
11
12
  from agno.db.schemas.memory import UserMemory
@@ -16,6 +17,7 @@ from agno.session.workflow import WorkflowSession
16
17
 
17
18
  TableType = Literal[
18
19
  "agents",
20
+ "culture",
19
21
  "evals",
20
22
  "knowledge",
21
23
  "memories",
@@ -204,6 +206,48 @@ def serialize_knowledge_row(knowledge_row: KnowledgeRow, knowledge_table_name: s
204
206
  return dict_
205
207
 
206
208
 
209
+ def deserialize_cultural_knowledge(cultural_knowledge_raw: dict) -> CulturalKnowledge:
210
+ copy = cultural_knowledge_raw.copy()
211
+
212
+ copy = deserialize_record_id(copy, "id")
213
+ copy = desurrealize_dates(copy)
214
+
215
+ # Extract content, categories, and notes from the content field
216
+ content_json = copy.get("content", {}) or {}
217
+ if isinstance(content_json, dict):
218
+ copy["content"] = content_json.get("content")
219
+ copy["categories"] = content_json.get("categories")
220
+ copy["notes"] = content_json.get("notes")
221
+
222
+ return CulturalKnowledge.from_dict(copy)
223
+
224
+
225
+ def serialize_cultural_knowledge(cultural_knowledge: CulturalKnowledge, culture_table_name: str) -> dict:
226
+ dict_ = asdict(cultural_knowledge)
227
+ if cultural_knowledge.id is not None:
228
+ dict_["id"] = RecordID(culture_table_name, cultural_knowledge.id)
229
+
230
+ # Serialize content, categories, and notes into a single content dict for DB storage
231
+ content_dict: Dict[str, Any] = {}
232
+ if cultural_knowledge.content is not None:
233
+ content_dict["content"] = cultural_knowledge.content
234
+ if cultural_knowledge.categories is not None:
235
+ content_dict["categories"] = cultural_knowledge.categories
236
+ if cultural_knowledge.notes is not None:
237
+ content_dict["notes"] = cultural_knowledge.notes
238
+
239
+ # Replace the separate fields with the combined content field
240
+ dict_["content"] = content_dict if content_dict else None
241
+ # Remove the now-redundant fields since they're in content
242
+ dict_.pop("categories", None)
243
+ dict_.pop("notes", None)
244
+
245
+ # surrealize dates
246
+ dict_ = surrealize_dates(dict_)
247
+
248
+ return dict_
249
+
250
+
207
251
  def desurrealize_eval_run_record(eval_run_record_raw: dict) -> dict:
208
252
  copy = eval_run_record_raw.copy()
209
253
 
@@ -249,6 +293,12 @@ def get_schema(table_type: TableType, table_name: str) -> str:
249
293
  DEFINE FIELD OVERWRITE created_at ON {table_name} TYPE datetime VALUE time::now();
250
294
  DEFINE FIELD OVERWRITE updated_at ON {table_name} TYPE datetime VALUE time::now();
251
295
  """)
296
+ elif table_type == "culture":
297
+ return dedent(f"""
298
+ {define_table}
299
+ DEFINE FIELD OVERWRITE created_at ON {table_name} TYPE datetime VALUE time::now();
300
+ DEFINE FIELD OVERWRITE updated_at ON {table_name} TYPE datetime VALUE time::now();
301
+ """)
252
302
  elif table_type == "sessions":
253
303
  return dedent(f"""
254
304
  {define_table}