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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from packaging import version as packaging_version
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
|
|
7
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
8
|
+
from agno.utils.log import log_error, log_info, log_warning
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MigrationManager:
|
|
12
|
+
"""Manager class to handle database migrations"""
|
|
13
|
+
|
|
14
|
+
available_versions: list[tuple[str, Version]] = [
|
|
15
|
+
("v2_0_0", packaging_version.parse("2.0.0")),
|
|
16
|
+
("v2_3_0", packaging_version.parse("2.3.0")),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def __init__(self, db: Union[AsyncBaseDb, BaseDb]):
|
|
20
|
+
self.db = db
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def latest_schema_version(self) -> Version:
|
|
24
|
+
return self.available_versions[-1][1]
|
|
25
|
+
|
|
26
|
+
async def up(self, target_version: Optional[str] = None, table_type: Optional[str] = None, force: bool = False):
|
|
27
|
+
"""Handle executing an up migration.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
target_version: The version to migrate to, e.g. "v3.0.0". If not provided, the latest available version will be used.
|
|
31
|
+
table_type: The type of table to migrate. If not provided, all table types will be considered.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# If not target version is provided, use the latest available version
|
|
35
|
+
if not target_version:
|
|
36
|
+
_target_version = self.latest_schema_version
|
|
37
|
+
log_info(
|
|
38
|
+
f"No target version provided. Will migrate to the latest available version: {str(_target_version)}"
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
_target_version = packaging_version.parse(target_version)
|
|
42
|
+
|
|
43
|
+
# Select tables to migrate
|
|
44
|
+
if table_type:
|
|
45
|
+
if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
|
|
46
|
+
log_warning(
|
|
47
|
+
f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
|
|
51
|
+
else:
|
|
52
|
+
tables = [
|
|
53
|
+
("memories", self.db.memory_table_name),
|
|
54
|
+
("sessions", self.db.session_table_name),
|
|
55
|
+
("metrics", self.db.metrics_table_name),
|
|
56
|
+
("evals", self.db.eval_table_name),
|
|
57
|
+
("knowledge", self.db.knowledge_table_name),
|
|
58
|
+
("culture", self.db.culture_table_name),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Handle migrations for each table separately (extend in future if needed):
|
|
62
|
+
for table_type, table_name in tables:
|
|
63
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
64
|
+
current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
|
|
65
|
+
else:
|
|
66
|
+
current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
|
|
67
|
+
|
|
68
|
+
if current_version is None:
|
|
69
|
+
log_info(f"Skipping migration: No version found for table {table_name}.")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# If the target version is less or equal to the current version, no migrations needed
|
|
73
|
+
if _target_version <= current_version and not force:
|
|
74
|
+
log_info(
|
|
75
|
+
f"Skipping migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
log_info(
|
|
80
|
+
f"Starting database migration for table {table_name}. Current version: {current_version}. Target version: {_target_version}."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Find files after the current version
|
|
84
|
+
latest_version = None
|
|
85
|
+
migration_executed = False
|
|
86
|
+
for version, normalised_version in self.available_versions:
|
|
87
|
+
if normalised_version > current_version:
|
|
88
|
+
if target_version and normalised_version > _target_version:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
log_info(f"Applying migration {normalised_version} on {table_name}")
|
|
92
|
+
migration_executed = await self._up_migration(version, table_type, table_name)
|
|
93
|
+
if migration_executed:
|
|
94
|
+
log_info(f"Successfully applied migration {normalised_version} on table {table_name}")
|
|
95
|
+
else:
|
|
96
|
+
log_info(f"Skipping application of migration {normalised_version} on table {table_name}")
|
|
97
|
+
|
|
98
|
+
latest_version = normalised_version.public
|
|
99
|
+
|
|
100
|
+
if migration_executed and latest_version:
|
|
101
|
+
log_info(f"Storing version {latest_version} in database for table {table_name}")
|
|
102
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
103
|
+
await self.db.upsert_schema_version(table_name, latest_version)
|
|
104
|
+
else:
|
|
105
|
+
self.db.upsert_schema_version(table_name, latest_version)
|
|
106
|
+
log_info(f"Successfully stored version {latest_version} in database for table {table_name}")
|
|
107
|
+
log_info("----------------------------------------------------------")
|
|
108
|
+
|
|
109
|
+
async def _up_migration(self, version: str, table_type: str, table_name: str) -> bool:
|
|
110
|
+
"""Run the database-specific logic to handle an up migration.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
version: The version to migrate to, e.g. "v3.0.0"
|
|
114
|
+
"""
|
|
115
|
+
migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
119
|
+
return await migration_module.async_up(self.db, table_type, table_name)
|
|
120
|
+
else:
|
|
121
|
+
return migration_module.up(self.db, table_type, table_name)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
log_error(f"Error running migration to version {version}: {e}")
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
async def down(self, target_version: str, table_type: Optional[str] = None, force: bool = False):
|
|
127
|
+
"""Handle executing a down migration.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
target_version: The version to migrate to. e.g. "v2.3.0"
|
|
131
|
+
table_type: The type of table to migrate. If not provided, all table types will be considered.
|
|
132
|
+
"""
|
|
133
|
+
_target_version = packaging_version.parse(target_version)
|
|
134
|
+
|
|
135
|
+
# Select tables to migrate
|
|
136
|
+
if table_type:
|
|
137
|
+
if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
|
|
138
|
+
log_warning(
|
|
139
|
+
f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
|
|
143
|
+
else:
|
|
144
|
+
tables = [
|
|
145
|
+
("memories", self.db.memory_table_name),
|
|
146
|
+
("sessions", self.db.session_table_name),
|
|
147
|
+
("metrics", self.db.metrics_table_name),
|
|
148
|
+
("evals", self.db.eval_table_name),
|
|
149
|
+
("knowledge", self.db.knowledge_table_name),
|
|
150
|
+
("culture", self.db.culture_table_name),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for table_type, table_name in tables:
|
|
154
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
155
|
+
current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
|
|
156
|
+
else:
|
|
157
|
+
current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
|
|
158
|
+
|
|
159
|
+
if _target_version >= current_version and not force:
|
|
160
|
+
log_warning(
|
|
161
|
+
f"Skipping down migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
migration_executed = False
|
|
166
|
+
# Run down migration for all versions between target and current (include down of current version)
|
|
167
|
+
# Apply down migrations in reverse order to ensure dependencies are met
|
|
168
|
+
for version, normalised_version in reversed(self.available_versions):
|
|
169
|
+
if normalised_version > _target_version:
|
|
170
|
+
log_info(f"Reverting migration {normalised_version} on table {table_name}")
|
|
171
|
+
migration_executed = await self._down_migration(version, table_type, table_name)
|
|
172
|
+
if migration_executed:
|
|
173
|
+
log_info(f"Successfully reverted migration {normalised_version} on table {table_name}")
|
|
174
|
+
else:
|
|
175
|
+
log_info(f"Skipping revert of migration {normalised_version} on table {table_name}")
|
|
176
|
+
|
|
177
|
+
if migration_executed:
|
|
178
|
+
log_info(f"Storing version {_target_version} in database for table {table_name}")
|
|
179
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
180
|
+
await self.db.upsert_schema_version(table_name, _target_version.public)
|
|
181
|
+
else:
|
|
182
|
+
self.db.upsert_schema_version(table_name, _target_version.public)
|
|
183
|
+
log_info(f"Successfully stored version {_target_version} in database for table {table_name}")
|
|
184
|
+
|
|
185
|
+
async def _down_migration(self, version: str, table_type: str, table_name: str) -> bool:
|
|
186
|
+
"""Run the database-specific logic to handle a down migration.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
version: The version to migrate to, e.g. "v3.0.0"
|
|
190
|
+
"""
|
|
191
|
+
migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
|
|
192
|
+
try:
|
|
193
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
194
|
+
return await migration_module.async_down(self.db, table_type, table_name)
|
|
195
|
+
else:
|
|
196
|
+
return migration_module.down(self.db, table_type, table_name)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
log_error(f"Error running migration to version {version}: {e}")
|
|
199
|
+
raise
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
def quote_db_identifier(db_type: str, identifier: str) -> str:
|
|
2
|
+
"""Add the right quotes to the given identifier string (table name, schema name) based on db type.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
db_type: The database type name (e.g., "PostgresDb", "MySQLDb", "SqliteDb")
|
|
6
|
+
identifier: The identifier string to add quotes to
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
The properly quoted identifier string
|
|
10
|
+
"""
|
|
11
|
+
if db_type in ("PostgresDb", "AsyncPostgresDb"):
|
|
12
|
+
return f'"{identifier}"'
|
|
13
|
+
elif db_type in ("MySQLDb", "AsyncMySQLDb", "SingleStoreDb"):
|
|
14
|
+
return f"`{identifier}`"
|
|
15
|
+
elif db_type in ("SqliteDb", "AsyncSqliteDb"):
|
|
16
|
+
return f'"{identifier}"'
|
|
17
|
+
else:
|
|
18
|
+
# Default to double quotes for unknown types
|
|
19
|
+
return f'"{identifier}"'
|
agno/db/migrations/v1_to_v2.py
CHANGED
|
@@ -7,9 +7,11 @@ from typing import Any, Dict, List, Optional, Union, cast
|
|
|
7
7
|
from sqlalchemy import text
|
|
8
8
|
|
|
9
9
|
from agno.db.base import BaseDb
|
|
10
|
+
from agno.db.migrations.utils import quote_db_identifier
|
|
10
11
|
from agno.db.schemas.memory import UserMemory
|
|
11
12
|
from agno.session import AgentSession, TeamSession, WorkflowSession
|
|
12
13
|
from agno.utils.log import log_error, log_info, log_warning
|
|
14
|
+
from agno.utils.string import sanitize_postgres_string, sanitize_postgres_strings
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -45,7 +47,10 @@ def convert_v1_metrics_to_v2(metrics_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def convert_any_metrics_in_data(data: Any) -> Any:
|
|
48
|
-
"""Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion.
|
|
50
|
+
"""Recursively find and convert any metrics dictionaries and handle v1 to v2 field conversion.
|
|
51
|
+
|
|
52
|
+
Also sanitizes all string values to remove null bytes for PostgreSQL compatibility.
|
|
53
|
+
"""
|
|
49
54
|
if isinstance(data, dict):
|
|
50
55
|
# First apply v1 to v2 field conversion (handles extra_data extraction, thinking/reasoning_content consolidation, etc.)
|
|
51
56
|
data = convert_v1_fields_to_v2(data)
|
|
@@ -62,13 +67,19 @@ def convert_any_metrics_in_data(data: Any) -> Any:
|
|
|
62
67
|
converted_dict[key] = convert_v1_metrics_to_v2(value)
|
|
63
68
|
else:
|
|
64
69
|
converted_dict[key] = convert_any_metrics_in_data(value)
|
|
65
|
-
|
|
70
|
+
|
|
71
|
+
# Sanitize all strings in the converted dict to remove null bytes
|
|
72
|
+
return sanitize_postgres_strings(converted_dict)
|
|
66
73
|
|
|
67
74
|
elif isinstance(data, list):
|
|
68
75
|
return [convert_any_metrics_in_data(item) for item in data]
|
|
69
76
|
|
|
77
|
+
elif isinstance(data, str):
|
|
78
|
+
# Sanitize string values to remove null bytes
|
|
79
|
+
return sanitize_postgres_string(data)
|
|
80
|
+
|
|
70
81
|
else:
|
|
71
|
-
# Not a dict or
|
|
82
|
+
# Not a dict, list, or string, return as-is
|
|
72
83
|
return data
|
|
73
84
|
|
|
74
85
|
|
|
@@ -485,15 +496,21 @@ def get_table_content_in_batches(db: BaseDb, db_schema: str, table_name: str, ba
|
|
|
485
496
|
else:
|
|
486
497
|
raise ValueError(f"Invalid database type: {type(db).__name__}")
|
|
487
498
|
|
|
499
|
+
db_type = type(db).__name__
|
|
500
|
+
quoted_schema = (
|
|
501
|
+
quote_db_identifier(db_type=db_type, identifier=db_schema) if db_schema and db_schema.strip() else None
|
|
502
|
+
)
|
|
503
|
+
quoted_table = quote_db_identifier(db_type=db_type, identifier=table_name)
|
|
504
|
+
|
|
488
505
|
offset = 0
|
|
489
506
|
while True:
|
|
490
507
|
# Create a new session for each batch to avoid transaction conflicts
|
|
491
508
|
with db.Session() as sess:
|
|
492
509
|
# Handle empty schema by omitting the schema prefix (needed for SQLite)
|
|
493
|
-
if
|
|
494
|
-
sql_query = f"SELECT * FROM {
|
|
510
|
+
if quoted_schema:
|
|
511
|
+
sql_query = f"SELECT * FROM {quoted_schema}.{quoted_table} LIMIT {batch_size} OFFSET {offset}"
|
|
495
512
|
else:
|
|
496
|
-
sql_query = f"SELECT * FROM {
|
|
513
|
+
sql_query = f"SELECT * FROM {quoted_table} LIMIT {batch_size} OFFSET {offset}"
|
|
497
514
|
|
|
498
515
|
result = sess.execute(text(sql_query))
|
|
499
516
|
batch = [row._asdict() for row in result]
|
|
@@ -530,13 +547,16 @@ def get_all_table_content(db, db_schema: str, table_name: str) -> list[dict[str,
|
|
|
530
547
|
|
|
531
548
|
|
|
532
549
|
def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]:
|
|
533
|
-
"""Parse v1 Agent sessions into v2 Agent sessions and Memories
|
|
550
|
+
"""Parse v1 Agent sessions into v2 Agent sessions and Memories
|
|
551
|
+
|
|
552
|
+
Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
|
|
553
|
+
"""
|
|
534
554
|
sessions_v2 = []
|
|
535
555
|
|
|
536
556
|
for item in v1_content:
|
|
537
557
|
session = {
|
|
538
558
|
"agent_id": item.get("agent_id"),
|
|
539
|
-
"agent_data": item.get("agent_data"),
|
|
559
|
+
"agent_data": sanitize_postgres_strings(item.get("agent_data")) if item.get("agent_data") else None,
|
|
540
560
|
"session_id": item.get("session_id"),
|
|
541
561
|
"user_id": item.get("user_id"),
|
|
542
562
|
"session_data": convert_session_data_comprehensively(item.get("session_data")),
|
|
@@ -545,6 +565,7 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
|
|
|
545
565
|
"created_at": item.get("created_at"),
|
|
546
566
|
"updated_at": item.get("updated_at"),
|
|
547
567
|
}
|
|
568
|
+
# Summary field sanitization is handled in convert_session_data_comprehensively
|
|
548
569
|
|
|
549
570
|
try:
|
|
550
571
|
agent_session = AgentSession.from_dict(session)
|
|
@@ -559,13 +580,16 @@ def parse_agent_sessions(v1_content: List[Dict[str, Any]]) -> List[AgentSession]
|
|
|
559
580
|
|
|
560
581
|
|
|
561
582
|
def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
|
|
562
|
-
"""Parse v1 Team sessions into v2 Team sessions and Memories
|
|
583
|
+
"""Parse v1 Team sessions into v2 Team sessions and Memories
|
|
584
|
+
|
|
585
|
+
Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
|
|
586
|
+
"""
|
|
563
587
|
sessions_v2 = []
|
|
564
588
|
|
|
565
589
|
for item in v1_content:
|
|
566
590
|
session = {
|
|
567
591
|
"team_id": item.get("team_id"),
|
|
568
|
-
"team_data": item.get("team_data"),
|
|
592
|
+
"team_data": sanitize_postgres_strings(item.get("team_data")) if item.get("team_data") else None,
|
|
569
593
|
"session_id": item.get("session_id"),
|
|
570
594
|
"user_id": item.get("user_id"),
|
|
571
595
|
"session_data": convert_session_data_comprehensively(item.get("session_data")),
|
|
@@ -574,6 +598,8 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
|
|
|
574
598
|
"created_at": item.get("created_at"),
|
|
575
599
|
"updated_at": item.get("updated_at"),
|
|
576
600
|
}
|
|
601
|
+
# Summary field sanitization is handled in convert_session_data_comprehensively
|
|
602
|
+
|
|
577
603
|
try:
|
|
578
604
|
team_session = TeamSession.from_dict(session)
|
|
579
605
|
except Exception as e:
|
|
@@ -587,13 +613,18 @@ def parse_team_sessions(v1_content: List[Dict[str, Any]]) -> List[TeamSession]:
|
|
|
587
613
|
|
|
588
614
|
|
|
589
615
|
def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSession]:
|
|
590
|
-
"""Parse v1 Workflow sessions into v2 Workflow sessions
|
|
616
|
+
"""Parse v1 Workflow sessions into v2 Workflow sessions
|
|
617
|
+
|
|
618
|
+
Sanitizes all string and JSON fields to remove null bytes for PostgreSQL compatibility.
|
|
619
|
+
"""
|
|
591
620
|
sessions_v2 = []
|
|
592
621
|
|
|
593
622
|
for item in v1_content:
|
|
594
623
|
session = {
|
|
595
624
|
"workflow_id": item.get("workflow_id"),
|
|
596
|
-
"workflow_data": item.get("workflow_data")
|
|
625
|
+
"workflow_data": sanitize_postgres_strings(item.get("workflow_data"))
|
|
626
|
+
if item.get("workflow_data")
|
|
627
|
+
else None,
|
|
597
628
|
"session_id": item.get("session_id"),
|
|
598
629
|
"user_id": item.get("user_id"),
|
|
599
630
|
"session_data": convert_session_data_comprehensively(item.get("session_data")),
|
|
@@ -601,9 +632,11 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
|
|
|
601
632
|
"created_at": item.get("created_at"),
|
|
602
633
|
"updated_at": item.get("updated_at"),
|
|
603
634
|
# Workflow v2 specific fields
|
|
604
|
-
"workflow_name": item.get("workflow_name"),
|
|
635
|
+
"workflow_name": sanitize_postgres_string(item.get("workflow_name")) if item.get("workflow_name") else None,
|
|
605
636
|
"runs": convert_any_metrics_in_data(item.get("runs")),
|
|
606
637
|
}
|
|
638
|
+
# Summary field sanitization is handled in convert_session_data_comprehensively
|
|
639
|
+
|
|
607
640
|
try:
|
|
608
641
|
workflow_session = WorkflowSession.from_dict(session)
|
|
609
642
|
except Exception as e:
|
|
@@ -617,18 +650,23 @@ def parse_workflow_sessions(v1_content: List[Dict[str, Any]]) -> List[WorkflowSe
|
|
|
617
650
|
|
|
618
651
|
|
|
619
652
|
def parse_memories(v1_content: List[Dict[str, Any]]) -> List[UserMemory]:
|
|
620
|
-
"""Parse v1 Memories into v2 Memories
|
|
653
|
+
"""Parse v1 Memories into v2 Memories
|
|
654
|
+
|
|
655
|
+
Sanitizes all string fields to remove null bytes for PostgreSQL compatibility.
|
|
656
|
+
"""
|
|
621
657
|
memories_v2 = []
|
|
622
658
|
|
|
623
659
|
for item in v1_content:
|
|
624
660
|
memory = {
|
|
625
661
|
"memory_id": item.get("memory_id"),
|
|
626
|
-
"memory": item.get("memory"),
|
|
627
|
-
"input": item.get("input"),
|
|
662
|
+
"memory": sanitize_postgres_strings(item.get("memory")) if item.get("memory") else None,
|
|
663
|
+
"input": sanitize_postgres_string(item.get("input")) if item.get("input") else None,
|
|
628
664
|
"updated_at": item.get("updated_at"),
|
|
629
665
|
"agent_id": item.get("agent_id"),
|
|
630
666
|
"team_id": item.get("team_id"),
|
|
631
667
|
"user_id": item.get("user_id"),
|
|
668
|
+
"topics": sanitize_postgres_strings(item.get("topics")) if item.get("topics") else None,
|
|
669
|
+
"feedback": sanitize_postgres_string(item.get("feedback")) if item.get("feedback") else None,
|
|
632
670
|
}
|
|
633
671
|
memories_v2.append(UserMemory.from_dict(memory))
|
|
634
672
|
|
|
File without changes
|