agno 2.3.9__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/agent/agent.py +0 -12
- 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 +102 -79
- agno/db/sqlite/sqlite.py +97 -69
- agno/db/surrealdb/surrealdb.py +2 -2
- agno/eval/accuracy.py +11 -8
- agno/eval/agent_as_judge.py +9 -8
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/embedder/openai.py +1 -1
- agno/knowledge/knowledge.py +22 -4
- agno/knowledge/utils.py +52 -7
- agno/models/base.py +34 -1
- agno/models/google/gemini.py +69 -40
- agno/models/message.py +3 -0
- agno/models/openai/chat.py +21 -0
- agno/os/routers/evals/utils.py +15 -37
- agno/os/routers/knowledge/knowledge.py +21 -9
- agno/team/team.py +14 -8
- agno/tools/function.py +37 -23
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +2 -5
- agno/tracing/exporter.py +2 -2
- agno/vectordb/base.py +15 -2
- agno/vectordb/pgvector/pgvector.py +8 -8
- agno/workflow/parallel.py +2 -0
- {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/METADATA +1 -1
- {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/RECORD +43 -42
- {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/WHEEL +0 -0
- {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/top_level.txt +0 -0
agno/db/sqlite/async_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.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
|
37
37
|
from sqlalchemy.schema import Index, UniqueConstraint
|
|
@@ -1825,7 +1825,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
1825
1825
|
Optional[KnowledgeRow]: The upserted knowledge row, or None if the operation fails.
|
|
1826
1826
|
"""
|
|
1827
1827
|
try:
|
|
1828
|
-
table = await self._get_table(table_type="knowledge")
|
|
1828
|
+
table = await self._get_table(table_type="knowledge", create_table_if_not_found=True)
|
|
1829
1829
|
if table is None:
|
|
1830
1830
|
return None
|
|
1831
1831
|
|
|
@@ -2196,10 +2196,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2196
2196
|
await sess.execute(table.delete())
|
|
2197
2197
|
|
|
2198
2198
|
except Exception as e:
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
log_warning(f"Exception deleting all cultural artifacts: {e}")
|
|
2202
|
-
raise e
|
|
2199
|
+
log_error(f"Exception deleting all cultural artifacts: {e}")
|
|
2203
2200
|
|
|
2204
2201
|
async def delete_cultural_knowledge(self, id: str) -> None:
|
|
2205
2202
|
"""Delete a cultural artifact from the database.
|
|
@@ -2227,7 +2224,6 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2227
2224
|
|
|
2228
2225
|
except Exception as e:
|
|
2229
2226
|
log_error(f"Error deleting cultural artifact: {e}")
|
|
2230
|
-
raise e
|
|
2231
2227
|
|
|
2232
2228
|
async def get_cultural_knowledge(
|
|
2233
2229
|
self, id: str, deserialize: Optional[bool] = True
|
|
@@ -2263,7 +2259,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2263
2259
|
|
|
2264
2260
|
except Exception as e:
|
|
2265
2261
|
log_error(f"Exception reading from cultural artifacts table: {e}")
|
|
2266
|
-
|
|
2262
|
+
return None
|
|
2267
2263
|
|
|
2268
2264
|
async def get_all_cultural_knowledge(
|
|
2269
2265
|
self,
|
|
@@ -2337,7 +2333,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2337
2333
|
|
|
2338
2334
|
except Exception as e:
|
|
2339
2335
|
log_error(f"Error reading from cultural artifacts table: {e}")
|
|
2340
|
-
|
|
2336
|
+
return [] if deserialize else ([], 0)
|
|
2341
2337
|
|
|
2342
2338
|
async def upsert_cultural_knowledge(
|
|
2343
2339
|
self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
|
|
@@ -2357,7 +2353,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2357
2353
|
Exception: If an error occurs during upsert.
|
|
2358
2354
|
"""
|
|
2359
2355
|
try:
|
|
2360
|
-
table = await self._get_table(table_type="culture")
|
|
2356
|
+
table = await self._get_table(table_type="culture", create_table_if_not_found=True)
|
|
2361
2357
|
if table is None:
|
|
2362
2358
|
return None
|
|
2363
2359
|
|
|
@@ -2440,86 +2436,113 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
2440
2436
|
# Fallback if spans table doesn't exist
|
|
2441
2437
|
return select(table, literal(0).label("total_spans"), literal(0).label("error_count"))
|
|
2442
2438
|
|
|
2443
|
-
|
|
2444
|
-
"""
|
|
2439
|
+
def _get_trace_component_level_expr(self, workflow_id_col, team_id_col, agent_id_col, name_col):
|
|
2440
|
+
"""Build a SQL CASE expression that returns the component level for a trace.
|
|
2441
|
+
|
|
2442
|
+
Component levels (higher = more important):
|
|
2443
|
+
- 3: Workflow root (.run or .arun with workflow_id)
|
|
2444
|
+
- 2: Team root (.run or .arun with team_id)
|
|
2445
|
+
- 1: Agent root (.run or .arun with agent_id)
|
|
2446
|
+
- 0: Child span (not a root)
|
|
2447
|
+
|
|
2448
|
+
Args:
|
|
2449
|
+
workflow_id_col: SQL column/expression for workflow_id
|
|
2450
|
+
team_id_col: SQL column/expression for team_id
|
|
2451
|
+
agent_id_col: SQL column/expression for agent_id
|
|
2452
|
+
name_col: SQL column/expression for name
|
|
2453
|
+
|
|
2454
|
+
Returns:
|
|
2455
|
+
SQLAlchemy CASE expression returning the component level as an integer.
|
|
2456
|
+
"""
|
|
2457
|
+
from sqlalchemy import and_, case, or_
|
|
2458
|
+
|
|
2459
|
+
is_root_name = or_(name_col.contains(".run"), name_col.contains(".arun"))
|
|
2460
|
+
|
|
2461
|
+
return case(
|
|
2462
|
+
# Workflow root (level 3)
|
|
2463
|
+
(and_(workflow_id_col.isnot(None), is_root_name), 3),
|
|
2464
|
+
# Team root (level 2)
|
|
2465
|
+
(and_(team_id_col.isnot(None), is_root_name), 2),
|
|
2466
|
+
# Agent root (level 1)
|
|
2467
|
+
(and_(agent_id_col.isnot(None), is_root_name), 1),
|
|
2468
|
+
# Child span or unknown (level 0)
|
|
2469
|
+
else_=0,
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
async def upsert_trace(self, trace: "Trace") -> None:
|
|
2473
|
+
"""Create or update a single trace record in the database.
|
|
2474
|
+
|
|
2475
|
+
Uses INSERT ... ON CONFLICT DO UPDATE (upsert) to handle concurrent inserts
|
|
2476
|
+
atomically and avoid race conditions.
|
|
2445
2477
|
|
|
2446
2478
|
Args:
|
|
2447
2479
|
trace: The Trace object to store (one per trace_id).
|
|
2448
2480
|
"""
|
|
2481
|
+
from sqlalchemy import case
|
|
2482
|
+
|
|
2449
2483
|
try:
|
|
2450
2484
|
table = await self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
2451
2485
|
if table is None:
|
|
2452
2486
|
return
|
|
2453
2487
|
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
existing = result.fetchone()
|
|
2458
|
-
|
|
2459
|
-
if existing:
|
|
2460
|
-
# workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
|
|
2461
|
-
|
|
2462
|
-
def get_component_level(workflow_id, team_id, agent_id, name):
|
|
2463
|
-
# Check if name indicates a root span
|
|
2464
|
-
is_root_name = ".run" in name or ".arun" in name
|
|
2465
|
-
|
|
2466
|
-
if not is_root_name:
|
|
2467
|
-
return 0 # Child span (not a root)
|
|
2468
|
-
elif workflow_id:
|
|
2469
|
-
return 3 # Workflow root
|
|
2470
|
-
elif team_id:
|
|
2471
|
-
return 2 # Team root
|
|
2472
|
-
elif agent_id:
|
|
2473
|
-
return 1 # Agent root
|
|
2474
|
-
else:
|
|
2475
|
-
return 0 # Unknown
|
|
2476
|
-
|
|
2477
|
-
existing_level = get_component_level(
|
|
2478
|
-
existing.workflow_id, existing.team_id, existing.agent_id, existing.name
|
|
2479
|
-
)
|
|
2480
|
-
new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
|
|
2488
|
+
trace_dict = trace.to_dict()
|
|
2489
|
+
trace_dict.pop("total_spans", None)
|
|
2490
|
+
trace_dict.pop("error_count", None)
|
|
2481
2491
|
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2492
|
+
async with self.async_session_factory() as sess, sess.begin():
|
|
2493
|
+
# Use upsert to handle concurrent inserts atomically
|
|
2494
|
+
# On conflict, update fields while preserving existing non-null context values
|
|
2495
|
+
# and keeping the earliest start_time
|
|
2496
|
+
insert_stmt = sqlite.insert(table).values(trace_dict)
|
|
2497
|
+
|
|
2498
|
+
# Build component level expressions for comparing trace priority
|
|
2499
|
+
new_level = self._get_trace_component_level_expr(
|
|
2500
|
+
insert_stmt.excluded.workflow_id,
|
|
2501
|
+
insert_stmt.excluded.team_id,
|
|
2502
|
+
insert_stmt.excluded.agent_id,
|
|
2503
|
+
insert_stmt.excluded.name,
|
|
2504
|
+
)
|
|
2505
|
+
existing_level = self._get_trace_component_level_expr(
|
|
2506
|
+
table.c.workflow_id,
|
|
2507
|
+
table.c.team_id,
|
|
2508
|
+
table.c.agent_id,
|
|
2509
|
+
table.c.name,
|
|
2510
|
+
)
|
|
2500
2511
|
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2512
|
+
# Build the ON CONFLICT DO UPDATE clause
|
|
2513
|
+
# Use MIN for start_time, MAX for end_time to capture full trace duration
|
|
2514
|
+
# SQLite stores timestamps as ISO strings, so string comparison works for ISO format
|
|
2515
|
+
# Duration is calculated as: (MAX(end_time) - MIN(start_time)) in milliseconds
|
|
2516
|
+
# SQLite doesn't have epoch extraction, so we calculate duration using julianday
|
|
2517
|
+
upsert_stmt = insert_stmt.on_conflict_do_update(
|
|
2518
|
+
index_elements=["trace_id"],
|
|
2519
|
+
set_={
|
|
2520
|
+
"end_time": func.max(table.c.end_time, insert_stmt.excluded.end_time),
|
|
2521
|
+
"start_time": func.min(table.c.start_time, insert_stmt.excluded.start_time),
|
|
2522
|
+
# Calculate duration in milliseconds using julianday (SQLite-specific)
|
|
2523
|
+
# julianday returns days, so multiply by 86400000 to get milliseconds
|
|
2524
|
+
"duration_ms": (
|
|
2525
|
+
func.julianday(func.max(table.c.end_time, insert_stmt.excluded.end_time))
|
|
2526
|
+
- func.julianday(func.min(table.c.start_time, insert_stmt.excluded.start_time))
|
|
2527
|
+
)
|
|
2528
|
+
* 86400000,
|
|
2529
|
+
"status": insert_stmt.excluded.status,
|
|
2530
|
+
# Update name only if new trace is from a higher-level component
|
|
2531
|
+
# Priority: workflow (3) > team (2) > agent (1) > child spans (0)
|
|
2532
|
+
"name": case(
|
|
2533
|
+
(new_level > existing_level, insert_stmt.excluded.name),
|
|
2534
|
+
else_=table.c.name,
|
|
2535
|
+
),
|
|
2536
|
+
# Preserve existing non-null context values using COALESCE
|
|
2537
|
+
"run_id": func.coalesce(insert_stmt.excluded.run_id, table.c.run_id),
|
|
2538
|
+
"session_id": func.coalesce(insert_stmt.excluded.session_id, table.c.session_id),
|
|
2539
|
+
"user_id": func.coalesce(insert_stmt.excluded.user_id, table.c.user_id),
|
|
2540
|
+
"agent_id": func.coalesce(insert_stmt.excluded.agent_id, table.c.agent_id),
|
|
2541
|
+
"team_id": func.coalesce(insert_stmt.excluded.team_id, table.c.team_id),
|
|
2542
|
+
"workflow_id": func.coalesce(insert_stmt.excluded.workflow_id, table.c.workflow_id),
|
|
2543
|
+
},
|
|
2544
|
+
)
|
|
2545
|
+
await sess.execute(upsert_stmt)
|
|
2523
2546
|
|
|
2524
2547
|
except Exception as e:
|
|
2525
2548
|
log_error(f"Error creating trace: {e}")
|
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/eval/accuracy.py
CHANGED
|
@@ -282,7 +282,8 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
282
282
|
) -> Optional[AccuracyEvaluation]:
|
|
283
283
|
"""Orchestrate the evaluation process."""
|
|
284
284
|
try:
|
|
285
|
-
|
|
285
|
+
response = evaluator_agent.run(evaluation_input, stream=False)
|
|
286
|
+
accuracy_agent_response = response.content
|
|
286
287
|
if accuracy_agent_response is None or not isinstance(accuracy_agent_response, AccuracyAgentResponse):
|
|
287
288
|
raise EvalError(f"Evaluator Agent returned an invalid response: {accuracy_agent_response}")
|
|
288
289
|
return AccuracyEvaluation(
|
|
@@ -306,7 +307,7 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
306
307
|
) -> Optional[AccuracyEvaluation]:
|
|
307
308
|
"""Orchestrate the evaluation process asynchronously."""
|
|
308
309
|
try:
|
|
309
|
-
response = await evaluator_agent.arun(evaluation_input)
|
|
310
|
+
response = await evaluator_agent.arun(evaluation_input, stream=False)
|
|
310
311
|
accuracy_agent_response = response.content
|
|
311
312
|
if accuracy_agent_response is None or not isinstance(accuracy_agent_response, AccuracyAgentResponse):
|
|
312
313
|
raise EvalError(f"Evaluator Agent returned an invalid response: {accuracy_agent_response}")
|
|
@@ -362,9 +363,11 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
362
363
|
agent_session_id = f"eval_{self.eval_id}_{i + 1}"
|
|
363
364
|
|
|
364
365
|
if self.agent is not None:
|
|
365
|
-
|
|
366
|
+
agent_response = self.agent.run(input=eval_input, session_id=agent_session_id, stream=False)
|
|
367
|
+
output = agent_response.content
|
|
366
368
|
elif self.team is not None:
|
|
367
|
-
|
|
369
|
+
team_response = self.team.run(input=eval_input, session_id=agent_session_id, stream=False)
|
|
370
|
+
output = team_response.content
|
|
368
371
|
|
|
369
372
|
if not output:
|
|
370
373
|
logger.error(f"Failed to generate a valid answer on iteration {i + 1}: {output}")
|
|
@@ -505,11 +508,11 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
505
508
|
agent_session_id = f"eval_{self.eval_id}_{i + 1}"
|
|
506
509
|
|
|
507
510
|
if self.agent is not None:
|
|
508
|
-
|
|
509
|
-
output =
|
|
511
|
+
agent_response = await self.agent.arun(input=eval_input, session_id=agent_session_id, stream=False)
|
|
512
|
+
output = agent_response.content
|
|
510
513
|
elif self.team is not None:
|
|
511
|
-
|
|
512
|
-
output =
|
|
514
|
+
team_response = await self.team.arun(input=eval_input, session_id=agent_session_id, stream=False)
|
|
515
|
+
output = team_response.content
|
|
513
516
|
|
|
514
517
|
if not output:
|
|
515
518
|
logger.error(f"Failed to generate a valid answer on iteration {i + 1}: {output}")
|
agno/eval/agent_as_judge.py
CHANGED
|
@@ -281,24 +281,25 @@ class AgentAsJudgeEval(BaseEval):
|
|
|
281
281
|
</output>
|
|
282
282
|
""")
|
|
283
283
|
|
|
284
|
-
response = evaluator_agent.run(prompt)
|
|
285
|
-
|
|
286
|
-
|
|
284
|
+
response = evaluator_agent.run(prompt, stream=False)
|
|
285
|
+
judge_response = response.content
|
|
286
|
+
if not isinstance(judge_response, (NumericJudgeResponse, BinaryJudgeResponse)):
|
|
287
|
+
raise EvalError(f"Invalid response: {judge_response}")
|
|
287
288
|
|
|
288
289
|
# Determine pass/fail based on scoring strategy and response type
|
|
289
|
-
if isinstance(
|
|
290
|
-
score =
|
|
290
|
+
if isinstance(judge_response, NumericJudgeResponse):
|
|
291
|
+
score = judge_response.score
|
|
291
292
|
passed = score >= self.threshold
|
|
292
293
|
else: # BinaryJudgeResponse
|
|
293
294
|
score = None
|
|
294
|
-
passed =
|
|
295
|
+
passed = judge_response.passed
|
|
295
296
|
|
|
296
297
|
evaluation = AgentAsJudgeEvaluation(
|
|
297
298
|
input=input,
|
|
298
299
|
output=output,
|
|
299
300
|
criteria=self.criteria,
|
|
300
301
|
score=score,
|
|
301
|
-
reason=
|
|
302
|
+
reason=judge_response.reason,
|
|
302
303
|
passed=passed,
|
|
303
304
|
)
|
|
304
305
|
|
|
@@ -332,7 +333,7 @@ class AgentAsJudgeEval(BaseEval):
|
|
|
332
333
|
</output>
|
|
333
334
|
""")
|
|
334
335
|
|
|
335
|
-
response = await evaluator_agent.arun(prompt)
|
|
336
|
+
response = await evaluator_agent.arun(prompt, stream=False)
|
|
336
337
|
judge_response = response.content
|
|
337
338
|
if not isinstance(judge_response, (NumericJudgeResponse, BinaryJudgeResponse)):
|
|
338
339
|
raise EvalError(f"Invalid response: {judge_response}")
|
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
|
|
@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
4
4
|
from typing_extensions import Literal
|
|
5
5
|
|
|
6
6
|
from agno.knowledge.embedder.base import Embedder
|
|
7
|
-
from agno.utils.log import
|
|
7
|
+
from agno.utils.log import log_info, log_warning
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
from openai import AsyncOpenAI
|
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
|
|