agno 2.1.9__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 (54) 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/reader/field_labeled_csv_reader.py +0 -2
  44. agno/memory/manager.py +28 -11
  45. agno/models/message.py +0 -1
  46. agno/os/app.py +28 -6
  47. agno/team/team.py +1 -1
  48. agno/tools/gmail.py +59 -14
  49. agno/workflow/router.py +1 -1
  50. {agno-2.1.9.dist-info → agno-2.1.10.dist-info}/METADATA +1 -1
  51. {agno-2.1.9.dist-info → agno-2.1.10.dist-info}/RECORD +54 -51
  52. {agno-2.1.9.dist-info → agno-2.1.10.dist-info}/WHEEL +0 -0
  53. {agno-2.1.9.dist-info → agno-2.1.10.dist-info}/licenses/LICENSE +0 -0
  54. {agno-2.1.9.dist-info → agno-2.1.10.dist-info}/top_level.txt +0 -0
agno/db/dynamo/dynamo.py CHANGED
@@ -13,6 +13,7 @@ from agno.db.dynamo.utils import (
13
13
  build_topic_filter_expression,
14
14
  calculate_date_metrics,
15
15
  create_table_if_not_exists,
16
+ deserialize_cultural_knowledge_from_db,
16
17
  deserialize_eval_record,
17
18
  deserialize_from_dynamodb_item,
18
19
  deserialize_knowledge_row,
@@ -23,10 +24,12 @@ from agno.db.dynamo.utils import (
23
24
  get_dates_to_calculate_metrics_for,
24
25
  merge_with_existing_session,
25
26
  prepare_session_data,
27
+ serialize_cultural_knowledge_for_db,
26
28
  serialize_eval_record,
27
29
  serialize_knowledge_row,
28
30
  serialize_to_dynamo_item,
29
31
  )
32
+ from agno.db.schemas.culture import CulturalKnowledge
30
33
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
31
34
  from agno.db.schemas.knowledge import KnowledgeRow
32
35
  from agno.db.schemas.memory import UserMemory
@@ -52,6 +55,7 @@ class DynamoDb(BaseDb):
52
55
  aws_access_key_id: Optional[str] = None,
53
56
  aws_secret_access_key: Optional[str] = None,
54
57
  session_table: Optional[str] = None,
58
+ culture_table: Optional[str] = None,
55
59
  memory_table: Optional[str] = None,
56
60
  metrics_table: Optional[str] = None,
57
61
  eval_table: Optional[str] = None,
@@ -67,6 +71,7 @@ class DynamoDb(BaseDb):
67
71
  aws_access_key_id: AWS access key ID.
68
72
  aws_secret_access_key: AWS secret access key.
69
73
  session_table: The name of the session table.
74
+ culture_table: The name of the culture table.
70
75
  memory_table: The name of the memory table.
71
76
  metrics_table: The name of the metrics table.
72
77
  eval_table: The name of the eval table.
@@ -80,6 +85,7 @@ class DynamoDb(BaseDb):
80
85
  super().__init__(
81
86
  id=id,
82
87
  session_table=session_table,
88
+ culture_table=culture_table,
83
89
  memory_table=memory_table,
84
90
  metrics_table=metrics_table,
85
91
  eval_table=eval_table,
@@ -168,6 +174,8 @@ class DynamoDb(BaseDb):
168
174
  table_name = self.eval_table_name
169
175
  elif table_type == "knowledge":
170
176
  table_name = self.knowledge_table_name
177
+ elif table_type == "culture":
178
+ table_name = self.culture_table_name
171
179
  else:
172
180
  raise ValueError(f"Unknown table type: {table_type}")
173
181
 
@@ -1885,3 +1893,157 @@ class DynamoDb(BaseDb):
1885
1893
  except Exception as e:
1886
1894
  log_error(f"Failed to rename eval run {eval_run_id}: {e}")
1887
1895
  raise e
1896
+
1897
+ # -- Culture methods --
1898
+
1899
+ def clear_cultural_knowledge(self) -> None:
1900
+ """Delete all cultural knowledge from the database."""
1901
+ try:
1902
+ table_name = self._get_table("culture")
1903
+ response = self.client.scan(TableName=table_name, ProjectionExpression="id")
1904
+
1905
+ with self.client.batch_writer(table_name) as batch:
1906
+ for item in response.get("Items", []):
1907
+ batch.delete_item(Key={"id": item["id"]})
1908
+ except Exception as e:
1909
+ log_error(f"Failed to clear cultural knowledge: {e}")
1910
+ raise e
1911
+
1912
+ def delete_cultural_knowledge(self, id: str) -> None:
1913
+ """Delete a cultural knowledge entry from the database."""
1914
+ try:
1915
+ table_name = self._get_table("culture")
1916
+ self.client.delete_item(TableName=table_name, Key={"id": {"S": id}})
1917
+ except Exception as e:
1918
+ log_error(f"Failed to delete cultural knowledge {id}: {e}")
1919
+ raise e
1920
+
1921
+ def get_cultural_knowledge(
1922
+ self, id: str, deserialize: Optional[bool] = True
1923
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1924
+ """Get a cultural knowledge entry from the database."""
1925
+ try:
1926
+ table_name = self._get_table("culture")
1927
+ response = self.client.get_item(TableName=table_name, Key={"id": {"S": id}})
1928
+
1929
+ item = response.get("Item")
1930
+ if not item:
1931
+ return None
1932
+
1933
+ db_row = deserialize_from_dynamodb_item(item)
1934
+ if not deserialize:
1935
+ return db_row
1936
+
1937
+ return deserialize_cultural_knowledge_from_db(db_row)
1938
+ except Exception as e:
1939
+ log_error(f"Failed to get cultural knowledge {id}: {e}")
1940
+ raise e
1941
+
1942
+ def get_all_cultural_knowledge(
1943
+ self,
1944
+ name: Optional[str] = None,
1945
+ agent_id: Optional[str] = None,
1946
+ team_id: Optional[str] = None,
1947
+ limit: Optional[int] = None,
1948
+ page: Optional[int] = None,
1949
+ sort_by: Optional[str] = None,
1950
+ sort_order: Optional[str] = None,
1951
+ deserialize: Optional[bool] = True,
1952
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1953
+ """Get all cultural knowledge from the database."""
1954
+ try:
1955
+ table_name = self._get_table("culture")
1956
+
1957
+ # Build filter expression
1958
+ filter_expressions = []
1959
+ expression_values = {}
1960
+
1961
+ if name:
1962
+ filter_expressions.append("#name = :name")
1963
+ expression_values[":name"] = {"S": name}
1964
+ if agent_id:
1965
+ filter_expressions.append("agent_id = :agent_id")
1966
+ expression_values[":agent_id"] = {"S": agent_id}
1967
+ if team_id:
1968
+ filter_expressions.append("team_id = :team_id")
1969
+ expression_values[":team_id"] = {"S": team_id}
1970
+
1971
+ scan_kwargs: Dict[str, Any] = {"TableName": table_name}
1972
+ if filter_expressions:
1973
+ scan_kwargs["FilterExpression"] = " AND ".join(filter_expressions)
1974
+ scan_kwargs["ExpressionAttributeValues"] = expression_values
1975
+ if name:
1976
+ scan_kwargs["ExpressionAttributeNames"] = {"#name": "name"}
1977
+
1978
+ # Execute scan
1979
+ response = self.client.scan(**scan_kwargs)
1980
+ items = response.get("Items", [])
1981
+
1982
+ # Continue scanning if there's more data
1983
+ while "LastEvaluatedKey" in response:
1984
+ scan_kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
1985
+ response = self.client.scan(**scan_kwargs)
1986
+ items.extend(response.get("Items", []))
1987
+
1988
+ # Deserialize items from DynamoDB format
1989
+ db_rows = [deserialize_from_dynamodb_item(item) for item in items]
1990
+
1991
+ # Apply sorting
1992
+ if sort_by:
1993
+ reverse = sort_order == "desc" if sort_order else False
1994
+ db_rows.sort(key=lambda x: x.get(sort_by, ""), reverse=reverse)
1995
+
1996
+ # Apply pagination
1997
+ total_count = len(db_rows)
1998
+ if limit and page:
1999
+ start = (page - 1) * limit
2000
+ db_rows = db_rows[start : start + limit]
2001
+ elif limit:
2002
+ db_rows = db_rows[:limit]
2003
+
2004
+ if not deserialize:
2005
+ return db_rows, total_count
2006
+
2007
+ return [deserialize_cultural_knowledge_from_db(row) for row in db_rows]
2008
+ except Exception as e:
2009
+ log_error(f"Failed to get all cultural knowledge: {e}")
2010
+ raise e
2011
+
2012
+ def upsert_cultural_knowledge(
2013
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
2014
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
2015
+ """Upsert a cultural knowledge entry into the database."""
2016
+ try:
2017
+ from uuid import uuid4
2018
+
2019
+ table_name = self._get_table("culture", create_table_if_not_found=True)
2020
+
2021
+ if not cultural_knowledge.id:
2022
+ cultural_knowledge.id = str(uuid4())
2023
+
2024
+ # Serialize content, categories, and notes into a dict for DB storage
2025
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
2026
+
2027
+ # Create the item dict with serialized content
2028
+ item_dict = {
2029
+ "id": cultural_knowledge.id,
2030
+ "name": cultural_knowledge.name,
2031
+ "summary": cultural_knowledge.summary,
2032
+ "content": content_dict if content_dict else None,
2033
+ "metadata": cultural_knowledge.metadata,
2034
+ "input": cultural_knowledge.input,
2035
+ "created_at": cultural_knowledge.created_at,
2036
+ "updated_at": int(time.time()),
2037
+ "agent_id": cultural_knowledge.agent_id,
2038
+ "team_id": cultural_knowledge.team_id,
2039
+ }
2040
+
2041
+ # Convert to DynamoDB format
2042
+ item = serialize_to_dynamo_item(item_dict)
2043
+ self.client.put_item(TableName=table_name, Item=item)
2044
+
2045
+ return self.get_cultural_knowledge(cultural_knowledge.id, deserialize=deserialize)
2046
+
2047
+ except Exception as e:
2048
+ log_error(f"Failed to upsert cultural knowledge: {e}")
2049
+ raise e
agno/db/dynamo/schemas.py CHANGED
@@ -243,6 +243,49 @@ METRICS_TABLE_SCHEMA = {
243
243
  "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
244
244
  }
245
245
 
246
+ CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
247
+ "TableName": "agno_cultural_knowledge",
248
+ "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
249
+ "AttributeDefinitions": [
250
+ {"AttributeName": "id", "AttributeType": "S"},
251
+ {"AttributeName": "name", "AttributeType": "S"},
252
+ {"AttributeName": "agent_id", "AttributeType": "S"},
253
+ {"AttributeName": "team_id", "AttributeType": "S"},
254
+ {"AttributeName": "created_at", "AttributeType": "N"},
255
+ ],
256
+ "GlobalSecondaryIndexes": [
257
+ {
258
+ "IndexName": "name-created_at-index",
259
+ "KeySchema": [
260
+ {"AttributeName": "name", "KeyType": "HASH"},
261
+ {"AttributeName": "created_at", "KeyType": "RANGE"},
262
+ ],
263
+ "Projection": {"ProjectionType": "ALL"},
264
+ "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
265
+ },
266
+ {
267
+ "IndexName": "agent_id-created_at-index",
268
+ "KeySchema": [
269
+ {"AttributeName": "agent_id", "KeyType": "HASH"},
270
+ {"AttributeName": "created_at", "KeyType": "RANGE"},
271
+ ],
272
+ "Projection": {"ProjectionType": "ALL"},
273
+ "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
274
+ },
275
+ {
276
+ "IndexName": "team_id-created_at-index",
277
+ "KeySchema": [
278
+ {"AttributeName": "team_id", "KeyType": "HASH"},
279
+ {"AttributeName": "created_at", "KeyType": "RANGE"},
280
+ ],
281
+ "Projection": {"ProjectionType": "ALL"},
282
+ "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
283
+ },
284
+ ],
285
+ "BillingMode": "PROVISIONED",
286
+ "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
287
+ }
288
+
246
289
 
247
290
  def get_table_schema_definition(table_type: str) -> Dict[str, Any]:
248
291
  """
@@ -260,6 +303,7 @@ def get_table_schema_definition(table_type: str) -> Dict[str, Any]:
260
303
  "evals": EVAL_TABLE_SCHEMA,
261
304
  "knowledge": KNOWLEDGE_TABLE_SCHEMA,
262
305
  "metrics": METRICS_TABLE_SCHEMA,
306
+ "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
263
307
  }
264
308
 
265
309
  schema = schemas.get(table_type, {})
agno/db/dynamo/utils.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Union
5
5
  from uuid import uuid4
6
6
 
7
7
  from agno.db.base import SessionType
8
+ from agno.db.schemas.culture import CulturalKnowledge
8
9
  from agno.db.schemas.evals import EvalRunRecord
9
10
  from agno.db.schemas.knowledge import KnowledgeRow
10
11
  from agno.session import Session
@@ -682,3 +683,61 @@ def process_query_results(
682
683
  log_error(f"Failed to deserialize item: {e}")
683
684
 
684
685
  return deserialized_items
686
+
687
+
688
+ # -- Cultural Knowledge util methods --
689
+ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
690
+ """Serialize a CulturalKnowledge object for database storage.
691
+
692
+ Converts the model's separate content, categories, and notes fields
693
+ into a single JSON dict for the database content column.
694
+ DynamoDB supports nested maps/dicts natively.
695
+
696
+ Args:
697
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
698
+
699
+ Returns:
700
+ Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
701
+ """
702
+ content_dict: Dict[str, Any] = {}
703
+ if cultural_knowledge.content is not None:
704
+ content_dict["content"] = cultural_knowledge.content
705
+ if cultural_knowledge.categories is not None:
706
+ content_dict["categories"] = cultural_knowledge.categories
707
+ if cultural_knowledge.notes is not None:
708
+ content_dict["notes"] = cultural_knowledge.notes
709
+
710
+ return content_dict if content_dict else {}
711
+
712
+
713
+ def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
714
+ """Deserialize a database row to a CulturalKnowledge object.
715
+
716
+ The database stores content as a dict containing content, categories, and notes.
717
+ This method extracts those fields and converts them back to the model format.
718
+
719
+ Args:
720
+ db_row (Dict[str, Any]): The database row as a dictionary.
721
+
722
+ Returns:
723
+ CulturalKnowledge: The cultural knowledge object.
724
+ """
725
+ # Extract content, categories, and notes from the content field
726
+ content_json = db_row.get("content", {}) or {}
727
+
728
+ return CulturalKnowledge.from_dict(
729
+ {
730
+ "id": db_row.get("id"),
731
+ "name": db_row.get("name"),
732
+ "summary": db_row.get("summary"),
733
+ "content": content_json.get("content"),
734
+ "categories": content_json.get("categories"),
735
+ "notes": content_json.get("notes"),
736
+ "metadata": db_row.get("metadata"),
737
+ "input": db_row.get("input"),
738
+ "created_at": db_row.get("created_at"),
739
+ "updated_at": db_row.get("updated_at"),
740
+ "agent_id": db_row.get("agent_id"),
741
+ "team_id": db_row.get("team_id"),
742
+ }
743
+ )
@@ -6,13 +6,18 @@ from uuid import uuid4
6
6
  from agno.db.base import BaseDb, SessionType
7
7
  from agno.db.firestore.utils import (
8
8
  apply_pagination,
9
+ apply_pagination_to_records,
9
10
  apply_sorting,
11
+ apply_sorting_to_records,
10
12
  bulk_upsert_metrics,
11
13
  calculate_date_metrics,
12
14
  create_collection_indexes,
15
+ deserialize_cultural_knowledge_from_db,
13
16
  fetch_all_sessions_data,
14
17
  get_dates_to_calculate_metrics_for,
18
+ serialize_cultural_knowledge_for_db,
15
19
  )
20
+ from agno.db.schemas.culture import CulturalKnowledge
16
21
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
17
22
  from agno.db.schemas.knowledge import KnowledgeRow
18
23
  from agno.db.schemas.memory import UserMemory
@@ -39,6 +44,7 @@ class FirestoreDb(BaseDb):
39
44
  metrics_collection: Optional[str] = None,
40
45
  eval_collection: Optional[str] = None,
41
46
  knowledge_collection: Optional[str] = None,
47
+ culture_collection: Optional[str] = None,
42
48
  id: Optional[str] = None,
43
49
  ):
44
50
  """
@@ -52,6 +58,7 @@ class FirestoreDb(BaseDb):
52
58
  metrics_collection (Optional[str]): Name of the collection to store metrics.
53
59
  eval_collection (Optional[str]): Name of the collection to store evaluation runs.
54
60
  knowledge_collection (Optional[str]): Name of the collection to store knowledge documents.
61
+ culture_collection (Optional[str]): Name of the collection to store cultural knowledge.
55
62
  id (Optional[str]): ID of the database.
56
63
 
57
64
  Raises:
@@ -68,6 +75,7 @@ class FirestoreDb(BaseDb):
68
75
  metrics_table=metrics_collection,
69
76
  eval_table=eval_collection,
70
77
  knowledge_table=knowledge_collection,
78
+ culture_table=culture_collection,
71
79
  )
72
80
 
73
81
  _client: Optional[Client] = db_client
@@ -146,6 +154,17 @@ class FirestoreDb(BaseDb):
146
154
  )
147
155
  return self.knowledge_collection
148
156
 
157
+ if table_type == "culture":
158
+ if not hasattr(self, "culture_collection"):
159
+ if self.culture_table_name is None:
160
+ raise ValueError("Culture collection was not provided on initialization")
161
+ self.culture_collection = self._get_or_create_collection(
162
+ collection_name=self.culture_table_name,
163
+ collection_type="culture",
164
+ create_collection_if_not_found=create_collection_if_not_found,
165
+ )
166
+ return self.culture_collection
167
+
149
168
  raise ValueError(f"Unknown table type: {table_type}")
150
169
 
151
170
  def _get_or_create_collection(
@@ -1008,6 +1027,213 @@ class FirestoreDb(BaseDb):
1008
1027
  log_error(f"Exception deleting all memories: {e}")
1009
1028
  raise e
1010
1029
 
1030
+ # -- Cultural Knowledge methods --
1031
+ def clear_cultural_knowledge(self) -> None:
1032
+ """Delete all cultural knowledge from the database.
1033
+
1034
+ Raises:
1035
+ Exception: If an error occurs during deletion.
1036
+ """
1037
+ try:
1038
+ collection_ref = self._get_collection(table_type="culture")
1039
+
1040
+ # Get all documents in the collection
1041
+ docs = collection_ref.stream()
1042
+
1043
+ # Delete all documents in batches
1044
+ batch = self.db_client.batch()
1045
+ batch_count = 0
1046
+
1047
+ for doc in docs:
1048
+ batch.delete(doc.reference)
1049
+ batch_count += 1
1050
+
1051
+ # Firestore batch has a limit of 500 operations
1052
+ if batch_count >= 500:
1053
+ batch.commit()
1054
+ batch = self.db_client.batch()
1055
+ batch_count = 0
1056
+
1057
+ # Commit remaining operations
1058
+ if batch_count > 0:
1059
+ batch.commit()
1060
+
1061
+ except Exception as e:
1062
+ log_error(f"Exception deleting all cultural knowledge: {e}")
1063
+ raise e
1064
+
1065
+ def delete_cultural_knowledge(self, id: str) -> None:
1066
+ """Delete cultural knowledge by ID.
1067
+
1068
+ Args:
1069
+ id (str): The ID of the cultural knowledge to delete.
1070
+
1071
+ Raises:
1072
+ Exception: If an error occurs during deletion.
1073
+ """
1074
+ try:
1075
+ collection_ref = self._get_collection(table_type="culture")
1076
+ docs = collection_ref.where(filter=FieldFilter("id", "==", id)).stream()
1077
+
1078
+ for doc in docs:
1079
+ doc.reference.delete()
1080
+ log_debug(f"Deleted cultural knowledge with ID: {id}")
1081
+
1082
+ except Exception as e:
1083
+ log_error(f"Error deleting cultural knowledge: {e}")
1084
+ raise e
1085
+
1086
+ def get_cultural_knowledge(
1087
+ self, id: str, deserialize: Optional[bool] = True
1088
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1089
+ """Get cultural knowledge by ID.
1090
+
1091
+ Args:
1092
+ id (str): The ID of the cultural knowledge to retrieve.
1093
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
1094
+
1095
+ Returns:
1096
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
1097
+
1098
+ Raises:
1099
+ Exception: If an error occurs during retrieval.
1100
+ """
1101
+ try:
1102
+ collection_ref = self._get_collection(table_type="culture")
1103
+ docs = collection_ref.where(filter=FieldFilter("id", "==", id)).limit(1).stream()
1104
+
1105
+ for doc in docs:
1106
+ result = doc.to_dict()
1107
+ if not deserialize:
1108
+ return result
1109
+ return deserialize_cultural_knowledge_from_db(result)
1110
+
1111
+ return None
1112
+
1113
+ except Exception as e:
1114
+ log_error(f"Error getting cultural knowledge: {e}")
1115
+ raise e
1116
+
1117
+ def get_all_cultural_knowledge(
1118
+ self,
1119
+ agent_id: Optional[str] = None,
1120
+ team_id: Optional[str] = None,
1121
+ name: Optional[str] = None,
1122
+ limit: Optional[int] = None,
1123
+ page: Optional[int] = None,
1124
+ sort_by: Optional[str] = None,
1125
+ sort_order: Optional[str] = None,
1126
+ deserialize: Optional[bool] = True,
1127
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1128
+ """Get all cultural knowledge with filtering and pagination.
1129
+
1130
+ Args:
1131
+ agent_id (Optional[str]): Filter by agent ID.
1132
+ team_id (Optional[str]): Filter by team ID.
1133
+ name (Optional[str]): Filter by name (case-insensitive partial match).
1134
+ limit (Optional[int]): Maximum number of results to return.
1135
+ page (Optional[int]): Page number for pagination.
1136
+ sort_by (Optional[str]): Field to sort by.
1137
+ sort_order (Optional[str]): Sort order ('asc' or 'desc').
1138
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
1139
+
1140
+ Returns:
1141
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1142
+ - When deserialize=True: List of CulturalKnowledge objects
1143
+ - When deserialize=False: Tuple with list of dictionaries and total count
1144
+
1145
+ Raises:
1146
+ Exception: If an error occurs during retrieval.
1147
+ """
1148
+ try:
1149
+ collection_ref = self._get_collection(table_type="culture")
1150
+
1151
+ # Build query with filters
1152
+ query = collection_ref
1153
+ if agent_id is not None:
1154
+ query = query.where(filter=FieldFilter("agent_id", "==", agent_id))
1155
+ if team_id is not None:
1156
+ query = query.where(filter=FieldFilter("team_id", "==", team_id))
1157
+
1158
+ # Get all matching documents
1159
+ docs = query.stream()
1160
+ results = [doc.to_dict() for doc in docs]
1161
+
1162
+ # Apply name filter (Firestore doesn't support regex in queries)
1163
+ if name is not None:
1164
+ results = [r for r in results if name.lower() in r.get("name", "").lower()]
1165
+
1166
+ total_count = len(results)
1167
+
1168
+ # Apply sorting and pagination to in-memory results
1169
+ sorted_results = apply_sorting_to_records(records=results, sort_by=sort_by, sort_order=sort_order)
1170
+ paginated_results = apply_pagination_to_records(records=sorted_results, limit=limit, page=page)
1171
+
1172
+ if not deserialize:
1173
+ return paginated_results, total_count
1174
+
1175
+ return [deserialize_cultural_knowledge_from_db(item) for item in paginated_results]
1176
+
1177
+ except Exception as e:
1178
+ log_error(f"Error getting all cultural knowledge: {e}")
1179
+ raise e
1180
+
1181
+ def upsert_cultural_knowledge(
1182
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
1183
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1184
+ """Upsert cultural knowledge in Firestore.
1185
+
1186
+ Args:
1187
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
1188
+ deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
1189
+
1190
+ Returns:
1191
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
1192
+
1193
+ Raises:
1194
+ Exception: If an error occurs during upsert.
1195
+ """
1196
+ try:
1197
+ collection_ref = self._get_collection(table_type="culture", create_collection_if_not_found=True)
1198
+
1199
+ # Serialize content, categories, and notes into a dict for DB storage
1200
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
1201
+
1202
+ # Create the update document with serialized content
1203
+ update_doc = {
1204
+ "id": cultural_knowledge.id,
1205
+ "name": cultural_knowledge.name,
1206
+ "summary": cultural_knowledge.summary,
1207
+ "content": content_dict if content_dict else None,
1208
+ "metadata": cultural_knowledge.metadata,
1209
+ "input": cultural_knowledge.input,
1210
+ "created_at": cultural_knowledge.created_at,
1211
+ "updated_at": int(time.time()),
1212
+ "agent_id": cultural_knowledge.agent_id,
1213
+ "team_id": cultural_knowledge.team_id,
1214
+ }
1215
+
1216
+ # Find and update or create new document
1217
+ docs = collection_ref.where(filter=FieldFilter("id", "==", cultural_knowledge.id)).limit(1).stream()
1218
+
1219
+ doc_found = False
1220
+ for doc in docs:
1221
+ doc.reference.set(update_doc)
1222
+ doc_found = True
1223
+ break
1224
+
1225
+ if not doc_found:
1226
+ collection_ref.add(update_doc)
1227
+
1228
+ if not deserialize:
1229
+ return update_doc
1230
+
1231
+ return deserialize_cultural_knowledge_from_db(update_doc)
1232
+
1233
+ except Exception as e:
1234
+ log_error(f"Error upserting cultural knowledge: {e}")
1235
+ raise e
1236
+
1011
1237
  # -- Metrics methods --
1012
1238
 
1013
1239
  def _get_all_sessions_for_metrics_calculation(
@@ -1388,6 +1614,9 @@ class FirestoreDb(BaseDb):
1388
1614
  """
1389
1615
  try:
1390
1616
  collection_ref = self._get_collection(table_type="evals")
1617
+ if not collection_ref:
1618
+ return None
1619
+
1391
1620
  docs = collection_ref.where(filter=FieldFilter("run_id", "==", eval_run_id)).stream()
1392
1621
 
1393
1622
  eval_run_raw = None
@@ -1528,6 +1757,8 @@ class FirestoreDb(BaseDb):
1528
1757
  """
1529
1758
  try:
1530
1759
  collection_ref = self._get_collection(table_type="evals")
1760
+ if not collection_ref:
1761
+ return None
1531
1762
 
1532
1763
  docs = collection_ref.where(filter=FieldFilter("run_id", "==", eval_run_id)).stream()
1533
1764
  doc_ref = next((doc.reference for doc in docs), None)
@@ -112,6 +112,15 @@ METRICS_COLLECTION_SCHEMA = [
112
112
  {"key": [("date", "ASCENDING"), ("aggregation_period", "ASCENDING")], "collection_group": False, "unique": True},
113
113
  ]
114
114
 
115
+ CULTURAL_KNOWLEDGE_COLLECTION_SCHEMA = [
116
+ {"key": "id", "unique": True},
117
+ {"key": "name"},
118
+ {"key": "agent_id"},
119
+ {"key": "team_id"},
120
+ {"key": "created_at"},
121
+ {"key": "updated_at"},
122
+ ]
123
+
115
124
 
116
125
  def get_collection_indexes(collection_type: str) -> List[Dict[str, Any]]:
117
126
  """Get the index definitions for a specific collection type."""
@@ -121,6 +130,7 @@ def get_collection_indexes(collection_type: str) -> List[Dict[str, Any]]:
121
130
  "metrics": METRICS_COLLECTION_SCHEMA,
122
131
  "evals": EVAL_COLLECTION_SCHEMA,
123
132
  "knowledge": KNOWLEDGE_COLLECTION_SCHEMA,
133
+ "culture": CULTURAL_KNOWLEDGE_COLLECTION_SCHEMA,
124
134
  }
125
135
 
126
136
  indexes = index_definitions.get(collection_type)