agno 2.3.3__py3-none-any.whl → 2.3.5__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 (108) hide show
  1. agno/agent/agent.py +177 -41
  2. agno/culture/manager.py +2 -2
  3. agno/db/base.py +330 -8
  4. agno/db/dynamo/dynamo.py +722 -2
  5. agno/db/dynamo/schemas.py +127 -0
  6. agno/db/firestore/firestore.py +573 -1
  7. agno/db/firestore/schemas.py +40 -0
  8. agno/db/gcs_json/gcs_json_db.py +446 -1
  9. agno/db/in_memory/in_memory_db.py +143 -1
  10. agno/db/json/json_db.py +438 -1
  11. agno/db/mongo/async_mongo.py +522 -0
  12. agno/db/mongo/mongo.py +523 -1
  13. agno/db/mongo/schemas.py +29 -0
  14. agno/db/mysql/mysql.py +536 -3
  15. agno/db/mysql/schemas.py +38 -0
  16. agno/db/postgres/async_postgres.py +546 -14
  17. agno/db/postgres/postgres.py +535 -2
  18. agno/db/postgres/schemas.py +38 -0
  19. agno/db/redis/redis.py +468 -1
  20. agno/db/redis/schemas.py +32 -0
  21. agno/db/singlestore/schemas.py +38 -0
  22. agno/db/singlestore/singlestore.py +523 -1
  23. agno/db/sqlite/async_sqlite.py +548 -9
  24. agno/db/sqlite/schemas.py +38 -0
  25. agno/db/sqlite/sqlite.py +537 -5
  26. agno/db/sqlite/utils.py +6 -8
  27. agno/db/surrealdb/models.py +25 -0
  28. agno/db/surrealdb/surrealdb.py +548 -1
  29. agno/eval/accuracy.py +10 -4
  30. agno/eval/performance.py +10 -4
  31. agno/eval/reliability.py +22 -13
  32. agno/exceptions.py +11 -0
  33. agno/hooks/__init__.py +3 -0
  34. agno/hooks/decorator.py +164 -0
  35. agno/knowledge/chunking/semantic.py +2 -2
  36. agno/models/aimlapi/aimlapi.py +17 -0
  37. agno/models/anthropic/claude.py +19 -12
  38. agno/models/aws/bedrock.py +3 -4
  39. agno/models/aws/claude.py +5 -1
  40. agno/models/azure/ai_foundry.py +2 -2
  41. agno/models/azure/openai_chat.py +8 -0
  42. agno/models/cerebras/cerebras.py +61 -4
  43. agno/models/cerebras/cerebras_openai.py +17 -0
  44. agno/models/cohere/chat.py +5 -1
  45. agno/models/cometapi/cometapi.py +18 -1
  46. agno/models/dashscope/dashscope.py +2 -3
  47. agno/models/deepinfra/deepinfra.py +18 -1
  48. agno/models/deepseek/deepseek.py +2 -3
  49. agno/models/fireworks/fireworks.py +18 -1
  50. agno/models/google/gemini.py +8 -2
  51. agno/models/groq/groq.py +5 -2
  52. agno/models/internlm/internlm.py +18 -1
  53. agno/models/langdb/langdb.py +13 -1
  54. agno/models/litellm/chat.py +2 -2
  55. agno/models/litellm/litellm_openai.py +18 -1
  56. agno/models/meta/llama_openai.py +19 -2
  57. agno/models/nebius/nebius.py +2 -3
  58. agno/models/nvidia/nvidia.py +20 -3
  59. agno/models/openai/chat.py +17 -2
  60. agno/models/openai/responses.py +17 -2
  61. agno/models/openrouter/openrouter.py +21 -2
  62. agno/models/perplexity/perplexity.py +17 -1
  63. agno/models/portkey/portkey.py +7 -6
  64. agno/models/requesty/requesty.py +19 -2
  65. agno/models/response.py +2 -1
  66. agno/models/sambanova/sambanova.py +20 -3
  67. agno/models/siliconflow/siliconflow.py +19 -2
  68. agno/models/together/together.py +20 -3
  69. agno/models/vercel/v0.py +20 -3
  70. agno/models/vllm/vllm.py +19 -14
  71. agno/models/xai/xai.py +19 -2
  72. agno/os/app.py +104 -0
  73. agno/os/config.py +13 -0
  74. agno/os/interfaces/whatsapp/router.py +0 -1
  75. agno/os/mcp.py +1 -0
  76. agno/os/router.py +31 -0
  77. agno/os/routers/traces/__init__.py +3 -0
  78. agno/os/routers/traces/schemas.py +414 -0
  79. agno/os/routers/traces/traces.py +499 -0
  80. agno/os/schema.py +22 -1
  81. agno/os/utils.py +57 -0
  82. agno/run/agent.py +1 -0
  83. agno/run/base.py +17 -0
  84. agno/run/team.py +4 -0
  85. agno/session/team.py +1 -0
  86. agno/table.py +10 -0
  87. agno/team/team.py +215 -65
  88. agno/tools/function.py +10 -8
  89. agno/tools/nano_banana.py +1 -1
  90. agno/tracing/__init__.py +12 -0
  91. agno/tracing/exporter.py +157 -0
  92. agno/tracing/schemas.py +276 -0
  93. agno/tracing/setup.py +111 -0
  94. agno/utils/agent.py +4 -4
  95. agno/utils/hooks.py +56 -1
  96. agno/vectordb/qdrant/qdrant.py +22 -22
  97. agno/workflow/condition.py +8 -0
  98. agno/workflow/loop.py +8 -0
  99. agno/workflow/parallel.py +8 -0
  100. agno/workflow/router.py +8 -0
  101. agno/workflow/step.py +20 -0
  102. agno/workflow/steps.py +8 -0
  103. agno/workflow/workflow.py +83 -17
  104. {agno-2.3.3.dist-info → agno-2.3.5.dist-info}/METADATA +2 -2
  105. {agno-2.3.3.dist-info → agno-2.3.5.dist-info}/RECORD +108 -98
  106. {agno-2.3.3.dist-info → agno-2.3.5.dist-info}/WHEEL +0 -0
  107. {agno-2.3.3.dist-info → agno-2.3.5.dist-info}/licenses/LICENSE +0 -0
  108. {agno-2.3.3.dist-info → agno-2.3.5.dist-info}/top_level.txt +0 -0
