agno 2.3.4__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.
- agno/agent/agent.py +177 -41
- agno/culture/manager.py +2 -2
- agno/db/base.py +330 -8
- agno/db/dynamo/dynamo.py +722 -2
- agno/db/dynamo/schemas.py +127 -0
- agno/db/firestore/firestore.py +573 -1
- agno/db/firestore/schemas.py +40 -0
- agno/db/gcs_json/gcs_json_db.py +446 -1
- agno/db/in_memory/in_memory_db.py +143 -1
- agno/db/json/json_db.py +438 -1
- agno/db/mongo/async_mongo.py +522 -0
- agno/db/mongo/mongo.py +523 -1
- agno/db/mongo/schemas.py +29 -0
- agno/db/mysql/mysql.py +536 -3
- agno/db/mysql/schemas.py +38 -0
- agno/db/postgres/async_postgres.py +541 -13
- agno/db/postgres/postgres.py +535 -2
- agno/db/postgres/schemas.py +38 -0
- agno/db/redis/redis.py +468 -1
- agno/db/redis/schemas.py +32 -0
- agno/db/singlestore/schemas.py +38 -0
- agno/db/singlestore/singlestore.py +523 -1
- agno/db/sqlite/async_sqlite.py +548 -9
- agno/db/sqlite/schemas.py +38 -0
- agno/db/sqlite/sqlite.py +537 -5
- agno/db/sqlite/utils.py +6 -8
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +548 -1
- agno/eval/accuracy.py +10 -4
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/exceptions.py +11 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/knowledge/chunking/semantic.py +2 -2
- agno/models/aimlapi/aimlapi.py +2 -3
- agno/models/anthropic/claude.py +18 -13
- agno/models/aws/bedrock.py +3 -4
- agno/models/aws/claude.py +5 -1
- agno/models/azure/ai_foundry.py +2 -2
- agno/models/azure/openai_chat.py +8 -0
- agno/models/cerebras/cerebras.py +63 -11
- agno/models/cerebras/cerebras_openai.py +2 -3
- agno/models/cohere/chat.py +1 -5
- agno/models/cometapi/cometapi.py +2 -3
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +2 -3
- agno/models/deepseek/deepseek.py +2 -3
- agno/models/fireworks/fireworks.py +2 -3
- agno/models/google/gemini.py +9 -7
- agno/models/groq/groq.py +2 -3
- agno/models/huggingface/huggingface.py +1 -5
- agno/models/ibm/watsonx.py +1 -5
- agno/models/internlm/internlm.py +2 -3
- agno/models/langdb/langdb.py +6 -4
- agno/models/litellm/chat.py +2 -2
- agno/models/litellm/litellm_openai.py +2 -3
- agno/models/meta/llama.py +1 -5
- agno/models/meta/llama_openai.py +4 -5
- agno/models/mistral/mistral.py +1 -5
- agno/models/nebius/nebius.py +2 -3
- agno/models/nvidia/nvidia.py +4 -5
- agno/models/openai/chat.py +14 -3
- agno/models/openai/responses.py +14 -3
- agno/models/openrouter/openrouter.py +4 -5
- agno/models/perplexity/perplexity.py +2 -3
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +4 -5
- agno/models/response.py +2 -1
- agno/models/sambanova/sambanova.py +4 -5
- agno/models/siliconflow/siliconflow.py +3 -4
- agno/models/together/together.py +4 -5
- agno/models/vercel/v0.py +4 -5
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +4 -5
- agno/os/app.py +104 -0
- agno/os/config.py +13 -0
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/mcp.py +1 -0
- agno/os/router.py +31 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/schema.py +10 -1
- agno/os/utils.py +57 -0
- agno/run/agent.py +1 -0
- agno/run/base.py +17 -0
- agno/run/team.py +4 -0
- agno/session/team.py +1 -0
- agno/table.py +10 -0
- agno/team/team.py +214 -65
- agno/tools/function.py +10 -8
- agno/tools/nano_banana.py +1 -1
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +4 -4
- agno/utils/hooks.py +56 -1
- agno/vectordb/qdrant/qdrant.py +22 -22
- agno/workflow/condition.py +8 -0
- agno/workflow/loop.py +8 -0
- agno/workflow/parallel.py +8 -0
- agno/workflow/router.py +8 -0
- agno/workflow/step.py +20 -0
- agno/workflow/steps.py +8 -0
- agno/workflow/workflow.py +83 -17
- {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/METADATA +2 -2
- {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/RECORD +112 -102
- {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/WHEEL +0 -0
- {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/top_level.txt +0 -0
agno/db/sqlite/schemas.py
CHANGED
|
@@ -95,6 +95,42 @@ METRICS_TABLE_SCHEMA = {
|
|
|
95
95
|
],
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
TRACE_TABLE_SCHEMA = {
|
|
99
|
+
"trace_id": {"type": String, "primary_key": True, "nullable": False},
|
|
100
|
+
"name": {"type": String, "nullable": False},
|
|
101
|
+
"status": {"type": String, "nullable": False, "index": True},
|
|
102
|
+
"start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
103
|
+
"end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
|
|
104
|
+
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
105
|
+
"run_id": {"type": String, "nullable": True, "index": True},
|
|
106
|
+
"session_id": {"type": String, "nullable": True, "index": True},
|
|
107
|
+
"user_id": {"type": String, "nullable": True, "index": True},
|
|
108
|
+
"agent_id": {"type": String, "nullable": True, "index": True},
|
|
109
|
+
"team_id": {"type": String, "nullable": True, "index": True},
|
|
110
|
+
"workflow_id": {"type": String, "nullable": True, "index": True},
|
|
111
|
+
"created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
SPAN_TABLE_SCHEMA = {
|
|
115
|
+
"span_id": {"type": String, "primary_key": True, "nullable": False},
|
|
116
|
+
"trace_id": {
|
|
117
|
+
"type": String,
|
|
118
|
+
"nullable": False,
|
|
119
|
+
"index": True,
|
|
120
|
+
"foreign_key": "agno_traces.trace_id", # Foreign key to traces table
|
|
121
|
+
},
|
|
122
|
+
"parent_span_id": {"type": String, "nullable": True, "index": True},
|
|
123
|
+
"name": {"type": String, "nullable": False},
|
|
124
|
+
"span_kind": {"type": String, "nullable": False},
|
|
125
|
+
"status_code": {"type": String, "nullable": False},
|
|
126
|
+
"status_message": {"type": String, "nullable": True},
|
|
127
|
+
"start_time": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
128
|
+
"end_time": {"type": String, "nullable": False}, # ISO 8601 datetime string
|
|
129
|
+
"duration_ms": {"type": BigInteger, "nullable": False},
|
|
130
|
+
"attributes": {"type": JSON, "nullable": True},
|
|
131
|
+
"created_at": {"type": String, "nullable": False, "index": True}, # ISO 8601 datetime string
|
|
132
|
+
}
|
|
133
|
+
|
|
98
134
|
CULTURAL_KNOWLEDGE_TABLE_SCHEMA = {
|
|
99
135
|
"id": {"type": String, "primary_key": True, "nullable": False},
|
|
100
136
|
"name": {"type": String, "nullable": False, "index": True},
|
|
@@ -132,6 +168,8 @@ def get_table_schema_definition(table_type: str) -> dict[str, Any]:
|
|
|
132
168
|
"metrics": METRICS_TABLE_SCHEMA,
|
|
133
169
|
"memories": USER_MEMORY_TABLE_SCHEMA,
|
|
134
170
|
"knowledge": KNOWLEDGE_TABLE_SCHEMA,
|
|
171
|
+
"traces": TRACE_TABLE_SCHEMA,
|
|
172
|
+
"spans": SPAN_TABLE_SCHEMA,
|
|
135
173
|
"culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
|
|
136
174
|
"versions": VERSIONS_TABLE_SCHEMA,
|
|
137
175
|
}
|
agno/db/sqlite/sqlite.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from datetime import date, datetime, timedelta, timezone
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, cast
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union, cast
|
|
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 BaseDb, SessionType
|
|
8
11
|
from agno.db.migrations.manager import MigrationManager
|
|
9
12
|
from agno.db.schemas.culture import CulturalKnowledge
|
|
@@ -28,11 +31,11 @@ from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
|
28
31
|
from agno.utils.string import generate_id
|
|
29
32
|
|
|
30
33
|
try:
|
|
31
|
-
from sqlalchemy import Column, MetaData, String, Table, func, select, text
|
|
34
|
+
from sqlalchemy import Column, MetaData, String, Table, func, select, text, update
|
|
32
35
|
from sqlalchemy.dialects import sqlite
|
|
33
36
|
from sqlalchemy.engine import Engine, create_engine
|
|
34
37
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
35
|
-
from sqlalchemy.schema import Index, UniqueConstraint
|
|
38
|
+
from sqlalchemy.schema import ForeignKey, Index, UniqueConstraint
|
|
36
39
|
except ImportError:
|
|
37
40
|
raise ImportError("`sqlalchemy` not installed. Please install it using `pip install sqlalchemy`")
|
|
38
41
|
|
|
@@ -49,6 +52,8 @@ class SqliteDb(BaseDb):
|
|
|
49
52
|
metrics_table: Optional[str] = None,
|
|
50
53
|
eval_table: Optional[str] = None,
|
|
51
54
|
knowledge_table: Optional[str] = None,
|
|
55
|
+
traces_table: Optional[str] = None,
|
|
56
|
+
spans_table: Optional[str] = None,
|
|
52
57
|
versions_table: Optional[str] = None,
|
|
53
58
|
id: Optional[str] = None,
|
|
54
59
|
):
|
|
@@ -71,6 +76,8 @@ class SqliteDb(BaseDb):
|
|
|
71
76
|
metrics_table (Optional[str]): Name of the table to store metrics.
|
|
72
77
|
eval_table (Optional[str]): Name of the table to store evaluation runs data.
|
|
73
78
|
knowledge_table (Optional[str]): Name of the table to store knowledge documents data.
|
|
79
|
+
traces_table (Optional[str]): Name of the table to store run traces.
|
|
80
|
+
spans_table (Optional[str]): Name of the table to store span events.
|
|
74
81
|
versions_table (Optional[str]): Name of the table to store schema versions.
|
|
75
82
|
id (Optional[str]): ID of the database.
|
|
76
83
|
|
|
@@ -89,6 +96,8 @@ class SqliteDb(BaseDb):
|
|
|
89
96
|
metrics_table=metrics_table,
|
|
90
97
|
eval_table=eval_table,
|
|
91
98
|
knowledge_table=knowledge_table,
|
|
99
|
+
traces_table=traces_table,
|
|
100
|
+
spans_table=spans_table,
|
|
92
101
|
versions_table=versions_table,
|
|
93
102
|
)
|
|
94
103
|
|
|
@@ -155,7 +164,7 @@ class SqliteDb(BaseDb):
|
|
|
155
164
|
Table: SQLAlchemy Table object
|
|
156
165
|
"""
|
|
157
166
|
try:
|
|
158
|
-
table_schema = get_table_schema_definition(table_type)
|
|
167
|
+
table_schema = get_table_schema_definition(table_type).copy()
|
|
159
168
|
|
|
160
169
|
columns: List[Column] = []
|
|
161
170
|
indexes: List[str] = []
|
|
@@ -177,6 +186,15 @@ class SqliteDb(BaseDb):
|
|
|
177
186
|
column_kwargs["unique"] = True
|
|
178
187
|
unique_constraints.append(col_name)
|
|
179
188
|
|
|
189
|
+
# Handle foreign key constraint
|
|
190
|
+
if "foreign_key" in col_config:
|
|
191
|
+
fk_ref = col_config["foreign_key"]
|
|
192
|
+
# For spans table, dynamically replace the traces table reference
|
|
193
|
+
# with the actual trace table name configured for this db instance
|
|
194
|
+
if table_type == "spans" and "trace_id" in fk_ref:
|
|
195
|
+
fk_ref = f"{self.trace_table_name}.trace_id"
|
|
196
|
+
column_args.append(ForeignKey(fk_ref))
|
|
197
|
+
|
|
180
198
|
columns.append(Column(*column_args, **column_kwargs)) # type: ignore
|
|
181
199
|
|
|
182
200
|
# Create the table object
|
|
@@ -275,6 +293,26 @@ class SqliteDb(BaseDb):
|
|
|
275
293
|
)
|
|
276
294
|
return self.knowledge_table
|
|
277
295
|
|
|
296
|
+
elif table_type == "traces":
|
|
297
|
+
self.traces_table = self._get_or_create_table(
|
|
298
|
+
table_name=self.trace_table_name,
|
|
299
|
+
table_type="traces",
|
|
300
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
301
|
+
)
|
|
302
|
+
return self.traces_table
|
|
303
|
+
|
|
304
|
+
elif table_type == "spans":
|
|
305
|
+
# Ensure traces table exists first (spans has FK to traces)
|
|
306
|
+
if create_table_if_not_found:
|
|
307
|
+
self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
308
|
+
|
|
309
|
+
self.spans_table = self._get_or_create_table(
|
|
310
|
+
table_name=self.span_table_name,
|
|
311
|
+
table_type="spans",
|
|
312
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
313
|
+
)
|
|
314
|
+
return self.spans_table
|
|
315
|
+
|
|
278
316
|
elif table_type == "culture":
|
|
279
317
|
self.culture_table = self._get_or_create_table(
|
|
280
318
|
table_name=self.culture_table_name,
|
|
@@ -316,7 +354,6 @@ class SqliteDb(BaseDb):
|
|
|
316
354
|
if not table_is_available:
|
|
317
355
|
if not create_table_if_not_found:
|
|
318
356
|
return None
|
|
319
|
-
|
|
320
357
|
return self._create_table(table_name=table_name, table_type=table_type)
|
|
321
358
|
|
|
322
359
|
# SQLite version of table validation (no schema)
|
|
@@ -2079,6 +2116,501 @@ class SqliteDb(BaseDb):
|
|
|
2079
2116
|
log_error(f"Error renaming eval run {eval_run_id}: {e}")
|
|
2080
2117
|
raise e
|
|
2081
2118
|
|
|
2119
|
+
# -- Trace methods --
|
|
2120
|
+
|
|
2121
|
+
def _get_traces_base_query(self, table: Table, spans_table: Optional[Table] = None):
|
|
2122
|
+
"""Build base query for traces with aggregated span counts.
|
|
2123
|
+
|
|
2124
|
+
Args:
|
|
2125
|
+
table: The traces table.
|
|
2126
|
+
spans_table: The spans table (optional).
|
|
2127
|
+
|
|
2128
|
+
Returns:
|
|
2129
|
+
SQLAlchemy select statement with total_spans and error_count calculated dynamically.
|
|
2130
|
+
"""
|
|
2131
|
+
from sqlalchemy import case, func, literal
|
|
2132
|
+
|
|
2133
|
+
if spans_table is not None:
|
|
2134
|
+
# JOIN with spans table to calculate total_spans and error_count
|
|
2135
|
+
return (
|
|
2136
|
+
select(
|
|
2137
|
+
table,
|
|
2138
|
+
func.coalesce(func.count(spans_table.c.span_id), 0).label("total_spans"),
|
|
2139
|
+
func.coalesce(func.sum(case((spans_table.c.status_code == "ERROR", 1), else_=0)), 0).label(
|
|
2140
|
+
"error_count"
|
|
2141
|
+
),
|
|
2142
|
+
)
|
|
2143
|
+
.select_from(table.outerjoin(spans_table, table.c.trace_id == spans_table.c.trace_id))
|
|
2144
|
+
.group_by(table.c.trace_id)
|
|
2145
|
+
)
|
|
2146
|
+
else:
|
|
2147
|
+
# Fallback if spans table doesn't exist
|
|
2148
|
+
return select(table, literal(0).label("total_spans"), literal(0).label("error_count"))
|
|
2149
|
+
|
|
2150
|
+
def create_trace(self, trace: "Trace") -> None:
|
|
2151
|
+
"""Create a single trace record in the database.
|
|
2152
|
+
|
|
2153
|
+
Args:
|
|
2154
|
+
trace: The Trace object to store (one per trace_id).
|
|
2155
|
+
"""
|
|
2156
|
+
try:
|
|
2157
|
+
table = self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
2158
|
+
if table is None:
|
|
2159
|
+
return
|
|
2160
|
+
|
|
2161
|
+
with self.Session() as sess, sess.begin():
|
|
2162
|
+
# Check if trace exists
|
|
2163
|
+
existing = sess.execute(table.select().where(table.c.trace_id == trace.trace_id)).fetchone()
|
|
2164
|
+
|
|
2165
|
+
if existing:
|
|
2166
|
+
# workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
|
|
2167
|
+
|
|
2168
|
+
def get_component_level(workflow_id, team_id, agent_id, name):
|
|
2169
|
+
# Check if name indicates a root span
|
|
2170
|
+
is_root_name = ".run" in name or ".arun" in name
|
|
2171
|
+
|
|
2172
|
+
if not is_root_name:
|
|
2173
|
+
return 0 # Child span (not a root)
|
|
2174
|
+
elif workflow_id:
|
|
2175
|
+
return 3 # Workflow root
|
|
2176
|
+
elif team_id:
|
|
2177
|
+
return 2 # Team root
|
|
2178
|
+
elif agent_id:
|
|
2179
|
+
return 1 # Agent root
|
|
2180
|
+
else:
|
|
2181
|
+
return 0 # Unknown
|
|
2182
|
+
|
|
2183
|
+
existing_level = get_component_level(
|
|
2184
|
+
existing.workflow_id, existing.team_id, existing.agent_id, existing.name
|
|
2185
|
+
)
|
|
2186
|
+
new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
|
|
2187
|
+
|
|
2188
|
+
# Only update name if new trace is from a higher or equal level
|
|
2189
|
+
should_update_name = new_level > existing_level
|
|
2190
|
+
|
|
2191
|
+
# Parse existing start_time to calculate correct duration
|
|
2192
|
+
existing_start_time_str = existing.start_time
|
|
2193
|
+
if isinstance(existing_start_time_str, str):
|
|
2194
|
+
existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
|
|
2195
|
+
else:
|
|
2196
|
+
existing_start_time = trace.start_time
|
|
2197
|
+
|
|
2198
|
+
recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
|
|
2199
|
+
|
|
2200
|
+
update_values = {
|
|
2201
|
+
"end_time": trace.end_time.isoformat(),
|
|
2202
|
+
"duration_ms": recalculated_duration_ms,
|
|
2203
|
+
"status": trace.status,
|
|
2204
|
+
"name": trace.name if should_update_name else existing.name,
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
# Update context fields ONLY if new value is not None (preserve non-null values)
|
|
2208
|
+
if trace.run_id is not None:
|
|
2209
|
+
update_values["run_id"] = trace.run_id
|
|
2210
|
+
if trace.session_id is not None:
|
|
2211
|
+
update_values["session_id"] = trace.session_id
|
|
2212
|
+
if trace.user_id is not None:
|
|
2213
|
+
update_values["user_id"] = trace.user_id
|
|
2214
|
+
if trace.agent_id is not None:
|
|
2215
|
+
update_values["agent_id"] = trace.agent_id
|
|
2216
|
+
if trace.team_id is not None:
|
|
2217
|
+
update_values["team_id"] = trace.team_id
|
|
2218
|
+
if trace.workflow_id is not None:
|
|
2219
|
+
update_values["workflow_id"] = trace.workflow_id
|
|
2220
|
+
|
|
2221
|
+
log_debug(
|
|
2222
|
+
f" Updating trace with context: run_id={update_values.get('run_id', 'unchanged')}, "
|
|
2223
|
+
f"session_id={update_values.get('session_id', 'unchanged')}, "
|
|
2224
|
+
f"user_id={update_values.get('user_id', 'unchanged')}, "
|
|
2225
|
+
f"agent_id={update_values.get('agent_id', 'unchanged')}, "
|
|
2226
|
+
f"team_id={update_values.get('team_id', 'unchanged')}, "
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2229
|
+
stmt = update(table).where(table.c.trace_id == trace.trace_id).values(**update_values)
|
|
2230
|
+
sess.execute(stmt)
|
|
2231
|
+
else:
|
|
2232
|
+
trace_dict = trace.to_dict()
|
|
2233
|
+
trace_dict.pop("total_spans", None)
|
|
2234
|
+
trace_dict.pop("error_count", None)
|
|
2235
|
+
stmt = sqlite.insert(table).values(trace_dict)
|
|
2236
|
+
sess.execute(stmt)
|
|
2237
|
+
|
|
2238
|
+
except Exception as e:
|
|
2239
|
+
log_error(f"Error creating trace: {e}")
|
|
2240
|
+
# Don't raise - tracing should not break the main application flow
|
|
2241
|
+
|
|
2242
|
+
def get_trace(
|
|
2243
|
+
self,
|
|
2244
|
+
trace_id: Optional[str] = None,
|
|
2245
|
+
run_id: Optional[str] = None,
|
|
2246
|
+
):
|
|
2247
|
+
"""Get a single trace by trace_id or other filters.
|
|
2248
|
+
|
|
2249
|
+
Args:
|
|
2250
|
+
trace_id: The unique trace identifier.
|
|
2251
|
+
run_id: Filter by run ID (returns first match).
|
|
2252
|
+
|
|
2253
|
+
Returns:
|
|
2254
|
+
Optional[Trace]: The trace if found, None otherwise.
|
|
2255
|
+
|
|
2256
|
+
Note:
|
|
2257
|
+
If multiple filters are provided, trace_id takes precedence.
|
|
2258
|
+
For other filters, the most recent trace is returned.
|
|
2259
|
+
"""
|
|
2260
|
+
try:
|
|
2261
|
+
from agno.tracing.schemas import Trace
|
|
2262
|
+
|
|
2263
|
+
table = self._get_table(table_type="traces")
|
|
2264
|
+
if table is None:
|
|
2265
|
+
return None
|
|
2266
|
+
|
|
2267
|
+
# Get spans table for JOIN
|
|
2268
|
+
spans_table = self._get_table(table_type="spans")
|
|
2269
|
+
|
|
2270
|
+
with self.Session() as sess:
|
|
2271
|
+
# Build query with aggregated span counts
|
|
2272
|
+
stmt = self._get_traces_base_query(table, spans_table)
|
|
2273
|
+
|
|
2274
|
+
if trace_id:
|
|
2275
|
+
stmt = stmt.where(table.c.trace_id == trace_id)
|
|
2276
|
+
elif run_id:
|
|
2277
|
+
stmt = stmt.where(table.c.run_id == run_id)
|
|
2278
|
+
else:
|
|
2279
|
+
log_debug("get_trace called without any filter parameters")
|
|
2280
|
+
return None
|
|
2281
|
+
|
|
2282
|
+
# Order by most recent and get first result
|
|
2283
|
+
stmt = stmt.order_by(table.c.start_time.desc()).limit(1)
|
|
2284
|
+
result = sess.execute(stmt).fetchone()
|
|
2285
|
+
|
|
2286
|
+
if result:
|
|
2287
|
+
return Trace.from_dict(dict(result._mapping))
|
|
2288
|
+
return None
|
|
2289
|
+
|
|
2290
|
+
except Exception as e:
|
|
2291
|
+
log_error(f"Error getting trace: {e}")
|
|
2292
|
+
return None
|
|
2293
|
+
|
|
2294
|
+
def get_traces(
|
|
2295
|
+
self,
|
|
2296
|
+
run_id: Optional[str] = None,
|
|
2297
|
+
session_id: Optional[str] = None,
|
|
2298
|
+
user_id: Optional[str] = None,
|
|
2299
|
+
agent_id: Optional[str] = None,
|
|
2300
|
+
team_id: Optional[str] = None,
|
|
2301
|
+
workflow_id: Optional[str] = None,
|
|
2302
|
+
status: Optional[str] = None,
|
|
2303
|
+
start_time: Optional[datetime] = None,
|
|
2304
|
+
end_time: Optional[datetime] = None,
|
|
2305
|
+
limit: Optional[int] = 20,
|
|
2306
|
+
page: Optional[int] = 1,
|
|
2307
|
+
) -> tuple[List, int]:
|
|
2308
|
+
"""Get traces matching the provided filters with pagination.
|
|
2309
|
+
|
|
2310
|
+
Args:
|
|
2311
|
+
run_id: Filter by run ID.
|
|
2312
|
+
session_id: Filter by session ID.
|
|
2313
|
+
user_id: Filter by user ID.
|
|
2314
|
+
agent_id: Filter by agent ID.
|
|
2315
|
+
team_id: Filter by team ID.
|
|
2316
|
+
workflow_id: Filter by workflow ID.
|
|
2317
|
+
status: Filter by status (OK, ERROR, UNSET).
|
|
2318
|
+
start_time: Filter traces starting after this datetime.
|
|
2319
|
+
end_time: Filter traces ending before this datetime.
|
|
2320
|
+
limit: Maximum number of traces to return per page.
|
|
2321
|
+
page: Page number (1-indexed).
|
|
2322
|
+
|
|
2323
|
+
Returns:
|
|
2324
|
+
tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
|
|
2325
|
+
"""
|
|
2326
|
+
try:
|
|
2327
|
+
from sqlalchemy import func
|
|
2328
|
+
|
|
2329
|
+
from agno.tracing.schemas import Trace
|
|
2330
|
+
|
|
2331
|
+
log_debug(
|
|
2332
|
+
f"get_traces called with filters: run_id={run_id}, session_id={session_id}, user_id={user_id}, agent_id={agent_id}, page={page}, limit={limit}"
|
|
2333
|
+
)
|
|
2334
|
+
|
|
2335
|
+
table = self._get_table(table_type="traces")
|
|
2336
|
+
if table is None:
|
|
2337
|
+
log_debug(" Traces table not found")
|
|
2338
|
+
return [], 0
|
|
2339
|
+
|
|
2340
|
+
# Get spans table for JOIN
|
|
2341
|
+
spans_table = self._get_table(table_type="spans")
|
|
2342
|
+
|
|
2343
|
+
with self.Session() as sess:
|
|
2344
|
+
# Build base query with aggregated span counts
|
|
2345
|
+
base_stmt = self._get_traces_base_query(table, spans_table)
|
|
2346
|
+
|
|
2347
|
+
# Apply filters
|
|
2348
|
+
if run_id:
|
|
2349
|
+
base_stmt = base_stmt.where(table.c.run_id == run_id)
|
|
2350
|
+
if session_id:
|
|
2351
|
+
log_debug(f"Filtering by session_id={session_id}")
|
|
2352
|
+
base_stmt = base_stmt.where(table.c.session_id == session_id)
|
|
2353
|
+
if user_id:
|
|
2354
|
+
base_stmt = base_stmt.where(table.c.user_id == user_id)
|
|
2355
|
+
if agent_id:
|
|
2356
|
+
base_stmt = base_stmt.where(table.c.agent_id == agent_id)
|
|
2357
|
+
if team_id:
|
|
2358
|
+
base_stmt = base_stmt.where(table.c.team_id == team_id)
|
|
2359
|
+
if workflow_id:
|
|
2360
|
+
base_stmt = base_stmt.where(table.c.workflow_id == workflow_id)
|
|
2361
|
+
if status:
|
|
2362
|
+
base_stmt = base_stmt.where(table.c.status == status)
|
|
2363
|
+
if start_time:
|
|
2364
|
+
# Convert datetime to ISO string for comparison
|
|
2365
|
+
base_stmt = base_stmt.where(table.c.start_time >= start_time.isoformat())
|
|
2366
|
+
if end_time:
|
|
2367
|
+
# Convert datetime to ISO string for comparison
|
|
2368
|
+
base_stmt = base_stmt.where(table.c.end_time <= end_time.isoformat())
|
|
2369
|
+
|
|
2370
|
+
# Get total count
|
|
2371
|
+
count_stmt = select(func.count()).select_from(base_stmt.alias())
|
|
2372
|
+
total_count = sess.execute(count_stmt).scalar() or 0
|
|
2373
|
+
log_debug(f"Total matching traces: {total_count}")
|
|
2374
|
+
|
|
2375
|
+
# Apply pagination
|
|
2376
|
+
offset = (page - 1) * limit if page and limit else 0
|
|
2377
|
+
paginated_stmt = base_stmt.order_by(table.c.start_time.desc()).limit(limit).offset(offset)
|
|
2378
|
+
|
|
2379
|
+
results = sess.execute(paginated_stmt).fetchall()
|
|
2380
|
+
log_debug(f"Returning page {page} with {len(results)} traces")
|
|
2381
|
+
|
|
2382
|
+
traces = [Trace.from_dict(dict(row._mapping)) for row in results]
|
|
2383
|
+
return traces, total_count
|
|
2384
|
+
|
|
2385
|
+
except Exception as e:
|
|
2386
|
+
log_error(f"Error getting traces: {e}")
|
|
2387
|
+
return [], 0
|
|
2388
|
+
|
|
2389
|
+
def get_trace_stats(
|
|
2390
|
+
self,
|
|
2391
|
+
user_id: Optional[str] = None,
|
|
2392
|
+
agent_id: Optional[str] = None,
|
|
2393
|
+
team_id: Optional[str] = None,
|
|
2394
|
+
workflow_id: Optional[str] = None,
|
|
2395
|
+
start_time: Optional[datetime] = None,
|
|
2396
|
+
end_time: Optional[datetime] = None,
|
|
2397
|
+
limit: Optional[int] = 20,
|
|
2398
|
+
page: Optional[int] = 1,
|
|
2399
|
+
) -> tuple[List[Dict[str, Any]], int]:
|
|
2400
|
+
"""Get trace statistics grouped by session.
|
|
2401
|
+
|
|
2402
|
+
Args:
|
|
2403
|
+
user_id: Filter by user ID.
|
|
2404
|
+
agent_id: Filter by agent ID.
|
|
2405
|
+
team_id: Filter by team ID.
|
|
2406
|
+
workflow_id: Filter by workflow ID.
|
|
2407
|
+
start_time: Filter sessions with traces created after this datetime.
|
|
2408
|
+
end_time: Filter sessions with traces created before this datetime.
|
|
2409
|
+
limit: Maximum number of sessions to return per page.
|
|
2410
|
+
page: Page number (1-indexed).
|
|
2411
|
+
|
|
2412
|
+
Returns:
|
|
2413
|
+
tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
|
|
2414
|
+
"""
|
|
2415
|
+
try:
|
|
2416
|
+
from sqlalchemy import func
|
|
2417
|
+
|
|
2418
|
+
log_debug(
|
|
2419
|
+
f"get_trace_stats called with filters: user_id={user_id}, agent_id={agent_id}, "
|
|
2420
|
+
f"workflow_id={workflow_id}, team_id={team_id}, "
|
|
2421
|
+
f"start_time={start_time}, end_time={end_time}, page={page}, limit={limit}"
|
|
2422
|
+
)
|
|
2423
|
+
|
|
2424
|
+
table = self._get_table(table_type="traces")
|
|
2425
|
+
if table is None:
|
|
2426
|
+
log_debug("Traces table not found")
|
|
2427
|
+
return [], 0
|
|
2428
|
+
|
|
2429
|
+
with self.Session() as sess:
|
|
2430
|
+
# Build base query grouped by session_id
|
|
2431
|
+
base_stmt = (
|
|
2432
|
+
select(
|
|
2433
|
+
table.c.session_id,
|
|
2434
|
+
table.c.user_id,
|
|
2435
|
+
table.c.agent_id,
|
|
2436
|
+
table.c.team_id,
|
|
2437
|
+
table.c.workflow_id,
|
|
2438
|
+
func.count(table.c.trace_id).label("total_traces"),
|
|
2439
|
+
func.min(table.c.created_at).label("first_trace_at"),
|
|
2440
|
+
func.max(table.c.created_at).label("last_trace_at"),
|
|
2441
|
+
)
|
|
2442
|
+
.where(table.c.session_id.isnot(None)) # Only sessions with session_id
|
|
2443
|
+
.group_by(
|
|
2444
|
+
table.c.session_id, table.c.user_id, table.c.agent_id, table.c.team_id, table.c.workflow_id
|
|
2445
|
+
)
|
|
2446
|
+
)
|
|
2447
|
+
|
|
2448
|
+
# Apply filters
|
|
2449
|
+
if user_id:
|
|
2450
|
+
base_stmt = base_stmt.where(table.c.user_id == user_id)
|
|
2451
|
+
if workflow_id:
|
|
2452
|
+
base_stmt = base_stmt.where(table.c.workflow_id == workflow_id)
|
|
2453
|
+
if team_id:
|
|
2454
|
+
base_stmt = base_stmt.where(table.c.team_id == team_id)
|
|
2455
|
+
if agent_id:
|
|
2456
|
+
base_stmt = base_stmt.where(table.c.agent_id == agent_id)
|
|
2457
|
+
if start_time:
|
|
2458
|
+
# Convert datetime to ISO string for comparison
|
|
2459
|
+
base_stmt = base_stmt.where(table.c.created_at >= start_time.isoformat())
|
|
2460
|
+
if end_time:
|
|
2461
|
+
# Convert datetime to ISO string for comparison
|
|
2462
|
+
base_stmt = base_stmt.where(table.c.created_at <= end_time.isoformat())
|
|
2463
|
+
|
|
2464
|
+
# Get total count of sessions
|
|
2465
|
+
count_stmt = select(func.count()).select_from(base_stmt.alias())
|
|
2466
|
+
total_count = sess.execute(count_stmt).scalar() or 0
|
|
2467
|
+
log_debug(f"Total matching sessions: {total_count}")
|
|
2468
|
+
|
|
2469
|
+
# Apply pagination and ordering
|
|
2470
|
+
offset = (page - 1) * limit if page and limit else 0
|
|
2471
|
+
paginated_stmt = base_stmt.order_by(func.max(table.c.created_at).desc()).limit(limit).offset(offset)
|
|
2472
|
+
|
|
2473
|
+
results = sess.execute(paginated_stmt).fetchall()
|
|
2474
|
+
log_debug(f"Returning page {page} with {len(results)} session stats")
|
|
2475
|
+
|
|
2476
|
+
# Convert to list of dicts with datetime objects
|
|
2477
|
+
from datetime import datetime
|
|
2478
|
+
|
|
2479
|
+
stats_list = []
|
|
2480
|
+
for row in results:
|
|
2481
|
+
# Convert ISO strings to datetime objects
|
|
2482
|
+
first_trace_at_str = row.first_trace_at
|
|
2483
|
+
last_trace_at_str = row.last_trace_at
|
|
2484
|
+
|
|
2485
|
+
# Parse ISO format strings to datetime objects
|
|
2486
|
+
first_trace_at = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
|
|
2487
|
+
last_trace_at = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
|
|
2488
|
+
|
|
2489
|
+
stats_list.append(
|
|
2490
|
+
{
|
|
2491
|
+
"session_id": row.session_id,
|
|
2492
|
+
"user_id": row.user_id,
|
|
2493
|
+
"agent_id": row.agent_id,
|
|
2494
|
+
"team_id": row.team_id,
|
|
2495
|
+
"workflow_id": row.workflow_id,
|
|
2496
|
+
"total_traces": row.total_traces,
|
|
2497
|
+
"first_trace_at": first_trace_at,
|
|
2498
|
+
"last_trace_at": last_trace_at,
|
|
2499
|
+
}
|
|
2500
|
+
)
|
|
2501
|
+
|
|
2502
|
+
return stats_list, total_count
|
|
2503
|
+
|
|
2504
|
+
except Exception as e:
|
|
2505
|
+
log_error(f"Error getting trace stats: {e}")
|
|
2506
|
+
return [], 0
|
|
2507
|
+
|
|
2508
|
+
# -- Span methods --
|
|
2509
|
+
|
|
2510
|
+
def create_span(self, span: "Span") -> None:
|
|
2511
|
+
"""Create a single span in the database.
|
|
2512
|
+
|
|
2513
|
+
Args:
|
|
2514
|
+
span: The Span object to store.
|
|
2515
|
+
"""
|
|
2516
|
+
try:
|
|
2517
|
+
table = self._get_table(table_type="spans", create_table_if_not_found=True)
|
|
2518
|
+
if table is None:
|
|
2519
|
+
return
|
|
2520
|
+
|
|
2521
|
+
with self.Session() as sess, sess.begin():
|
|
2522
|
+
stmt = sqlite.insert(table).values(span.to_dict())
|
|
2523
|
+
sess.execute(stmt)
|
|
2524
|
+
|
|
2525
|
+
except Exception as e:
|
|
2526
|
+
log_error(f"Error creating span: {e}")
|
|
2527
|
+
|
|
2528
|
+
def create_spans(self, spans: List) -> None:
|
|
2529
|
+
"""Create multiple spans in the database as a batch.
|
|
2530
|
+
|
|
2531
|
+
Args:
|
|
2532
|
+
spans: List of Span objects to store.
|
|
2533
|
+
"""
|
|
2534
|
+
if not spans:
|
|
2535
|
+
return
|
|
2536
|
+
|
|
2537
|
+
try:
|
|
2538
|
+
table = self._get_table(table_type="spans", create_table_if_not_found=True)
|
|
2539
|
+
if table is None:
|
|
2540
|
+
return
|
|
2541
|
+
|
|
2542
|
+
with self.Session() as sess, sess.begin():
|
|
2543
|
+
for span in spans:
|
|
2544
|
+
stmt = sqlite.insert(table).values(span.to_dict())
|
|
2545
|
+
sess.execute(stmt)
|
|
2546
|
+
|
|
2547
|
+
except Exception as e:
|
|
2548
|
+
log_error(f"Error creating spans batch: {e}")
|
|
2549
|
+
|
|
2550
|
+
def get_span(self, span_id: str):
|
|
2551
|
+
"""Get a single span by its span_id.
|
|
2552
|
+
|
|
2553
|
+
Args:
|
|
2554
|
+
span_id: The unique span identifier.
|
|
2555
|
+
|
|
2556
|
+
Returns:
|
|
2557
|
+
Optional[Span]: The span if found, None otherwise.
|
|
2558
|
+
"""
|
|
2559
|
+
try:
|
|
2560
|
+
from agno.tracing.schemas import Span
|
|
2561
|
+
|
|
2562
|
+
table = self._get_table(table_type="spans")
|
|
2563
|
+
if table is None:
|
|
2564
|
+
return None
|
|
2565
|
+
|
|
2566
|
+
with self.Session() as sess:
|
|
2567
|
+
stmt = table.select().where(table.c.span_id == span_id)
|
|
2568
|
+
result = sess.execute(stmt).fetchone()
|
|
2569
|
+
if result:
|
|
2570
|
+
return Span.from_dict(dict(result._mapping))
|
|
2571
|
+
return None
|
|
2572
|
+
|
|
2573
|
+
except Exception as e:
|
|
2574
|
+
log_error(f"Error getting span: {e}")
|
|
2575
|
+
return None
|
|
2576
|
+
|
|
2577
|
+
def get_spans(
|
|
2578
|
+
self,
|
|
2579
|
+
trace_id: Optional[str] = None,
|
|
2580
|
+
parent_span_id: Optional[str] = None,
|
|
2581
|
+
) -> List:
|
|
2582
|
+
"""Get spans matching the provided filters.
|
|
2583
|
+
|
|
2584
|
+
Args:
|
|
2585
|
+
trace_id: Filter by trace ID.
|
|
2586
|
+
parent_span_id: Filter by parent span ID.
|
|
2587
|
+
|
|
2588
|
+
Returns:
|
|
2589
|
+
List[Span]: List of matching spans.
|
|
2590
|
+
"""
|
|
2591
|
+
try:
|
|
2592
|
+
from agno.tracing.schemas import Span
|
|
2593
|
+
|
|
2594
|
+
table = self._get_table(table_type="spans")
|
|
2595
|
+
if table is None:
|
|
2596
|
+
return []
|
|
2597
|
+
|
|
2598
|
+
with self.Session() as sess:
|
|
2599
|
+
stmt = table.select()
|
|
2600
|
+
|
|
2601
|
+
# Apply filters
|
|
2602
|
+
if trace_id:
|
|
2603
|
+
stmt = stmt.where(table.c.trace_id == trace_id)
|
|
2604
|
+
if parent_span_id:
|
|
2605
|
+
stmt = stmt.where(table.c.parent_span_id == parent_span_id)
|
|
2606
|
+
|
|
2607
|
+
results = sess.execute(stmt).fetchall()
|
|
2608
|
+
return [Span.from_dict(dict(row._mapping)) for row in results]
|
|
2609
|
+
|
|
2610
|
+
except Exception as e:
|
|
2611
|
+
log_error(f"Error getting spans: {e}")
|
|
2612
|
+
return []
|
|
2613
|
+
|
|
2082
2614
|
# -- Migrations --
|
|
2083
2615
|
|
|
2084
2616
|
def migrate_table_from_v1_to_v2(self, v1_db_schema: str, v1_table_name: str, v1_table_type: str):
|