agno 2.1.2__py3-none-any.whl → 2.3.13__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 +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -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/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- 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 +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/db/redis/redis.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.redis.utils import (
|
|
8
11
|
apply_filters,
|
|
@@ -10,14 +13,17 @@ from agno.db.redis.utils import (
|
|
|
10
13
|
apply_sorting,
|
|
11
14
|
calculate_date_metrics,
|
|
12
15
|
create_index_entries,
|
|
16
|
+
deserialize_cultural_knowledge_from_db,
|
|
13
17
|
deserialize_data,
|
|
14
18
|
fetch_all_sessions_data,
|
|
15
19
|
generate_redis_key,
|
|
16
20
|
get_all_keys_for_table,
|
|
17
21
|
get_dates_to_calculate_metrics_for,
|
|
18
22
|
remove_index_entries,
|
|
23
|
+
serialize_cultural_knowledge_for_db,
|
|
19
24
|
serialize_data,
|
|
20
25
|
)
|
|
26
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
21
27
|
from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
|
|
22
28
|
from agno.db.schemas.knowledge import KnowledgeRow
|
|
23
29
|
from agno.db.schemas.memory import UserMemory
|
|
@@ -26,7 +32,7 @@ from agno.utils.log import log_debug, log_error, log_info
|
|
|
26
32
|
from agno.utils.string import generate_id
|
|
27
33
|
|
|
28
34
|
try:
|
|
29
|
-
from redis import Redis
|
|
35
|
+
from redis import Redis, RedisCluster
|
|
30
36
|
except ImportError:
|
|
31
37
|
raise ImportError("`redis` not installed. Please install it using `pip install redis`")
|
|
32
38
|
|
|
@@ -35,7 +41,7 @@ class RedisDb(BaseDb):
|
|
|
35
41
|
def __init__(
|
|
36
42
|
self,
|
|
37
43
|
id: Optional[str] = None,
|
|
38
|
-
redis_client: Optional[Redis] = None,
|
|
44
|
+
redis_client: Optional[Union[Redis, RedisCluster]] = None,
|
|
39
45
|
db_url: Optional[str] = None,
|
|
40
46
|
db_prefix: str = "agno",
|
|
41
47
|
expire: Optional[int] = None,
|
|
@@ -44,6 +50,9 @@ class RedisDb(BaseDb):
|
|
|
44
50
|
metrics_table: Optional[str] = None,
|
|
45
51
|
eval_table: Optional[str] = None,
|
|
46
52
|
knowledge_table: Optional[str] = None,
|
|
53
|
+
culture_table: Optional[str] = None,
|
|
54
|
+
traces_table: Optional[str] = None,
|
|
55
|
+
spans_table: Optional[str] = None,
|
|
47
56
|
):
|
|
48
57
|
"""
|
|
49
58
|
Interface for interacting with a Redis database.
|
|
@@ -53,6 +62,8 @@ class RedisDb(BaseDb):
|
|
|
53
62
|
2. Use the db_url
|
|
54
63
|
3. Raise an error if neither is provided
|
|
55
64
|
|
|
65
|
+
db_url only supports single-node Redis connections, if you need Redis Cluster support, provide a redis_client.
|
|
66
|
+
|
|
56
67
|
Args:
|
|
57
68
|
id (Optional[str]): The ID of the database.
|
|
58
69
|
redis_client (Optional[Redis]): Redis client instance to use. If not provided a new client will be created.
|
|
@@ -64,6 +75,9 @@ class RedisDb(BaseDb):
|
|
|
64
75
|
metrics_table (Optional[str]): Name of the table to store metrics
|
|
65
76
|
eval_table (Optional[str]): Name of the table to store evaluation runs
|
|
66
77
|
knowledge_table (Optional[str]): Name of the table to store knowledge documents
|
|
78
|
+
culture_table (Optional[str]): Name of the table to store cultural knowledge
|
|
79
|
+
traces_table (Optional[str]): Name of the table to store traces
|
|
80
|
+
spans_table (Optional[str]): Name of the table to store spans
|
|
67
81
|
|
|
68
82
|
Raises:
|
|
69
83
|
ValueError: If neither redis_client nor db_url is provided.
|
|
@@ -80,6 +94,9 @@ class RedisDb(BaseDb):
|
|
|
80
94
|
metrics_table=metrics_table,
|
|
81
95
|
eval_table=eval_table,
|
|
82
96
|
knowledge_table=knowledge_table,
|
|
97
|
+
culture_table=culture_table,
|
|
98
|
+
traces_table=traces_table,
|
|
99
|
+
spans_table=spans_table,
|
|
83
100
|
)
|
|
84
101
|
|
|
85
102
|
self.db_prefix = db_prefix
|
|
@@ -94,6 +111,10 @@ class RedisDb(BaseDb):
|
|
|
94
111
|
|
|
95
112
|
# -- DB methods --
|
|
96
113
|
|
|
114
|
+
def table_exists(self, table_name: str) -> bool:
|
|
115
|
+
"""Redis implementation, always returns True."""
|
|
116
|
+
return True
|
|
117
|
+
|
|
97
118
|
def _get_table_name(self, table_type: str) -> str:
|
|
98
119
|
"""Get the active table name for the given table type."""
|
|
99
120
|
if table_type == "sessions":
|
|
@@ -111,6 +132,15 @@ class RedisDb(BaseDb):
|
|
|
111
132
|
elif table_type == "knowledge":
|
|
112
133
|
return self.knowledge_table_name
|
|
113
134
|
|
|
135
|
+
elif table_type == "culture":
|
|
136
|
+
return self.culture_table_name
|
|
137
|
+
|
|
138
|
+
elif table_type == "traces":
|
|
139
|
+
return self.trace_table_name
|
|
140
|
+
|
|
141
|
+
elif table_type == "spans":
|
|
142
|
+
return self.span_table_name
|
|
143
|
+
|
|
114
144
|
else:
|
|
115
145
|
raise ValueError(f"Unknown table type: {table_type}")
|
|
116
146
|
|
|
@@ -239,6 +269,14 @@ class RedisDb(BaseDb):
|
|
|
239
269
|
log_error(f"Error getting all records for {table_type}: {e}")
|
|
240
270
|
return []
|
|
241
271
|
|
|
272
|
+
def get_latest_schema_version(self):
|
|
273
|
+
"""Get the latest version of the database schema."""
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
def upsert_schema_version(self, version: str) -> None:
|
|
277
|
+
"""Upsert the schema version into the database."""
|
|
278
|
+
pass
|
|
279
|
+
|
|
242
280
|
# -- Session methods --
|
|
243
281
|
|
|
244
282
|
def delete_session(self, session_id: str) -> bool:
|
|
@@ -318,8 +356,6 @@ class RedisDb(BaseDb):
|
|
|
318
356
|
# Apply filters
|
|
319
357
|
if user_id is not None and session.get("user_id") != user_id:
|
|
320
358
|
return None
|
|
321
|
-
if session_type is not None and session.get("session_type") != session_type:
|
|
322
|
-
return None
|
|
323
359
|
|
|
324
360
|
if not deserialize:
|
|
325
361
|
return session
|
|
@@ -589,7 +625,7 @@ class RedisDb(BaseDb):
|
|
|
589
625
|
raise e
|
|
590
626
|
|
|
591
627
|
def upsert_sessions(
|
|
592
|
-
self, sessions: List[Session], deserialize: Optional[bool] = True
|
|
628
|
+
self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
|
|
593
629
|
) -> List[Union[Session, Dict[str, Any]]]:
|
|
594
630
|
"""
|
|
595
631
|
Bulk upsert multiple sessions for improved performance on large datasets.
|
|
@@ -820,12 +856,14 @@ class RedisDb(BaseDb):
|
|
|
820
856
|
self,
|
|
821
857
|
limit: Optional[int] = None,
|
|
822
858
|
page: Optional[int] = None,
|
|
859
|
+
user_id: Optional[str] = None,
|
|
823
860
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
824
861
|
"""Get user memory stats from Redis.
|
|
825
862
|
|
|
826
863
|
Args:
|
|
827
864
|
limit (Optional[int]): The maximum number of stats to return.
|
|
828
865
|
page (Optional[int]): The page number to return.
|
|
866
|
+
user_id (Optional[str]): User ID for filtering.
|
|
829
867
|
|
|
830
868
|
Returns:
|
|
831
869
|
Tuple[List[Dict[str, Any]], int]: A tuple containing the list of stats and the total number of stats.
|
|
@@ -840,6 +878,9 @@ class RedisDb(BaseDb):
|
|
|
840
878
|
user_stats = {}
|
|
841
879
|
for memory in all_memories:
|
|
842
880
|
memory_user_id = memory.get("user_id")
|
|
881
|
+
# filter by user_id if provided
|
|
882
|
+
if user_id is not None and memory_user_id != user_id:
|
|
883
|
+
continue
|
|
843
884
|
if memory_user_id is None:
|
|
844
885
|
continue
|
|
845
886
|
|
|
@@ -892,6 +933,9 @@ class RedisDb(BaseDb):
|
|
|
892
933
|
"memory_id": memory.memory_id,
|
|
893
934
|
"memory": memory.memory,
|
|
894
935
|
"topics": memory.topics,
|
|
936
|
+
"input": memory.input,
|
|
937
|
+
"feedback": memory.feedback,
|
|
938
|
+
"created_at": memory.created_at,
|
|
895
939
|
"updated_at": int(time.time()),
|
|
896
940
|
}
|
|
897
941
|
|
|
@@ -912,7 +956,7 @@ class RedisDb(BaseDb):
|
|
|
912
956
|
raise e
|
|
913
957
|
|
|
914
958
|
def upsert_memories(
|
|
915
|
-
self, memories: List[UserMemory], deserialize: Optional[bool] = True
|
|
959
|
+
self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
|
|
916
960
|
) -> List[Union[UserMemory, Dict[str, Any]]]:
|
|
917
961
|
"""
|
|
918
962
|
Bulk upsert multiple user memories for improved performance on large datasets.
|
|
@@ -1475,3 +1519,623 @@ class RedisDb(BaseDb):
|
|
|
1475
1519
|
except Exception as e:
|
|
1476
1520
|
log_error(f"Error updating eval run name {eval_run_id}: {e}")
|
|
1477
1521
|
raise
|
|
1522
|
+
|
|
1523
|
+
# -- Cultural Knowledge methods --
|
|
1524
|
+
def clear_cultural_knowledge(self) -> None:
|
|
1525
|
+
"""Delete all cultural knowledge from the database.
|
|
1526
|
+
|
|
1527
|
+
Raises:
|
|
1528
|
+
Exception: If an error occurs during deletion.
|
|
1529
|
+
"""
|
|
1530
|
+
try:
|
|
1531
|
+
keys = get_all_keys_for_table(redis_client=self.redis_client, prefix=self.db_prefix, table_type="culture")
|
|
1532
|
+
|
|
1533
|
+
if keys:
|
|
1534
|
+
self.redis_client.delete(*keys)
|
|
1535
|
+
|
|
1536
|
+
except Exception as e:
|
|
1537
|
+
log_error(f"Exception deleting all cultural knowledge: {e}")
|
|
1538
|
+
raise e
|
|
1539
|
+
|
|
1540
|
+
def delete_cultural_knowledge(self, id: str) -> None:
|
|
1541
|
+
"""Delete cultural knowledge by ID.
|
|
1542
|
+
|
|
1543
|
+
Args:
|
|
1544
|
+
id (str): The ID of the cultural knowledge to delete.
|
|
1545
|
+
|
|
1546
|
+
Raises:
|
|
1547
|
+
Exception: If an error occurs during deletion.
|
|
1548
|
+
"""
|
|
1549
|
+
try:
|
|
1550
|
+
if self._delete_record("culture", id, index_fields=["name", "agent_id", "team_id"]):
|
|
1551
|
+
log_debug(f"Successfully deleted cultural knowledge id: {id}")
|
|
1552
|
+
else:
|
|
1553
|
+
log_debug(f"No cultural knowledge found with id: {id}")
|
|
1554
|
+
|
|
1555
|
+
except Exception as e:
|
|
1556
|
+
log_error(f"Error deleting cultural knowledge: {e}")
|
|
1557
|
+
raise e
|
|
1558
|
+
|
|
1559
|
+
def get_cultural_knowledge(
|
|
1560
|
+
self, id: str, deserialize: Optional[bool] = True
|
|
1561
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
1562
|
+
"""Get cultural knowledge by ID.
|
|
1563
|
+
|
|
1564
|
+
Args:
|
|
1565
|
+
id (str): The ID of the cultural knowledge to retrieve.
|
|
1566
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
|
|
1567
|
+
|
|
1568
|
+
Returns:
|
|
1569
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
|
|
1570
|
+
|
|
1571
|
+
Raises:
|
|
1572
|
+
Exception: If an error occurs during retrieval.
|
|
1573
|
+
"""
|
|
1574
|
+
try:
|
|
1575
|
+
cultural_knowledge = self._get_record("culture", id)
|
|
1576
|
+
|
|
1577
|
+
if cultural_knowledge is None:
|
|
1578
|
+
return None
|
|
1579
|
+
|
|
1580
|
+
if not deserialize:
|
|
1581
|
+
return cultural_knowledge
|
|
1582
|
+
|
|
1583
|
+
return deserialize_cultural_knowledge_from_db(cultural_knowledge)
|
|
1584
|
+
|
|
1585
|
+
except Exception as e:
|
|
1586
|
+
log_error(f"Error getting cultural knowledge: {e}")
|
|
1587
|
+
raise e
|
|
1588
|
+
|
|
1589
|
+
def get_all_cultural_knowledge(
|
|
1590
|
+
self,
|
|
1591
|
+
agent_id: Optional[str] = None,
|
|
1592
|
+
team_id: Optional[str] = None,
|
|
1593
|
+
name: Optional[str] = None,
|
|
1594
|
+
limit: Optional[int] = None,
|
|
1595
|
+
page: Optional[int] = None,
|
|
1596
|
+
sort_by: Optional[str] = None,
|
|
1597
|
+
sort_order: Optional[str] = None,
|
|
1598
|
+
deserialize: Optional[bool] = True,
|
|
1599
|
+
) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
1600
|
+
"""Get all cultural knowledge with filtering and pagination.
|
|
1601
|
+
|
|
1602
|
+
Args:
|
|
1603
|
+
agent_id (Optional[str]): Filter by agent ID.
|
|
1604
|
+
team_id (Optional[str]): Filter by team ID.
|
|
1605
|
+
name (Optional[str]): Filter by name (case-insensitive partial match).
|
|
1606
|
+
limit (Optional[int]): Maximum number of results to return.
|
|
1607
|
+
page (Optional[int]): Page number for pagination.
|
|
1608
|
+
sort_by (Optional[str]): Field to sort by.
|
|
1609
|
+
sort_order (Optional[str]): Sort order ('asc' or 'desc').
|
|
1610
|
+
deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
|
|
1611
|
+
|
|
1612
|
+
Returns:
|
|
1613
|
+
Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
|
|
1614
|
+
- When deserialize=True: List of CulturalKnowledge objects
|
|
1615
|
+
- When deserialize=False: Tuple with list of dictionaries and total count
|
|
1616
|
+
|
|
1617
|
+
Raises:
|
|
1618
|
+
Exception: If an error occurs during retrieval.
|
|
1619
|
+
"""
|
|
1620
|
+
try:
|
|
1621
|
+
all_cultural_knowledge = self._get_all_records("culture")
|
|
1622
|
+
|
|
1623
|
+
# Apply filters
|
|
1624
|
+
filtered_items = []
|
|
1625
|
+
for item in all_cultural_knowledge:
|
|
1626
|
+
if agent_id is not None and item.get("agent_id") != agent_id:
|
|
1627
|
+
continue
|
|
1628
|
+
if team_id is not None and item.get("team_id") != team_id:
|
|
1629
|
+
continue
|
|
1630
|
+
if name is not None and name.lower() not in item.get("name", "").lower():
|
|
1631
|
+
continue
|
|
1632
|
+
|
|
1633
|
+
filtered_items.append(item)
|
|
1634
|
+
|
|
1635
|
+
sorted_items = apply_sorting(records=filtered_items, sort_by=sort_by, sort_order=sort_order)
|
|
1636
|
+
paginated_items = apply_pagination(records=sorted_items, limit=limit, page=page)
|
|
1637
|
+
|
|
1638
|
+
if not deserialize:
|
|
1639
|
+
return paginated_items, len(filtered_items)
|
|
1640
|
+
|
|
1641
|
+
return [deserialize_cultural_knowledge_from_db(item) for item in paginated_items]
|
|
1642
|
+
|
|
1643
|
+
except Exception as e:
|
|
1644
|
+
log_error(f"Error getting all cultural knowledge: {e}")
|
|
1645
|
+
raise e
|
|
1646
|
+
|
|
1647
|
+
def upsert_cultural_knowledge(
|
|
1648
|
+
self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
|
|
1649
|
+
) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
|
|
1650
|
+
"""Upsert cultural knowledge in Redis.
|
|
1651
|
+
|
|
1652
|
+
Args:
|
|
1653
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
|
|
1654
|
+
deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
|
|
1655
|
+
|
|
1656
|
+
Returns:
|
|
1657
|
+
Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
|
|
1658
|
+
|
|
1659
|
+
Raises:
|
|
1660
|
+
Exception: If an error occurs during upsert.
|
|
1661
|
+
"""
|
|
1662
|
+
try:
|
|
1663
|
+
# Serialize content, categories, and notes into a dict for DB storage
|
|
1664
|
+
content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
|
|
1665
|
+
item_id = cultural_knowledge.id or str(uuid4())
|
|
1666
|
+
|
|
1667
|
+
# Create the item dict with serialized content
|
|
1668
|
+
data = {
|
|
1669
|
+
"id": item_id,
|
|
1670
|
+
"name": cultural_knowledge.name,
|
|
1671
|
+
"summary": cultural_knowledge.summary,
|
|
1672
|
+
"content": content_dict if content_dict else None,
|
|
1673
|
+
"metadata": cultural_knowledge.metadata,
|
|
1674
|
+
"input": cultural_knowledge.input,
|
|
1675
|
+
"created_at": cultural_knowledge.created_at,
|
|
1676
|
+
"updated_at": int(time.time()),
|
|
1677
|
+
"agent_id": cultural_knowledge.agent_id,
|
|
1678
|
+
"team_id": cultural_knowledge.team_id,
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
success = self._store_record("culture", item_id, data, index_fields=["name", "agent_id", "team_id"])
|
|
1682
|
+
|
|
1683
|
+
if not success:
|
|
1684
|
+
return None
|
|
1685
|
+
|
|
1686
|
+
if not deserialize:
|
|
1687
|
+
return data
|
|
1688
|
+
|
|
1689
|
+
return deserialize_cultural_knowledge_from_db(data)
|
|
1690
|
+
|
|
1691
|
+
except Exception as e:
|
|
1692
|
+
log_error(f"Error upserting cultural knowledge: {e}")
|
|
1693
|
+
raise e
|
|
1694
|
+
|
|
1695
|
+
# --- Traces ---
|
|
1696
|
+
def upsert_trace(self, trace: "Trace") -> None:
|
|
1697
|
+
"""Create or update a single trace record in the database.
|
|
1698
|
+
|
|
1699
|
+
Args:
|
|
1700
|
+
trace: The Trace object to store (one per trace_id).
|
|
1701
|
+
"""
|
|
1702
|
+
try:
|
|
1703
|
+
# Check if trace already exists
|
|
1704
|
+
existing = self._get_record("traces", trace.trace_id)
|
|
1705
|
+
|
|
1706
|
+
if existing:
|
|
1707
|
+
# workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
|
|
1708
|
+
def get_component_level(
|
|
1709
|
+
workflow_id: Optional[str], team_id: Optional[str], agent_id: Optional[str], name: str
|
|
1710
|
+
) -> int:
|
|
1711
|
+
# Check if name indicates a root span
|
|
1712
|
+
is_root_name = ".run" in name or ".arun" in name
|
|
1713
|
+
|
|
1714
|
+
if not is_root_name:
|
|
1715
|
+
return 0 # Child span (not a root)
|
|
1716
|
+
elif workflow_id:
|
|
1717
|
+
return 3 # Workflow root
|
|
1718
|
+
elif team_id:
|
|
1719
|
+
return 2 # Team root
|
|
1720
|
+
elif agent_id:
|
|
1721
|
+
return 1 # Agent root
|
|
1722
|
+
else:
|
|
1723
|
+
return 0 # Unknown
|
|
1724
|
+
|
|
1725
|
+
existing_level = get_component_level(
|
|
1726
|
+
existing.get("workflow_id"),
|
|
1727
|
+
existing.get("team_id"),
|
|
1728
|
+
existing.get("agent_id"),
|
|
1729
|
+
existing.get("name", ""),
|
|
1730
|
+
)
|
|
1731
|
+
new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
|
|
1732
|
+
|
|
1733
|
+
# Only update name if new trace is from a higher or equal level
|
|
1734
|
+
should_update_name = new_level > existing_level
|
|
1735
|
+
|
|
1736
|
+
# Parse existing start_time to calculate correct duration
|
|
1737
|
+
existing_start_time_str = existing.get("start_time")
|
|
1738
|
+
if isinstance(existing_start_time_str, str):
|
|
1739
|
+
existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
|
|
1740
|
+
else:
|
|
1741
|
+
existing_start_time = trace.start_time
|
|
1742
|
+
|
|
1743
|
+
recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
|
|
1744
|
+
|
|
1745
|
+
# Update existing record
|
|
1746
|
+
existing["end_time"] = trace.end_time.isoformat()
|
|
1747
|
+
existing["duration_ms"] = recalculated_duration_ms
|
|
1748
|
+
existing["status"] = trace.status
|
|
1749
|
+
if should_update_name:
|
|
1750
|
+
existing["name"] = trace.name
|
|
1751
|
+
|
|
1752
|
+
# Update context fields ONLY if new value is not None (preserve non-null values)
|
|
1753
|
+
if trace.run_id is not None:
|
|
1754
|
+
existing["run_id"] = trace.run_id
|
|
1755
|
+
if trace.session_id is not None:
|
|
1756
|
+
existing["session_id"] = trace.session_id
|
|
1757
|
+
if trace.user_id is not None:
|
|
1758
|
+
existing["user_id"] = trace.user_id
|
|
1759
|
+
if trace.agent_id is not None:
|
|
1760
|
+
existing["agent_id"] = trace.agent_id
|
|
1761
|
+
if trace.team_id is not None:
|
|
1762
|
+
existing["team_id"] = trace.team_id
|
|
1763
|
+
if trace.workflow_id is not None:
|
|
1764
|
+
existing["workflow_id"] = trace.workflow_id
|
|
1765
|
+
|
|
1766
|
+
log_debug(
|
|
1767
|
+
f" Updating trace with context: run_id={existing.get('run_id', 'unchanged')}, "
|
|
1768
|
+
f"session_id={existing.get('session_id', 'unchanged')}, "
|
|
1769
|
+
f"user_id={existing.get('user_id', 'unchanged')}, "
|
|
1770
|
+
f"agent_id={existing.get('agent_id', 'unchanged')}, "
|
|
1771
|
+
f"team_id={existing.get('team_id', 'unchanged')}, "
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
self._store_record(
|
|
1775
|
+
"traces",
|
|
1776
|
+
trace.trace_id,
|
|
1777
|
+
existing,
|
|
1778
|
+
index_fields=["run_id", "session_id", "user_id", "agent_id", "team_id", "workflow_id", "status"],
|
|
1779
|
+
)
|
|
1780
|
+
else:
|
|
1781
|
+
trace_dict = trace.to_dict()
|
|
1782
|
+
trace_dict.pop("total_spans", None)
|
|
1783
|
+
trace_dict.pop("error_count", None)
|
|
1784
|
+
self._store_record(
|
|
1785
|
+
"traces",
|
|
1786
|
+
trace.trace_id,
|
|
1787
|
+
trace_dict,
|
|
1788
|
+
index_fields=["run_id", "session_id", "user_id", "agent_id", "team_id", "workflow_id", "status"],
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
except Exception as e:
|
|
1792
|
+
log_error(f"Error creating trace: {e}")
|
|
1793
|
+
# Don't raise - tracing should not break the main application flow
|
|
1794
|
+
|
|
1795
|
+
def get_trace(
|
|
1796
|
+
self,
|
|
1797
|
+
trace_id: Optional[str] = None,
|
|
1798
|
+
run_id: Optional[str] = None,
|
|
1799
|
+
):
|
|
1800
|
+
"""Get a single trace by trace_id or other filters.
|
|
1801
|
+
|
|
1802
|
+
Args:
|
|
1803
|
+
trace_id: The unique trace identifier.
|
|
1804
|
+
run_id: Filter by run ID (returns first match).
|
|
1805
|
+
|
|
1806
|
+
Returns:
|
|
1807
|
+
Optional[Trace]: The trace if found, None otherwise.
|
|
1808
|
+
|
|
1809
|
+
Note:
|
|
1810
|
+
If multiple filters are provided, trace_id takes precedence.
|
|
1811
|
+
For other filters, the most recent trace is returned.
|
|
1812
|
+
"""
|
|
1813
|
+
try:
|
|
1814
|
+
from agno.tracing.schemas import Trace as TraceSchema
|
|
1815
|
+
|
|
1816
|
+
if trace_id:
|
|
1817
|
+
result = self._get_record("traces", trace_id)
|
|
1818
|
+
if result:
|
|
1819
|
+
# Calculate total_spans and error_count
|
|
1820
|
+
all_spans = self._get_all_records("spans")
|
|
1821
|
+
trace_spans = [s for s in all_spans if s.get("trace_id") == trace_id]
|
|
1822
|
+
result["total_spans"] = len(trace_spans)
|
|
1823
|
+
result["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
|
|
1824
|
+
return TraceSchema.from_dict(result)
|
|
1825
|
+
return None
|
|
1826
|
+
|
|
1827
|
+
elif run_id:
|
|
1828
|
+
all_traces = self._get_all_records("traces")
|
|
1829
|
+
matching = [t for t in all_traces if t.get("run_id") == run_id]
|
|
1830
|
+
if matching:
|
|
1831
|
+
# Sort by start_time descending and get most recent
|
|
1832
|
+
matching.sort(key=lambda x: x.get("start_time", ""), reverse=True)
|
|
1833
|
+
result = matching[0]
|
|
1834
|
+
# Calculate total_spans and error_count
|
|
1835
|
+
all_spans = self._get_all_records("spans")
|
|
1836
|
+
trace_spans = [s for s in all_spans if s.get("trace_id") == result.get("trace_id")]
|
|
1837
|
+
result["total_spans"] = len(trace_spans)
|
|
1838
|
+
result["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
|
|
1839
|
+
return TraceSchema.from_dict(result)
|
|
1840
|
+
return None
|
|
1841
|
+
|
|
1842
|
+
else:
|
|
1843
|
+
log_debug("get_trace called without any filter parameters")
|
|
1844
|
+
return None
|
|
1845
|
+
|
|
1846
|
+
except Exception as e:
|
|
1847
|
+
log_error(f"Error getting trace: {e}")
|
|
1848
|
+
return None
|
|
1849
|
+
|
|
1850
|
+
def get_traces(
|
|
1851
|
+
self,
|
|
1852
|
+
run_id: Optional[str] = None,
|
|
1853
|
+
session_id: Optional[str] = None,
|
|
1854
|
+
user_id: Optional[str] = None,
|
|
1855
|
+
agent_id: Optional[str] = None,
|
|
1856
|
+
team_id: Optional[str] = None,
|
|
1857
|
+
workflow_id: Optional[str] = None,
|
|
1858
|
+
status: Optional[str] = None,
|
|
1859
|
+
start_time: Optional[datetime] = None,
|
|
1860
|
+
end_time: Optional[datetime] = None,
|
|
1861
|
+
limit: Optional[int] = 20,
|
|
1862
|
+
page: Optional[int] = 1,
|
|
1863
|
+
) -> tuple[List, int]:
|
|
1864
|
+
"""Get traces matching the provided filters.
|
|
1865
|
+
|
|
1866
|
+
Args:
|
|
1867
|
+
run_id: Filter by run ID.
|
|
1868
|
+
session_id: Filter by session ID.
|
|
1869
|
+
user_id: Filter by user ID.
|
|
1870
|
+
agent_id: Filter by agent ID.
|
|
1871
|
+
team_id: Filter by team ID.
|
|
1872
|
+
workflow_id: Filter by workflow ID.
|
|
1873
|
+
status: Filter by status (OK, ERROR, UNSET).
|
|
1874
|
+
start_time: Filter traces starting after this datetime.
|
|
1875
|
+
end_time: Filter traces ending before this datetime.
|
|
1876
|
+
limit: Maximum number of traces to return per page.
|
|
1877
|
+
page: Page number (1-indexed).
|
|
1878
|
+
|
|
1879
|
+
Returns:
|
|
1880
|
+
tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
|
|
1881
|
+
"""
|
|
1882
|
+
try:
|
|
1883
|
+
from agno.tracing.schemas import Trace as TraceSchema
|
|
1884
|
+
|
|
1885
|
+
log_debug(
|
|
1886
|
+
f"get_traces called with filters: run_id={run_id}, session_id={session_id}, "
|
|
1887
|
+
f"user_id={user_id}, agent_id={agent_id}, page={page}, limit={limit}"
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1890
|
+
all_traces = self._get_all_records("traces")
|
|
1891
|
+
all_spans = self._get_all_records("spans")
|
|
1892
|
+
|
|
1893
|
+
# Apply filters
|
|
1894
|
+
filtered_traces = []
|
|
1895
|
+
for trace in all_traces:
|
|
1896
|
+
if run_id and trace.get("run_id") != run_id:
|
|
1897
|
+
continue
|
|
1898
|
+
if session_id and trace.get("session_id") != session_id:
|
|
1899
|
+
continue
|
|
1900
|
+
if user_id and trace.get("user_id") != user_id:
|
|
1901
|
+
continue
|
|
1902
|
+
if agent_id and trace.get("agent_id") != agent_id:
|
|
1903
|
+
continue
|
|
1904
|
+
if team_id and trace.get("team_id") != team_id:
|
|
1905
|
+
continue
|
|
1906
|
+
if workflow_id and trace.get("workflow_id") != workflow_id:
|
|
1907
|
+
continue
|
|
1908
|
+
if status and trace.get("status") != status:
|
|
1909
|
+
continue
|
|
1910
|
+
if start_time:
|
|
1911
|
+
trace_start = trace.get("start_time", "")
|
|
1912
|
+
if trace_start and trace_start < start_time.isoformat():
|
|
1913
|
+
continue
|
|
1914
|
+
if end_time:
|
|
1915
|
+
trace_end = trace.get("end_time", "")
|
|
1916
|
+
if trace_end and trace_end > end_time.isoformat():
|
|
1917
|
+
continue
|
|
1918
|
+
|
|
1919
|
+
filtered_traces.append(trace)
|
|
1920
|
+
|
|
1921
|
+
total_count = len(filtered_traces)
|
|
1922
|
+
|
|
1923
|
+
# Sort by start_time descending
|
|
1924
|
+
filtered_traces.sort(key=lambda x: x.get("start_time", ""), reverse=True)
|
|
1925
|
+
|
|
1926
|
+
# Apply pagination
|
|
1927
|
+
paginated_traces = apply_pagination(records=filtered_traces, limit=limit, page=page)
|
|
1928
|
+
|
|
1929
|
+
traces = []
|
|
1930
|
+
for row in paginated_traces:
|
|
1931
|
+
# Calculate total_spans and error_count
|
|
1932
|
+
trace_spans = [s for s in all_spans if s.get("trace_id") == row.get("trace_id")]
|
|
1933
|
+
row["total_spans"] = len(trace_spans)
|
|
1934
|
+
row["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
|
|
1935
|
+
traces.append(TraceSchema.from_dict(row))
|
|
1936
|
+
|
|
1937
|
+
return traces, total_count
|
|
1938
|
+
|
|
1939
|
+
except Exception as e:
|
|
1940
|
+
log_error(f"Error getting traces: {e}")
|
|
1941
|
+
return [], 0
|
|
1942
|
+
|
|
1943
|
+
def get_trace_stats(
|
|
1944
|
+
self,
|
|
1945
|
+
user_id: Optional[str] = None,
|
|
1946
|
+
agent_id: Optional[str] = None,
|
|
1947
|
+
team_id: Optional[str] = None,
|
|
1948
|
+
workflow_id: Optional[str] = None,
|
|
1949
|
+
start_time: Optional[datetime] = None,
|
|
1950
|
+
end_time: Optional[datetime] = None,
|
|
1951
|
+
limit: Optional[int] = 20,
|
|
1952
|
+
page: Optional[int] = 1,
|
|
1953
|
+
) -> tuple[List[Dict[str, Any]], int]:
|
|
1954
|
+
"""Get trace statistics grouped by session.
|
|
1955
|
+
|
|
1956
|
+
Args:
|
|
1957
|
+
user_id: Filter by user ID.
|
|
1958
|
+
agent_id: Filter by agent ID.
|
|
1959
|
+
team_id: Filter by team ID.
|
|
1960
|
+
workflow_id: Filter by workflow ID.
|
|
1961
|
+
start_time: Filter sessions with traces created after this datetime.
|
|
1962
|
+
end_time: Filter sessions with traces created before this datetime.
|
|
1963
|
+
limit: Maximum number of sessions to return per page.
|
|
1964
|
+
page: Page number (1-indexed).
|
|
1965
|
+
|
|
1966
|
+
Returns:
|
|
1967
|
+
tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
|
|
1968
|
+
Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
|
|
1969
|
+
first_trace_at, last_trace_at.
|
|
1970
|
+
"""
|
|
1971
|
+
try:
|
|
1972
|
+
log_debug(
|
|
1973
|
+
f"get_trace_stats called with filters: user_id={user_id}, agent_id={agent_id}, "
|
|
1974
|
+
f"workflow_id={workflow_id}, team_id={team_id}, "
|
|
1975
|
+
f"start_time={start_time}, end_time={end_time}, page={page}, limit={limit}"
|
|
1976
|
+
)
|
|
1977
|
+
|
|
1978
|
+
all_traces = self._get_all_records("traces")
|
|
1979
|
+
|
|
1980
|
+
# Filter traces and group by session_id
|
|
1981
|
+
session_stats: Dict[str, Dict[str, Any]] = {}
|
|
1982
|
+
for trace in all_traces:
|
|
1983
|
+
trace_session_id = trace.get("session_id")
|
|
1984
|
+
if not trace_session_id:
|
|
1985
|
+
continue
|
|
1986
|
+
|
|
1987
|
+
# Apply filters
|
|
1988
|
+
if user_id and trace.get("user_id") != user_id:
|
|
1989
|
+
continue
|
|
1990
|
+
if agent_id and trace.get("agent_id") != agent_id:
|
|
1991
|
+
continue
|
|
1992
|
+
if team_id and trace.get("team_id") != team_id:
|
|
1993
|
+
continue
|
|
1994
|
+
if workflow_id and trace.get("workflow_id") != workflow_id:
|
|
1995
|
+
continue
|
|
1996
|
+
|
|
1997
|
+
created_at = trace.get("created_at", "")
|
|
1998
|
+
if start_time and created_at < start_time.isoformat():
|
|
1999
|
+
continue
|
|
2000
|
+
if end_time and created_at > end_time.isoformat():
|
|
2001
|
+
continue
|
|
2002
|
+
|
|
2003
|
+
if trace_session_id not in session_stats:
|
|
2004
|
+
session_stats[trace_session_id] = {
|
|
2005
|
+
"session_id": trace_session_id,
|
|
2006
|
+
"user_id": trace.get("user_id"),
|
|
2007
|
+
"agent_id": trace.get("agent_id"),
|
|
2008
|
+
"team_id": trace.get("team_id"),
|
|
2009
|
+
"workflow_id": trace.get("workflow_id"),
|
|
2010
|
+
"total_traces": 0,
|
|
2011
|
+
"first_trace_at": created_at,
|
|
2012
|
+
"last_trace_at": created_at,
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
session_stats[trace_session_id]["total_traces"] += 1
|
|
2016
|
+
if created_at < session_stats[trace_session_id]["first_trace_at"]:
|
|
2017
|
+
session_stats[trace_session_id]["first_trace_at"] = created_at
|
|
2018
|
+
if created_at > session_stats[trace_session_id]["last_trace_at"]:
|
|
2019
|
+
session_stats[trace_session_id]["last_trace_at"] = created_at
|
|
2020
|
+
|
|
2021
|
+
# Convert to list and sort by last_trace_at descending
|
|
2022
|
+
stats_list = list(session_stats.values())
|
|
2023
|
+
stats_list.sort(key=lambda x: x.get("last_trace_at", ""), reverse=True)
|
|
2024
|
+
|
|
2025
|
+
total_count = len(stats_list)
|
|
2026
|
+
|
|
2027
|
+
# Apply pagination
|
|
2028
|
+
paginated_stats = apply_pagination(records=stats_list, limit=limit, page=page)
|
|
2029
|
+
|
|
2030
|
+
# Convert ISO strings to datetime objects
|
|
2031
|
+
for stat in paginated_stats:
|
|
2032
|
+
first_trace_at_str = stat["first_trace_at"]
|
|
2033
|
+
last_trace_at_str = stat["last_trace_at"]
|
|
2034
|
+
stat["first_trace_at"] = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
|
|
2035
|
+
stat["last_trace_at"] = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
|
|
2036
|
+
|
|
2037
|
+
return paginated_stats, total_count
|
|
2038
|
+
|
|
2039
|
+
except Exception as e:
|
|
2040
|
+
log_error(f"Error getting trace stats: {e}")
|
|
2041
|
+
return [], 0
|
|
2042
|
+
|
|
2043
|
+
# --- Spans ---
|
|
2044
|
+
def create_span(self, span: "Span") -> None:
|
|
2045
|
+
"""Create a single span in the database.
|
|
2046
|
+
|
|
2047
|
+
Args:
|
|
2048
|
+
span: The Span object to store.
|
|
2049
|
+
"""
|
|
2050
|
+
try:
|
|
2051
|
+
self._store_record(
|
|
2052
|
+
"spans",
|
|
2053
|
+
span.span_id,
|
|
2054
|
+
span.to_dict(),
|
|
2055
|
+
index_fields=["trace_id", "parent_span_id"],
|
|
2056
|
+
)
|
|
2057
|
+
|
|
2058
|
+
except Exception as e:
|
|
2059
|
+
log_error(f"Error creating span: {e}")
|
|
2060
|
+
|
|
2061
|
+
def create_spans(self, spans: List) -> None:
|
|
2062
|
+
"""Create multiple spans in the database as a batch.
|
|
2063
|
+
|
|
2064
|
+
Args:
|
|
2065
|
+
spans: List of Span objects to store.
|
|
2066
|
+
"""
|
|
2067
|
+
if not spans:
|
|
2068
|
+
return
|
|
2069
|
+
|
|
2070
|
+
try:
|
|
2071
|
+
for span in spans:
|
|
2072
|
+
self._store_record(
|
|
2073
|
+
"spans",
|
|
2074
|
+
span.span_id,
|
|
2075
|
+
span.to_dict(),
|
|
2076
|
+
index_fields=["trace_id", "parent_span_id"],
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
except Exception as e:
|
|
2080
|
+
log_error(f"Error creating spans batch: {e}")
|
|
2081
|
+
|
|
2082
|
+
def get_span(self, span_id: str):
|
|
2083
|
+
"""Get a single span by its span_id.
|
|
2084
|
+
|
|
2085
|
+
Args:
|
|
2086
|
+
span_id: The unique span identifier.
|
|
2087
|
+
|
|
2088
|
+
Returns:
|
|
2089
|
+
Optional[Span]: The span if found, None otherwise.
|
|
2090
|
+
"""
|
|
2091
|
+
try:
|
|
2092
|
+
from agno.tracing.schemas import Span as SpanSchema
|
|
2093
|
+
|
|
2094
|
+
result = self._get_record("spans", span_id)
|
|
2095
|
+
if result:
|
|
2096
|
+
return SpanSchema.from_dict(result)
|
|
2097
|
+
return None
|
|
2098
|
+
|
|
2099
|
+
except Exception as e:
|
|
2100
|
+
log_error(f"Error getting span: {e}")
|
|
2101
|
+
return None
|
|
2102
|
+
|
|
2103
|
+
def get_spans(
|
|
2104
|
+
self,
|
|
2105
|
+
trace_id: Optional[str] = None,
|
|
2106
|
+
parent_span_id: Optional[str] = None,
|
|
2107
|
+
limit: Optional[int] = 1000,
|
|
2108
|
+
) -> List:
|
|
2109
|
+
"""Get spans matching the provided filters.
|
|
2110
|
+
|
|
2111
|
+
Args:
|
|
2112
|
+
trace_id: Filter by trace ID.
|
|
2113
|
+
parent_span_id: Filter by parent span ID.
|
|
2114
|
+
limit: Maximum number of spans to return.
|
|
2115
|
+
|
|
2116
|
+
Returns:
|
|
2117
|
+
List[Span]: List of matching spans.
|
|
2118
|
+
"""
|
|
2119
|
+
try:
|
|
2120
|
+
from agno.tracing.schemas import Span as SpanSchema
|
|
2121
|
+
|
|
2122
|
+
all_spans = self._get_all_records("spans")
|
|
2123
|
+
|
|
2124
|
+
# Apply filters
|
|
2125
|
+
filtered_spans = []
|
|
2126
|
+
for span in all_spans:
|
|
2127
|
+
if trace_id and span.get("trace_id") != trace_id:
|
|
2128
|
+
continue
|
|
2129
|
+
if parent_span_id and span.get("parent_span_id") != parent_span_id:
|
|
2130
|
+
continue
|
|
2131
|
+
filtered_spans.append(span)
|
|
2132
|
+
|
|
2133
|
+
# Apply limit
|
|
2134
|
+
if limit:
|
|
2135
|
+
filtered_spans = filtered_spans[:limit]
|
|
2136
|
+
|
|
2137
|
+
return [SpanSchema.from_dict(s) for s in filtered_spans]
|
|
2138
|
+
|
|
2139
|
+
except Exception as e:
|
|
2140
|
+
log_error(f"Error getting spans: {e}")
|
|
2141
|
+
return []
|