@@ -4,6 +4,9 @@ from datetime import date, datetime, timedelta, timezone
4
4
  from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
+ if TYPE_CHECKING:
8
+ from agno.tracing.schemas import Span, Trace
9
+
7
10
  from agno.db.base import AsyncBaseDb, SessionType
8
11
  from agno.db.mongo.utils import (
9
12
  apply_pagination,
@@ -144,6 +147,8 @@ class AsyncMongoDb(AsyncBaseDb):
144
147
  eval_collection: Optional[str] = None,
145
148
  knowledge_collection: Optional[str] = None,
146
149
  culture_collection: Optional[str] = None,
150
+ traces_collection: Optional[str] = None,
151
+ spans_collection: Optional[str] = None,
147
152
  id: Optional[str] = None,
148
153
  ):
149
154
  """
@@ -165,6 +170,8 @@ class AsyncMongoDb(AsyncBaseDb):
165
170
  eval_collection (Optional[str]): Name of the collection to store evaluation runs.
166
171
  knowledge_collection (Optional[str]): Name of the collection to store knowledge documents.
167
172
  culture_collection (Optional[str]): Name of the collection to store cultural knowledge.
173
+ traces_collection (Optional[str]): Name of the collection to store traces.
174
+ spans_collection (Optional[str]): Name of the collection to store spans.
168
175
  id (Optional[str]): ID of the database.
169
176
 
170
177
  Raises:
@@ -185,6 +192,8 @@ class AsyncMongoDb(AsyncBaseDb):
185
192
  eval_table=eval_collection,
186
193
  knowledge_table=knowledge_collection,
187
194
  culture_table=culture_collection,
195
+ traces_table=traces_collection,
196
+ spans_table=spans_collection,
188
197
  )
189
198
 
190
199
  # Detect client type if provided
@@ -410,6 +419,28 @@ class AsyncMongoDb(AsyncBaseDb):
410
419
  )
411
420
  return self.culture_collection
412
421
 
422
+ if table_type == "traces":
423
+ if reset_cache or not hasattr(self, "traces_collection"):
424
+ if self.trace_table_name is None:
425
+ raise ValueError("Traces collection was not provided on initialization")
426
+ self.traces_collection = await self._get_or_create_collection(
427
+ collection_name=self.trace_table_name,
428
+ collection_type="traces",
429
+ create_collection_if_not_found=create_collection_if_not_found,
430
+ )
431
+ return self.traces_collection
432
+
433
+ if table_type == "spans":
434
+ if reset_cache or not hasattr(self, "spans_collection"):
435
+ if self.span_table_name is None:
436
+ raise ValueError("Spans collection was not provided on initialization")
437
+ self.spans_collection = await self._get_or_create_collection(
438
+ collection_name=self.span_table_name,
439
+ collection_type="spans",
440
+ create_collection_if_not_found=create_collection_if_not_found,
441
+ )
442
+ return self.spans_collection
443
+
413
444
  raise ValueError(f"Unknown table type: {table_type}")
414
445
 
415
446
  async def _get_or_create_collection(
@@ -2158,3 +2189,494 @@ class AsyncMongoDb(AsyncBaseDb):
2158
2189
  except Exception as e:
2159
2190
  log_error(f"Error updating eval run name {eval_run_id}: {e}")
2160
2191
  raise e
2192
+
2193
+ # --- Traces ---
2194
+ async def create_trace(self, trace: "Trace") -> None:
2195
+ """Create a single trace record in the database.
2196
+
2197
+ Args:
2198
+ trace: The Trace object to store (one per trace_id).
2199
+ """
2200
+ try:
2201
+ collection = await self._get_collection(table_type="traces", create_collection_if_not_found=True)
2202
+ if collection is None:
2203
+ return
2204
+
2205
+ # Check if trace already exists
2206
+ existing = await collection.find_one({"trace_id": trace.trace_id})
2207
+
2208
+ if existing:
2209
+ # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
2210
+ def get_component_level(
2211
+ workflow_id: Optional[str], team_id: Optional[str], agent_id: Optional[str], name: str
2212
+ ) -> int:
2213
+ # Check if name indicates a root span
2214
+ is_root_name = ".run" in name or ".arun" in name
2215
+
2216
+ if not is_root_name:
2217
+ return 0 # Child span (not a root)
2218
+ elif workflow_id:
2219
+ return 3 # Workflow root
2220
+ elif team_id:
2221
+ return 2 # Team root
2222
+ elif agent_id:
2223
+ return 1 # Agent root
2224
+ else:
2225
+ return 0 # Unknown
2226
+
2227
+ existing_level = get_component_level(
2228
+ existing.get("workflow_id"),
2229
+ existing.get("team_id"),
2230
+ existing.get("agent_id"),
2231
+ existing.get("name", ""),
2232
+ )
2233
+ new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
2234
+
2235
+ # Only update name if new trace is from a higher or equal level
2236
+ should_update_name = new_level > existing_level
2237
+
2238
+ # Parse existing start_time to calculate correct duration
2239
+ existing_start_time_str = existing.get("start_time")
2240
+ if isinstance(existing_start_time_str, str):
2241
+ existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
2242
+ else:
2243
+ existing_start_time = trace.start_time
2244
+
2245
+ recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
2246
+
2247
+ update_values: Dict[str, Any] = {
2248
+ "end_time": trace.end_time.isoformat(),
2249
+ "duration_ms": recalculated_duration_ms,
2250
+ "status": trace.status,
2251
+ "name": trace.name if should_update_name else existing.get("name"),
2252
+ }
2253
+
2254
+ # Update context fields ONLY if new value is not None (preserve non-null values)
2255
+ if trace.run_id is not None:
2256
+ update_values["run_id"] = trace.run_id
2257
+ if trace.session_id is not None:
2258
+ update_values["session_id"] = trace.session_id
2259
+ if trace.user_id is not None:
2260
+ update_values["user_id"] = trace.user_id
2261
+ if trace.agent_id is not None:
2262
+ update_values["agent_id"] = trace.agent_id
2263
+ if trace.team_id is not None:
2264
+ update_values["team_id"] = trace.team_id
2265
+ if trace.workflow_id is not None:
2266
+ update_values["workflow_id"] = trace.workflow_id
2267
+
2268
+ log_debug(
2269
+ f" Updating trace with context: run_id={update_values.get('run_id', 'unchanged')}, "
2270
+ f"session_id={update_values.get('session_id', 'unchanged')}, "
2271
+ f"user_id={update_values.get('user_id', 'unchanged')}, "
2272
+ f"agent_id={update_values.get('agent_id', 'unchanged')}, "
2273
+ f"team_id={update_values.get('team_id', 'unchanged')}, "
2274
+ )
2275
+
2276
+ await collection.update_one({"trace_id": trace.trace_id}, {"$set": update_values})
2277
+ else:
2278
+ trace_dict = trace.to_dict()
2279
+ trace_dict.pop("total_spans", None)
2280
+ trace_dict.pop("error_count", None)
2281
+ await collection.insert_one(trace_dict)
2282
+
2283
+ except Exception as e:
2284
+ log_error(f"Error creating trace: {e}")
2285
+ # Don't raise - tracing should not break the main application flow
2286
+
2287
+ async def get_trace(
2288
+ self,
2289
+ trace_id: Optional[str] = None,
2290
+ run_id: Optional[str] = None,
2291
+ ):
2292
+ """Get a single trace by trace_id or other filters.
2293
+
2294
+ Args:
2295
+ trace_id: The unique trace identifier.
2296
+ run_id: Filter by run ID (returns first match).
2297
+
2298
+ Returns:
2299
+ Optional[Trace]: The trace if found, None otherwise.
2300
+
2301
+ Note:
2302
+ If multiple filters are provided, trace_id takes precedence.
2303
+ For other filters, the most recent trace is returned.
2304
+ """
2305
+ try:
2306
+ from agno.tracing.schemas import Trace as TraceSchema
2307
+
2308
+ collection = await self._get_collection(table_type="traces")
2309
+ if collection is None:
2310
+ return None
2311
+
2312
+ # Get spans collection for aggregation
2313
+ spans_collection = await self._get_collection(table_type="spans")
2314
+
2315
+ query: Dict[str, Any] = {}
2316
+ if trace_id:
2317
+ query["trace_id"] = trace_id
2318
+ elif run_id:
2319
+ query["run_id"] = run_id
2320
+ else:
2321
+ log_debug("get_trace called without any filter parameters")
2322
+ return None
2323
+
2324
+ # Find trace with sorting by most recent
2325
+ result = await collection.find_one(query, sort=[("start_time", -1)])
2326
+
2327
+ if result:
2328
+ # Calculate total_spans and error_count from spans collection
2329
+ total_spans = 0
2330
+ error_count = 0
2331
+ if spans_collection is not None:
2332
+ total_spans = await spans_collection.count_documents({"trace_id": result["trace_id"]})
2333
+ error_count = await spans_collection.count_documents(
2334
+ {"trace_id": result["trace_id"], "status_code": "ERROR"}
2335
+ )
2336
+
2337
+ result["total_spans"] = total_spans
2338
+ result["error_count"] = error_count
2339
+ # Remove MongoDB's _id field
2340
+ result.pop("_id", None)
2341
+ return TraceSchema.from_dict(result)
2342
+ return None
2343
+
2344
+ except Exception as e:
2345
+ log_error(f"Error getting trace: {e}")
2346
+ return None
2347
+
2348
+ async def get_traces(
2349
+ self,
2350
+ run_id: Optional[str] = None,
2351
+ session_id: Optional[str] = None,
2352
+ user_id: Optional[str] = None,
2353
+ agent_id: Optional[str] = None,
2354
+ team_id: Optional[str] = None,
2355
+ workflow_id: Optional[str] = None,
2356
+ status: Optional[str] = None,
2357
+ start_time: Optional[datetime] = None,
2358
+ end_time: Optional[datetime] = None,
2359
+ limit: Optional[int] = 20,
2360
+ page: Optional[int] = 1,
2361
+ ) -> tuple[List, int]:
2362
+ """Get traces matching the provided filters with pagination.
2363
+
2364
+ Args:
2365
+ run_id: Filter by run ID.
2366
+ session_id: Filter by session ID.
2367
+ user_id: Filter by user ID.
2368
+ agent_id: Filter by agent ID.
2369
+ team_id: Filter by team ID.
2370
+ workflow_id: Filter by workflow ID.
2371
+ status: Filter by status (OK, ERROR, UNSET).
2372
+ start_time: Filter traces starting after this datetime.
2373
+ end_time: Filter traces ending before this datetime.
2374
+ limit: Maximum number of traces to return per page.
2375
+ page: Page number (1-indexed).
2376
+
2377
+ Returns:
2378
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
2379
+ """
2380
+ try:
2381
+ from agno.tracing.schemas import Trace as TraceSchema
2382
+
2383
+ log_debug(
2384
+ f"get_traces called with filters: run_id={run_id}, session_id={session_id}, "
2385
+ f"user_id={user_id}, agent_id={agent_id}, page={page}, limit={limit}"
2386
+ )
2387
+
2388
+ collection = await self._get_collection(table_type="traces")
2389
+ if collection is None:
2390
+ log_debug("Traces collection not found")
2391
+ return [], 0
2392
+
2393
+ # Get spans collection for aggregation
2394
+ spans_collection = await self._get_collection(table_type="spans")
2395
+
2396
+ # Build query
2397
+ query: Dict[str, Any] = {}
2398
+ if run_id:
2399
+ query["run_id"] = run_id
2400
+ if session_id:
2401
+ log_debug(f"Filtering by session_id={session_id}")
2402
+ query["session_id"] = session_id
2403
+ if user_id:
2404
+ query["user_id"] = user_id
2405
+ if agent_id:
2406
+ query["agent_id"] = agent_id
2407
+ if team_id:
2408
+ query["team_id"] = team_id
2409
+ if workflow_id:
2410
+ query["workflow_id"] = workflow_id
2411
+ if status:
2412
+ query["status"] = status
2413
+ if start_time:
2414
+ query["start_time"] = {"$gte": start_time.isoformat()}
2415
+ if end_time:
2416
+ if "end_time" in query:
2417
+ query["end_time"]["$lte"] = end_time.isoformat()
2418
+ else:
2419
+ query["end_time"] = {"$lte": end_time.isoformat()}
2420
+
2421
+ # Get total count
2422
+ total_count = await collection.count_documents(query)
2423
+ log_debug(f"Total matching traces: {total_count}")
2424
+
2425
+ # Apply pagination
2426
+ skip = ((page or 1) - 1) * (limit or 20)
2427
+ cursor = collection.find(query).sort("start_time", -1).skip(skip).limit(limit or 20)
2428
+
2429
+ results = await cursor.to_list(length=None)
2430
+ log_debug(f"Returning page {page} with {len(results)} traces")
2431
+
2432
+ traces = []
2433
+ for row in results:
2434
+ # Calculate total_spans and error_count from spans collection
2435
+ total_spans = 0
2436
+ error_count = 0
2437
+ if spans_collection is not None:
2438
+ total_spans = await spans_collection.count_documents({"trace_id": row["trace_id"]})
2439
+ error_count = await spans_collection.count_documents(
2440
+ {"trace_id": row["trace_id"], "status_code": "ERROR"}
2441
+ )
2442
+
2443
+ row["total_spans"] = total_spans
2444
+ row["error_count"] = error_count
2445
+ # Remove MongoDB's _id field
2446
+ row.pop("_id", None)
2447
+ traces.append(TraceSchema.from_dict(row))
2448
+
2449
+ return traces, total_count
2450
+
2451
+ except Exception as e:
2452
+ log_error(f"Error getting traces: {e}")
2453
+ return [], 0
2454
+
2455
+ async def get_trace_stats(
2456
+ self,
2457
+ user_id: Optional[str] = None,
2458
+ agent_id: Optional[str] = None,
2459
+ team_id: Optional[str] = None,
2460
+ workflow_id: Optional[str] = None,
2461
+ start_time: Optional[datetime] = None,
2462
+ end_time: Optional[datetime] = None,
2463
+ limit: Optional[int] = 20,
2464
+ page: Optional[int] = 1,
2465
+ ) -> tuple[List[Dict[str, Any]], int]:
2466
+ """Get trace statistics grouped by session.
2467
+
2468
+ Args:
2469
+ user_id: Filter by user ID.
2470
+ agent_id: Filter by agent ID.
2471
+ team_id: Filter by team ID.
2472
+ workflow_id: Filter by workflow ID.
2473
+ start_time: Filter sessions with traces created after this datetime.
2474
+ end_time: Filter sessions with traces created before this datetime.
2475
+ limit: Maximum number of sessions to return per page.
2476
+ page: Page number (1-indexed).
2477
+
2478
+ Returns:
2479
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
2480
+ Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
2481
+ workflow_id, first_trace_at, last_trace_at.
2482
+ """
2483
+ try:
2484
+ log_debug(
2485
+ f"get_trace_stats called with filters: user_id={user_id}, agent_id={agent_id}, "
2486
+ f"workflow_id={workflow_id}, team_id={team_id}, "
2487
+ f"start_time={start_time}, end_time={end_time}, page={page}, limit={limit}"
2488
+ )
2489
+
2490
+ collection = await self._get_collection(table_type="traces")
2491
+ if collection is None:
2492
+ log_debug("Traces collection not found")
2493
+ return [], 0
2494
+
2495
+ # Build match stage
2496
+ match_stage: Dict[str, Any] = {"session_id": {"$ne": None}}
2497
+ if user_id:
2498
+ match_stage["user_id"] = user_id
2499
+ if agent_id:
2500
+ match_stage["agent_id"] = agent_id
2501
+ if team_id:
2502
+ match_stage["team_id"] = team_id
2503
+ if workflow_id:
2504
+ match_stage["workflow_id"] = workflow_id
2505
+ if start_time:
2506
+ match_stage["created_at"] = {"$gte": start_time.isoformat()}
2507
+ if end_time:
2508
+ if "created_at" in match_stage:
2509
+ match_stage["created_at"]["$lte"] = end_time.isoformat()
2510
+ else:
2511
+ match_stage["created_at"] = {"$lte": end_time.isoformat()}
2512
+
2513
+ # Build aggregation pipeline
2514
+ pipeline: List[Dict[str, Any]] = [
2515
+ {"$match": match_stage},
2516
+ {
2517
+ "$group": {
2518
+ "_id": "$session_id",
2519
+ "user_id": {"$first": "$user_id"},
2520
+ "agent_id": {"$first": "$agent_id"},
2521
+ "team_id": {"$first": "$team_id"},
2522
+ "workflow_id": {"$first": "$workflow_id"},
2523
+ "total_traces": {"$sum": 1},
2524
+ "first_trace_at": {"$min": "$created_at"},
2525
+ "last_trace_at": {"$max": "$created_at"},
2526
+ }
2527
+ },
2528
+ {"$sort": {"last_trace_at": -1}},
2529
+ ]
2530
+
2531
+ # Get total count
2532
+ count_pipeline = pipeline + [{"$count": "total"}]
2533
+ count_result = await collection.aggregate(count_pipeline).to_list(length=1)
2534
+ total_count = count_result[0]["total"] if count_result else 0
2535
+ log_debug(f"Total matching sessions: {total_count}")
2536
+
2537
+ # Apply pagination
2538
+ skip = ((page or 1) - 1) * (limit or 20)
2539
+ pipeline.append({"$skip": skip})
2540
+ pipeline.append({"$limit": limit or 20})
2541
+
2542
+ results = await collection.aggregate(pipeline).to_list(length=None)
2543
+ log_debug(f"Returning page {page} with {len(results)} session stats")
2544
+
2545
+ # Convert to list of dicts with datetime objects
2546
+ stats_list = []
2547
+ for row in results:
2548
+ # Convert ISO strings to datetime objects
2549
+ first_trace_at_str = row["first_trace_at"]
2550
+ last_trace_at_str = row["last_trace_at"]
2551
+
2552
+ # Parse ISO format strings to datetime objects
2553
+ first_trace_at = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
2554
+ last_trace_at = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
2555
+
2556
+ stats_list.append(
2557
+ {
2558
+ "session_id": row["_id"],
2559
+ "user_id": row["user_id"],
2560
+ "agent_id": row["agent_id"],
2561
+ "team_id": row["team_id"],
2562
+ "workflow_id": row["workflow_id"],
2563
+ "total_traces": row["total_traces"],
2564
+ "first_trace_at": first_trace_at,
2565
+ "last_trace_at": last_trace_at,
2566
+ }
2567
+ )
2568
+
2569
+ return stats_list, total_count
2570
+
2571
+ except Exception as e:
2572
+ log_error(f"Error getting trace stats: {e}")
2573
+ return [], 0
2574
+
2575
+ # --- Spans ---
2576
+ async def create_span(self, span: "Span") -> None:
2577
+ """Create a single span in the database.
2578
+
2579
+ Args:
2580
+ span: The Span object to store.
2581
+ """
2582
+ try:
2583
+ collection = await self._get_collection(table_type="spans", create_collection_if_not_found=True)
2584
+ if collection is None:
2585
+ return
2586
+
2587
+ await collection.insert_one(span.to_dict())
2588
+
2589
+ except Exception as e:
2590
+ log_error(f"Error creating span: {e}")
2591
+
2592
+ async def create_spans(self, spans: List) -> None:
2593
+ """Create multiple spans in the database as a batch.
2594
+
2595
+ Args:
2596
+ spans: List of Span objects to store.
2597
+ """
2598
+ if not spans:
2599
+ return
2600
+
2601
+ try:
2602
+ collection = await self._get_collection(table_type="spans", create_collection_if_not_found=True)
2603
+ if collection is None:
2604
+ return
2605
+
2606
+ span_dicts = [span.to_dict() for span in spans]
2607
+ await collection.insert_many(span_dicts)
2608
+
2609
+ except Exception as e:
2610
+ log_error(f"Error creating spans batch: {e}")
2611
+
2612
+ async def get_span(self, span_id: str):
2613
+ """Get a single span by its span_id.
2614
+
2615
+ Args:
2616
+ span_id: The unique span identifier.
2617
+
2618
+ Returns:
2619
+ Optional[Span]: The span if found, None otherwise.
2620
+ """
2621
+ try:
2622
+ from agno.tracing.schemas import Span as SpanSchema
2623
+
2624
+ collection = await self._get_collection(table_type="spans")
2625
+ if collection is None:
2626
+ return None
2627
+
2628
+ result = await collection.find_one({"span_id": span_id})
2629
+ if result:
2630
+ # Remove MongoDB's _id field
2631
+ result.pop("_id", None)
2632
+ return SpanSchema.from_dict(result)
2633
+ return None
2634
+
2635
+ except Exception as e:
2636
+ log_error(f"Error getting span: {e}")
2637
+ return None
2638
+
2639
+ async def get_spans(
2640
+ self,
2641
+ trace_id: Optional[str] = None,
2642
+ parent_span_id: Optional[str] = None,
2643
+ limit: Optional[int] = 1000,
2644
+ ) -> List:
2645
+ """Get spans matching the provided filters.
2646
+
2647
+ Args:
2648
+ trace_id: Filter by trace ID.
2649
+ parent_span_id: Filter by parent span ID.
2650
+ limit: Maximum number of spans to return.
2651
+
2652
+ Returns:
2653
+ List[Span]: List of matching spans.
2654
+ """
2655
+ try:
2656
+ from agno.tracing.schemas import Span as SpanSchema
2657
+
2658
+ collection = await self._get_collection(table_type="spans")
2659
+ if collection is None:
2660
+ return []
2661
+
2662
+ # Build query
2663
+ query: Dict[str, Any] = {}
2664
+ if trace_id:
2665
+ query["trace_id"] = trace_id
2666
+ if parent_span_id:
2667
+ query["parent_span_id"] = parent_span_id
2668
+
2669
+ cursor = collection.find(query).limit(limit or 1000)
2670
+ results = await cursor.to_list(length=None)
2671
+
2672
+ spans = []
2673
+ for row in results:
2674
+ # Remove MongoDB's _id field
2675
+ row.pop("_id", None)
2676
+ spans.append(SpanSchema.from_dict(row))
2677
+
2678
+ return spans
2679
+
2680
+ except Exception as e:
2681
+ log_error(f"Error getting spans: {e}")
2682
+ return []