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.
- agno/agent/agent.py +646 -133
- 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/knowledge.py +4 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +0 -2
- agno/memory/manager.py +28 -11
- agno/models/message.py +4 -0
- agno/os/app.py +28 -6
- agno/team/team.py +9 -9
- agno/tools/gmail.py +59 -14
- agno/tools/googlecalendar.py +13 -20
- agno/workflow/condition.py +31 -9
- agno/workflow/router.py +31 -9
- {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/METADATA +1 -1
- {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/RECORD +57 -54
- {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/WHEEL +0 -0
- {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.8.dist-info → agno-2.1.10.dist-info}/top_level.txt +0 -0
agno/db/surrealdb/surrealdb.py
CHANGED
|
@@ -7,6 +7,7 @@ from agno.db.postgres.utils import (
|
|
|
7
7
|
get_dates_to_calculate_metrics_for,
|
|
8
8
|
)
|
|
9
9
|
from agno.db.schemas import UserMemory
|
|
10
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
10
11
|
from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
|
|
11
12
|
from agno.db.schemas.knowledge import KnowledgeRow
|
|
12
13
|
from agno.db.surrealdb import utils
|
|
@@ -19,6 +20,7 @@ from agno.db.surrealdb.metrics import (
|
|
|
19
20
|
)
|
|
20
21
|
from agno.db.surrealdb.models import (
|
|
21
22
|
TableType,
|
|
23
|
+
deserialize_cultural_knowledge,
|
|
22
24
|
deserialize_eval_run_record,
|
|
23
25
|
deserialize_knowledge_row,
|
|
24
26
|
deserialize_session,
|
|
@@ -30,6 +32,7 @@ from agno.db.surrealdb.models import (
|
|
|
30
32
|
desurrealize_user_memory,
|
|
31
33
|
get_schema,
|
|
32
34
|
get_session_type,
|
|
35
|
+
serialize_cultural_knowledge,
|
|
33
36
|
serialize_eval_run_record,
|
|
34
37
|
serialize_knowledge_row,
|
|
35
38
|
serialize_session,
|
|
@@ -60,6 +63,7 @@ class SurrealDb(BaseDb):
|
|
|
60
63
|
metrics_table: Optional[str] = None,
|
|
61
64
|
eval_table: Optional[str] = None,
|
|
62
65
|
knowledge_table: Optional[str] = None,
|
|
66
|
+
culture_table: Optional[str] = None,
|
|
63
67
|
id: Optional[str] = None,
|
|
64
68
|
):
|
|
65
69
|
"""
|
|
@@ -80,6 +84,7 @@ class SurrealDb(BaseDb):
|
|
|
80
84
|
metrics_table=metrics_table,
|
|
81
85
|
eval_table=eval_table,
|
|
82
86
|
knowledge_table=knowledge_table,
|
|
87
|
+
culture_table=culture_table,
|
|
83
88
|
)
|
|
84
89
|
self._client = client
|
|
85
90
|
self._db_url = db_url
|
|
@@ -101,6 +106,7 @@ class SurrealDb(BaseDb):
|
|
|
101
106
|
def table_names(self) -> dict[TableType, str]:
|
|
102
107
|
return {
|
|
103
108
|
"agents": self._agents_table_name,
|
|
109
|
+
"culture": self.culture_table_name,
|
|
104
110
|
"evals": self.eval_table_name,
|
|
105
111
|
"knowledge": self.knowledge_table_name,
|
|
106
112
|
"memories": self.memory_table_name,
|
|
@@ -127,6 +133,8 @@ class SurrealDb(BaseDb):
|
|
|
127
133
|
table_name = self.memory_table_name
|
|
128
134
|
elif table_type == "knowledge":
|
|
129
135
|
table_name = self.knowledge_table_name
|
|
136
|
+
elif table_type == "culture":
|
|
137
|
+
table_name = self.culture_table_name
|
|
130
138
|
elif table_type == "users":
|
|
131
139
|
table_name = self._users_table_name
|
|
132
140
|
elif table_type == "agents":
|
|
@@ -463,6 +471,152 @@ class SurrealDb(BaseDb):
|
|
|
463
471
|
table = self._get_table("memories")
|
|
464
472
|
_ = self.client.delete(table)
|
|
465
473
|
|
|
474
|
+
# -- Cultural Knowledge methods --
|
|
475
|
+
def clear_cultural_knowledge(self) -> None:
|
|
476
|
+
"""Delete all cultural knowledge from the database.
|
|
477
|
+
|
|
478
|
+
Raises:
|
|
479
|
+
Exception: If an error occurs during deletion.
|
|
480
|
+
"""
|
|
481
|
+
table = self._get_table("culture")
|
|
482
|
+
_ = self.client.delete(table)
|
|
483
|
+
|
|
484
|
+
def delete_cultural_knowledge(self, id: str) -> None:
|
|
485
|
+
"""Delete cultural knowledge by ID.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
id (str): The ID of the cultural knowledge to delete.
|
|
489
|
+
|
|
490
|
+
Raises:
|
|
491
|
+
Exception: If an error occurs during deletion.
|
|
492
|
+
"""
|
|
493
|
+
table = self._get_table("culture")
|
|
494
|
+
rec_id = RecordID(table, id)
|
|
495
|
+
self.client.delete(rec_id)
|
|
496
|
+
|
|
497
|
+
def get_cultural_knowledge(
|
|
498
|
+
self, id: str, deserialize: Optional[bool] = True
|
|
499
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
500
|
+
"""Get cultural knowledge by ID.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
id (str): The ID of the cultural knowledge to retrieve.
|
|
504
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
|
|
508
|
+
|
|
509
|
+
Raises:
|
|
510
|
+
Exception: If an error occurs during retrieval.
|
|
511
|
+
"""
|
|
512
|
+
table = self._get_table("culture")
|
|
513
|
+
rec_id = RecordID(table, id)
|
|
514
|
+
result = self.client.select(rec_id)
|
|
515
|
+
|
|
516
|
+
if result is None:
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
if not deserialize:
|
|
520
|
+
return result # type: ignore
|
|
521
|
+
|
|
522
|
+
return deserialize_cultural_knowledge(result) # type: ignore
|
|
523
|
+
|
|
524
|
+
def get_all_cultural_knowledge(
|
|
525
|
+
self,
|
|
526
|
+
agent_id: Optional[str] = None,
|
|
527
|
+
team_id: Optional[str] = None,
|
|
528
|
+
name: Optional[str] = None,
|
|
529
|
+
limit: Optional[int] = None,
|
|
530
|
+
page: Optional[int] = None,
|
|
531
|
+
sort_by: Optional[str] = None,
|
|
532
|
+
sort_order: Optional[str] = None,
|
|
533
|
+
deserialize: Optional[bool] = True,
|
|
534
|
+
) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
535
|
+
"""Get all cultural knowledge with filtering and pagination.
|
|
536
|
+
|
|
537
|
+
Args:
|
|
538
|
+
agent_id (Optional[str]): Filter by agent ID.
|
|
539
|
+
team_id (Optional[str]): Filter by team ID.
|
|
540
|
+
name (Optional[str]): Filter by name (case-insensitive partial match).
|
|
541
|
+
limit (Optional[int]): Maximum number of results to return.
|
|
542
|
+
page (Optional[int]): Page number for pagination.
|
|
543
|
+
sort_by (Optional[str]): Field to sort by.
|
|
544
|
+
sort_order (Optional[str]): Sort order ('asc' or 'desc').
|
|
545
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
549
|
+
- When deserialize=True: List of CulturalKnowledge objects
|
|
550
|
+
- When deserialize=False: Tuple with list of dictionaries and total count
|
|
551
|
+
|
|
552
|
+
Raises:
|
|
553
|
+
Exception: If an error occurs during retrieval.
|
|
554
|
+
"""
|
|
555
|
+
table = self._get_table("culture")
|
|
556
|
+
|
|
557
|
+
# Build where clauses
|
|
558
|
+
where_clauses: List[WhereClause] = []
|
|
559
|
+
if agent_id is not None:
|
|
560
|
+
agent_rec_id = RecordID(self._get_table("agents"), agent_id)
|
|
561
|
+
where_clauses.append(("agent", "=", agent_rec_id)) # type: ignore
|
|
562
|
+
if team_id is not None:
|
|
563
|
+
team_rec_id = RecordID(self._get_table("teams"), team_id)
|
|
564
|
+
where_clauses.append(("team", "=", team_rec_id)) # type: ignore
|
|
565
|
+
if name is not None:
|
|
566
|
+
where_clauses.append(("string::lowercase(name)", "CONTAINS", name.lower())) # type: ignore
|
|
567
|
+
|
|
568
|
+
# Build query for total count
|
|
569
|
+
count_query = COUNT_QUERY.format(
|
|
570
|
+
table=table,
|
|
571
|
+
where=""
|
|
572
|
+
if not where_clauses
|
|
573
|
+
else f"WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}", # type: ignore
|
|
574
|
+
)
|
|
575
|
+
params = {chr(97 + i): w[2] for i, w in enumerate(where_clauses)} # type: ignore
|
|
576
|
+
total_count = self._query_one(count_query, params, int) or 0
|
|
577
|
+
|
|
578
|
+
# Build main query
|
|
579
|
+
order_limit = order_limit_start(sort_by, sort_order, limit, page)
|
|
580
|
+
query = f"SELECT * FROM {table}"
|
|
581
|
+
if where_clauses:
|
|
582
|
+
query += f" WHERE {' AND '.join(f'{w[0]} {w[1]} ${chr(97 + i)}' for i, w in enumerate(where_clauses))}" # type: ignore
|
|
583
|
+
query += order_limit
|
|
584
|
+
|
|
585
|
+
results = self._query(query, params, list) or []
|
|
586
|
+
|
|
587
|
+
if not deserialize:
|
|
588
|
+
return results, total_count # type: ignore
|
|
589
|
+
|
|
590
|
+
return [deserialize_cultural_knowledge(r) for r in results] # type: ignore
|
|
591
|
+
|
|
592
|
+
def upsert_cultural_knowledge(
|
|
593
|
+
self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
|
|
594
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
595
|
+
"""Upsert cultural knowledge in SurrealDB.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
|
|
599
|
+
deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
Exception: If an error occurs during upsert.
|
|
606
|
+
"""
|
|
607
|
+
table = self._get_table("culture", create_table_if_not_found=True)
|
|
608
|
+
serialized = serialize_cultural_knowledge(cultural_knowledge, table)
|
|
609
|
+
|
|
610
|
+
result = self.client.upsert(serialized["id"], serialized)
|
|
611
|
+
|
|
612
|
+
if result is None:
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
if not deserialize:
|
|
616
|
+
return result # type: ignore
|
|
617
|
+
|
|
618
|
+
return deserialize_cultural_knowledge(result) # type: ignore
|
|
619
|
+
|
|
466
620
|
def delete_user_memory(self, memory_id: str, user_id: Optional[str] = None) -> None:
|
|
467
621
|
"""Delete a user memory from the database.
|
|
468
622
|
|
agno/db/surrealdb/utils.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
-
from typing import Any, Optional, Sequence, TypeVar, Union, cast
|
|
2
|
+
from typing import Any, Dict, Optional, Sequence, TypeVar, Union, cast
|
|
3
3
|
|
|
4
4
|
from surrealdb import BlockingHttpSurrealConnection, BlockingWsSurrealConnection, Surreal
|
|
5
5
|
|
|
6
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
6
7
|
from agno.utils.log import logger
|
|
7
8
|
|
|
8
9
|
RecordType = TypeVar("RecordType")
|
|
@@ -85,3 +86,62 @@ def query_one(
|
|
|
85
86
|
raise ValueError(f"Expected single record, got {len(response)} records: {response}")
|
|
86
87
|
else:
|
|
87
88
|
raise ValueError(f"Unexpected response type: {type(response)}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# -- Cultural Knowledge util methods --
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
|
|
95
|
+
"""Serialize a CulturalKnowledge object for database storage.
|
|
96
|
+
|
|
97
|
+
Converts the model's separate content, categories, and notes fields
|
|
98
|
+
into a single dict for the database content field.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict[str, Any]: A dictionary with content, categories, and notes.
|
|
105
|
+
"""
|
|
106
|
+
content_dict: Dict[str, Any] = {}
|
|
107
|
+
if cultural_knowledge.content is not None:
|
|
108
|
+
content_dict["content"] = cultural_knowledge.content
|
|
109
|
+
if cultural_knowledge.categories is not None:
|
|
110
|
+
content_dict["categories"] = cultural_knowledge.categories
|
|
111
|
+
if cultural_knowledge.notes is not None:
|
|
112
|
+
content_dict["notes"] = cultural_knowledge.notes
|
|
113
|
+
|
|
114
|
+
return content_dict if content_dict else {}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
|
|
118
|
+
"""Deserialize a database row to a CulturalKnowledge object.
|
|
119
|
+
|
|
120
|
+
The database stores content as a dict containing content, categories, and notes.
|
|
121
|
+
This method extracts those fields and converts them back to the model format.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
db_row (Dict[str, Any]): The database row as a dictionary.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
CulturalKnowledge: The cultural knowledge object.
|
|
128
|
+
"""
|
|
129
|
+
# Extract content, categories, and notes from the content field
|
|
130
|
+
content_json = db_row.get("content", {}) or {}
|
|
131
|
+
|
|
132
|
+
return CulturalKnowledge.from_dict(
|
|
133
|
+
{
|
|
134
|
+
"id": db_row.get("id"),
|
|
135
|
+
"name": db_row.get("name"),
|
|
136
|
+
"summary": db_row.get("summary"),
|
|
137
|
+
"content": content_json.get("content"),
|
|
138
|
+
"categories": content_json.get("categories"),
|
|
139
|
+
"notes": content_json.get("notes"),
|
|
140
|
+
"metadata": db_row.get("metadata"),
|
|
141
|
+
"input": db_row.get("input"),
|
|
142
|
+
"created_at": db_row.get("created_at"),
|
|
143
|
+
"updated_at": db_row.get("updated_at"),
|
|
144
|
+
"agent_id": db_row.get("agent_id"),
|
|
145
|
+
"team_id": db_row.get("team_id"),
|
|
146
|
+
}
|
|
147
|
+
)
|
agno/knowledge/knowledge.py
CHANGED
|
@@ -122,6 +122,7 @@ class Knowledge:
|
|
|
122
122
|
exclude=exclude,
|
|
123
123
|
upsert=upsert,
|
|
124
124
|
skip_if_exists=skip_if_exists,
|
|
125
|
+
reader=reader,
|
|
125
126
|
)
|
|
126
127
|
for url in urls:
|
|
127
128
|
await self.add_content_async(
|
|
@@ -133,6 +134,7 @@ class Knowledge:
|
|
|
133
134
|
exclude=exclude,
|
|
134
135
|
upsert=upsert,
|
|
135
136
|
skip_if_exists=skip_if_exists,
|
|
137
|
+
reader=reader,
|
|
136
138
|
)
|
|
137
139
|
for i, text_content in enumerate(text_contents):
|
|
138
140
|
content_name = f"{name}_{i}" if name else f"text_content_{i}"
|
|
@@ -146,6 +148,7 @@ class Knowledge:
|
|
|
146
148
|
exclude=exclude,
|
|
147
149
|
upsert=upsert,
|
|
148
150
|
skip_if_exists=skip_if_exists,
|
|
151
|
+
reader=reader,
|
|
149
152
|
)
|
|
150
153
|
if topics:
|
|
151
154
|
await self.add_content_async(
|
|
@@ -168,6 +171,7 @@ class Knowledge:
|
|
|
168
171
|
remote_content=remote_content,
|
|
169
172
|
upsert=upsert,
|
|
170
173
|
skip_if_exists=skip_if_exists,
|
|
174
|
+
reader=reader,
|
|
171
175
|
)
|
|
172
176
|
|
|
173
177
|
else:
|
|
@@ -33,8 +33,6 @@ class FieldLabeledCSVReader(Reader):
|
|
|
33
33
|
self.format_headers = format_headers
|
|
34
34
|
self.skip_empty_fields = skip_empty_fields
|
|
35
35
|
|
|
36
|
-
logger.info(f"FieldLabeledCSVReader initialized - chunk_title: {chunk_title}, field_names: {self.field_names}")
|
|
37
|
-
|
|
38
36
|
@classmethod
|
|
39
37
|
def get_supported_chunking_strategies(cls) -> List[ChunkingStrategyType]:
|
|
40
38
|
"""Chunking is not supported - each row is already a logical document unit."""
|
agno/memory/manager.py
CHANGED
|
@@ -12,7 +12,13 @@ from agno.db.schemas import UserMemory
|
|
|
12
12
|
from agno.models.base import Model
|
|
13
13
|
from agno.models.message import Message
|
|
14
14
|
from agno.tools.function import Function
|
|
15
|
-
from agno.utils.log import
|
|
15
|
+
from agno.utils.log import (
|
|
16
|
+
log_debug,
|
|
17
|
+
log_error,
|
|
18
|
+
log_warning,
|
|
19
|
+
set_log_level_to_debug,
|
|
20
|
+
set_log_level_to_info,
|
|
21
|
+
)
|
|
16
22
|
from agno.utils.prompts import get_json_output_prompt
|
|
17
23
|
from agno.utils.string import parse_response_model_str
|
|
18
24
|
|
|
@@ -21,7 +27,8 @@ class MemorySearchResponse(BaseModel):
|
|
|
21
27
|
"""Model for Memory Search Response."""
|
|
22
28
|
|
|
23
29
|
memory_ids: List[str] = Field(
|
|
24
|
-
...,
|
|
30
|
+
...,
|
|
31
|
+
description="The IDs of the memories that are most semantically similar to the query.",
|
|
25
32
|
)
|
|
26
33
|
|
|
27
34
|
|
|
@@ -32,11 +39,11 @@ class MemoryManager:
|
|
|
32
39
|
# Model used for memory management
|
|
33
40
|
model: Optional[Model] = None
|
|
34
41
|
|
|
35
|
-
# Provide the system message for the manager as a string. If not provided,
|
|
42
|
+
# Provide the system message for the manager as a string. If not provided, the default system message will be used.
|
|
36
43
|
system_message: Optional[str] = None
|
|
37
|
-
# Provide the memory capture instructions for the manager as a string. If not provided,
|
|
44
|
+
# Provide the memory capture instructions for the manager as a string. If not provided, the default memory capture instructions will be used.
|
|
38
45
|
memory_capture_instructions: Optional[str] = None
|
|
39
|
-
# Additional instructions for the manager. These instructions are appended to the default system
|
|
46
|
+
# Additional instructions for the manager. These instructions are appended to the default system message.
|
|
40
47
|
additional_instructions: Optional[str] = None
|
|
41
48
|
|
|
42
49
|
# Whether memories were created in the last run
|
|
@@ -731,14 +738,16 @@ class MemoryManager:
|
|
|
731
738
|
if self.system_message is not None:
|
|
732
739
|
return Message(role="system", content=self.system_message)
|
|
733
740
|
|
|
734
|
-
memory_capture_instructions = self.memory_capture_instructions or dedent(
|
|
741
|
+
memory_capture_instructions = self.memory_capture_instructions or dedent(
|
|
742
|
+
"""\
|
|
735
743
|
Memories should capture personal information about the user that is relevant to the current conversation, such as:
|
|
736
744
|
- Personal facts: name, age, occupation, location, interests, and preferences
|
|
737
745
|
- Opinions and preferences: what the user likes, dislikes, enjoys, or finds frustrating
|
|
738
746
|
- Significant life events or experiences shared by the user
|
|
739
747
|
- Important context about the user's current situation, challenges, or goals
|
|
740
748
|
- Any other details that offer meaningful insight into the user's personality, perspective, or needs
|
|
741
|
-
"""
|
|
749
|
+
"""
|
|
750
|
+
)
|
|
742
751
|
|
|
743
752
|
# -*- Return a system message for the memory manager
|
|
744
753
|
system_prompt_lines = [
|
|
@@ -852,7 +861,9 @@ class MemoryManager:
|
|
|
852
861
|
|
|
853
862
|
# Generate a response from the Model (includes running function calls)
|
|
854
863
|
response = model_copy.response(
|
|
855
|
-
messages=messages_for_model,
|
|
864
|
+
messages=messages_for_model,
|
|
865
|
+
tools=self._tools_for_model,
|
|
866
|
+
functions=self._functions_for_model,
|
|
856
867
|
)
|
|
857
868
|
|
|
858
869
|
if response.tool_calls is not None and len(response.tool_calls) > 0:
|
|
@@ -928,7 +939,9 @@ class MemoryManager:
|
|
|
928
939
|
|
|
929
940
|
# Generate a response from the Model (includes running function calls)
|
|
930
941
|
response = await model_copy.aresponse(
|
|
931
|
-
messages=messages_for_model,
|
|
942
|
+
messages=messages_for_model,
|
|
943
|
+
tools=self._tools_for_model,
|
|
944
|
+
functions=self._functions_for_model,
|
|
932
945
|
)
|
|
933
946
|
|
|
934
947
|
if response.tool_calls is not None and len(response.tool_calls) > 0:
|
|
@@ -983,7 +996,9 @@ class MemoryManager:
|
|
|
983
996
|
|
|
984
997
|
# Generate a response from the Model (includes running function calls)
|
|
985
998
|
response = model_copy.response(
|
|
986
|
-
messages=messages_for_model,
|
|
999
|
+
messages=messages_for_model,
|
|
1000
|
+
tools=self._tools_for_model,
|
|
1001
|
+
functions=self._functions_for_model,
|
|
987
1002
|
)
|
|
988
1003
|
|
|
989
1004
|
if response.tool_calls is not None and len(response.tool_calls) > 0:
|
|
@@ -1051,7 +1066,9 @@ class MemoryManager:
|
|
|
1051
1066
|
|
|
1052
1067
|
# Generate a response from the Model (includes running function calls)
|
|
1053
1068
|
response = await model_copy.aresponse(
|
|
1054
|
-
messages=messages_for_model,
|
|
1069
|
+
messages=messages_for_model,
|
|
1070
|
+
tools=self._tools_for_model,
|
|
1071
|
+
functions=self._functions_for_model,
|
|
1055
1072
|
)
|
|
1056
1073
|
|
|
1057
1074
|
if response.tool_calls is not None and len(response.tool_calls) > 0:
|
agno/models/message.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from time import time
|
|
3
3
|
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
4
|
+
from uuid import uuid4
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, ConfigDict, Field
|
|
6
7
|
|
|
@@ -51,6 +52,8 @@ class Citations(BaseModel):
|
|
|
51
52
|
class Message(BaseModel):
|
|
52
53
|
"""Message sent to the Model"""
|
|
53
54
|
|
|
55
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
56
|
+
|
|
54
57
|
# The role of the message author.
|
|
55
58
|
# One of system, user, assistant, or tool.
|
|
56
59
|
role: str
|
|
@@ -259,6 +262,7 @@ class Message(BaseModel):
|
|
|
259
262
|
def to_dict(self) -> Dict[str, Any]:
|
|
260
263
|
"""Returns the message as a dictionary."""
|
|
261
264
|
message_dict = {
|
|
265
|
+
"id": self.id,
|
|
262
266
|
"content": self.content,
|
|
263
267
|
"reasoning_content": self.reasoning_content,
|
|
264
268
|
"from_history": self.from_history,
|
agno/os/app.py
CHANGED
|
@@ -13,6 +13,7 @@ from starlette.requests import Request
|
|
|
13
13
|
|
|
14
14
|
from agno.agent.agent import Agent
|
|
15
15
|
from agno.db.base import AsyncBaseDb, BaseDb
|
|
16
|
+
from agno.knowledge.knowledge import Knowledge
|
|
16
17
|
from agno.os.config import (
|
|
17
18
|
AgentOSConfig,
|
|
18
19
|
DatabaseConfig,
|
|
@@ -98,6 +99,7 @@ class AgentOS:
|
|
|
98
99
|
agents: Optional[List[Agent]] = None,
|
|
99
100
|
teams: Optional[List[Team]] = None,
|
|
100
101
|
workflows: Optional[List[Workflow]] = None,
|
|
102
|
+
knowledge: Optional[List[Knowledge]] = None,
|
|
101
103
|
interfaces: Optional[List[BaseInterface]] = None,
|
|
102
104
|
a2a_interface: bool = False,
|
|
103
105
|
config: Optional[Union[str, AgentOSConfig]] = None,
|
|
@@ -122,6 +124,7 @@ class AgentOS:
|
|
|
122
124
|
agents: List of agents to include in the OS
|
|
123
125
|
teams: List of teams to include in the OS
|
|
124
126
|
workflows: List of workflows to include in the OS
|
|
127
|
+
knowledge: List of knowledge bases to include in the OS
|
|
125
128
|
interfaces: List of interfaces to include in the OS
|
|
126
129
|
a2a_interface: Whether to expose the OS agents and teams in an A2A server
|
|
127
130
|
config: Configuration file path or AgentOSConfig instance
|
|
@@ -133,8 +136,8 @@ class AgentOS:
|
|
|
133
136
|
telemetry: Whether to enable telemetry
|
|
134
137
|
|
|
135
138
|
"""
|
|
136
|
-
if not agents and not workflows and not teams:
|
|
137
|
-
raise ValueError("Either agents, teams or
|
|
139
|
+
if not agents and not workflows and not teams and not knowledge:
|
|
140
|
+
raise ValueError("Either agents, teams, workflows or knowledge bases must be provided.")
|
|
138
141
|
|
|
139
142
|
self.config = load_yaml_config(config) if isinstance(config, str) else config
|
|
140
143
|
|
|
@@ -143,7 +146,7 @@ class AgentOS:
|
|
|
143
146
|
self.teams: Optional[List[Team]] = teams
|
|
144
147
|
self.interfaces = interfaces or []
|
|
145
148
|
self.a2a_interface = a2a_interface
|
|
146
|
-
|
|
149
|
+
self.knowledge = knowledge
|
|
147
150
|
self.settings: AgnoAPISettings = settings or AgnoAPISettings()
|
|
148
151
|
|
|
149
152
|
self._app_set = False
|
|
@@ -480,6 +483,10 @@ class AgentOS:
|
|
|
480
483
|
if workflow.db:
|
|
481
484
|
self._register_db_with_validation(dbs, workflow.db)
|
|
482
485
|
|
|
486
|
+
for knowledge_base in self.knowledge or []:
|
|
487
|
+
if knowledge_base.contents_db:
|
|
488
|
+
self._register_db_with_validation(knowledge_dbs, knowledge_base.contents_db)
|
|
489
|
+
|
|
483
490
|
for interface in self.interfaces or []:
|
|
484
491
|
if interface.agent and interface.agent.db:
|
|
485
492
|
self._register_db_with_validation(dbs, interface.agent.db)
|
|
@@ -534,14 +541,29 @@ class AgentOS:
|
|
|
534
541
|
|
|
535
542
|
def _auto_discover_knowledge_instances(self) -> None:
|
|
536
543
|
"""Auto-discover the knowledge instances used by all contextual agents, teams and workflows."""
|
|
537
|
-
|
|
544
|
+
seen_ids = set()
|
|
545
|
+
knowledge_instances: List[Knowledge] = []
|
|
546
|
+
|
|
547
|
+
def _add_knowledge_if_not_duplicate(knowledge: "Knowledge") -> None:
|
|
548
|
+
"""Add knowledge instance if it's not already in the list (by object identity or db_id)."""
|
|
549
|
+
# Use database ID if available, otherwise use object ID as fallback
|
|
550
|
+
if not knowledge.contents_db:
|
|
551
|
+
return
|
|
552
|
+
if knowledge.contents_db.id in seen_ids:
|
|
553
|
+
return
|
|
554
|
+
seen_ids.add(knowledge.contents_db.id)
|
|
555
|
+
knowledge_instances.append(knowledge)
|
|
556
|
+
|
|
538
557
|
for agent in self.agents or []:
|
|
539
558
|
if agent.knowledge:
|
|
540
|
-
|
|
559
|
+
_add_knowledge_if_not_duplicate(agent.knowledge)
|
|
541
560
|
|
|
542
561
|
for team in self.teams or []:
|
|
543
562
|
if team.knowledge:
|
|
544
|
-
|
|
563
|
+
_add_knowledge_if_not_duplicate(team.knowledge)
|
|
564
|
+
|
|
565
|
+
for knowledge_base in self.knowledge or []:
|
|
566
|
+
_add_knowledge_if_not_duplicate(knowledge_base)
|
|
545
567
|
|
|
546
568
|
self.knowledge_instances = knowledge_instances
|
|
547
569
|
|
agno/team/team.py
CHANGED
|
@@ -1674,11 +1674,11 @@ class Team:
|
|
|
1674
1674
|
)
|
|
1675
1675
|
self.model = cast(Model, self.model)
|
|
1676
1676
|
|
|
1677
|
-
if metadata is not None:
|
|
1678
|
-
if
|
|
1679
|
-
merge_dictionaries(metadata, self.metadata)
|
|
1680
|
-
else:
|
|
1677
|
+
if self.metadata is not None:
|
|
1678
|
+
if metadata is None:
|
|
1681
1679
|
metadata = self.metadata
|
|
1680
|
+
else:
|
|
1681
|
+
merge_dictionaries(metadata, self.metadata)
|
|
1682
1682
|
|
|
1683
1683
|
# Create a new run_response for this attempt
|
|
1684
1684
|
run_response = TeamRunOutput(
|
|
@@ -2201,7 +2201,7 @@ class Team:
|
|
|
2201
2201
|
|
|
2202
2202
|
# Execute post-hooks after output is generated but before response is returned
|
|
2203
2203
|
if self.post_hooks is not None:
|
|
2204
|
-
self.
|
|
2204
|
+
await self._aexecute_post_hooks(
|
|
2205
2205
|
hooks=self.post_hooks, # type: ignore
|
|
2206
2206
|
run_output=run_response,
|
|
2207
2207
|
session_state=session_state,
|
|
@@ -2428,11 +2428,11 @@ class Team:
|
|
|
2428
2428
|
|
|
2429
2429
|
self.model = cast(Model, self.model)
|
|
2430
2430
|
|
|
2431
|
-
if metadata is not None:
|
|
2432
|
-
if
|
|
2433
|
-
merge_dictionaries(metadata, self.metadata)
|
|
2434
|
-
else:
|
|
2431
|
+
if self.metadata is not None:
|
|
2432
|
+
if metadata is None:
|
|
2435
2433
|
metadata = self.metadata
|
|
2434
|
+
else:
|
|
2435
|
+
merge_dictionaries(metadata, self.metadata)
|
|
2436
2436
|
|
|
2437
2437
|
# Get knowledge filters
|
|
2438
2438
|
effective_filters = knowledge_filters
|
agno/tools/gmail.py
CHANGED
|
@@ -124,20 +124,6 @@ class GmailTools(Toolkit):
|
|
|
124
124
|
self.scopes = scopes or self.DEFAULT_SCOPES
|
|
125
125
|
self.port = port
|
|
126
126
|
|
|
127
|
-
""" tools functions:
|
|
128
|
-
enable_get_latest_emails (bool): Enable getting latest emails.
|
|
129
|
-
enable_get_emails_from_user (bool): Enable getting emails from specific user.
|
|
130
|
-
enable_get_unread_emails (bool): Enable getting unread emails.
|
|
131
|
-
enable_get_starred_emails (bool): Enable getting starred emails.
|
|
132
|
-
enable_get_emails_by_context (bool): Enable getting emails by context.
|
|
133
|
-
enable_get_emails_by_date (bool): Enable getting emails by date.
|
|
134
|
-
enable_get_emails_by_thread (bool): Enable getting emails by thread.
|
|
135
|
-
enable_create_draft_email (bool): Enable creating draft emails.
|
|
136
|
-
enable_send_email (bool): Enable sending emails.
|
|
137
|
-
enable_send_email_reply (bool): Enable sending email replies.
|
|
138
|
-
all (bool): Enable all tools.
|
|
139
|
-
"""
|
|
140
|
-
|
|
141
127
|
tools: List[Any] = [
|
|
142
128
|
# Reading emails
|
|
143
129
|
self.get_latest_emails,
|
|
@@ -148,6 +134,9 @@ class GmailTools(Toolkit):
|
|
|
148
134
|
self.get_emails_by_date,
|
|
149
135
|
self.get_emails_by_thread,
|
|
150
136
|
self.search_emails,
|
|
137
|
+
# Email management
|
|
138
|
+
self.mark_email_as_read,
|
|
139
|
+
self.mark_email_as_unread,
|
|
151
140
|
# Composing emails
|
|
152
141
|
self.create_draft_email,
|
|
153
142
|
self.send_email,
|
|
@@ -173,12 +162,18 @@ class GmailTools(Toolkit):
|
|
|
173
162
|
"get_emails_by_thread",
|
|
174
163
|
"search_emails",
|
|
175
164
|
]
|
|
165
|
+
modify_operations = ["mark_email_as_read", "mark_email_as_unread"]
|
|
176
166
|
if any(read_operation in self.functions for read_operation in read_operations):
|
|
177
167
|
read_scope = "https://www.googleapis.com/auth/gmail.readonly"
|
|
178
168
|
write_scope = "https://www.googleapis.com/auth/gmail.modify"
|
|
179
169
|
if read_scope not in self.scopes and write_scope not in self.scopes:
|
|
180
170
|
raise ValueError(f"The scope {read_scope} is required for email reading operations")
|
|
181
171
|
|
|
172
|
+
if any(modify_operation in self.functions for modify_operation in modify_operations):
|
|
173
|
+
modify_scope = "https://www.googleapis.com/auth/gmail.modify"
|
|
174
|
+
if modify_scope not in self.scopes:
|
|
175
|
+
raise ValueError(f"The scope {modify_scope} is required for email modification operations")
|
|
176
|
+
|
|
182
177
|
def _auth(self) -> None:
|
|
183
178
|
"""Authenticate with Gmail API"""
|
|
184
179
|
token_file = Path(self.token_path or "token.json")
|
|
@@ -555,6 +550,56 @@ class GmailTools(Toolkit):
|
|
|
555
550
|
except Exception as error:
|
|
556
551
|
return f"Unexpected error retrieving emails with query '{query}': {type(error).__name__}: {error}"
|
|
557
552
|
|
|
553
|
+
@authenticate
|
|
554
|
+
def mark_email_as_read(self, message_id: str) -> str:
|
|
555
|
+
"""
|
|
556
|
+
Mark a specific email as read by removing the 'UNREAD' label.
|
|
557
|
+
This is crucial for long polling scenarios to prevent processing the same email multiple times.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
message_id (str): The ID of the message to mark as read
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
str: Success message or error description
|
|
564
|
+
"""
|
|
565
|
+
try:
|
|
566
|
+
# Remove the UNREAD label to mark the email as read
|
|
567
|
+
modify_request = {"removeLabelIds": ["UNREAD"]}
|
|
568
|
+
|
|
569
|
+
self.service.users().messages().modify(userId="me", id=message_id, body=modify_request).execute() # type: ignore
|
|
570
|
+
|
|
571
|
+
return f"Successfully marked email {message_id} as read. Labels removed: UNREAD"
|
|
572
|
+
|
|
573
|
+
except HttpError as error:
|
|
574
|
+
return f"HTTP Error marking email {message_id} as read: {error}"
|
|
575
|
+
except Exception as error:
|
|
576
|
+
return f"Error marking email {message_id} as read: {type(error).__name__}: {error}"
|
|
577
|
+
|
|
578
|
+
@authenticate
|
|
579
|
+
def mark_email_as_unread(self, message_id: str) -> str:
|
|
580
|
+
"""
|
|
581
|
+
Mark a specific email as unread by adding the 'UNREAD' label.
|
|
582
|
+
This is useful for flagging emails that need attention or re-processing.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
message_id (str): The ID of the message to mark as unread
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
str: Success message or error description
|
|
589
|
+
"""
|
|
590
|
+
try:
|
|
591
|
+
# Add the UNREAD label to mark the email as unread
|
|
592
|
+
modify_request = {"addLabelIds": ["UNREAD"]}
|
|
593
|
+
|
|
594
|
+
self.service.users().messages().modify(userId="me", id=message_id, body=modify_request).execute() # type: ignore
|
|
595
|
+
|
|
596
|
+
return f"Successfully marked email {message_id} as unread. Labels added: UNREAD"
|
|
597
|
+
|
|
598
|
+
except HttpError as error:
|
|
599
|
+
return f"HTTP Error marking email {message_id} as unread: {error}"
|
|
600
|
+
except Exception as error:
|
|
601
|
+
return f"Error marking email {message_id} as unread: {type(error).__name__}: {error}"
|
|
602
|
+
|
|
558
603
|
def _validate_email_params(self, to: str, subject: str, body: str) -> None:
|
|
559
604
|
"""Validate email parameters."""
|
|
560
605
|
if not to:
|