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.
Files changed (43) hide show
  1. agno/agent/agent.py +0 -12
  2. agno/db/base.py +5 -5
  3. agno/db/dynamo/dynamo.py +2 -2
  4. agno/db/firestore/firestore.py +2 -2
  5. agno/db/gcs_json/gcs_json_db.py +2 -2
  6. agno/db/in_memory/in_memory_db.py +2 -2
  7. agno/db/json/json_db.py +2 -2
  8. agno/db/mongo/async_mongo.py +171 -69
  9. agno/db/mongo/mongo.py +171 -77
  10. agno/db/mysql/async_mysql.py +93 -69
  11. agno/db/mysql/mysql.py +93 -68
  12. agno/db/postgres/async_postgres.py +104 -78
  13. agno/db/postgres/postgres.py +97 -69
  14. agno/db/redis/redis.py +2 -2
  15. agno/db/singlestore/singlestore.py +91 -66
  16. agno/db/sqlite/async_sqlite.py +102 -79
  17. agno/db/sqlite/sqlite.py +97 -69
  18. agno/db/surrealdb/surrealdb.py +2 -2
  19. agno/eval/accuracy.py +11 -8
  20. agno/eval/agent_as_judge.py +9 -8
  21. agno/knowledge/chunking/fixed.py +4 -1
  22. agno/knowledge/embedder/openai.py +1 -1
  23. agno/knowledge/knowledge.py +22 -4
  24. agno/knowledge/utils.py +52 -7
  25. agno/models/base.py +34 -1
  26. agno/models/google/gemini.py +69 -40
  27. agno/models/message.py +3 -0
  28. agno/models/openai/chat.py +21 -0
  29. agno/os/routers/evals/utils.py +15 -37
  30. agno/os/routers/knowledge/knowledge.py +21 -9
  31. agno/team/team.py +14 -8
  32. agno/tools/function.py +37 -23
  33. agno/tools/shopify.py +1519 -0
  34. agno/tools/spotify.py +2 -5
  35. agno/tracing/exporter.py +2 -2
  36. agno/vectordb/base.py +15 -2
  37. agno/vectordb/pgvector/pgvector.py +8 -8
  38. agno/workflow/parallel.py +2 -0
  39. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/METADATA +1 -1
  40. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/RECORD +43 -42
  41. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/WHEEL +0 -0
  42. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/licenses/LICENSE +0 -0
  43. {agno-2.3.9.dist-info → agno-2.3.11.dist-info}/top_level.txt +0 -0
