agno 2.2.13__py3-none-any.whl → 2.4.3__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/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- 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 +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -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 +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -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 +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/db/mysql/mysql.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from datetime import date, datetime, timedelta, timezone
|
|
3
|
-
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple, Union
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from agno.tracing.schemas import Span, Trace
|
|
7
8
|
|
|
8
9
|
from agno.db.base import BaseDb, SessionType
|
|
10
|
+
from agno.db.migrations.manager import MigrationManager
|
|
9
11
|
from agno.db.mysql.schemas import get_table_schema_definition
|
|
10
12
|
from agno.db.mysql.utils import (
|
|
11
13
|
apply_sorting,
|
|
@@ -28,7 +30,7 @@ from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
|
28
30
|
from agno.utils.string import generate_id
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
|
-
from sqlalchemy import TEXT, and_, cast, func, update
|
|
33
|
+
from sqlalchemy import TEXT, ForeignKey, Index, UniqueConstraint, and_, cast, func, update
|
|
32
34
|
from sqlalchemy.dialects import mysql
|
|
33
35
|
from sqlalchemy.engine import Engine, create_engine
|
|
34
36
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
@@ -41,6 +43,7 @@ except ImportError:
|
|
|
41
43
|
class MySQLDb(BaseDb):
|
|
42
44
|
def __init__(
|
|
43
45
|
self,
|
|
46
|
+
id: Optional[str] = None,
|
|
44
47
|
db_engine: Optional[Engine] = None,
|
|
45
48
|
db_schema: Optional[str] = None,
|
|
46
49
|
db_url: Optional[str] = None,
|
|
@@ -50,7 +53,10 @@ class MySQLDb(BaseDb):
|
|
|
50
53
|
metrics_table: Optional[str] = None,
|
|
51
54
|
eval_table: Optional[str] = None,
|
|
52
55
|
knowledge_table: Optional[str] = None,
|
|
53
|
-
|
|
56
|
+
traces_table: Optional[str] = None,
|
|
57
|
+
spans_table: Optional[str] = None,
|
|
58
|
+
versions_table: Optional[str] = None,
|
|
59
|
+
create_schema: bool = True,
|
|
54
60
|
):
|
|
55
61
|
"""
|
|
56
62
|
Interface for interacting with a MySQL database.
|
|
@@ -61,6 +67,7 @@ class MySQLDb(BaseDb):
|
|
|
61
67
|
3. Raise an error if neither is provided
|
|
62
68
|
|
|
63
69
|
Args:
|
|
70
|
+
id (Optional[str]): ID of the database.
|
|
64
71
|
db_url (Optional[str]): The database URL to connect to.
|
|
65
72
|
db_engine (Optional[Engine]): The SQLAlchemy database engine to use.
|
|
66
73
|
db_schema (Optional[str]): The database schema to use.
|
|
@@ -70,7 +77,11 @@ class MySQLDb(BaseDb):
|
|
|
70
77
|
metrics_table (Optional[str]): Name of the table to store metrics.
|
|
71
78
|
eval_table (Optional[str]): Name of the table to store evaluation runs data.
|
|
72
79
|
knowledge_table (Optional[str]): Name of the table to store knowledge content.
|
|
73
|
-
|
|
80
|
+
traces_table (Optional[str]): Name of the table to store run traces.
|
|
81
|
+
spans_table (Optional[str]): Name of the table to store span events.
|
|
82
|
+
versions_table (Optional[str]): Name of the table to store schema versions.
|
|
83
|
+
create_schema (bool): Whether to automatically create the database schema if it doesn't exist.
|
|
84
|
+
Set to False if schema is managed externally (e.g., via migrations). Defaults to True.
|
|
74
85
|
|
|
75
86
|
Raises:
|
|
76
87
|
ValueError: If neither db_url nor db_engine is provided.
|
|
@@ -90,6 +101,9 @@ class MySQLDb(BaseDb):
|
|
|
90
101
|
metrics_table=metrics_table,
|
|
91
102
|
eval_table=eval_table,
|
|
92
103
|
knowledge_table=knowledge_table,
|
|
104
|
+
traces_table=traces_table,
|
|
105
|
+
spans_table=spans_table,
|
|
106
|
+
versions_table=versions_table,
|
|
93
107
|
)
|
|
94
108
|
|
|
95
109
|
_engine: Optional[Engine] = db_engine
|
|
@@ -101,11 +115,21 @@ class MySQLDb(BaseDb):
|
|
|
101
115
|
self.db_url: Optional[str] = db_url
|
|
102
116
|
self.db_engine: Engine = _engine
|
|
103
117
|
self.db_schema: str = db_schema if db_schema is not None else "ai"
|
|
104
|
-
self.metadata: MetaData = MetaData()
|
|
118
|
+
self.metadata: MetaData = MetaData(schema=self.db_schema)
|
|
119
|
+
self.create_schema: bool = create_schema
|
|
105
120
|
|
|
106
121
|
# Initialize database session
|
|
107
122
|
self.Session: scoped_session = scoped_session(sessionmaker(bind=self.db_engine))
|
|
108
123
|
|
|
124
|
+
def close(self) -> None:
|
|
125
|
+
"""Close database connections and dispose of the connection pool.
|
|
126
|
+
|
|
127
|
+
Should be called during application shutdown to properly release
|
|
128
|
+
all database connections.
|
|
129
|
+
"""
|
|
130
|
+
if self.db_engine is not None:
|
|
131
|
+
self.db_engine.dispose()
|
|
132
|
+
|
|
109
133
|
# -- DB methods --
|
|
110
134
|
def table_exists(self, table_name: str) -> bool:
|
|
111
135
|
"""Check if a table with the given name exists in the MySQL database.
|
|
@@ -119,22 +143,22 @@ class MySQLDb(BaseDb):
|
|
|
119
143
|
with self.Session() as sess:
|
|
120
144
|
return is_table_available(session=sess, table_name=table_name, db_schema=self.db_schema)
|
|
121
145
|
|
|
122
|
-
def _create_table(self, table_name: str, table_type: str
|
|
146
|
+
def _create_table(self, table_name: str, table_type: str) -> Table:
|
|
123
147
|
"""
|
|
124
148
|
Create a table with the appropriate schema based on the table type.
|
|
125
149
|
|
|
126
150
|
Args:
|
|
127
151
|
table_name (str): Name of the table to create
|
|
128
152
|
table_type (str): Type of table (used to get schema definition)
|
|
129
|
-
db_schema (str): Database schema name
|
|
130
153
|
|
|
131
154
|
Returns:
|
|
132
155
|
Table: SQLAlchemy Table object
|
|
133
156
|
"""
|
|
134
157
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
# Pass traces_table_name and db_schema for spans table foreign key resolution
|
|
159
|
+
table_schema = get_table_schema_definition(
|
|
160
|
+
table_type, traces_table_name=self.trace_table_name, db_schema=self.db_schema
|
|
161
|
+
).copy()
|
|
138
162
|
|
|
139
163
|
columns: List[Column] = []
|
|
140
164
|
indexes: List[str] = []
|
|
@@ -154,11 +178,15 @@ class MySQLDb(BaseDb):
|
|
|
154
178
|
if col_config.get("unique", False):
|
|
155
179
|
column_kwargs["unique"] = True
|
|
156
180
|
unique_constraints.append(col_name)
|
|
181
|
+
|
|
182
|
+
# Handle foreign key constraint
|
|
183
|
+
if "foreign_key" in col_config:
|
|
184
|
+
column_args.append(ForeignKey(col_config["foreign_key"]))
|
|
185
|
+
|
|
157
186
|
columns.append(Column(*column_args, **column_kwargs)) # type: ignore
|
|
158
187
|
|
|
159
188
|
# Create the table object
|
|
160
|
-
|
|
161
|
-
table = Table(table_name, table_metadata, *columns, schema=db_schema)
|
|
189
|
+
table = Table(table_name, self.metadata, *columns, schema=self.db_schema)
|
|
162
190
|
|
|
163
191
|
# Add multi-column unique constraints with table-specific names
|
|
164
192
|
for constraint in schema_unique_constraints:
|
|
@@ -171,17 +199,22 @@ class MySQLDb(BaseDb):
|
|
|
171
199
|
idx_name = f"idx_{table_name}_{idx_col}"
|
|
172
200
|
table.append_constraint(Index(idx_name, idx_col))
|
|
173
201
|
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
if self.create_schema:
|
|
203
|
+
with self.Session() as sess, sess.begin():
|
|
204
|
+
create_schema(session=sess, db_schema=self.db_schema)
|
|
176
205
|
|
|
177
206
|
# Create table
|
|
178
|
-
|
|
207
|
+
table_created = False
|
|
208
|
+
if not self.table_exists(table_name):
|
|
209
|
+
table.create(self.db_engine, checkfirst=True)
|
|
210
|
+
log_debug(f"Successfully created table '{table_name}'")
|
|
211
|
+
table_created = True
|
|
212
|
+
else:
|
|
213
|
+
log_debug(f"Table {self.db_schema}.{table_name} already exists, skipping creation")
|
|
179
214
|
|
|
180
215
|
# Create indexes
|
|
181
216
|
for idx in table.indexes:
|
|
182
217
|
try:
|
|
183
|
-
log_debug(f"Creating index: {idx.name}")
|
|
184
|
-
|
|
185
218
|
# Check if index already exists
|
|
186
219
|
with self.Session() as sess:
|
|
187
220
|
exists_query = text(
|
|
@@ -190,24 +223,35 @@ class MySQLDb(BaseDb):
|
|
|
190
223
|
)
|
|
191
224
|
exists = (
|
|
192
225
|
sess.execute(
|
|
193
|
-
exists_query,
|
|
226
|
+
exists_query,
|
|
227
|
+
{"schema": self.db_schema, "table_name": table_name, "index_name": idx.name},
|
|
194
228
|
).scalar()
|
|
195
229
|
is not None
|
|
196
230
|
)
|
|
197
231
|
if exists:
|
|
198
|
-
log_debug(
|
|
232
|
+
log_debug(
|
|
233
|
+
f"Index {idx.name} already exists in {self.db_schema}.{table_name}, skipping creation"
|
|
234
|
+
)
|
|
199
235
|
continue
|
|
200
236
|
|
|
201
237
|
idx.create(self.db_engine)
|
|
202
238
|
|
|
239
|
+
log_debug(f"Created index: {idx.name} for table {self.db_schema}.{table_name}")
|
|
203
240
|
except Exception as e:
|
|
204
241
|
log_error(f"Error creating index {idx.name}: {e}")
|
|
205
242
|
|
|
206
|
-
|
|
243
|
+
# Store the schema version for the created table
|
|
244
|
+
if table_name != self.versions_table_name and table_created:
|
|
245
|
+
latest_schema_version = MigrationManager(self).latest_schema_version
|
|
246
|
+
self.upsert_schema_version(table_name=table_name, version=latest_schema_version.public)
|
|
247
|
+
log_info(
|
|
248
|
+
f"Successfully stored version {latest_schema_version.public} in database for table {table_name}"
|
|
249
|
+
)
|
|
250
|
+
|
|
207
251
|
return table
|
|
208
252
|
|
|
209
253
|
except Exception as e:
|
|
210
|
-
log_error(f"Could not create table {db_schema}.{table_name}: {e}")
|
|
254
|
+
log_error(f"Could not create table {self.db_schema}.{table_name}: {e}")
|
|
211
255
|
raise
|
|
212
256
|
|
|
213
257
|
def _create_all_tables(self):
|
|
@@ -218,17 +262,20 @@ class MySQLDb(BaseDb):
|
|
|
218
262
|
(self.metrics_table_name, "metrics"),
|
|
219
263
|
(self.eval_table_name, "evals"),
|
|
220
264
|
(self.knowledge_table_name, "knowledge"),
|
|
265
|
+
(self.culture_table_name, "culture"),
|
|
266
|
+
(self.trace_table_name, "traces"),
|
|
267
|
+
(self.span_table_name, "spans"),
|
|
268
|
+
(self.versions_table_name, "versions"),
|
|
221
269
|
]
|
|
222
270
|
|
|
223
271
|
for table_name, table_type in tables_to_create:
|
|
224
|
-
self.
|
|
272
|
+
self._get_or_create_table(table_name=table_name, table_type=table_type, create_table_if_not_found=True)
|
|
225
273
|
|
|
226
274
|
def _get_table(self, table_type: str, create_table_if_not_found: Optional[bool] = False) -> Optional[Table]:
|
|
227
275
|
if table_type == "sessions":
|
|
228
276
|
self.session_table = self._get_or_create_table(
|
|
229
277
|
table_name=self.session_table_name,
|
|
230
278
|
table_type="sessions",
|
|
231
|
-
db_schema=self.db_schema,
|
|
232
279
|
create_table_if_not_found=create_table_if_not_found,
|
|
233
280
|
)
|
|
234
281
|
return self.session_table
|
|
@@ -237,7 +284,6 @@ class MySQLDb(BaseDb):
|
|
|
237
284
|
self.memory_table = self._get_or_create_table(
|
|
238
285
|
table_name=self.memory_table_name,
|
|
239
286
|
table_type="memories",
|
|
240
|
-
db_schema=self.db_schema,
|
|
241
287
|
create_table_if_not_found=create_table_if_not_found,
|
|
242
288
|
)
|
|
243
289
|
return self.memory_table
|
|
@@ -246,7 +292,6 @@ class MySQLDb(BaseDb):
|
|
|
246
292
|
self.metrics_table = self._get_or_create_table(
|
|
247
293
|
table_name=self.metrics_table_name,
|
|
248
294
|
table_type="metrics",
|
|
249
|
-
db_schema=self.db_schema,
|
|
250
295
|
create_table_if_not_found=create_table_if_not_found,
|
|
251
296
|
)
|
|
252
297
|
return self.metrics_table
|
|
@@ -255,7 +300,6 @@ class MySQLDb(BaseDb):
|
|
|
255
300
|
self.eval_table = self._get_or_create_table(
|
|
256
301
|
table_name=self.eval_table_name,
|
|
257
302
|
table_type="evals",
|
|
258
|
-
db_schema=self.db_schema,
|
|
259
303
|
create_table_if_not_found=create_table_if_not_found,
|
|
260
304
|
)
|
|
261
305
|
return self.eval_table
|
|
@@ -264,7 +308,6 @@ class MySQLDb(BaseDb):
|
|
|
264
308
|
self.knowledge_table = self._get_or_create_table(
|
|
265
309
|
table_name=self.knowledge_table_name,
|
|
266
310
|
table_type="knowledge",
|
|
267
|
-
db_schema=self.db_schema,
|
|
268
311
|
create_table_if_not_found=create_table_if_not_found,
|
|
269
312
|
)
|
|
270
313
|
return self.knowledge_table
|
|
@@ -273,15 +316,42 @@ class MySQLDb(BaseDb):
|
|
|
273
316
|
self.culture_table = self._get_or_create_table(
|
|
274
317
|
table_name=self.culture_table_name,
|
|
275
318
|
table_type="culture",
|
|
276
|
-
db_schema=self.db_schema,
|
|
277
319
|
create_table_if_not_found=create_table_if_not_found,
|
|
278
320
|
)
|
|
279
321
|
return self.culture_table
|
|
280
322
|
|
|
323
|
+
if table_type == "versions":
|
|
324
|
+
self.versions_table = self._get_or_create_table(
|
|
325
|
+
table_name=self.versions_table_name,
|
|
326
|
+
table_type="versions",
|
|
327
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
328
|
+
)
|
|
329
|
+
return self.versions_table
|
|
330
|
+
|
|
331
|
+
if table_type == "traces":
|
|
332
|
+
self.traces_table = self._get_or_create_table(
|
|
333
|
+
table_name=self.trace_table_name,
|
|
334
|
+
table_type="traces",
|
|
335
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
336
|
+
)
|
|
337
|
+
return self.traces_table
|
|
338
|
+
|
|
339
|
+
if table_type == "spans":
|
|
340
|
+
# Ensure traces table exists first (spans has FK to traces)
|
|
341
|
+
if create_table_if_not_found:
|
|
342
|
+
self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
343
|
+
|
|
344
|
+
self.spans_table = self._get_or_create_table(
|
|
345
|
+
table_name=self.span_table_name,
|
|
346
|
+
table_type="spans",
|
|
347
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
348
|
+
)
|
|
349
|
+
return self.spans_table
|
|
350
|
+
|
|
281
351
|
raise ValueError(f"Unknown table type: {table_type}")
|
|
282
352
|
|
|
283
353
|
def _get_or_create_table(
|
|
284
|
-
self, table_name: str, table_type: str,
|
|
354
|
+
self, table_name: str, table_type: str, create_table_if_not_found: Optional[bool] = False
|
|
285
355
|
) -> Optional[Table]:
|
|
286
356
|
"""
|
|
287
357
|
Check if the table exists and is valid, else create it.
|
|
@@ -289,38 +359,71 @@ class MySQLDb(BaseDb):
|
|
|
289
359
|
Args:
|
|
290
360
|
table_name (str): Name of the table to get or create
|
|
291
361
|
table_type (str): Type of table (used to get schema definition)
|
|
292
|
-
db_schema (str): Database schema name
|
|
293
362
|
|
|
294
363
|
Returns:
|
|
295
364
|
Table: SQLAlchemy Table object representing the schema.
|
|
296
365
|
"""
|
|
297
366
|
|
|
298
367
|
with self.Session() as sess, sess.begin():
|
|
299
|
-
table_is_available = is_table_available(session=sess, table_name=table_name, db_schema=db_schema)
|
|
368
|
+
table_is_available = is_table_available(session=sess, table_name=table_name, db_schema=self.db_schema)
|
|
300
369
|
|
|
301
370
|
if not table_is_available:
|
|
302
371
|
if not create_table_if_not_found:
|
|
303
372
|
return None
|
|
304
373
|
|
|
305
|
-
|
|
374
|
+
created_table = self._create_table(table_name=table_name, table_type=table_type)
|
|
375
|
+
|
|
376
|
+
return created_table
|
|
306
377
|
|
|
307
378
|
if not is_valid_table(
|
|
308
379
|
db_engine=self.db_engine,
|
|
309
380
|
table_name=table_name,
|
|
310
381
|
table_type=table_type,
|
|
311
|
-
db_schema=db_schema,
|
|
382
|
+
db_schema=self.db_schema,
|
|
312
383
|
):
|
|
313
|
-
raise ValueError(f"Table {db_schema}.{table_name} has an invalid schema")
|
|
384
|
+
raise ValueError(f"Table {self.db_schema}.{table_name} has an invalid schema")
|
|
314
385
|
|
|
315
386
|
try:
|
|
316
|
-
table = Table(table_name, self.metadata, schema=db_schema, autoload_with=self.db_engine)
|
|
317
|
-
log_debug(f"Loaded existing table {db_schema}.{table_name}")
|
|
387
|
+
table = Table(table_name, self.metadata, schema=self.db_schema, autoload_with=self.db_engine)
|
|
318
388
|
return table
|
|
319
389
|
|
|
320
390
|
except Exception as e:
|
|
321
|
-
log_error(f"Error loading existing table {db_schema}.{table_name}: {e}")
|
|
391
|
+
log_error(f"Error loading existing table {self.db_schema}.{table_name}: {e}")
|
|
322
392
|
raise
|
|
323
393
|
|
|
394
|
+
def get_latest_schema_version(self, table_name: str) -> str:
|
|
395
|
+
"""Get the latest version of the database schema."""
|
|
396
|
+
table = self._get_table(table_type="versions", create_table_if_not_found=True)
|
|
397
|
+
with self.Session() as sess:
|
|
398
|
+
# Latest version for the given table
|
|
399
|
+
stmt = select(table).where(table.c.table_name == table_name).order_by(table.c.version.desc()).limit(1) # type: ignore
|
|
400
|
+
result = sess.execute(stmt).fetchone()
|
|
401
|
+
if result is None:
|
|
402
|
+
return "2.0.0"
|
|
403
|
+
version_dict = dict(result._mapping)
|
|
404
|
+
return version_dict.get("version") or "2.0.0"
|
|
405
|
+
|
|
406
|
+
def upsert_schema_version(self, table_name: str, version: str) -> None:
|
|
407
|
+
"""Upsert the schema version into the database."""
|
|
408
|
+
table = self._get_table(table_type="versions", create_table_if_not_found=True)
|
|
409
|
+
if table is None:
|
|
410
|
+
return
|
|
411
|
+
current_datetime = datetime.now().isoformat()
|
|
412
|
+
with self.Session() as sess, sess.begin():
|
|
413
|
+
stmt = mysql.insert(table).values( # type: ignore
|
|
414
|
+
table_name=table_name,
|
|
415
|
+
version=version,
|
|
416
|
+
created_at=current_datetime, # Store as ISO format string
|
|
417
|
+
updated_at=current_datetime,
|
|
418
|
+
)
|
|
419
|
+
# Update version if table_name already exists
|
|
420
|
+
stmt = stmt.on_duplicate_key_update(
|
|
421
|
+
version=version,
|
|
422
|
+
created_at=current_datetime,
|
|
423
|
+
updated_at=current_datetime,
|
|
424
|
+
)
|
|
425
|
+
sess.execute(stmt)
|
|
426
|
+
|
|
324
427
|
# -- Session methods --
|
|
325
428
|
def delete_session(self, session_id: str) -> bool:
|
|
326
429
|
"""
|
|
@@ -454,7 +557,7 @@ class MySQLDb(BaseDb):
|
|
|
454
557
|
Args:
|
|
455
558
|
session_type (Optional[SessionType]): The type of sessions to get.
|
|
456
559
|
user_id (Optional[str]): The ID of the user to filter by.
|
|
457
|
-
|
|
560
|
+
component_id (Optional[str]): The ID of the agent / workflow to filter by.
|
|
458
561
|
start_timestamp (Optional[int]): The start timestamp to filter by.
|
|
459
562
|
end_timestamp (Optional[int]): The end timestamp to filter by.
|
|
460
563
|
session_name (Optional[str]): The name of the session to filter by.
|
|
@@ -463,7 +566,6 @@ class MySQLDb(BaseDb):
|
|
|
463
566
|
sort_by (Optional[str]): The field to sort by. Defaults to None.
|
|
464
567
|
sort_order (Optional[str]): The sort order. Defaults to None.
|
|
465
568
|
deserialize (Optional[bool]): Whether to serialize the sessions. Defaults to True.
|
|
466
|
-
create_table_if_not_found (Optional[bool]): Whether to create the table if it doesn't exist.
|
|
467
569
|
|
|
468
570
|
Returns:
|
|
469
571
|
Union[List[Session], Tuple[List[Dict], int]]:
|
|
@@ -1020,9 +1122,12 @@ class MySQLDb(BaseDb):
|
|
|
1020
1122
|
except Exception as e:
|
|
1021
1123
|
log_error(f"Error deleting user memories: {e}")
|
|
1022
1124
|
|
|
1023
|
-
def get_all_memory_topics(self) -> List[str]:
|
|
1125
|
+
def get_all_memory_topics(self, user_id: Optional[str] = None) -> List[str]:
|
|
1024
1126
|
"""Get all memory topics from the database.
|
|
1025
1127
|
|
|
1128
|
+
Args:
|
|
1129
|
+
user_id (Optional[str]): Optional user ID to filter topics.
|
|
1130
|
+
|
|
1026
1131
|
Returns:
|
|
1027
1132
|
List[str]: List of memory topics.
|
|
1028
1133
|
"""
|
|
@@ -1195,7 +1300,7 @@ class MySQLDb(BaseDb):
|
|
|
1195
1300
|
log_error(f"Exception clearing user memories: {e}")
|
|
1196
1301
|
|
|
1197
1302
|
def get_user_memory_stats(
|
|
1198
|
-
self, limit: Optional[int] = None, page: Optional[int] = None
|
|
1303
|
+
self, limit: Optional[int] = None, page: Optional[int] = None, user_id: Optional[str] = None
|
|
1199
1304
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
1200
1305
|
"""Get user memories stats.
|
|
1201
1306
|
|
|
@@ -1224,17 +1329,20 @@ class MySQLDb(BaseDb):
|
|
|
1224
1329
|
return [], 0
|
|
1225
1330
|
|
|
1226
1331
|
with self.Session() as sess, sess.begin():
|
|
1227
|
-
stmt = (
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
func.max(table.c.updated_at).label("last_memory_updated_at"),
|
|
1232
|
-
)
|
|
1233
|
-
.where(table.c.user_id.is_not(None))
|
|
1234
|
-
.group_by(table.c.user_id)
|
|
1235
|
-
.order_by(func.max(table.c.updated_at).desc())
|
|
1332
|
+
stmt = select(
|
|
1333
|
+
table.c.user_id,
|
|
1334
|
+
func.count(table.c.memory_id).label("total_memories"),
|
|
1335
|
+
func.max(table.c.updated_at).label("last_memory_updated_at"),
|
|
1236
1336
|
)
|
|
1237
1337
|
|
|
1338
|
+
if user_id is not None:
|
|
1339
|
+
stmt = stmt.where(table.c.user_id == user_id)
|
|
1340
|
+
else:
|
|
1341
|
+
stmt = stmt.where(table.c.user_id.is_not(None))
|
|
1342
|
+
|
|
1343
|
+
stmt = stmt.group_by(table.c.user_id)
|
|
1344
|
+
stmt = stmt.order_by(func.max(table.c.updated_at).desc())
|
|
1345
|
+
|
|
1238
1346
|
count_stmt = select(func.count()).select_from(stmt.alias())
|
|
1239
1347
|
total_count = sess.execute(count_stmt).scalar()
|
|
1240
1348
|
|
|
@@ -1287,6 +1395,8 @@ class MySQLDb(BaseDb):
|
|
|
1287
1395
|
if memory.memory_id is None:
|
|
1288
1396
|
memory.memory_id = str(uuid4())
|
|
1289
1397
|
|
|
1398
|
+
current_time = int(time.time())
|
|
1399
|
+
|
|
1290
1400
|
stmt = mysql.insert(table).values(
|
|
1291
1401
|
memory_id=memory.memory_id,
|
|
1292
1402
|
memory=memory.memory,
|
|
@@ -1295,7 +1405,9 @@ class MySQLDb(BaseDb):
|
|
|
1295
1405
|
agent_id=memory.agent_id,
|
|
1296
1406
|
team_id=memory.team_id,
|
|
1297
1407
|
topics=memory.topics,
|
|
1298
|
-
|
|
1408
|
+
feedback=memory.feedback,
|
|
1409
|
+
created_at=memory.created_at,
|
|
1410
|
+
updated_at=memory.created_at,
|
|
1299
1411
|
)
|
|
1300
1412
|
stmt = stmt.on_duplicate_key_update(
|
|
1301
1413
|
memory=memory.memory,
|
|
@@ -1303,7 +1415,10 @@ class MySQLDb(BaseDb):
|
|
|
1303
1415
|
input=memory.input,
|
|
1304
1416
|
agent_id=memory.agent_id,
|
|
1305
1417
|
team_id=memory.team_id,
|
|
1306
|
-
|
|
1418
|
+
feedback=memory.feedback,
|
|
1419
|
+
updated_at=current_time,
|
|
1420
|
+
# Preserve created_at on update - don't overwrite existing value
|
|
1421
|
+
created_at=table.c.created_at,
|
|
1307
1422
|
)
|
|
1308
1423
|
sess.execute(stmt)
|
|
1309
1424
|
|
|
@@ -1358,12 +1473,14 @@ class MySQLDb(BaseDb):
|
|
|
1358
1473
|
# Prepare bulk data
|
|
1359
1474
|
bulk_data = []
|
|
1360
1475
|
current_time = int(time.time())
|
|
1476
|
+
|
|
1361
1477
|
for memory in memories:
|
|
1362
1478
|
if memory.memory_id is None:
|
|
1363
1479
|
memory.memory_id = str(uuid4())
|
|
1364
1480
|
|
|
1365
1481
|
# Use preserved updated_at if flag is set and value exists, otherwise use current time
|
|
1366
1482
|
updated_at = memory.updated_at if preserve_updated_at else current_time
|
|
1483
|
+
|
|
1367
1484
|
bulk_data.append(
|
|
1368
1485
|
{
|
|
1369
1486
|
"memory_id": memory.memory_id,
|
|
@@ -1373,6 +1490,8 @@ class MySQLDb(BaseDb):
|
|
|
1373
1490
|
"agent_id": memory.agent_id,
|
|
1374
1491
|
"team_id": memory.team_id,
|
|
1375
1492
|
"topics": memory.topics,
|
|
1493
|
+
"feedback": memory.feedback,
|
|
1494
|
+
"created_at": memory.created_at,
|
|
1376
1495
|
"updated_at": updated_at,
|
|
1377
1496
|
}
|
|
1378
1497
|
)
|
|
@@ -1388,7 +1507,10 @@ class MySQLDb(BaseDb):
|
|
|
1388
1507
|
input=stmt.inserted.input,
|
|
1389
1508
|
agent_id=stmt.inserted.agent_id,
|
|
1390
1509
|
team_id=stmt.inserted.team_id,
|
|
1510
|
+
feedback=stmt.inserted.feedback,
|
|
1391
1511
|
updated_at=stmt.inserted.updated_at,
|
|
1512
|
+
# Preserve created_at on update
|
|
1513
|
+
created_at=table.c.created_at,
|
|
1392
1514
|
)
|
|
1393
1515
|
sess.execute(stmt, bulk_data)
|
|
1394
1516
|
|
|
@@ -2306,3 +2428,550 @@ class MySQLDb(BaseDb):
|
|
|
2306
2428
|
for memory in memories:
|
|
2307
2429
|
self.upsert_user_memory(memory)
|
|
2308
2430
|
log_info(f"Migrated {len(memories)} memories to table: {self.memory_table}")
|
|
2431
|
+
|
|
2432
|
+
# --- Traces ---
|
|
2433
|
+
def _get_traces_base_query(self, table: Table, spans_table: Optional[Table] = None):
|
|
2434
|
+
"""Build base query for traces with aggregated span counts.
|
|
2435
|
+
|
|
2436
|
+
Args:
|
|
2437
|
+
table: The traces table.
|
|
2438
|
+
spans_table: The spans table (optional).
|
|
2439
|
+
|
|
2440
|
+
Returns:
|
|
2441
|
+
SQLAlchemy select statement with total_spans and error_count calculated dynamically.
|
|
2442
|
+
"""
|
|
2443
|
+
from sqlalchemy import case, literal
|
|
2444
|
+
|
|
2445
|
+
if spans_table is not None:
|
|
2446
|
+
# JOIN with spans table to calculate total_spans and error_count
|
|
2447
|
+
return (
|
|
2448
|
+
select(
|
|
2449
|
+
table,
|
|
2450
|
+
func.coalesce(func.count(spans_table.c.span_id), 0).label("total_spans"),
|
|
2451
|
+
func.coalesce(func.sum(case((spans_table.c.status_code == "ERROR", 1), else_=0)), 0).label(
|
|
2452
|
+
"error_count"
|
|
2453
|
+
),
|
|
2454
|
+
)
|
|
2455
|
+
.select_from(table.outerjoin(spans_table, table.c.trace_id == spans_table.c.trace_id))
|
|
2456
|
+
.group_by(table.c.trace_id)
|
|
2457
|
+
)
|
|
2458
|
+
else:
|
|
2459
|
+
# Fallback if spans table doesn't exist
|
|
2460
|
+
return select(table, literal(0).label("total_spans"), literal(0).label("error_count"))
|
|
2461
|
+
|
|
2462
|
+
def _get_trace_component_level_expr(self, workflow_id_col, team_id_col, agent_id_col, name_col):
|
|
2463
|
+
"""Build a SQL CASE expression that returns the component level for a trace.
|
|
2464
|
+
|
|
2465
|
+
Component levels (higher = more important):
|
|
2466
|
+
- 3: Workflow root (.run or .arun with workflow_id)
|
|
2467
|
+
- 2: Team root (.run or .arun with team_id)
|
|
2468
|
+
- 1: Agent root (.run or .arun with agent_id)
|
|
2469
|
+
- 0: Child span (not a root)
|
|
2470
|
+
|
|
2471
|
+
Args:
|
|
2472
|
+
workflow_id_col: SQL column/expression for workflow_id
|
|
2473
|
+
team_id_col: SQL column/expression for team_id
|
|
2474
|
+
agent_id_col: SQL column/expression for agent_id
|
|
2475
|
+
name_col: SQL column/expression for name
|
|
2476
|
+
|
|
2477
|
+
Returns:
|
|
2478
|
+
SQLAlchemy CASE expression returning the component level as an integer.
|
|
2479
|
+
"""
|
|
2480
|
+
from sqlalchemy import and_, case, or_
|
|
2481
|
+
|
|
2482
|
+
is_root_name = or_(name_col.like("%.run%"), name_col.like("%.arun%"))
|
|
2483
|
+
|
|
2484
|
+
return case(
|
|
2485
|
+
# Workflow root (level 3)
|
|
2486
|
+
(and_(workflow_id_col.isnot(None), is_root_name), 3),
|
|
2487
|
+
# Team root (level 2)
|
|
2488
|
+
(and_(team_id_col.isnot(None), is_root_name), 2),
|
|
2489
|
+
# Agent root (level 1)
|
|
2490
|
+
(and_(agent_id_col.isnot(None), is_root_name), 1),
|
|
2491
|
+
# Child span or unknown (level 0)
|
|
2492
|
+
else_=0,
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
def upsert_trace(self, trace: "Trace") -> None:
|
|
2496
|
+
"""Create or update a single trace record in the database.
|
|
2497
|
+
|
|
2498
|
+
Uses INSERT ... ON DUPLICATE KEY UPDATE (upsert) to handle concurrent inserts
|
|
2499
|
+
atomically and avoid race conditions.
|
|
2500
|
+
|
|
2501
|
+
Args:
|
|
2502
|
+
trace: The Trace object to store (one per trace_id).
|
|
2503
|
+
"""
|
|
2504
|
+
from sqlalchemy import case
|
|
2505
|
+
|
|
2506
|
+
try:
|
|
2507
|
+
table = self._get_table(table_type="traces", create_table_if_not_found=True)
|
|
2508
|
+
if table is None:
|
|
2509
|
+
return
|
|
2510
|
+
|
|
2511
|
+
trace_dict = trace.to_dict()
|
|
2512
|
+
trace_dict.pop("total_spans", None)
|
|
2513
|
+
trace_dict.pop("error_count", None)
|
|
2514
|
+
|
|
2515
|
+
with self.Session() as sess, sess.begin():
|
|
2516
|
+
# Use upsert to handle concurrent inserts atomically
|
|
2517
|
+
# On conflict, update fields while preserving existing non-null context values
|
|
2518
|
+
# and keeping the earliest start_time
|
|
2519
|
+
insert_stmt = mysql.insert(table).values(trace_dict)
|
|
2520
|
+
|
|
2521
|
+
# Build component level expressions for comparing trace priority
|
|
2522
|
+
new_level = self._get_trace_component_level_expr(
|
|
2523
|
+
insert_stmt.inserted.workflow_id,
|
|
2524
|
+
insert_stmt.inserted.team_id,
|
|
2525
|
+
insert_stmt.inserted.agent_id,
|
|
2526
|
+
insert_stmt.inserted.name,
|
|
2527
|
+
)
|
|
2528
|
+
existing_level = self._get_trace_component_level_expr(
|
|
2529
|
+
table.c.workflow_id,
|
|
2530
|
+
table.c.team_id,
|
|
2531
|
+
table.c.agent_id,
|
|
2532
|
+
table.c.name,
|
|
2533
|
+
)
|
|
2534
|
+
|
|
2535
|
+
# Build the ON DUPLICATE KEY UPDATE clause
|
|
2536
|
+
# Use LEAST for start_time, GREATEST for end_time to capture full trace duration
|
|
2537
|
+
# MySQL stores timestamps as ISO strings, so string comparison works for ISO format
|
|
2538
|
+
# Duration is calculated using TIMESTAMPDIFF in microseconds then converted to ms
|
|
2539
|
+
upsert_stmt = insert_stmt.on_duplicate_key_update(
|
|
2540
|
+
end_time=func.greatest(table.c.end_time, insert_stmt.inserted.end_time),
|
|
2541
|
+
start_time=func.least(table.c.start_time, insert_stmt.inserted.start_time),
|
|
2542
|
+
# Calculate duration in milliseconds using TIMESTAMPDIFF
|
|
2543
|
+
# TIMESTAMPDIFF(MICROSECOND, start, end) / 1000 gives milliseconds
|
|
2544
|
+
duration_ms=func.timestampdiff(
|
|
2545
|
+
text("MICROSECOND"),
|
|
2546
|
+
func.least(table.c.start_time, insert_stmt.inserted.start_time),
|
|
2547
|
+
func.greatest(table.c.end_time, insert_stmt.inserted.end_time),
|
|
2548
|
+
)
|
|
2549
|
+
/ 1000,
|
|
2550
|
+
status=insert_stmt.inserted.status,
|
|
2551
|
+
# Update name only if new trace is from a higher-level component
|
|
2552
|
+
# Priority: workflow (3) > team (2) > agent (1) > child spans (0)
|
|
2553
|
+
name=case(
|
|
2554
|
+
(new_level > existing_level, insert_stmt.inserted.name),
|
|
2555
|
+
else_=table.c.name,
|
|
2556
|
+
),
|
|
2557
|
+
# Preserve existing non-null context values using COALESCE
|
|
2558
|
+
run_id=func.coalesce(insert_stmt.inserted.run_id, table.c.run_id),
|
|
2559
|
+
session_id=func.coalesce(insert_stmt.inserted.session_id, table.c.session_id),
|
|
2560
|
+
user_id=func.coalesce(insert_stmt.inserted.user_id, table.c.user_id),
|
|
2561
|
+
agent_id=func.coalesce(insert_stmt.inserted.agent_id, table.c.agent_id),
|
|
2562
|
+
team_id=func.coalesce(insert_stmt.inserted.team_id, table.c.team_id),
|
|
2563
|
+
workflow_id=func.coalesce(insert_stmt.inserted.workflow_id, table.c.workflow_id),
|
|
2564
|
+
)
|
|
2565
|
+
sess.execute(upsert_stmt)
|
|
2566
|
+
|
|
2567
|
+
except Exception as e:
|
|
2568
|
+
log_error(f"Error creating trace: {e}")
|
|
2569
|
+
# Don't raise - tracing should not break the main application flow
|
|
2570
|
+
|
|
2571
|
+
def get_trace(
|
|
2572
|
+
self,
|
|
2573
|
+
trace_id: Optional[str] = None,
|
|
2574
|
+
run_id: Optional[str] = None,
|
|
2575
|
+
):
|
|
2576
|
+
"""Get a single trace by trace_id or other filters.
|
|
2577
|
+
|
|
2578
|
+
Args:
|
|
2579
|
+
trace_id: The unique trace identifier.
|
|
2580
|
+
run_id: Filter by run ID (returns first match).
|
|
2581
|
+
|
|
2582
|
+
Returns:
|
|
2583
|
+
Optional[Trace]: The trace if found, None otherwise.
|
|
2584
|
+
|
|
2585
|
+
Note:
|
|
2586
|
+
If multiple filters are provided, trace_id takes precedence.
|
|
2587
|
+
For other filters, the most recent trace is returned.
|
|
2588
|
+
"""
|
|
2589
|
+
try:
|
|
2590
|
+
from agno.tracing.schemas import Trace
|
|
2591
|
+
|
|
2592
|
+
table = self._get_table(table_type="traces")
|
|
2593
|
+
if table is None:
|
|
2594
|
+
return None
|
|
2595
|
+
|
|
2596
|
+
# Get spans table for JOIN
|
|
2597
|
+
spans_table = self._get_table(table_type="spans")
|
|
2598
|
+
|
|
2599
|
+
with self.Session() as sess:
|
|
2600
|
+
# Build query with aggregated span counts
|
|
2601
|
+
stmt = self._get_traces_base_query(table, spans_table)
|
|
2602
|
+
|
|
2603
|
+
if trace_id:
|
|
2604
|
+
stmt = stmt.where(table.c.trace_id == trace_id)
|
|
2605
|
+
elif run_id:
|
|
2606
|
+
stmt = stmt.where(table.c.run_id == run_id)
|
|
2607
|
+
else:
|
|
2608
|
+
log_debug("get_trace called without any filter parameters")
|
|
2609
|
+
return None
|
|
2610
|
+
|
|
2611
|
+
# Order by most recent and get first result
|
|
2612
|
+
stmt = stmt.order_by(table.c.start_time.desc()).limit(1)
|
|
2613
|
+
result = sess.execute(stmt).fetchone()
|
|
2614
|
+
|
|
2615
|
+
if result:
|
|
2616
|
+
return Trace.from_dict(dict(result._mapping))
|
|
2617
|
+
return None
|
|
2618
|
+
|
|
2619
|
+
except Exception as e:
|
|
2620
|
+
log_error(f"Error getting trace: {e}")
|
|
2621
|
+
return None
|
|
2622
|
+
|
|
2623
|
+
def get_traces(
|
|
2624
|
+
self,
|
|
2625
|
+
run_id: Optional[str] = None,
|
|
2626
|
+
session_id: Optional[str] = None,
|
|
2627
|
+
user_id: Optional[str] = None,
|
|
2628
|
+
agent_id: Optional[str] = None,
|
|
2629
|
+
team_id: Optional[str] = None,
|
|
2630
|
+
workflow_id: Optional[str] = None,
|
|
2631
|
+
status: Optional[str] = None,
|
|
2632
|
+
start_time: Optional[datetime] = None,
|
|
2633
|
+
end_time: Optional[datetime] = None,
|
|
2634
|
+
limit: Optional[int] = 20,
|
|
2635
|
+
page: Optional[int] = 1,
|
|
2636
|
+
) -> tuple[List, int]:
|
|
2637
|
+
"""Get traces matching the provided filters with pagination.
|
|
2638
|
+
|
|
2639
|
+
Args:
|
|
2640
|
+
run_id: Filter by run ID.
|
|
2641
|
+
session_id: Filter by session ID.
|
|
2642
|
+
user_id: Filter by user ID.
|
|
2643
|
+
agent_id: Filter by agent ID.
|
|
2644
|
+
team_id: Filter by team ID.
|
|
2645
|
+
workflow_id: Filter by workflow ID.
|
|
2646
|
+
status: Filter by status (OK, ERROR, UNSET).
|
|
2647
|
+
start_time: Filter traces starting after this datetime.
|
|
2648
|
+
end_time: Filter traces ending before this datetime.
|
|
2649
|
+
limit: Maximum number of traces to return per page.
|
|
2650
|
+
page: Page number (1-indexed).
|
|
2651
|
+
|
|
2652
|
+
Returns:
|
|
2653
|
+
tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
|
|
2654
|
+
"""
|
|
2655
|
+
try:
|
|
2656
|
+
from agno.tracing.schemas import Trace
|
|
2657
|
+
|
|
2658
|
+
log_debug(
|
|
2659
|
+
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}"
|
|
2660
|
+
)
|
|
2661
|
+
|
|
2662
|
+
table = self._get_table(table_type="traces")
|
|
2663
|
+
if table is None:
|
|
2664
|
+
log_debug("Traces table not found")
|
|
2665
|
+
return [], 0
|
|
2666
|
+
|
|
2667
|
+
# Get spans table for JOIN
|
|
2668
|
+
spans_table = self._get_table(table_type="spans")
|
|
2669
|
+
|
|
2670
|
+
with self.Session() as sess:
|
|
2671
|
+
# Build base query with aggregated span counts
|
|
2672
|
+
base_stmt = self._get_traces_base_query(table, spans_table)
|
|
2673
|
+
|
|
2674
|
+
# Apply filters
|
|
2675
|
+
if run_id:
|
|
2676
|
+
base_stmt = base_stmt.where(table.c.run_id == run_id)
|
|
2677
|
+
if session_id:
|
|
2678
|
+
base_stmt = base_stmt.where(table.c.session_id == session_id)
|
|
2679
|
+
if user_id:
|
|
2680
|
+
base_stmt = base_stmt.where(table.c.user_id == user_id)
|
|
2681
|
+
if agent_id:
|
|
2682
|
+
base_stmt = base_stmt.where(table.c.agent_id == agent_id)
|
|
2683
|
+
if team_id:
|
|
2684
|
+
base_stmt = base_stmt.where(table.c.team_id == team_id)
|
|
2685
|
+
if workflow_id:
|
|
2686
|
+
base_stmt = base_stmt.where(table.c.workflow_id == workflow_id)
|
|
2687
|
+
if status:
|
|
2688
|
+
base_stmt = base_stmt.where(table.c.status == status)
|
|
2689
|
+
if start_time:
|
|
2690
|
+
# Convert datetime to ISO string for comparison
|
|
2691
|
+
base_stmt = base_stmt.where(table.c.start_time >= start_time.isoformat())
|
|
2692
|
+
if end_time:
|
|
2693
|
+
# Convert datetime to ISO string for comparison
|
|
2694
|
+
base_stmt = base_stmt.where(table.c.end_time <= end_time.isoformat())
|
|
2695
|
+
|
|
2696
|
+
# Get total count
|
|
2697
|
+
count_stmt = select(func.count()).select_from(base_stmt.alias())
|
|
2698
|
+
total_count = sess.execute(count_stmt).scalar() or 0
|
|
2699
|
+
|
|
2700
|
+
# Apply pagination
|
|
2701
|
+
offset = (page - 1) * limit if page and limit else 0
|
|
2702
|
+
paginated_stmt = base_stmt.order_by(table.c.start_time.desc()).limit(limit).offset(offset)
|
|
2703
|
+
|
|
2704
|
+
results = sess.execute(paginated_stmt).fetchall()
|
|
2705
|
+
|
|
2706
|
+
traces = [Trace.from_dict(dict(row._mapping)) for row in results]
|
|
2707
|
+
return traces, total_count
|
|
2708
|
+
|
|
2709
|
+
except Exception as e:
|
|
2710
|
+
log_error(f"Error getting traces: {e}")
|
|
2711
|
+
return [], 0
|
|
2712
|
+
|
|
2713
|
+
def get_trace_stats(
|
|
2714
|
+
self,
|
|
2715
|
+
user_id: Optional[str] = None,
|
|
2716
|
+
agent_id: Optional[str] = None,
|
|
2717
|
+
team_id: Optional[str] = None,
|
|
2718
|
+
workflow_id: Optional[str] = None,
|
|
2719
|
+
start_time: Optional[datetime] = None,
|
|
2720
|
+
end_time: Optional[datetime] = None,
|
|
2721
|
+
limit: Optional[int] = 20,
|
|
2722
|
+
page: Optional[int] = 1,
|
|
2723
|
+
) -> tuple[List[Dict[str, Any]], int]:
|
|
2724
|
+
"""Get trace statistics grouped by session.
|
|
2725
|
+
|
|
2726
|
+
Args:
|
|
2727
|
+
user_id: Filter by user ID.
|
|
2728
|
+
agent_id: Filter by agent ID.
|
|
2729
|
+
team_id: Filter by team ID.
|
|
2730
|
+
workflow_id: Filter by workflow ID.
|
|
2731
|
+
start_time: Filter sessions with traces created after this datetime.
|
|
2732
|
+
end_time: Filter sessions with traces created before this datetime.
|
|
2733
|
+
limit: Maximum number of sessions to return per page.
|
|
2734
|
+
page: Page number (1-indexed).
|
|
2735
|
+
|
|
2736
|
+
Returns:
|
|
2737
|
+
tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
|
|
2738
|
+
Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
|
|
2739
|
+
workflow_id, first_trace_at, last_trace_at.
|
|
2740
|
+
"""
|
|
2741
|
+
try:
|
|
2742
|
+
table = self._get_table(table_type="traces")
|
|
2743
|
+
if table is None:
|
|
2744
|
+
log_debug("Traces table not found")
|
|
2745
|
+
return [], 0
|
|
2746
|
+
|
|
2747
|
+
with self.Session() as sess:
|
|
2748
|
+
# Build base query grouped by session_id
|
|
2749
|
+
base_stmt = (
|
|
2750
|
+
select(
|
|
2751
|
+
table.c.session_id,
|
|
2752
|
+
table.c.user_id,
|
|
2753
|
+
table.c.agent_id,
|
|
2754
|
+
table.c.team_id,
|
|
2755
|
+
table.c.workflow_id,
|
|
2756
|
+
func.count(table.c.trace_id).label("total_traces"),
|
|
2757
|
+
func.min(table.c.created_at).label("first_trace_at"),
|
|
2758
|
+
func.max(table.c.created_at).label("last_trace_at"),
|
|
2759
|
+
)
|
|
2760
|
+
.where(table.c.session_id.isnot(None)) # Only sessions with session_id
|
|
2761
|
+
.group_by(
|
|
2762
|
+
table.c.session_id, table.c.user_id, table.c.agent_id, table.c.team_id, table.c.workflow_id
|
|
2763
|
+
)
|
|
2764
|
+
)
|
|
2765
|
+
|
|
2766
|
+
# Apply filters
|
|
2767
|
+
if user_id:
|
|
2768
|
+
base_stmt = base_stmt.where(table.c.user_id == user_id)
|
|
2769
|
+
if workflow_id:
|
|
2770
|
+
base_stmt = base_stmt.where(table.c.workflow_id == workflow_id)
|
|
2771
|
+
if team_id:
|
|
2772
|
+
base_stmt = base_stmt.where(table.c.team_id == team_id)
|
|
2773
|
+
if agent_id:
|
|
2774
|
+
base_stmt = base_stmt.where(table.c.agent_id == agent_id)
|
|
2775
|
+
if start_time:
|
|
2776
|
+
# Convert datetime to ISO string for comparison
|
|
2777
|
+
base_stmt = base_stmt.where(table.c.created_at >= start_time.isoformat())
|
|
2778
|
+
if end_time:
|
|
2779
|
+
# Convert datetime to ISO string for comparison
|
|
2780
|
+
base_stmt = base_stmt.where(table.c.created_at <= end_time.isoformat())
|
|
2781
|
+
|
|
2782
|
+
# Get total count of sessions
|
|
2783
|
+
count_stmt = select(func.count()).select_from(base_stmt.alias())
|
|
2784
|
+
total_count = sess.execute(count_stmt).scalar() or 0
|
|
2785
|
+
|
|
2786
|
+
# Apply pagination and ordering
|
|
2787
|
+
offset = (page - 1) * limit if page and limit else 0
|
|
2788
|
+
paginated_stmt = base_stmt.order_by(func.max(table.c.created_at).desc()).limit(limit).offset(offset)
|
|
2789
|
+
|
|
2790
|
+
results = sess.execute(paginated_stmt).fetchall()
|
|
2791
|
+
|
|
2792
|
+
# Convert to list of dicts with datetime objects
|
|
2793
|
+
stats_list = []
|
|
2794
|
+
for row in results:
|
|
2795
|
+
# Convert ISO strings to datetime objects
|
|
2796
|
+
first_trace_at_str = row.first_trace_at
|
|
2797
|
+
last_trace_at_str = row.last_trace_at
|
|
2798
|
+
|
|
2799
|
+
# Parse ISO format strings to datetime objects
|
|
2800
|
+
first_trace_at = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
|
|
2801
|
+
last_trace_at = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
|
|
2802
|
+
|
|
2803
|
+
stats_list.append(
|
|
2804
|
+
{
|
|
2805
|
+
"session_id": row.session_id,
|
|
2806
|
+
"user_id": row.user_id,
|
|
2807
|
+
"agent_id": row.agent_id,
|
|
2808
|
+
"team_id": row.team_id,
|
|
2809
|
+
"workflow_id": row.workflow_id,
|
|
2810
|
+
"total_traces": row.total_traces,
|
|
2811
|
+
"first_trace_at": first_trace_at,
|
|
2812
|
+
"last_trace_at": last_trace_at,
|
|
2813
|
+
}
|
|
2814
|
+
)
|
|
2815
|
+
|
|
2816
|
+
return stats_list, total_count
|
|
2817
|
+
|
|
2818
|
+
except Exception as e:
|
|
2819
|
+
log_error(f"Error getting trace stats: {e}")
|
|
2820
|
+
return [], 0
|
|
2821
|
+
|
|
2822
|
+
# --- Spans ---
|
|
2823
|
+
def create_span(self, span: "Span") -> None:
|
|
2824
|
+
"""Create a single span in the database.
|
|
2825
|
+
|
|
2826
|
+
Args:
|
|
2827
|
+
span: The Span object to store.
|
|
2828
|
+
"""
|
|
2829
|
+
try:
|
|
2830
|
+
table = self._get_table(table_type="spans", create_table_if_not_found=True)
|
|
2831
|
+
if table is None:
|
|
2832
|
+
return
|
|
2833
|
+
|
|
2834
|
+
with self.Session() as sess, sess.begin():
|
|
2835
|
+
stmt = mysql.insert(table).values(span.to_dict())
|
|
2836
|
+
sess.execute(stmt)
|
|
2837
|
+
|
|
2838
|
+
except Exception as e:
|
|
2839
|
+
log_error(f"Error creating span: {e}")
|
|
2840
|
+
|
|
2841
|
+
def create_spans(self, spans: List) -> None:
|
|
2842
|
+
"""Create multiple spans in the database as a batch.
|
|
2843
|
+
|
|
2844
|
+
Args:
|
|
2845
|
+
spans: List of Span objects to store.
|
|
2846
|
+
"""
|
|
2847
|
+
if not spans:
|
|
2848
|
+
return
|
|
2849
|
+
|
|
2850
|
+
try:
|
|
2851
|
+
table = self._get_table(table_type="spans", create_table_if_not_found=True)
|
|
2852
|
+
if table is None:
|
|
2853
|
+
return
|
|
2854
|
+
|
|
2855
|
+
with self.Session() as sess, sess.begin():
|
|
2856
|
+
for span in spans:
|
|
2857
|
+
stmt = mysql.insert(table).values(span.to_dict())
|
|
2858
|
+
sess.execute(stmt)
|
|
2859
|
+
|
|
2860
|
+
except Exception as e:
|
|
2861
|
+
log_error(f"Error creating spans batch: {e}")
|
|
2862
|
+
|
|
2863
|
+
def get_span(self, span_id: str):
|
|
2864
|
+
"""Get a single span by its span_id.
|
|
2865
|
+
|
|
2866
|
+
Args:
|
|
2867
|
+
span_id: The unique span identifier.
|
|
2868
|
+
|
|
2869
|
+
Returns:
|
|
2870
|
+
Optional[Span]: The span if found, None otherwise.
|
|
2871
|
+
"""
|
|
2872
|
+
try:
|
|
2873
|
+
from agno.tracing.schemas import Span
|
|
2874
|
+
|
|
2875
|
+
table = self._get_table(table_type="spans")
|
|
2876
|
+
if table is None:
|
|
2877
|
+
return None
|
|
2878
|
+
|
|
2879
|
+
with self.Session() as sess:
|
|
2880
|
+
stmt = select(table).where(table.c.span_id == span_id)
|
|
2881
|
+
result = sess.execute(stmt).fetchone()
|
|
2882
|
+
if result:
|
|
2883
|
+
return Span.from_dict(dict(result._mapping))
|
|
2884
|
+
return None
|
|
2885
|
+
|
|
2886
|
+
except Exception as e:
|
|
2887
|
+
log_error(f"Error getting span: {e}")
|
|
2888
|
+
return None
|
|
2889
|
+
|
|
2890
|
+
def get_spans(
|
|
2891
|
+
self,
|
|
2892
|
+
trace_id: Optional[str] = None,
|
|
2893
|
+
parent_span_id: Optional[str] = None,
|
|
2894
|
+
limit: Optional[int] = 1000,
|
|
2895
|
+
) -> List:
|
|
2896
|
+
"""Get spans matching the provided filters.
|
|
2897
|
+
|
|
2898
|
+
Args:
|
|
2899
|
+
trace_id: Filter by trace ID.
|
|
2900
|
+
parent_span_id: Filter by parent span ID.
|
|
2901
|
+
limit: Maximum number of spans to return.
|
|
2902
|
+
|
|
2903
|
+
Returns:
|
|
2904
|
+
List[Span]: List of matching spans.
|
|
2905
|
+
"""
|
|
2906
|
+
try:
|
|
2907
|
+
from agno.tracing.schemas import Span
|
|
2908
|
+
|
|
2909
|
+
table = self._get_table(table_type="spans")
|
|
2910
|
+
if table is None:
|
|
2911
|
+
return []
|
|
2912
|
+
|
|
2913
|
+
with self.Session() as sess:
|
|
2914
|
+
stmt = select(table)
|
|
2915
|
+
|
|
2916
|
+
# Apply filters
|
|
2917
|
+
if trace_id:
|
|
2918
|
+
stmt = stmt.where(table.c.trace_id == trace_id)
|
|
2919
|
+
if parent_span_id:
|
|
2920
|
+
stmt = stmt.where(table.c.parent_span_id == parent_span_id)
|
|
2921
|
+
|
|
2922
|
+
if limit:
|
|
2923
|
+
stmt = stmt.limit(limit)
|
|
2924
|
+
|
|
2925
|
+
results = sess.execute(stmt).fetchall()
|
|
2926
|
+
return [Span.from_dict(dict(row._mapping)) for row in results]
|
|
2927
|
+
|
|
2928
|
+
except Exception as e:
|
|
2929
|
+
log_error(f"Error getting spans: {e}")
|
|
2930
|
+
return []
|
|
2931
|
+
|
|
2932
|
+
# -- Learning methods (stubs) --
|
|
2933
|
+
def get_learning(
|
|
2934
|
+
self,
|
|
2935
|
+
learning_type: str,
|
|
2936
|
+
user_id: Optional[str] = None,
|
|
2937
|
+
agent_id: Optional[str] = None,
|
|
2938
|
+
team_id: Optional[str] = None,
|
|
2939
|
+
session_id: Optional[str] = None,
|
|
2940
|
+
namespace: Optional[str] = None,
|
|
2941
|
+
entity_id: Optional[str] = None,
|
|
2942
|
+
entity_type: Optional[str] = None,
|
|
2943
|
+
) -> Optional[Dict[str, Any]]:
|
|
2944
|
+
raise NotImplementedError("Learning methods not yet implemented for MySQLDb")
|
|
2945
|
+
|
|
2946
|
+
def upsert_learning(
|
|
2947
|
+
self,
|
|
2948
|
+
id: str,
|
|
2949
|
+
learning_type: str,
|
|
2950
|
+
content: Dict[str, Any],
|
|
2951
|
+
user_id: Optional[str] = None,
|
|
2952
|
+
agent_id: Optional[str] = None,
|
|
2953
|
+
team_id: Optional[str] = None,
|
|
2954
|
+
session_id: Optional[str] = None,
|
|
2955
|
+
namespace: Optional[str] = None,
|
|
2956
|
+
entity_id: Optional[str] = None,
|
|
2957
|
+
entity_type: Optional[str] = None,
|
|
2958
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
2959
|
+
) -> None:
|
|
2960
|
+
raise NotImplementedError("Learning methods not yet implemented for MySQLDb")
|
|
2961
|
+
|
|
2962
|
+
def delete_learning(self, id: str) -> bool:
|
|
2963
|
+
raise NotImplementedError("Learning methods not yet implemented for MySQLDb")
|
|
2964
|
+
|
|
2965
|
+
def get_learnings(
|
|
2966
|
+
self,
|
|
2967
|
+
learning_type: Optional[str] = None,
|
|
2968
|
+
user_id: Optional[str] = None,
|
|
2969
|
+
agent_id: Optional[str] = None,
|
|
2970
|
+
team_id: Optional[str] = None,
|
|
2971
|
+
session_id: Optional[str] = None,
|
|
2972
|
+
namespace: Optional[str] = None,
|
|
2973
|
+
entity_id: Optional[str] = None,
|
|
2974
|
+
entity_type: Optional[str] = None,
|
|
2975
|
+
limit: Optional[int] = None,
|
|
2976
|
+
) -> List[Dict[str, Any]]:
|
|
2977
|
+
raise NotImplementedError("Learning methods not yet implemented for MySQLDb")
|