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
agno/db/mongo/mongo.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import time
2
2
  from datetime import date, datetime, timedelta, timezone
3
- from typing import Any, Dict, List, Optional, Tuple, Union
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
4
4
  from uuid import uuid4
5
5
 
6
+ if TYPE_CHECKING:
7
+ from agno.tracing.schemas import Span, Trace
8
+
6
9
  from agno.db.base import BaseDb, SessionType
7
10
  from agno.db.mongo.utils import (
8
11
  apply_pagination,
@@ -45,6 +48,8 @@ class MongoDb(BaseDb):
45
48
  eval_collection: Optional[str] = None,
46
49
  knowledge_collection: Optional[str] = None,
47
50
  culture_collection: Optional[str] = None,
51
+ traces_collection: Optional[str] = None,
52
+ spans_collection: Optional[str] = None,
48
53
  id: Optional[str] = None,
49
54
  ):
50
55
  """
@@ -60,6 +65,8 @@ class MongoDb(BaseDb):
60
65
  eval_collection (Optional[str]): Name of the collection to store evaluation runs.
61
66
  knowledge_collection (Optional[str]): Name of the collection to store knowledge documents.
62
67
  culture_collection (Optional[str]): Name of the collection to store cultural knowledge.
68
+ traces_collection (Optional[str]): Name of the collection to store traces.
69
+ spans_collection (Optional[str]): Name of the collection to store spans.
63
70
  id (Optional[str]): ID of the database.
64
71
 
65
72
  Raises:
@@ -79,6 +86,8 @@ class MongoDb(BaseDb):
79
86
  eval_table=eval_collection,
80
87
  knowledge_table=knowledge_collection,
81
88
  culture_table=culture_collection,
89
+ traces_table=traces_collection,
90
+ spans_table=spans_collection,
82
91
  )
83
92
 
84
93
  _client: Optional[MongoClient] = db_client
@@ -203,6 +212,28 @@ class MongoDb(BaseDb):
203
212
  )
204
213
  return self.culture_collection
205
214
 
215
+ if table_type == "traces":
216
+ if not hasattr(self, "traces_collection"):
217
+ if self.trace_table_name is None:
218
+ raise ValueError("Traces collection was not provided on initialization")
219
+ self.traces_collection = self._get_or_create_collection(
220
+ collection_name=self.trace_table_name,
221
+ collection_type="traces",
222
+ create_collection_if_not_found=create_collection_if_not_found,
223
+ )
224
+ return self.traces_collection
225
+
226
+ if table_type == "spans":
227
+ if not hasattr(self, "spans_collection"):
228
+ if self.span_table_name is None:
229
+ raise ValueError("Spans collection was not provided on initialization")
230
+ self.spans_collection = self._get_or_create_collection(
231
+ collection_name=self.span_table_name,
232
+ collection_type="spans",
233
+ create_collection_if_not_found=create_collection_if_not_found,
234
+ )
235
+ return self.spans_collection
236
+
206
237
  raise ValueError(f"Unknown table type: {table_type}")
207
238
 
208
239
  def _get_or_create_collection(
@@ -1995,3 +2026,494 @@ class MongoDb(BaseDb):
1995
2026
  for memory in memories:
1996
2027
  self.upsert_user_memory(memory)
1997
2028
  log_info(f"Migrated {len(memories)} memories to collection: {self.memory_table_name}")
2029
+
2030
+ # --- Traces ---
2031
+ def create_trace(self, trace: "Trace") -> None:
2032
+ """Create a single trace record in the database.
2033
+
2034
+ Args:
2035
+ trace: The Trace object to store (one per trace_id).
2036
+ """
2037
+ try:
2038
+ collection = self._get_collection(table_type="traces", create_collection_if_not_found=True)
2039
+ if collection is None:
2040
+ return
2041
+
2042
+ # Check if trace already exists
2043
+ existing = collection.find_one({"trace_id": trace.trace_id})
2044
+
2045
+ if existing:
2046
+ # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
2047
+ def get_component_level(
2048
+ workflow_id: Optional[str], team_id: Optional[str], agent_id: Optional[str], name: str
2049
+ ) -> int:
2050
+ # Check if name indicates a root span
2051
+ is_root_name = ".run" in name or ".arun" in name
2052
+
2053
+ if not is_root_name:
2054
+ return 0 # Child span (not a root)
2055
+ elif workflow_id:
2056
+ return 3 # Workflow root
2057
+ elif team_id:
2058
+ return 2 # Team root
2059
+ elif agent_id:
2060
+ return 1 # Agent root
2061
+ else:
2062
+ return 0 # Unknown
2063
+
2064
+ existing_level = get_component_level(
2065
+ existing.get("workflow_id"),
2066
+ existing.get("team_id"),
2067
+ existing.get("agent_id"),
2068
+ existing.get("name", ""),
2069
+ )
2070
+ new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
2071
+
2072
+ # Only update name if new trace is from a higher or equal level
2073
+ should_update_name = new_level > existing_level
2074
+
2075
+ # Parse existing start_time to calculate correct duration
2076
+ existing_start_time_str = existing.get("start_time")
2077
+ if isinstance(existing_start_time_str, str):
2078
+ existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
2079
+ else:
2080
+ existing_start_time = trace.start_time
2081
+
2082
+ recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
2083
+
2084
+ update_values: Dict[str, Any] = {
2085
+ "end_time": trace.end_time.isoformat(),
2086
+ "duration_ms": recalculated_duration_ms,
2087
+ "status": trace.status,
2088
+ "name": trace.name if should_update_name else existing.get("name"),
2089
+ }
2090
+
2091
+ # Update context fields ONLY if new value is not None (preserve non-null values)
2092
+ if trace.run_id is not None:
2093
+ update_values["run_id"] = trace.run_id
2094
+ if trace.session_id is not None:
2095
+ update_values["session_id"] = trace.session_id
2096
+ if trace.user_id is not None:
2097
+ update_values["user_id"] = trace.user_id
2098
+ if trace.agent_id is not None:
2099
+ update_values["agent_id"] = trace.agent_id
2100
+ if trace.team_id is not None:
2101
+ update_values["team_id"] = trace.team_id
2102
+ if trace.workflow_id is not None:
2103
+ update_values["workflow_id"] = trace.workflow_id
2104
+
2105
+ log_debug(
2106
+ f" Updating trace with context: run_id={update_values.get('run_id', 'unchanged')}, "
2107
+ f"session_id={update_values.get('session_id', 'unchanged')}, "
2108
+ f"user_id={update_values.get('user_id', 'unchanged')}, "
2109
+ f"agent_id={update_values.get('agent_id', 'unchanged')}, "
2110
+ f"team_id={update_values.get('team_id', 'unchanged')}, "
2111
+ )
2112
+
2113
+ collection.update_one({"trace_id": trace.trace_id}, {"$set": update_values})
2114
+ else:
2115
+ trace_dict = trace.to_dict()
2116
+ trace_dict.pop("total_spans", None)
2117
+ trace_dict.pop("error_count", None)
2118
+ collection.insert_one(trace_dict)
2119
+
2120
+ except Exception as e:
2121
+ log_error(f"Error creating trace: {e}")
2122
+ # Don't raise - tracing should not break the main application flow
2123
+
2124
+ def get_trace(
2125
+ self,
2126
+ trace_id: Optional[str] = None,
2127
+ run_id: Optional[str] = None,
2128
+ ):
2129
+ """Get a single trace by trace_id or other filters.
2130
+
2131
+ Args:
2132
+ trace_id: The unique trace identifier.
2133
+ run_id: Filter by run ID (returns first match).
2134
+
2135
+ Returns:
2136
+ Optional[Trace]: The trace if found, None otherwise.
2137
+
2138
+ Note:
2139
+ If multiple filters are provided, trace_id takes precedence.
2140
+ For other filters, the most recent trace is returned.
2141
+ """
2142
+ try:
2143
+ from agno.tracing.schemas import Trace as TraceSchema
2144
+
2145
+ collection = self._get_collection(table_type="traces")
2146
+ if collection is None:
2147
+ return None
2148
+
2149
+ # Get spans collection for aggregation
2150
+ spans_collection = self._get_collection(table_type="spans")
2151
+
2152
+ query: Dict[str, Any] = {}
2153
+ if trace_id:
2154
+ query["trace_id"] = trace_id
2155
+ elif run_id:
2156
+ query["run_id"] = run_id
2157
+ else:
2158
+ log_debug("get_trace called without any filter parameters")
2159
+ return None
2160
+
2161
+ # Find trace with sorting by most recent
2162
+ result = collection.find_one(query, sort=[("start_time", -1)])
2163
+
2164
+ if result:
2165
+ # Calculate total_spans and error_count from spans collection
2166
+ total_spans = 0
2167
+ error_count = 0
2168
+ if spans_collection is not None:
2169
+ total_spans = spans_collection.count_documents({"trace_id": result["trace_id"]})
2170
+ error_count = spans_collection.count_documents(
2171
+ {"trace_id": result["trace_id"], "status_code": "ERROR"}
2172
+ )
2173
+
2174
+ result["total_spans"] = total_spans
2175
+ result["error_count"] = error_count
2176
+ # Remove MongoDB's _id field
2177
+ result.pop("_id", None)
2178
+ return TraceSchema.from_dict(result)
2179
+ return None
2180
+
2181
+ except Exception as e:
2182
+ log_error(f"Error getting trace: {e}")
2183
+ return None
2184
+
2185
+ def get_traces(
2186
+ self,
2187
+ run_id: Optional[str] = None,
2188
+ session_id: Optional[str] = None,
2189
+ user_id: Optional[str] = None,
2190
+ agent_id: Optional[str] = None,
2191
+ team_id: Optional[str] = None,
2192
+ workflow_id: Optional[str] = None,
2193
+ status: Optional[str] = None,
2194
+ start_time: Optional[datetime] = None,
2195
+ end_time: Optional[datetime] = None,
2196
+ limit: Optional[int] = 20,
2197
+ page: Optional[int] = 1,
2198
+ ) -> tuple[List, int]:
2199
+ """Get traces matching the provided filters with pagination.
2200
+
2201
+ Args:
2202
+ run_id: Filter by run ID.
2203
+ session_id: Filter by session ID.
2204
+ user_id: Filter by user ID.
2205
+ agent_id: Filter by agent ID.
2206
+ team_id: Filter by team ID.
2207
+ workflow_id: Filter by workflow ID.
2208
+ status: Filter by status (OK, ERROR, UNSET).
2209
+ start_time: Filter traces starting after this datetime.
2210
+ end_time: Filter traces ending before this datetime.
2211
+ limit: Maximum number of traces to return per page.
2212
+ page: Page number (1-indexed).
2213
+
2214
+ Returns:
2215
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
2216
+ """
2217
+ try:
2218
+ from agno.tracing.schemas import Trace as TraceSchema
2219
+
2220
+ log_debug(
2221
+ f"get_traces called with filters: run_id={run_id}, session_id={session_id}, "
2222
+ f"user_id={user_id}, agent_id={agent_id}, page={page}, limit={limit}"
2223
+ )
2224
+
2225
+ collection = self._get_collection(table_type="traces")
2226
+ if collection is None:
2227
+ log_debug("Traces collection not found")
2228
+ return [], 0
2229
+
2230
+ # Get spans collection for aggregation
2231
+ spans_collection = self._get_collection(table_type="spans")
2232
+
2233
+ # Build query
2234
+ query: Dict[str, Any] = {}
2235
+ if run_id:
2236
+ query["run_id"] = run_id
2237
+ if session_id:
2238
+ log_debug(f"Filtering by session_id={session_id}")
2239
+ query["session_id"] = session_id
2240
+ if user_id:
2241
+ query["user_id"] = user_id
2242
+ if agent_id:
2243
+ query["agent_id"] = agent_id
2244
+ if team_id:
2245
+ query["team_id"] = team_id
2246
+ if workflow_id:
2247
+ query["workflow_id"] = workflow_id
2248
+ if status:
2249
+ query["status"] = status
2250
+ if start_time:
2251
+ query["start_time"] = {"$gte": start_time.isoformat()}
2252
+ if end_time:
2253
+ if "end_time" in query:
2254
+ query["end_time"]["$lte"] = end_time.isoformat()
2255
+ else:
2256
+ query["end_time"] = {"$lte": end_time.isoformat()}
2257
+
2258
+ # Get total count
2259
+ total_count = collection.count_documents(query)
2260
+ log_debug(f"Total matching traces: {total_count}")
2261
+
2262
+ # Apply pagination
2263
+ skip = ((page or 1) - 1) * (limit or 20)
2264
+ cursor = collection.find(query).sort("start_time", -1).skip(skip).limit(limit or 20)
2265
+
2266
+ results = list(cursor)
2267
+ log_debug(f"Returning page {page} with {len(results)} traces")
2268
+
2269
+ traces = []
2270
+ for row in results:
2271
+ # Calculate total_spans and error_count from spans collection
2272
+ total_spans = 0
2273
+ error_count = 0
2274
+ if spans_collection is not None:
2275
+ total_spans = spans_collection.count_documents({"trace_id": row["trace_id"]})
2276
+ error_count = spans_collection.count_documents(
2277
+ {"trace_id": row["trace_id"], "status_code": "ERROR"}
2278
+ )
2279
+
2280
+ row["total_spans"] = total_spans
2281
+ row["error_count"] = error_count
2282
+ # Remove MongoDB's _id field
2283
+ row.pop("_id", None)
2284
+ traces.append(TraceSchema.from_dict(row))
2285
+
2286
+ return traces, total_count
2287
+
2288
+ except Exception as e:
2289
+ log_error(f"Error getting traces: {e}")
2290
+ return [], 0
2291
+
2292
+ def get_trace_stats(
2293
+ self,
2294
+ user_id: Optional[str] = None,
2295
+ agent_id: Optional[str] = None,
2296
+ team_id: Optional[str] = None,
2297
+ workflow_id: Optional[str] = None,
2298
+ start_time: Optional[datetime] = None,
2299
+ end_time: Optional[datetime] = None,
2300
+ limit: Optional[int] = 20,
2301
+ page: Optional[int] = 1,
2302
+ ) -> tuple[List[Dict[str, Any]], int]:
2303
+ """Get trace statistics grouped by session.
2304
+
2305
+ Args:
2306
+ user_id: Filter by user ID.
2307
+ agent_id: Filter by agent ID.
2308
+ team_id: Filter by team ID.
2309
+ workflow_id: Filter by workflow ID.
2310
+ start_time: Filter sessions with traces created after this datetime.
2311
+ end_time: Filter sessions with traces created before this datetime.
2312
+ limit: Maximum number of sessions to return per page.
2313
+ page: Page number (1-indexed).
2314
+
2315
+ Returns:
2316
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
2317
+ Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
2318
+ workflow_id, first_trace_at, last_trace_at.
2319
+ """
2320
+ try:
2321
+ log_debug(
2322
+ f"get_trace_stats called with filters: user_id={user_id}, agent_id={agent_id}, "
2323
+ f"workflow_id={workflow_id}, team_id={team_id}, "
2324
+ f"start_time={start_time}, end_time={end_time}, page={page}, limit={limit}"
2325
+ )
2326
+
2327
+ collection = self._get_collection(table_type="traces")
2328
+ if collection is None:
2329
+ log_debug("Traces collection not found")
2330
+ return [], 0
2331
+
2332
+ # Build match stage
2333
+ match_stage: Dict[str, Any] = {"session_id": {"$ne": None}}
2334
+ if user_id:
2335
+ match_stage["user_id"] = user_id
2336
+ if agent_id:
2337
+ match_stage["agent_id"] = agent_id
2338
+ if team_id:
2339
+ match_stage["team_id"] = team_id
2340
+ if workflow_id:
2341
+ match_stage["workflow_id"] = workflow_id
2342
+ if start_time:
2343
+ match_stage["created_at"] = {"$gte": start_time.isoformat()}
2344
+ if end_time:
2345
+ if "created_at" in match_stage:
2346
+ match_stage["created_at"]["$lte"] = end_time.isoformat()
2347
+ else:
2348
+ match_stage["created_at"] = {"$lte": end_time.isoformat()}
2349
+
2350
+ # Build aggregation pipeline
2351
+ pipeline: List[Dict[str, Any]] = [
2352
+ {"$match": match_stage},
2353
+ {
2354
+ "$group": {
2355
+ "_id": "$session_id",
2356
+ "user_id": {"$first": "$user_id"},
2357
+ "agent_id": {"$first": "$agent_id"},
2358
+ "team_id": {"$first": "$team_id"},
2359
+ "workflow_id": {"$first": "$workflow_id"},
2360
+ "total_traces": {"$sum": 1},
2361
+ "first_trace_at": {"$min": "$created_at"},
2362
+ "last_trace_at": {"$max": "$created_at"},
2363
+ }
2364
+ },
2365
+ {"$sort": {"last_trace_at": -1}},
2366
+ ]
2367
+
2368
+ # Get total count
2369
+ count_pipeline = pipeline + [{"$count": "total"}]
2370
+ count_result = list(collection.aggregate(count_pipeline))
2371
+ total_count = count_result[0]["total"] if count_result else 0
2372
+ log_debug(f"Total matching sessions: {total_count}")
2373
+
2374
+ # Apply pagination
2375
+ skip = ((page or 1) - 1) * (limit or 20)
2376
+ pipeline.append({"$skip": skip})
2377
+ pipeline.append({"$limit": limit or 20})
2378
+
2379
+ results = list(collection.aggregate(pipeline))
2380
+ log_debug(f"Returning page {page} with {len(results)} session stats")
2381
+
2382
+ # Convert to list of dicts with datetime objects
2383
+ stats_list = []
2384
+ for row in results:
2385
+ # Convert ISO strings to datetime objects
2386
+ first_trace_at_str = row["first_trace_at"]
2387
+ last_trace_at_str = row["last_trace_at"]
2388
+
2389
+ # Parse ISO format strings to datetime objects
2390
+ first_trace_at = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
2391
+ last_trace_at = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
2392
+
2393
+ stats_list.append(
2394
+ {
2395
+ "session_id": row["_id"],
2396
+ "user_id": row["user_id"],
2397
+ "agent_id": row["agent_id"],
2398
+ "team_id": row["team_id"],
2399
+ "workflow_id": row["workflow_id"],
2400
+ "total_traces": row["total_traces"],
2401
+ "first_trace_at": first_trace_at,
2402
+ "last_trace_at": last_trace_at,
2403
+ }
2404
+ )
2405
+
2406
+ return stats_list, total_count
2407
+
2408
+ except Exception as e:
2409
+ log_error(f"Error getting trace stats: {e}")
2410
+ return [], 0
2411
+
2412
+ # --- Spans ---
2413
+ def create_span(self, span: "Span") -> None:
2414
+ """Create a single span in the database.
2415
+
2416
+ Args:
2417
+ span: The Span object to store.
2418
+ """
2419
+ try:
2420
+ collection = self._get_collection(table_type="spans", create_collection_if_not_found=True)
2421
+ if collection is None:
2422
+ return
2423
+
2424
+ collection.insert_one(span.to_dict())
2425
+
2426
+ except Exception as e:
2427
+ log_error(f"Error creating span: {e}")
2428
+
2429
+ def create_spans(self, spans: List) -> None:
2430
+ """Create multiple spans in the database as a batch.
2431
+
2432
+ Args:
2433
+ spans: List of Span objects to store.
2434
+ """
2435
+ if not spans:
2436
+ return
2437
+
2438
+ try:
2439
+ collection = self._get_collection(table_type="spans", create_collection_if_not_found=True)
2440
+ if collection is None:
2441
+ return
2442
+
2443
+ span_dicts = [span.to_dict() for span in spans]
2444
+ collection.insert_many(span_dicts)
2445
+
2446
+ except Exception as e:
2447
+ log_error(f"Error creating spans batch: {e}")
2448
+
2449
+ def get_span(self, span_id: str):
2450
+ """Get a single span by its span_id.
2451
+
2452
+ Args:
2453
+ span_id: The unique span identifier.
2454
+
2455
+ Returns:
2456
+ Optional[Span]: The span if found, None otherwise.
2457
+ """
2458
+ try:
2459
+ from agno.tracing.schemas import Span as SpanSchema
2460
+
2461
+ collection = self._get_collection(table_type="spans")
2462
+ if collection is None:
2463
+ return None
2464
+
2465
+ result = collection.find_one({"span_id": span_id})
2466
+ if result:
2467
+ # Remove MongoDB's _id field
2468
+ result.pop("_id", None)
2469
+ return SpanSchema.from_dict(result)
2470
+ return None
2471
+
2472
+ except Exception as e:
2473
+ log_error(f"Error getting span: {e}")
2474
+ return None
2475
+
2476
+ def get_spans(
2477
+ self,
2478
+ trace_id: Optional[str] = None,
2479
+ parent_span_id: Optional[str] = None,
2480
+ limit: Optional[int] = 1000,
2481
+ ) -> List:
2482
+ """Get spans matching the provided filters.
2483
+
2484
+ Args:
2485
+ trace_id: Filter by trace ID.
2486
+ parent_span_id: Filter by parent span ID.
2487
+ limit: Maximum number of spans to return.
2488
+
2489
+ Returns:
2490
+ List[Span]: List of matching spans.
2491
+ """
2492
+ try:
2493
+ from agno.tracing.schemas import Span as SpanSchema
2494
+
2495
+ collection = self._get_collection(table_type="spans")
2496
+ if collection is None:
2497
+ return []
2498
+
2499
+ # Build query
2500
+ query: Dict[str, Any] = {}
2501
+ if trace_id:
2502
+ query["trace_id"] = trace_id
2503
+ if parent_span_id:
2504
+ query["parent_span_id"] = parent_span_id
2505
+
2506
+ cursor = collection.find(query).limit(limit or 1000)
2507
+ results = list(cursor)
2508
+
2509
+ spans = []
2510
+ for row in results:
2511
+ # Remove MongoDB's _id field
2512
+ row.pop("_id", None)
2513
+ spans.append(SpanSchema.from_dict(row))
2514
+
2515
+ return spans
2516
+
2517
+ except Exception as e:
2518
+ log_error(f"Error getting spans: {e}")
2519
+ return []
agno/db/mongo/schemas.py CHANGED
@@ -71,6 +71,33 @@ CULTURAL_KNOWLEDGE_COLLECTION_SCHEMA = [
71
71
  {"key": "updated_at"},
72
72
  ]
73
73
 
74
+ TRACE_COLLECTION_SCHEMA = [
75
+ {"key": "trace_id", "unique": True},
76
+ {"key": "name"},
77
+ {"key": "status"},
78
+ {"key": "run_id"},
79
+ {"key": "session_id"},
80
+ {"key": "user_id"},
81
+ {"key": "agent_id"},
82
+ {"key": "team_id"},
83
+ {"key": "workflow_id"},
84
+ {"key": "start_time"},
85
+ {"key": "end_time"},
86
+ {"key": "created_at"},
87
+ ]
88
+
89
+ SPAN_COLLECTION_SCHEMA = [
90
+ {"key": "span_id", "unique": True},
91
+ {"key": "trace_id"},
92
+ {"key": "parent_span_id"},
93
+ {"key": "name"},
94
+ {"key": "span_kind"},
95
+ {"key": "status_code"},
96
+ {"key": "start_time"},
97
+ {"key": "end_time"},
98
+ {"key": "created_at"},
99
+ ]
100
+
74
101
 
75
102
  def get_collection_indexes(collection_type: str) -> List[Dict[str, Any]]:
76
103
  """Get the index definitions for a specific collection type."""
@@ -81,6 +108,8 @@ def get_collection_indexes(collection_type: str) -> List[Dict[str, Any]]:
81
108
  "evals": EVAL_COLLECTION_SCHEMA,
82
109
  "knowledge": KNOWLEDGE_COLLECTION_SCHEMA,
83
110
  "culture": CULTURAL_KNOWLEDGE_COLLECTION_SCHEMA,
111
+ "traces": TRACE_COLLECTION_SCHEMA,
112
+ "spans": SPAN_COLLECTION_SCHEMA,
84
113
  }
85
114
 
86
115
  indexes = index_definitions.get(collection_type)