@@ -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, update
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
- from agno.utils.log import log_warning
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
- raise e
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
- raise e
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
- async def create_trace(self, trace: "Trace") -> None:
2444
- """Create a single trace record in the database.
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
- async with self.async_session_factory() as sess, sess.begin():
2455
- # Check if trace exists
2456
- result = await sess.execute(select(table).where(table.c.trace_id == trace.trace_id))
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
- # Only update name if new trace is from a higher or equal level
2483
- should_update_name = new_level > existing_level
2484
-
2485
- # Parse existing start_time to calculate correct duration
2486
- existing_start_time_str = existing.start_time
2487
- if isinstance(existing_start_time_str, str):
2488
- existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
2489
- else:
2490
- existing_start_time = trace.start_time
2491
-
2492
- recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
2493
-
2494
- update_values = {
2495
- "end_time": trace.end_time.isoformat(),
2496
- "duration_ms": recalculated_duration_ms,
2497
- "status": trace.status,
2498
- "name": trace.name if should_update_name else existing.name,
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
- # Update context fields ONLY if new value is not None (preserve non-null values)
2502
- if trace.run_id is not None:
2503
- update_values["run_id"] = trace.run_id
2504
- if trace.session_id is not None:
2505
- update_values["session_id"] = trace.session_id
2506
- if trace.user_id is not None:
2507
- update_values["user_id"] = trace.user_id
2508
- if trace.agent_id is not None:
2509
- update_values["agent_id"] = trace.agent_id
2510
- if trace.team_id is not None:
2511
- update_values["team_id"] = trace.team_id
2512
- if trace.workflow_id is not None:
2513
- update_values["workflow_id"] = trace.workflow_id
2514
-
2515
- stmt = update(table).where(table.c.trace_id == trace.trace_id).values(**update_values)
2516
- await sess.execute(stmt)
2517
- else:
2518
- trace_dict = trace.to_dict()
2519
- trace_dict.pop("total_spans", None)
2520
- trace_dict.pop("error_count", None)
2521
- stmt = sqlite.insert(table).values(trace_dict)
2522
- await sess.execute(stmt)
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, update
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 create_trace(self, trace: "Trace") -> None:
2149
- """Create a single trace record in the database.
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
- with self.Session() as sess, sess.begin():
2160
- # Check if trace exists
2161
- existing = sess.execute(table.select().where(table.c.trace_id == trace.trace_id)).fetchone()
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
- # Parse existing start_time to calculate correct duration
2190
- existing_start_time_str = existing.start_time
2191
- if isinstance(existing_start_time_str, str):
2192
- existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
2193
- else:
2194
- existing_start_time = trace.start_time
2195
-
2196
- recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
2197
-
2198
- update_values = {
2199
- "end_time": trace.end_time.isoformat(),
2200
- "duration_ms": recalculated_duration_ms,
2201
- "status": trace.status,
2202
- "name": trace.name if should_update_name else existing.name,
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
- # Update context fields ONLY if new value is not None (preserve non-null values)
2206
- if trace.run_id is not None:
2207
- update_values["run_id"] = trace.run_id
2208
- if trace.session_id is not None:
2209
- update_values["session_id"] = trace.session_id
2210
- if trace.user_id is not None:
2211
- update_values["user_id"] = trace.user_id
2212
- if trace.agent_id is not None:
2213
- update_values["agent_id"] = trace.agent_id
2214
- if trace.team_id is not None:
2215
- update_values["team_id"] = trace.team_id
2216
- if trace.workflow_id is not None:
2217
- update_values["workflow_id"] = trace.workflow_id
2218
-
2219
- stmt = update(table).where(table.c.trace_id == trace.trace_id).values(**update_values)
2220
- sess.execute(stmt)
2221
- else:
2222
- trace_dict = trace.to_dict()
2223
- trace_dict.pop("total_spans", None)
2224
- trace_dict.pop("error_count", None)
2225
- stmt = sqlite.insert(table).values(trace_dict)
2226
- sess.execute(stmt)
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}")
@@ -1390,8 +1390,8 @@ class SurrealDb(BaseDb):
1390
1390
  return deserialize_eval_run_record(raw)
1391
1391
 
1392
1392
  # --- Traces ---
1393
- def create_trace(self, trace: "Trace") -> None:
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
- accuracy_agent_response = evaluator_agent.run(evaluation_input).content
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
- output = self.agent.run(input=eval_input, session_id=agent_session_id).content
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
- output = self.team.run(input=eval_input, session_id=agent_session_id).content
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
- response = await self.agent.arun(input=eval_input, session_id=agent_session_id)
509
- output = response.content
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
- response = await self.team.arun(input=eval_input, session_id=agent_session_id) # type: ignore
512
- output = response.content
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}")
@@ -281,24 +281,25 @@ class AgentAsJudgeEval(BaseEval):
281
281
  </output>
282
282
  """)
283
283
 
284
- response = evaluator_agent.run(prompt).content
285
- if not isinstance(response, (NumericJudgeResponse, BinaryJudgeResponse)):
286
- raise EvalError(f"Invalid response: {response}")
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(response, NumericJudgeResponse):
290
- score = response.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 = response.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=response.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}")
@@ -53,5 +53,8 @@ class FixedSizeChunking(ChunkingStrategy):
53
53
  )
54
54
  )
55
55
  chunk_number += 1
56
- start = end - self.overlap
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 log_warning, log_info
7
+ from agno.utils.log import log_info, log_warning
8
8
 
9
9
  try:
10
10
  from openai import AsyncOpenAI
@@ -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 and content.metadata:
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 and content.metadata:
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