agno 2.1.9__py3-none-any.whl → 2.2.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.
- agno/agent/agent.py +2048 -1204
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +954 -0
- agno/db/async_postgres/async_postgres.py +232 -0
- agno/db/async_postgres/schemas.py +15 -0
- agno/db/async_postgres/utils.py +58 -0
- agno/db/base.py +83 -6
- agno/db/dynamo/dynamo.py +162 -0
- agno/db/dynamo/schemas.py +44 -0
- agno/db/dynamo/utils.py +59 -0
- agno/db/firestore/firestore.py +231 -0
- agno/db/firestore/schemas.py +10 -0
- agno/db/firestore/utils.py +96 -0
- agno/db/gcs_json/gcs_json_db.py +190 -0
- agno/db/gcs_json/utils.py +58 -0
- agno/db/in_memory/in_memory_db.py +118 -0
- agno/db/in_memory/utils.py +58 -0
- agno/db/json/json_db.py +129 -0
- agno/db/json/utils.py +58 -0
- agno/db/mongo/mongo.py +222 -0
- agno/db/mongo/schemas.py +10 -0
- agno/db/mongo/utils.py +59 -0
- agno/db/mysql/mysql.py +232 -1
- agno/db/mysql/schemas.py +14 -0
- agno/db/mysql/utils.py +58 -0
- agno/db/postgres/postgres.py +242 -0
- agno/db/postgres/schemas.py +15 -0
- agno/db/postgres/utils.py +58 -0
- agno/db/redis/redis.py +181 -0
- agno/db/redis/schemas.py +14 -0
- agno/db/redis/utils.py +58 -0
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/singlestore/schemas.py +14 -0
- agno/db/singlestore/singlestore.py +231 -0
- agno/db/singlestore/utils.py +58 -0
- agno/db/sqlite/schemas.py +14 -0
- agno/db/sqlite/sqlite.py +274 -7
- agno/db/sqlite/utils.py +62 -0
- agno/db/surrealdb/models.py +51 -1
- agno/db/surrealdb/surrealdb.py +154 -0
- agno/db/surrealdb/utils.py +61 -1
- agno/knowledge/reader/field_labeled_csv_reader.py +0 -2
- agno/memory/manager.py +28 -11
- agno/models/anthropic/claude.py +2 -2
- agno/models/message.py +0 -1
- agno/models/ollama/chat.py +7 -2
- agno/os/app.py +29 -7
- agno/os/interfaces/a2a/router.py +2 -2
- agno/os/interfaces/agui/router.py +2 -2
- agno/os/router.py +7 -7
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/health.py +6 -2
- agno/os/routers/knowledge/schemas.py +49 -47
- agno/os/routers/memory/schemas.py +16 -16
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +382 -7
- agno/os/schema.py +254 -231
- agno/os/utils.py +1 -1
- agno/run/agent.py +49 -1
- agno/run/team.py +43 -0
- agno/session/summary.py +45 -13
- agno/session/team.py +90 -5
- agno/team/team.py +1118 -857
- agno/tools/gmail.py +59 -14
- agno/utils/agent.py +372 -0
- agno/utils/events.py +144 -2
- agno/utils/print_response/agent.py +10 -6
- agno/utils/print_response/team.py +6 -4
- agno/utils/print_response/workflow.py +7 -5
- agno/utils/team.py +9 -8
- agno/workflow/condition.py +17 -9
- agno/workflow/loop.py +18 -10
- agno/workflow/parallel.py +14 -6
- agno/workflow/router.py +17 -9
- agno/workflow/step.py +14 -6
- agno/workflow/steps.py +14 -6
- agno/workflow/workflow.py +245 -122
- {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/METADATA +60 -23
- {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/RECORD +83 -79
- {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/WHEEL +0 -0
- {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.9.dist-info → agno-2.2.0.dist-info}/top_level.txt +0 -0
agno/db/postgres/schemas.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
6
7
|
from sqlalchemy.types import JSON, BigInteger, Boolean, Date, String
|
|
7
8
|
except ImportError:
|
|
8
9
|
raise ImportError("`sqlalchemy` not installed. Please install it using `pip install sqlalchemy`")
|
|
@@ -98,6 +99,19 @@ METRICS_TABLE_SCHEMA = {
|
|
|
98
99
|
],
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
|
|
103
|
+
"id": {"type": String, "primary_key": True, "nullable": False},
|
|
104
|
+
"name": {"type": String, "nullable": False, "index": True},
|
|
105
|
+
"summary": {"type": String, "nullable": True},
|
|
106
|
+
"content": {"type": JSONB, "nullable": True},
|
|
107
|
+
"metadata": {"type": JSONB, "nullable": True},
|
|
108
|
+
"input": {"type": String, "nullable": True},
|
|
109
|
+
"created_at": {"type": BigInteger, "nullable": True},
|
|
110
|
+
"updated_at": {"type": BigInteger, "nullable": True},
|
|
111
|
+
"agent_id": {"type": String, "nullable": True},
|
|
112
|
+
"team_id": {"type": String, "nullable": True},
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
|
|
102
116
|
def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
103
117
|
"""
|
|
@@ -115,6 +129,7 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
|
115
129
|
"metrics": METRICS_TABLE_SCHEMA,
|
|
116
130
|
"memories": MEMORY_TABLE_SCHEMA,
|
|
117
131
|
"knowledge": KNOWLEDGE_TABLE_SCHEMA,
|
|
132
|
+
"culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
schema = schemas.get(table_type, {})
|
agno/db/postgres/utils.py
CHANGED
|
@@ -8,6 +8,7 @@ from uuid import uuid4
|
|
|
8
8
|
from sqlalchemy import Engine
|
|
9
9
|
|
|
10
10
|
from agno.db.postgres.schemas import get_table_schema_definition
|
|
11
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
11
12
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
12
13
|
|
|
13
14
|
try:
|
|
@@ -278,3 +279,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
|
|
|
278
279
|
if days_diff <= 0:
|
|
279
280
|
return []
|
|
280
281
|
return [starting_date + timedelta(days=x) for x in range(days_diff)]
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# -- Cultural Knowledge util methods --
|
|
285
|
+
def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
|
|
286
|
+
"""Serialize a CulturalKnowledge object for database storage.
|
|
287
|
+
|
|
288
|
+
Converts the model's separate content, categories, and notes fields
|
|
289
|
+
into a single JSON dict for the database content column.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Dict[str, Any]: A dictionary with the content field as JSON containing content, categories, and notes.
|
|
296
|
+
"""
|
|
297
|
+
content_dict: Dict[str, Any] = {}
|
|
298
|
+
if cultural_knowledge.content is not None:
|
|
299
|
+
content_dict["content"] = cultural_knowledge.content
|
|
300
|
+
if cultural_knowledge.categories is not None:
|
|
301
|
+
content_dict["categories"] = cultural_knowledge.categories
|
|
302
|
+
if cultural_knowledge.notes is not None:
|
|
303
|
+
content_dict["notes"] = cultural_knowledge.notes
|
|
304
|
+
|
|
305
|
+
return content_dict if content_dict else {}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
|
|
309
|
+
"""Deserialize a database row to a CulturalKnowledge object.
|
|
310
|
+
|
|
311
|
+
The database stores content as a JSON dict containing content, categories, and notes.
|
|
312
|
+
This method extracts those fields and converts them back to the model format.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
db_row (Dict[str, Any]): The database row as a dictionary.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
CulturalKnowledge: The cultural knowledge object.
|
|
319
|
+
"""
|
|
320
|
+
# Extract content, categories, and notes from the JSON content field
|
|
321
|
+
content_json = db_row.get("content", {}) or {}
|
|
322
|
+
|
|
323
|
+
return CulturalKnowledge.from_dict(
|
|
324
|
+
{
|
|
325
|
+
"id": db_row.get("id"),
|
|
326
|
+
"name": db_row.get("name"),
|
|
327
|
+
"summary": db_row.get("summary"),
|
|
328
|
+
"content": content_json.get("content"),
|
|
329
|
+
"categories": content_json.get("categories"),
|
|
330
|
+
"notes": content_json.get("notes"),
|
|
331
|
+
"metadata": db_row.get("metadata"),
|
|
332
|
+
"input": db_row.get("input"),
|
|
333
|
+
"created_at": db_row.get("created_at"),
|
|
334
|
+
"updated_at": db_row.get("updated_at"),
|
|
335
|
+
"agent_id": db_row.get("agent_id"),
|
|
336
|
+
"team_id": db_row.get("team_id"),
|
|
337
|
+
}
|
|
338
|
+
)
|
agno/db/redis/redis.py
CHANGED
|
@@ -10,14 +10,17 @@ from agno.db.redis.utils import (
|
|
|
10
10
|
apply_sorting,
|
|
11
11
|
calculate_date_metrics,
|
|
12
12
|
create_index_entries,
|
|
13
|
+
deserialize_cultural_knowledge_from_db,
|
|
13
14
|
deserialize_data,
|
|
14
15
|
fetch_all_sessions_data,
|
|
15
16
|
generate_redis_key,
|
|
16
17
|
get_all_keys_for_table,
|
|
17
18
|
get_dates_to_calculate_metrics_for,
|
|
18
19
|
remove_index_entries,
|
|
20
|
+
serialize_cultural_knowledge_for_db,
|
|
19
21
|
serialize_data,
|
|
20
22
|
)
|
|
23
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
21
24
|
from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
|
|
22
25
|
from agno.db.schemas.knowledge import KnowledgeRow
|
|
23
26
|
from agno.db.schemas.memory import UserMemory
|
|
@@ -44,6 +47,7 @@ class RedisDb(BaseDb):
|
|
|
44
47
|
metrics_table: Optional[str] = None,
|
|
45
48
|
eval_table: Optional[str] = None,
|
|
46
49
|
knowledge_table: Optional[str] = None,
|
|
50
|
+
culture_table: Optional[str] = None,
|
|
47
51
|
):
|
|
48
52
|
"""
|
|
49
53
|
Interface for interacting with a Redis database.
|
|
@@ -64,6 +68,7 @@ class RedisDb(BaseDb):
|
|
|
64
68
|
metrics_table (Optional[str]): Name of the table to store metrics
|
|
65
69
|
eval_table (Optional[str]): Name of the table to store evaluation runs
|
|
66
70
|
knowledge_table (Optional[str]): Name of the table to store knowledge documents
|
|
71
|
+
culture_table (Optional[str]): Name of the table to store cultural knowledge
|
|
67
72
|
|
|
68
73
|
Raises:
|
|
69
74
|
ValueError: If neither redis_client nor db_url is provided.
|
|
@@ -80,6 +85,7 @@ class RedisDb(BaseDb):
|
|
|
80
85
|
metrics_table=metrics_table,
|
|
81
86
|
eval_table=eval_table,
|
|
82
87
|
knowledge_table=knowledge_table,
|
|
88
|
+
culture_table=culture_table,
|
|
83
89
|
)
|
|
84
90
|
|
|
85
91
|
self.db_prefix = db_prefix
|
|
@@ -111,6 +117,9 @@ class RedisDb(BaseDb):
|
|
|
111
117
|
elif table_type == "knowledge":
|
|
112
118
|
return self.knowledge_table_name
|
|
113
119
|
|
|
120
|
+
elif table_type == "culture":
|
|
121
|
+
return self.culture_table_name
|
|
122
|
+
|
|
114
123
|
else:
|
|
115
124
|
raise ValueError(f"Unknown table type: {table_type}")
|
|
116
125
|
|
|
@@ -1475,3 +1484,175 @@ class RedisDb(BaseDb):
|
|
|
1475
1484
|
except Exception as e:
|
|
1476
1485
|
log_error(f"Error updating eval run name {eval_run_id}: {e}")
|
|
1477
1486
|
raise
|
|
1487
|
+
|
|
1488
|
+
# -- Cultural Knowledge methods --
|
|
1489
|
+
def clear_cultural_knowledge(self) -> None:
|
|
1490
|
+
"""Delete all cultural knowledge from the database.
|
|
1491
|
+
|
|
1492
|
+
Raises:
|
|
1493
|
+
Exception: If an error occurs during deletion.
|
|
1494
|
+
"""
|
|
1495
|
+
try:
|
|
1496
|
+
keys = get_all_keys_for_table(redis_client=self.redis_client, prefix=self.db_prefix, table_type="culture")
|
|
1497
|
+
|
|
1498
|
+
if keys:
|
|
1499
|
+
self.redis_client.delete(*keys)
|
|
1500
|
+
|
|
1501
|
+
except Exception as e:
|
|
1502
|
+
log_error(f"Exception deleting all cultural knowledge: {e}")
|
|
1503
|
+
raise e
|
|
1504
|
+
|
|
1505
|
+
def delete_cultural_knowledge(self, id: str) -> None:
|
|
1506
|
+
"""Delete cultural knowledge by ID.
|
|
1507
|
+
|
|
1508
|
+
Args:
|
|
1509
|
+
id (str): The ID of the cultural knowledge to delete.
|
|
1510
|
+
|
|
1511
|
+
Raises:
|
|
1512
|
+
Exception: If an error occurs during deletion.
|
|
1513
|
+
"""
|
|
1514
|
+
try:
|
|
1515
|
+
if self._delete_record("culture", id, index_fields=["name", "agent_id", "team_id"]):
|
|
1516
|
+
log_debug(f"Successfully deleted cultural knowledge id: {id}")
|
|
1517
|
+
else:
|
|
1518
|
+
log_debug(f"No cultural knowledge found with id: {id}")
|
|
1519
|
+
|
|
1520
|
+
except Exception as e:
|
|
1521
|
+
log_error(f"Error deleting cultural knowledge: {e}")
|
|
1522
|
+
raise e
|
|
1523
|
+
|
|
1524
|
+
def get_cultural_knowledge(
|
|
1525
|
+
self, id: str, deserialize: Optional[bool] = True
|
|
1526
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
1527
|
+
"""Get cultural knowledge by ID.
|
|
1528
|
+
|
|
1529
|
+
Args:
|
|
1530
|
+
id (str): The ID of the cultural knowledge to retrieve.
|
|
1531
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
|
|
1532
|
+
|
|
1533
|
+
Returns:
|
|
1534
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
|
|
1535
|
+
|
|
1536
|
+
Raises:
|
|
1537
|
+
Exception: If an error occurs during retrieval.
|
|
1538
|
+
"""
|
|
1539
|
+
try:
|
|
1540
|
+
cultural_knowledge = self._get_record("culture", id)
|
|
1541
|
+
|
|
1542
|
+
if cultural_knowledge is None:
|
|
1543
|
+
return None
|
|
1544
|
+
|
|
1545
|
+
if not deserialize:
|
|
1546
|
+
return cultural_knowledge
|
|
1547
|
+
|
|
1548
|
+
return deserialize_cultural_knowledge_from_db(cultural_knowledge)
|
|
1549
|
+
|
|
1550
|
+
except Exception as e:
|
|
1551
|
+
log_error(f"Error getting cultural knowledge: {e}")
|
|
1552
|
+
raise e
|
|
1553
|
+
|
|
1554
|
+
def get_all_cultural_knowledge(
|
|
1555
|
+
self,
|
|
1556
|
+
agent_id: Optional[str] = None,
|
|
1557
|
+
team_id: Optional[str] = None,
|
|
1558
|
+
name: Optional[str] = None,
|
|
1559
|
+
limit: Optional[int] = None,
|
|
1560
|
+
page: Optional[int] = None,
|
|
1561
|
+
sort_by: Optional[str] = None,
|
|
1562
|
+
sort_order: Optional[str] = None,
|
|
1563
|
+
deserialize: Optional[bool] = True,
|
|
1564
|
+
) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
1565
|
+
"""Get all cultural knowledge with filtering and pagination.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
agent_id (Optional[str]): Filter by agent ID.
|
|
1569
|
+
team_id (Optional[str]): Filter by team ID.
|
|
1570
|
+
name (Optional[str]): Filter by name (case-insensitive partial match).
|
|
1571
|
+
limit (Optional[int]): Maximum number of results to return.
|
|
1572
|
+
page (Optional[int]): Page number for pagination.
|
|
1573
|
+
sort_by (Optional[str]): Field to sort by.
|
|
1574
|
+
sort_order (Optional[str]): Sort order ('asc' or 'desc').
|
|
1575
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
|
|
1576
|
+
|
|
1577
|
+
Returns:
|
|
1578
|
+
Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
1579
|
+
- When deserialize=True: List of CulturalKnowledge objects
|
|
1580
|
+
- When deserialize=False: Tuple with list of dictionaries and total count
|
|
1581
|
+
|
|
1582
|
+
Raises:
|
|
1583
|
+
Exception: If an error occurs during retrieval.
|
|
1584
|
+
"""
|
|
1585
|
+
try:
|
|
1586
|
+
all_cultural_knowledge = self._get_all_records("culture")
|
|
1587
|
+
|
|
1588
|
+
# Apply filters
|
|
1589
|
+
filtered_items = []
|
|
1590
|
+
for item in all_cultural_knowledge:
|
|
1591
|
+
if agent_id is not None and item.get("agent_id") != agent_id:
|
|
1592
|
+
continue
|
|
1593
|
+
if team_id is not None and item.get("team_id") != team_id:
|
|
1594
|
+
continue
|
|
1595
|
+
if name is not None and name.lower() not in item.get("name", "").lower():
|
|
1596
|
+
continue
|
|
1597
|
+
|
|
1598
|
+
filtered_items.append(item)
|
|
1599
|
+
|
|
1600
|
+
sorted_items = apply_sorting(records=filtered_items, sort_by=sort_by, sort_order=sort_order)
|
|
1601
|
+
paginated_items = apply_pagination(records=sorted_items, limit=limit, page=page)
|
|
1602
|
+
|
|
1603
|
+
if not deserialize:
|
|
1604
|
+
return paginated_items, len(filtered_items)
|
|
1605
|
+
|
|
1606
|
+
return [deserialize_cultural_knowledge_from_db(item) for item in paginated_items]
|
|
1607
|
+
|
|
1608
|
+
except Exception as e:
|
|
1609
|
+
log_error(f"Error getting all cultural knowledge: {e}")
|
|
1610
|
+
raise e
|
|
1611
|
+
|
|
1612
|
+
def upsert_cultural_knowledge(
|
|
1613
|
+
self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
|
|
1614
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
1615
|
+
"""Upsert cultural knowledge in Redis.
|
|
1616
|
+
|
|
1617
|
+
Args:
|
|
1618
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
|
|
1619
|
+
deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
|
|
1620
|
+
|
|
1621
|
+
Returns:
|
|
1622
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
|
|
1623
|
+
|
|
1624
|
+
Raises:
|
|
1625
|
+
Exception: If an error occurs during upsert.
|
|
1626
|
+
"""
|
|
1627
|
+
try:
|
|
1628
|
+
# Serialize content, categories, and notes into a dict for DB storage
|
|
1629
|
+
content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
|
|
1630
|
+
item_id = cultural_knowledge.id or str(uuid4())
|
|
1631
|
+
|
|
1632
|
+
# Create the item dict with serialized content
|
|
1633
|
+
data = {
|
|
1634
|
+
"id": item_id,
|
|
1635
|
+
"name": cultural_knowledge.name,
|
|
1636
|
+
"summary": cultural_knowledge.summary,
|
|
1637
|
+
"content": content_dict if content_dict else None,
|
|
1638
|
+
"metadata": cultural_knowledge.metadata,
|
|
1639
|
+
"input": cultural_knowledge.input,
|
|
1640
|
+
"created_at": cultural_knowledge.created_at,
|
|
1641
|
+
"updated_at": int(time.time()),
|
|
1642
|
+
"agent_id": cultural_knowledge.agent_id,
|
|
1643
|
+
"team_id": cultural_knowledge.team_id,
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
success = self._store_record("culture", item_id, data, index_fields=["name", "agent_id", "team_id"])
|
|
1647
|
+
|
|
1648
|
+
if not success:
|
|
1649
|
+
return None
|
|
1650
|
+
|
|
1651
|
+
if not deserialize:
|
|
1652
|
+
return data
|
|
1653
|
+
|
|
1654
|
+
return deserialize_cultural_knowledge_from_db(data)
|
|
1655
|
+
|
|
1656
|
+
except Exception as e:
|
|
1657
|
+
log_error(f"Error upserting cultural knowledge: {e}")
|
|
1658
|
+
raise e
|
agno/db/redis/schemas.py
CHANGED
|
@@ -81,6 +81,20 @@ KNOWLEDGE_SCHEMA = {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
CULTURAL_KNOWLEDGE_SCHEMA = {
|
|
85
|
+
"id": {"type": "string", "primary_key": True},
|
|
86
|
+
"name": {"type": "string"},
|
|
87
|
+
"summary": {"type": "string"},
|
|
88
|
+
"content": {"type": "json"},
|
|
89
|
+
"metadata": {"type": "json"},
|
|
90
|
+
"input": {"type": "string"},
|
|
91
|
+
"created_at": {"type": "integer"},
|
|
92
|
+
"updated_at": {"type": "integer"},
|
|
93
|
+
"agent_id": {"type": "string"},
|
|
94
|
+
"team_id": {"type": "string"},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
84
98
|
def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
85
99
|
"""
|
|
86
100
|
Get the expected schema definition for the given table.
|
agno/db/redis/utils.py
CHANGED
|
@@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta, timezone
|
|
|
6
6
|
from typing import Any, Dict, List, Optional
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
9
10
|
from agno.utils.log import log_warning
|
|
10
11
|
|
|
11
12
|
try:
|
|
@@ -286,3 +287,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
|
|
|
286
287
|
if days_diff <= 0:
|
|
287
288
|
return []
|
|
288
289
|
return [starting_date + timedelta(days=x) for x in range(days_diff)]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# -- Cultural Knowledge util methods --
|
|
293
|
+
def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
|
|
294
|
+
"""Serialize a CulturalKnowledge object for database storage.
|
|
295
|
+
|
|
296
|
+
Converts the model's separate content, categories, and notes fields
|
|
297
|
+
into a single dict for the database content column.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
|
|
304
|
+
"""
|
|
305
|
+
content_dict: Dict[str, Any] = {}
|
|
306
|
+
if cultural_knowledge.content is not None:
|
|
307
|
+
content_dict["content"] = cultural_knowledge.content
|
|
308
|
+
if cultural_knowledge.categories is not None:
|
|
309
|
+
content_dict["categories"] = cultural_knowledge.categories
|
|
310
|
+
if cultural_knowledge.notes is not None:
|
|
311
|
+
content_dict["notes"] = cultural_knowledge.notes
|
|
312
|
+
|
|
313
|
+
return content_dict if content_dict else {}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
|
|
317
|
+
"""Deserialize a database row to a CulturalKnowledge object.
|
|
318
|
+
|
|
319
|
+
The database stores content as a dict containing content, categories, and notes.
|
|
320
|
+
This method extracts those fields and converts them back to the model format.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
db_row (Dict[str, Any]): The database row as a dictionary.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
CulturalKnowledge: The cultural knowledge object.
|
|
327
|
+
"""
|
|
328
|
+
# Extract content, categories, and notes from the content field
|
|
329
|
+
content_json = db_row.get("content", {}) or {}
|
|
330
|
+
|
|
331
|
+
return CulturalKnowledge.from_dict(
|
|
332
|
+
{
|
|
333
|
+
"id": db_row.get("id"),
|
|
334
|
+
"name": db_row.get("name"),
|
|
335
|
+
"summary": db_row.get("summary"),
|
|
336
|
+
"content": content_json.get("content"),
|
|
337
|
+
"categories": content_json.get("categories"),
|
|
338
|
+
"notes": content_json.get("notes"),
|
|
339
|
+
"metadata": db_row.get("metadata"),
|
|
340
|
+
"input": db_row.get("input"),
|
|
341
|
+
"created_at": db_row.get("created_at"),
|
|
342
|
+
"updated_at": db_row.get("updated_at"),
|
|
343
|
+
"agent_id": db_row.get("agent_id"),
|
|
344
|
+
"team_id": db_row.get("team_id"),
|
|
345
|
+
}
|
|
346
|
+
)
|
agno/db/schemas/__init__.py
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
from typing_extensions import List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CulturalKnowledge:
|
|
10
|
+
"""Model for Cultural Knowledge
|
|
11
|
+
|
|
12
|
+
Notice: Culture is an experimental feature and is subject to change.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# The id of the cultural knowledge, auto-generated if not provided
|
|
16
|
+
id: Optional[str] = None
|
|
17
|
+
name: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
content: Optional[str] = None
|
|
20
|
+
categories: Optional[List[str]] = None
|
|
21
|
+
notes: Optional[List[str]] = None
|
|
22
|
+
|
|
23
|
+
summary: Optional[str] = None
|
|
24
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
25
|
+
input: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
created_at: Optional[int] = field(default=None)
|
|
28
|
+
updated_at: Optional[int] = field(default=None)
|
|
29
|
+
|
|
30
|
+
agent_id: Optional[str] = None
|
|
31
|
+
team_id: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
if self.name is not None and not self.name.strip():
|
|
35
|
+
raise ValueError("name must be a non-empty string")
|
|
36
|
+
self.created_at = _now_epoch_s() if self.created_at is None else _to_epoch_s(self.created_at)
|
|
37
|
+
self.updated_at = self.created_at if self.updated_at is None else _to_epoch_s(self.updated_at)
|
|
38
|
+
|
|
39
|
+
def bump_updated_at(self) -> None:
|
|
40
|
+
"""Bump updated_at to now (UTC)."""
|
|
41
|
+
self.updated_at = _now_epoch_s()
|
|
42
|
+
|
|
43
|
+
def preview(self) -> Dict[str, Any]:
|
|
44
|
+
"""Return a preview of the cultural knowledge"""
|
|
45
|
+
_preview: Dict[str, Any] = {
|
|
46
|
+
"name": self.name,
|
|
47
|
+
}
|
|
48
|
+
if self.categories is not None:
|
|
49
|
+
_preview["categories"] = self.categories
|
|
50
|
+
if self.summary is not None:
|
|
51
|
+
_preview["summary"] = self.summary[:100] + "..." if len(self.summary) > 100 else self.summary
|
|
52
|
+
if self.content is not None:
|
|
53
|
+
_preview["content"] = self.content[:100] + "..." if len(self.content) > 100 else self.content
|
|
54
|
+
if self.notes is not None:
|
|
55
|
+
_preview["notes"] = [note[:100] + "..." if len(note) > 100 else note for note in self.notes]
|
|
56
|
+
return _preview
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
59
|
+
_dict = {
|
|
60
|
+
"id": self.id,
|
|
61
|
+
"name": self.name,
|
|
62
|
+
"summary": self.summary,
|
|
63
|
+
"content": self.content,
|
|
64
|
+
"categories": self.categories,
|
|
65
|
+
"metadata": self.metadata,
|
|
66
|
+
"notes": self.notes,
|
|
67
|
+
"input": self.input,
|
|
68
|
+
"created_at": (_epoch_to_rfc3339_z(self.created_at) if self.created_at is not None else None),
|
|
69
|
+
"updated_at": (_epoch_to_rfc3339_z(self.updated_at) if self.updated_at is not None else None),
|
|
70
|
+
"agent_id": self.agent_id,
|
|
71
|
+
"team_id": self.team_id,
|
|
72
|
+
}
|
|
73
|
+
return {k: v for k, v in _dict.items() if v is not None}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: Dict[str, Any]) -> "CulturalKnowledge":
|
|
77
|
+
d = dict(data)
|
|
78
|
+
|
|
79
|
+
# Preserve 0 and None explicitly; only process if key exists
|
|
80
|
+
if "created_at" in d and d["created_at"] is not None:
|
|
81
|
+
d["created_at"] = _to_epoch_s(d["created_at"])
|
|
82
|
+
if "updated_at" in d and d["updated_at"] is not None:
|
|
83
|
+
d["updated_at"] = _to_epoch_s(d["updated_at"])
|
|
84
|
+
|
|
85
|
+
return cls(**d)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _now_epoch_s() -> int:
|
|
89
|
+
return int(datetime.now(timezone.utc).timestamp())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _to_epoch_s(value: Union[int, float, str, datetime]) -> int:
|
|
93
|
+
"""Normalize various datetime representations to epoch seconds (UTC)."""
|
|
94
|
+
if isinstance(value, (int, float)):
|
|
95
|
+
# assume value is already in seconds
|
|
96
|
+
return int(value)
|
|
97
|
+
|
|
98
|
+
if isinstance(value, datetime):
|
|
99
|
+
dt = value
|
|
100
|
+
if dt.tzinfo is None:
|
|
101
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
102
|
+
return int(dt.timestamp())
|
|
103
|
+
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
s = value.strip()
|
|
106
|
+
if s.endswith("Z"):
|
|
107
|
+
s = s[:-1] + "+00:00"
|
|
108
|
+
try:
|
|
109
|
+
dt = datetime.fromisoformat(s)
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
raise ValueError(f"Unsupported datetime string: {value!r}") from e
|
|
112
|
+
if dt.tzinfo is None:
|
|
113
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
114
|
+
return int(dt.timestamp())
|
|
115
|
+
|
|
116
|
+
raise TypeError(f"Unsupported datetime value: {type(value)}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _epoch_to_rfc3339_z(ts: Union[int, float]) -> str:
|
|
120
|
+
return datetime.fromtimestamp(float(ts), tz=timezone.utc).isoformat().replace("+00:00", "Z")
|
agno/db/singlestore/schemas.py
CHANGED
|
@@ -92,6 +92,19 @@ METRICS_TABLE_SCHEMA = {
|
|
|
92
92
|
"completed": {"type": Boolean, "nullable": False, "default": False},
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
|
|
96
|
+
"id": {"type": lambda: String(128), "primary_key": True, "nullable": False},
|
|
97
|
+
"name": {"type": lambda: String(255), "nullable": False, "index": True},
|
|
98
|
+
"summary": {"type": Text, "nullable": True},
|
|
99
|
+
"content": {"type": JSON, "nullable": True},
|
|
100
|
+
"metadata": {"type": JSON, "nullable": True},
|
|
101
|
+
"input": {"type": Text, "nullable": True},
|
|
102
|
+
"created_at": {"type": BigInteger, "nullable": True},
|
|
103
|
+
"updated_at": {"type": BigInteger, "nullable": True},
|
|
104
|
+
"agent_id": {"type": lambda: String(128), "nullable": True},
|
|
105
|
+
"team_id": {"type": lambda: String(128), "nullable": True},
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
|
|
96
109
|
def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
97
110
|
"""
|
|
@@ -107,6 +120,7 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
|
107
120
|
"metrics": METRICS_TABLE_SCHEMA,
|
|
108
121
|
"memories": USER_MEMORY_TABLE_SCHEMA,
|
|
109
122
|
"knowledge": KNOWLEDGE_TABLE_SCHEMA,
|
|
123
|
+
"culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
|
|
110
124
|
}
|
|
111
125
|
schema = schemas.get(table_type, {})
|
|
112
126
|
|