agno 2.3.10__py3-none-any.whl → 2.3.11__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/db/base.py +5 -5
- agno/db/dynamo/dynamo.py +2 -2
- agno/db/firestore/firestore.py +2 -2
- agno/db/gcs_json/gcs_json_db.py +2 -2
- agno/db/in_memory/in_memory_db.py +2 -2
- agno/db/json/json_db.py +2 -2
- agno/db/mongo/async_mongo.py +171 -69
- agno/db/mongo/mongo.py +171 -77
- agno/db/mysql/async_mysql.py +93 -69
- agno/db/mysql/mysql.py +93 -68
- agno/db/postgres/async_postgres.py +104 -78
- agno/db/postgres/postgres.py +97 -69
- agno/db/redis/redis.py +2 -2
- agno/db/singlestore/singlestore.py +91 -66
- agno/db/sqlite/async_sqlite.py +101 -78
- agno/db/sqlite/sqlite.py +97 -69
- agno/db/surrealdb/surrealdb.py +2 -2
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/knowledge.py +22 -4
- agno/knowledge/utils.py +52 -7
- agno/models/openai/chat.py +21 -0
- agno/os/routers/knowledge/knowledge.py +21 -9
- agno/tracing/exporter.py +2 -2
- agno/vectordb/base.py +15 -2
- agno/vectordb/pgvector/pgvector.py +7 -7
- {agno-2.3.10.dist-info → agno-2.3.11.dist-info}/METADATA +1 -1
- {agno-2.3.10.dist-info → agno-2.3.11.dist-info}/RECORD +30 -30
- {agno-2.3.10.dist-info → agno-2.3.11.dist-info}/WHEEL +0 -0
- {agno-2.3.10.dist-info → agno-2.3.11.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.10.dist-info → agno-2.3.11.dist-info}/top_level.txt +0 -0
agno/db/sqlite/sqlite.py
CHANGED
|
@@ -31,7 +31,7 @@ from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
|
31
31
|
from agno.utils.string import generate_id
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
|
-
from sqlalchemy import Column, MetaData, String, Table, func, select, text
|
|
34
|
+
from sqlalchemy import Column, MetaData, String, Table, func, select, text
|
|
35
35
|
from sqlalchemy.dialects import sqlite
|
|
36
36
|
from sqlalchemy.engine import Engine, create_engine
|
|
37
37
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
@@ -2145,85 +2145,113 @@ class SqliteDb(BaseDb):
|
|
|
2145
2145
|
# Fallback if spans table doesn't exist
|
|
2146
2146
|
return select(table, literal(0).label("total_spans"), literal(0).label("error_count"))
|
|
2147
2147
|
|
|
2148
|
-
def
|
|
2149
|
-
"""
|
|
2148
|
+
def _get_trace_component_level_expr(self, workflow_id_col, team_id_col, agent_id_col, name_col):
|
|
2149
|
+
"""Build a SQL CASE expression that returns the component level for a trace.
|
|
2150
|
+
|
|
2151
|
+
Component levels (higher = more important):
|
|
2152
|
+
- 3: Workflow root (.run or .arun with workflow_id)
|
|
2153
|
+
- 2: Team root (.run or .arun with team_id)
|
|
2154
|
+
- 1: Agent root (.run or .arun with agent_id)
|
|
2155
|
+
- 0: Child span (not a root)
|
|
2156
|
+
|
|
2157
|
+
Args:
|
|
2158
|
+
workflow_id_col: SQL column/expression for workflow_id
|
|
2159
|
+
team_id_col: SQL column/expression for team_id
|
|
2160
|
+
agent_id_col: SQL column/expression for agent_id
|
|
2161
|
+
name_col: SQL column/expression for name
|
|
2162
|
+
|
|
2163
|
+
Returns:
|
|
2164
|
+
SQLAlchemy CASE expression returning the component level as an integer.
|
|
2165
|
+
"""
|
|
2166
|
+
from sqlalchemy import and_, case, or_
|
|
2167
|
+
|
|
2168
|
+
is_root_name = or_(name_col.contains(".run"), name_col.contains(".arun"))
|
|
2169
|
+
|
|
2170
|
+
return case(
|
|
2171
|
+
# Workflow root (level 3)
|
|
2172
|
+
(and_(workflow_id_col.isnot(None), is_root_name), 3),
|
|
2173
|
+
# Team root (level 2)
|
|
2174
|
+
(and_(team_id_col.isnot(None), is_root_name), 2),
|
|
2175
|
+
# Agent root (level 1)
|
|
2176
|
+
(and_(agent_id_col.isnot(None), is_root_name), 1),
|
|
2177
|
+
# Child span or unknown (level 0)
|
|
2178
|
+
else_=0,
|
|
2179
|
+
)
|
|
2180
|
+
|
|
2181
|
+
def upsert_trace(self, trace: "Trace") -> None:
|
|
2182
|
+
"""Create or update a single trace record in the database.
|
|
2183
|
+
|
|
2184
|
+
Uses INSERT ... ON CONFLICT DO UPDATE (upsert) to handle concurrent inserts
|
|
2185
|
+
atomically and avoid race conditions.
|
|
2150
2186
|
|
|
2151
2187
|
Args:
|
|
2152
2188
|
trace: The Trace object to store (one per trace_id).
|
|
2153
2189
|
"""
|
|
2190
|
+
from sqlalchemy import case
|
|
2191
|
+
|
|
2154
2192
|
try:
|
|
2155
2193
|
table = self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
2156
2194
|
if table is None:
|
|
2157
2195
|
return
|
|
2158
2196
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
if existing:
|
|
2164
|
-
# workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
|
|
2165
|
-
|
|
2166
|
-
def get_component_level(workflow_id, team_id, agent_id, name):
|
|
2167
|
-
# Check if name indicates a root span
|
|
2168
|
-
is_root_name = ".run" in name or ".arun" in name
|
|
2169
|
-
|
|
2170
|
-
if not is_root_name:
|
|
2171
|
-
return 0 # Child span (not a root)
|
|
2172
|
-
elif workflow_id:
|
|
2173
|
-
return 3 # Workflow root
|
|
2174
|
-
elif team_id:
|
|
2175
|
-
return 2 # Team root
|
|
2176
|
-
elif agent_id:
|
|
2177
|
-
return 1 # Agent root
|
|
2178
|
-
else:
|
|
2179
|
-
return 0 # Unknown
|
|
2180
|
-
|
|
2181
|
-
existing_level = get_component_level(
|
|
2182
|
-
existing.workflow_id, existing.team_id, existing.agent_id, existing.name
|
|
2183
|
-
)
|
|
2184
|
-
new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
|
|
2185
|
-
|
|
2186
|
-
# Only update name if new trace is from a higher or equal level
|
|
2187
|
-
should_update_name = new_level > existing_level
|
|
2197
|
+
trace_dict = trace.to_dict()
|
|
2198
|
+
trace_dict.pop("total_spans", None)
|
|
2199
|
+
trace_dict.pop("error_count", None)
|
|
2188
2200
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2201
|
+
with self.Session() as sess, sess.begin():
|
|
2202
|
+
# Use upsert to handle concurrent inserts atomically
|
|
2203
|
+
# On conflict, update fields while preserving existing non-null context values
|
|
2204
|
+
# and keeping the earliest start_time
|
|
2205
|
+
insert_stmt = sqlite.insert(table).values(trace_dict)
|
|
2206
|
+
|
|
2207
|
+
# Build component level expressions for comparing trace priority
|
|
2208
|
+
new_level = self._get_trace_component_level_expr(
|
|
2209
|
+
insert_stmt.excluded.workflow_id,
|
|
2210
|
+
insert_stmt.excluded.team_id,
|
|
2211
|
+
insert_stmt.excluded.agent_id,
|
|
2212
|
+
insert_stmt.excluded.name,
|
|
2213
|
+
)
|
|
2214
|
+
existing_level = self._get_trace_component_level_expr(
|
|
2215
|
+
table.c.workflow_id,
|
|
2216
|
+
table.c.team_id,
|
|
2217
|
+
table.c.agent_id,
|
|
2218
|
+
table.c.name,
|
|
2219
|
+
)
|
|
2204
2220
|
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2221
|
+
# Build the ON CONFLICT DO UPDATE clause
|
|
2222
|
+
# Use MIN for start_time, MAX for end_time to capture full trace duration
|
|
2223
|
+
# SQLite stores timestamps as ISO strings, so string comparison works for ISO format
|
|
2224
|
+
# Duration is calculated as: (MAX(end_time) - MIN(start_time)) in milliseconds
|
|
2225
|
+
# SQLite doesn't have epoch extraction, so we calculate duration using julianday
|
|
2226
|
+
upsert_stmt = insert_stmt.on_conflict_do_update(
|
|
2227
|
+
index_elements=["trace_id"],
|
|
2228
|
+
set_={
|
|
2229
|
+
"end_time": func.max(table.c.end_time, insert_stmt.excluded.end_time),
|
|
2230
|
+
"start_time": func.min(table.c.start_time, insert_stmt.excluded.start_time),
|
|
2231
|
+
# Calculate duration in milliseconds using julianday (SQLite-specific)
|
|
2232
|
+
# julianday returns days, so multiply by 86400000 to get milliseconds
|
|
2233
|
+
"duration_ms": (
|
|
2234
|
+
func.julianday(func.max(table.c.end_time, insert_stmt.excluded.end_time))
|
|
2235
|
+
- func.julianday(func.min(table.c.start_time, insert_stmt.excluded.start_time))
|
|
2236
|
+
)
|
|
2237
|
+
* 86400000,
|
|
2238
|
+
"status": insert_stmt.excluded.status,
|
|
2239
|
+
# Update name only if new trace is from a higher-level component
|
|
2240
|
+
# Priority: workflow (3) > team (2) > agent (1) > child spans (0)
|
|
2241
|
+
"name": case(
|
|
2242
|
+
(new_level > existing_level, insert_stmt.excluded.name),
|
|
2243
|
+
else_=table.c.name,
|
|
2244
|
+
),
|
|
2245
|
+
# Preserve existing non-null context values using COALESCE
|
|
2246
|
+
"run_id": func.coalesce(insert_stmt.excluded.run_id, table.c.run_id),
|
|
2247
|
+
"session_id": func.coalesce(insert_stmt.excluded.session_id, table.c.session_id),
|
|
2248
|
+
"user_id": func.coalesce(insert_stmt.excluded.user_id, table.c.user_id),
|
|
2249
|
+
"agent_id": func.coalesce(insert_stmt.excluded.agent_id, table.c.agent_id),
|
|
2250
|
+
"team_id": func.coalesce(insert_stmt.excluded.team_id, table.c.team_id),
|
|
2251
|
+
"workflow_id": func.coalesce(insert_stmt.excluded.workflow_id, table.c.workflow_id),
|
|
2252
|
+
},
|
|
2253
|
+
)
|
|
2254
|
+
sess.execute(upsert_stmt)
|
|
2227
2255
|
|
|
2228
2256
|
except Exception as e:
|
|
2229
2257
|
log_error(f"Error creating trace: {e}")
|
agno/db/surrealdb/surrealdb.py
CHANGED
|
@@ -1390,8 +1390,8 @@ class SurrealDb(BaseDb):
|
|
|
1390
1390
|
return deserialize_eval_run_record(raw)
|
|
1391
1391
|
|
|
1392
1392
|
# --- Traces ---
|
|
1393
|
-
def
|
|
1394
|
-
"""Create a single trace record in the database.
|
|
1393
|
+
def upsert_trace(self, trace: "Trace") -> None:
|
|
1394
|
+
"""Create or update a single trace record in the database.
|
|
1395
1395
|
|
|
1396
1396
|
Args:
|
|
1397
1397
|
trace: The Trace object to store (one per trace_id).
|
agno/knowledge/chunking/fixed.py
CHANGED
|
@@ -53,5 +53,8 @@ class FixedSizeChunking(ChunkingStrategy):
|
|
|
53
53
|
)
|
|
54
54
|
)
|
|
55
55
|
chunk_number += 1
|
|
56
|
-
start
|
|
56
|
+
# Ensure start always advances by at least 1 to prevent infinite loops
|
|
57
|
+
# when overlap is large relative to chunk_size
|
|
58
|
+
new_start = max(start + 1, end - self.overlap)
|
|
59
|
+
start = new_start
|
|
57
60
|
return chunked_documents
|
agno/knowledge/knowledge.py
CHANGED
|
@@ -1960,8 +1960,8 @@ class Knowledge:
|
|
|
1960
1960
|
content_row.updated_at = int(time.time())
|
|
1961
1961
|
self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
|
|
1962
1962
|
|
|
1963
|
-
if self.vector_db
|
|
1964
|
-
self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata)
|
|
1963
|
+
if self.vector_db:
|
|
1964
|
+
self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata or {})
|
|
1965
1965
|
|
|
1966
1966
|
return content_row.to_dict()
|
|
1967
1967
|
|
|
@@ -2006,8 +2006,8 @@ class Knowledge:
|
|
|
2006
2006
|
else:
|
|
2007
2007
|
self.contents_db.upsert_knowledge_content(knowledge_row=content_row)
|
|
2008
2008
|
|
|
2009
|
-
if self.vector_db
|
|
2010
|
-
self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata)
|
|
2009
|
+
if self.vector_db:
|
|
2010
|
+
self.vector_db.update_metadata(content_id=content.id, metadata=content.metadata or {})
|
|
2011
2011
|
|
|
2012
2012
|
return content_row.to_dict()
|
|
2013
2013
|
|
|
@@ -2783,6 +2783,24 @@ class Knowledge:
|
|
|
2783
2783
|
"""Get all currently loaded readers (only returns readers that have been used)."""
|
|
2784
2784
|
if self.readers is None:
|
|
2785
2785
|
self.readers = {}
|
|
2786
|
+
elif not isinstance(self.readers, dict):
|
|
2787
|
+
# Defensive check: if readers is not a dict (e.g., was set to a list), convert it
|
|
2788
|
+
if isinstance(self.readers, list):
|
|
2789
|
+
readers_dict: Dict[str, Reader] = {}
|
|
2790
|
+
for reader in self.readers:
|
|
2791
|
+
if isinstance(reader, Reader):
|
|
2792
|
+
reader_key = self._generate_reader_key(reader)
|
|
2793
|
+
# Handle potential duplicate keys by appending index if needed
|
|
2794
|
+
original_key = reader_key
|
|
2795
|
+
counter = 1
|
|
2796
|
+
while reader_key in readers_dict:
|
|
2797
|
+
reader_key = f"{original_key}_{counter}"
|
|
2798
|
+
counter += 1
|
|
2799
|
+
readers_dict[reader_key] = reader
|
|
2800
|
+
self.readers = readers_dict
|
|
2801
|
+
else:
|
|
2802
|
+
# For any other unexpected type, reset to empty dict
|
|
2803
|
+
self.readers = {}
|
|
2786
2804
|
|
|
2787
2805
|
return self.readers
|
|
2788
2806
|
|
agno/knowledge/utils.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
|
+
from agno.knowledge.reader.base import Reader
|
|
3
4
|
from agno.knowledge.reader.reader_factory import ReaderFactory
|
|
4
5
|
from agno.knowledge.types import ContentType
|
|
5
6
|
from agno.utils.log import log_debug
|
|
@@ -75,8 +76,33 @@ def get_reader_info(reader_key: str) -> Dict:
|
|
|
75
76
|
raise ValueError(f"Unknown reader: {reader_key}. Error: {str(e)}")
|
|
76
77
|
|
|
77
78
|
|
|
78
|
-
def
|
|
79
|
-
"""Get information about
|
|
79
|
+
def get_reader_info_from_instance(reader: Reader, reader_id: str) -> Dict:
|
|
80
|
+
"""Get information about a reader instance."""
|
|
81
|
+
try:
|
|
82
|
+
reader_class = reader.__class__
|
|
83
|
+
supported_strategies = reader_class.get_supported_chunking_strategies()
|
|
84
|
+
supported_content_types = reader_class.get_supported_content_types()
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"id": reader_id,
|
|
88
|
+
"name": getattr(reader, "name", reader_class.__name__),
|
|
89
|
+
"description": getattr(reader, "description", f"Custom {reader_class.__name__}"),
|
|
90
|
+
"chunking_strategies": [strategy.value for strategy in supported_strategies],
|
|
91
|
+
"content_types": [ct.value for ct in supported_content_types],
|
|
92
|
+
}
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ValueError(f"Failed to get info for reader '{reader_id}': {str(e)}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_all_readers_info(knowledge_instance: Optional[Any] = None) -> List[Dict]:
|
|
98
|
+
"""Get information about all available readers, including custom readers from a Knowledge instance.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
knowledge_instance: Optional Knowledge instance to include custom readers from.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of reader info dictionaries.
|
|
105
|
+
"""
|
|
80
106
|
readers_info = []
|
|
81
107
|
keys = ReaderFactory.get_all_reader_keys()
|
|
82
108
|
for key in keys:
|
|
@@ -88,18 +114,35 @@ def get_all_readers_info() -> List[Dict]:
|
|
|
88
114
|
# Log the error but don't fail the entire request
|
|
89
115
|
log_debug(f"Skipping reader '{key}': {e}")
|
|
90
116
|
continue
|
|
117
|
+
|
|
118
|
+
# Add custom readers from knowledge instance if provided
|
|
119
|
+
if knowledge_instance is not None:
|
|
120
|
+
custom_readers = knowledge_instance.get_readers()
|
|
121
|
+
if isinstance(custom_readers, dict):
|
|
122
|
+
for reader_id, reader in custom_readers.items():
|
|
123
|
+
try:
|
|
124
|
+
reader_info = get_reader_info_from_instance(reader, reader_id)
|
|
125
|
+
# Only add if not already present (custom readers take precedence)
|
|
126
|
+
if not any(r["id"] == reader_id for r in readers_info):
|
|
127
|
+
readers_info.append(reader_info)
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
log_debug(f"Skipping custom reader '{reader_id}': {e}")
|
|
130
|
+
continue
|
|
131
|
+
|
|
91
132
|
return readers_info
|
|
92
133
|
|
|
93
134
|
|
|
94
|
-
def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
|
|
135
|
+
def get_content_types_to_readers_mapping(knowledge_instance: Optional[Any] = None) -> Dict[str, List[str]]:
|
|
95
136
|
"""Get mapping of content types to list of reader IDs that support them.
|
|
96
137
|
|
|
138
|
+
Args:
|
|
139
|
+
knowledge_instance: Optional Knowledge instance to include custom readers from.
|
|
140
|
+
|
|
97
141
|
Returns:
|
|
98
142
|
Dictionary mapping content type strings (ContentType enum values) to list of reader IDs.
|
|
99
143
|
"""
|
|
100
144
|
content_type_mapping: Dict[str, List[str]] = {}
|
|
101
|
-
readers_info = get_all_readers_info()
|
|
102
|
-
|
|
145
|
+
readers_info = get_all_readers_info(knowledge_instance)
|
|
103
146
|
for reader_info in readers_info:
|
|
104
147
|
reader_id = reader_info["id"]
|
|
105
148
|
content_types = reader_info.get("content_types", [])
|
|
@@ -107,7 +150,9 @@ def get_content_types_to_readers_mapping() -> Dict[str, List[str]]:
|
|
|
107
150
|
for content_type in content_types:
|
|
108
151
|
if content_type not in content_type_mapping:
|
|
109
152
|
content_type_mapping[content_type] = []
|
|
110
|
-
|
|
153
|
+
# Avoid duplicates
|
|
154
|
+
if reader_id not in content_type_mapping[content_type]:
|
|
155
|
+
content_type_mapping[content_type].append(reader_id)
|
|
111
156
|
|
|
112
157
|
return content_type_mapping
|
|
113
158
|
|
agno/models/openai/chat.py
CHANGED
|
@@ -820,6 +820,16 @@ class OpenAIChat(Model):
|
|
|
820
820
|
if response.usage is not None:
|
|
821
821
|
model_response.response_usage = self._get_metrics(response.usage)
|
|
822
822
|
|
|
823
|
+
if model_response.provider_data is None:
|
|
824
|
+
model_response.provider_data = {}
|
|
825
|
+
|
|
826
|
+
if response.id:
|
|
827
|
+
model_response.provider_data["id"] = response.id
|
|
828
|
+
if response.system_fingerprint:
|
|
829
|
+
model_response.provider_data["system_fingerprint"] = response.system_fingerprint
|
|
830
|
+
if response.model_extra:
|
|
831
|
+
model_response.provider_data["model_extra"] = response.model_extra
|
|
832
|
+
|
|
823
833
|
return model_response
|
|
824
834
|
|
|
825
835
|
def _parse_provider_response_delta(self, response_delta: ChatCompletionChunk) -> ModelResponse:
|
|
@@ -842,6 +852,17 @@ class OpenAIChat(Model):
|
|
|
842
852
|
if choice_delta.content is not None:
|
|
843
853
|
model_response.content = choice_delta.content
|
|
844
854
|
|
|
855
|
+
# We only want to handle these if content is present
|
|
856
|
+
if model_response.provider_data is None:
|
|
857
|
+
model_response.provider_data = {}
|
|
858
|
+
|
|
859
|
+
if response_delta.id:
|
|
860
|
+
model_response.provider_data["id"] = response_delta.id
|
|
861
|
+
if response_delta.system_fingerprint:
|
|
862
|
+
model_response.provider_data["system_fingerprint"] = response_delta.system_fingerprint
|
|
863
|
+
if response_delta.model_extra:
|
|
864
|
+
model_response.provider_data["model_extra"] = response_delta.model_extra
|
|
865
|
+
|
|
845
866
|
# Add tool calls
|
|
846
867
|
if choice_delta.tool_calls is not None:
|
|
847
868
|
model_response.tool_calls = choice_delta.tool_calls # type: ignore
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import math
|
|
4
|
-
from typing import Dict, List, Optional
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
6
|
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Path, Query, UploadFile
|
|
7
7
|
|
|
@@ -874,8 +874,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
874
874
|
) -> ConfigResponseSchema:
|
|
875
875
|
knowledge = get_knowledge_instance_by_db_id(knowledge_instances, db_id)
|
|
876
876
|
|
|
877
|
-
# Get factory readers info
|
|
878
|
-
readers_info = get_all_readers_info()
|
|
877
|
+
# Get factory readers info (including custom readers from this knowledge instance)
|
|
878
|
+
readers_info = get_all_readers_info(knowledge)
|
|
879
879
|
reader_schemas = {}
|
|
880
880
|
# Add factory readers
|
|
881
881
|
for reader_info in readers_info:
|
|
@@ -887,7 +887,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
887
887
|
)
|
|
888
888
|
|
|
889
889
|
# Add custom readers from knowledge.readers
|
|
890
|
-
|
|
890
|
+
readers_result: Any = knowledge.get_readers() or {}
|
|
891
|
+
print(f"readers_result: {readers_result}")
|
|
892
|
+
# Ensure readers_dict is a dictionary (defensive check)
|
|
893
|
+
if not isinstance(readers_result, dict):
|
|
894
|
+
readers_dict: Dict[str, Reader] = {}
|
|
895
|
+
else:
|
|
896
|
+
readers_dict = readers_result
|
|
891
897
|
if readers_dict:
|
|
892
898
|
for reader_id, reader in readers_dict.items():
|
|
893
899
|
# Get chunking strategies from the reader
|
|
@@ -907,8 +913,8 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Knowledge]) -> AP
|
|
|
907
913
|
chunkers=chunking_strategies,
|
|
908
914
|
)
|
|
909
915
|
|
|
910
|
-
# Get content types to readers mapping
|
|
911
|
-
types_of_readers = get_content_types_to_readers_mapping()
|
|
916
|
+
# Get content types to readers mapping (including custom readers from this knowledge instance)
|
|
917
|
+
types_of_readers = get_content_types_to_readers_mapping(knowledge)
|
|
912
918
|
chunkers_list = get_all_chunkers_info()
|
|
913
919
|
|
|
914
920
|
# Convert chunkers list to dictionary format expected by schema
|
|
@@ -961,20 +967,26 @@ async def process_content(
|
|
|
961
967
|
try:
|
|
962
968
|
if reader_id:
|
|
963
969
|
reader = None
|
|
964
|
-
|
|
965
|
-
|
|
970
|
+
# Use get_readers() to ensure we get a dict (handles list conversion)
|
|
971
|
+
custom_readers = knowledge.get_readers()
|
|
972
|
+
if custom_readers and reader_id in custom_readers:
|
|
973
|
+
reader = custom_readers[reader_id]
|
|
974
|
+
log_debug(f"Found custom reader: {reader.__class__.__name__}")
|
|
966
975
|
else:
|
|
976
|
+
# Try to resolve from factory readers
|
|
967
977
|
key = reader_id.lower().strip().replace("-", "_").replace(" ", "_")
|
|
968
978
|
candidates = [key] + ([key[:-6]] if key.endswith("reader") else [])
|
|
969
979
|
for cand in candidates:
|
|
970
980
|
try:
|
|
971
981
|
reader = ReaderFactory.create_reader(cand)
|
|
972
|
-
log_debug(f"Resolved reader: {reader.__class__.__name__}")
|
|
982
|
+
log_debug(f"Resolved reader from factory: {reader.__class__.__name__}")
|
|
973
983
|
break
|
|
974
984
|
except Exception:
|
|
975
985
|
continue
|
|
976
986
|
if reader:
|
|
977
987
|
content.reader = reader
|
|
988
|
+
else:
|
|
989
|
+
log_debug(f"Could not resolve reader with id: {reader_id}")
|
|
978
990
|
if chunker and content.reader:
|
|
979
991
|
# Set the chunker name on the reader - let the reader handle it internally
|
|
980
992
|
content.reader.set_chunking_strategy_from_string(chunker, chunk_size=chunk_size, overlap=chunk_overlap)
|
agno/tracing/exporter.py
CHANGED
|
@@ -90,7 +90,7 @@ class DatabaseSpanExporter(SpanExporter):
|
|
|
90
90
|
# Create trace record (aggregate of all spans)
|
|
91
91
|
trace = create_trace_from_spans(spans)
|
|
92
92
|
if trace:
|
|
93
|
-
self.db.
|
|
93
|
+
self.db.upsert_trace(trace)
|
|
94
94
|
|
|
95
95
|
# Create span records
|
|
96
96
|
self.db.create_spans(spans)
|
|
@@ -124,7 +124,7 @@ class DatabaseSpanExporter(SpanExporter):
|
|
|
124
124
|
# Create trace record (aggregate of all spans)
|
|
125
125
|
trace = create_trace_from_spans(spans)
|
|
126
126
|
if trace:
|
|
127
|
-
create_trace_result = self.db.
|
|
127
|
+
create_trace_result = self.db.upsert_trace(trace)
|
|
128
128
|
if create_trace_result is not None:
|
|
129
129
|
await create_trace_result
|
|
130
130
|
|
agno/vectordb/base.py
CHANGED
|
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
|
|
|
2
2
|
from typing import Any, Dict, List, Optional
|
|
3
3
|
|
|
4
4
|
from agno.knowledge.document import Document
|
|
5
|
+
from agno.utils.log import log_warning
|
|
5
6
|
from agno.utils.string import generate_id
|
|
6
7
|
|
|
7
8
|
|
|
@@ -114,9 +115,21 @@ class VectorDb(ABC):
|
|
|
114
115
|
def delete_by_metadata(self, metadata: Dict[str, Any]) -> bool:
|
|
115
116
|
raise NotImplementedError
|
|
116
117
|
|
|
117
|
-
@abstractmethod
|
|
118
118
|
def update_metadata(self, content_id: str, metadata: Dict[str, Any]) -> None:
|
|
119
|
-
|
|
119
|
+
"""
|
|
120
|
+
Update the metadata for documents with the given content_id.
|
|
121
|
+
|
|
122
|
+
Default implementation logs a warning. Subclasses should override this method
|
|
123
|
+
to provide their specific implementation.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content_id (str): The content ID to update
|
|
127
|
+
metadata (Dict[str, Any]): The metadata to update
|
|
128
|
+
"""
|
|
129
|
+
log_warning(
|
|
130
|
+
f"{self.__class__.__name__}.update_metadata() is not implemented. "
|
|
131
|
+
f"Metadata update for content_id '{content_id}' was skipped."
|
|
132
|
+
)
|
|
120
133
|
|
|
121
134
|
@abstractmethod
|
|
122
135
|
def delete_by_content_id(self, content_id: str) -> bool:
|
|
@@ -695,25 +695,24 @@ class PgVector(VectorDb):
|
|
|
695
695
|
Update the metadata for a document.
|
|
696
696
|
|
|
697
697
|
Args:
|
|
698
|
-
|
|
698
|
+
content_id (str): The ID of the document.
|
|
699
699
|
metadata (Dict[str, Any]): The metadata to update.
|
|
700
700
|
"""
|
|
701
|
+
print("metadata is: ", metadata)
|
|
701
702
|
try:
|
|
702
703
|
with self.Session() as sess:
|
|
703
|
-
# Merge JSONB
|
|
704
|
+
# Merge JSONB for metadata, but replace filters entirely (absolute value)
|
|
704
705
|
stmt = (
|
|
705
706
|
update(self.table)
|
|
706
707
|
.where(self.table.c.content_id == content_id)
|
|
707
708
|
.values(
|
|
708
709
|
meta_data=func.coalesce(self.table.c.meta_data, text("'{}'::jsonb")).op("||")(
|
|
709
|
-
bindparam("md",
|
|
710
|
-
),
|
|
711
|
-
filters=func.coalesce(self.table.c.filters, text("'{}'::jsonb")).op("||")(
|
|
712
|
-
bindparam("ft", metadata, type_=postgresql.JSONB)
|
|
710
|
+
bindparam("md", type_=postgresql.JSONB)
|
|
713
711
|
),
|
|
712
|
+
filters=bindparam("ft", type_=postgresql.JSONB),
|
|
714
713
|
)
|
|
715
714
|
)
|
|
716
|
-
sess.execute(stmt)
|
|
715
|
+
sess.execute(stmt, {"md": metadata, "ft": metadata})
|
|
717
716
|
sess.commit()
|
|
718
717
|
except Exception as e:
|
|
719
718
|
log_error(f"Error updating metadata for document {content_id}: {e}")
|
|
@@ -1111,6 +1110,7 @@ class PgVector(VectorDb):
|
|
|
1111
1110
|
search_results = self.reranker.rerank(query=query, documents=search_results)
|
|
1112
1111
|
|
|
1113
1112
|
log_info(f"Found {len(search_results)} documents")
|
|
1113
|
+
|
|
1114
1114
|
return search_results
|
|
1115
1115
|
except Exception as e:
|
|
1116
1116
|
log_error(f"Error during hybrid search: {e}")
|