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,475 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
|
6
|
+
|
|
7
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
8
|
+
from agno.db.base import ComponentType as DbComponentType
|
|
9
|
+
from agno.os.auth import get_authentication_dependency
|
|
10
|
+
from agno.os.schema import (
|
|
11
|
+
BadRequestResponse,
|
|
12
|
+
ComponentConfigResponse,
|
|
13
|
+
ComponentCreate,
|
|
14
|
+
ComponentResponse,
|
|
15
|
+
ComponentType,
|
|
16
|
+
ComponentUpdate,
|
|
17
|
+
ConfigCreate,
|
|
18
|
+
ConfigUpdate,
|
|
19
|
+
InternalServerErrorResponse,
|
|
20
|
+
NotFoundResponse,
|
|
21
|
+
PaginatedResponse,
|
|
22
|
+
PaginationInfo,
|
|
23
|
+
UnauthenticatedResponse,
|
|
24
|
+
ValidationErrorResponse,
|
|
25
|
+
)
|
|
26
|
+
from agno.os.settings import AgnoAPISettings
|
|
27
|
+
from agno.registry import Registry
|
|
28
|
+
from agno.utils.log import log_error, log_warning
|
|
29
|
+
from agno.utils.string import generate_id_from_name
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_db_in_config(
|
|
35
|
+
config: Dict[str, Any],
|
|
36
|
+
os_db: BaseDb,
|
|
37
|
+
registry: Optional[Registry] = None,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Resolve db reference in config by looking up in registry or OS db.
|
|
41
|
+
|
|
42
|
+
If config contains a db dict with an id, this function will:
|
|
43
|
+
1. Check if the id matches the OS db
|
|
44
|
+
2. Check if the id exists in the registry
|
|
45
|
+
3. Convert the found db to a dict for serialization
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: The config dict that may contain a db reference
|
|
49
|
+
os_db: The OS database instance
|
|
50
|
+
registry: Optional registry containing registered databases
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Updated config dict with resolved db
|
|
54
|
+
"""
|
|
55
|
+
component_db = config.get("db")
|
|
56
|
+
if component_db is not None and isinstance(component_db, dict):
|
|
57
|
+
component_db_id = component_db.get("id")
|
|
58
|
+
if component_db_id is not None:
|
|
59
|
+
resolved_db = None
|
|
60
|
+
# First check if it matches the OS db
|
|
61
|
+
if component_db_id == os_db.id:
|
|
62
|
+
resolved_db = os_db
|
|
63
|
+
# Then check the registry
|
|
64
|
+
elif registry is not None:
|
|
65
|
+
resolved_db = registry.get_db(component_db_id)
|
|
66
|
+
|
|
67
|
+
# Store the full db dict for serialization
|
|
68
|
+
if resolved_db is not None:
|
|
69
|
+
config["db"] = resolved_db.to_dict()
|
|
70
|
+
else:
|
|
71
|
+
log_error(f"Could not resolve db with id: {component_db_id}")
|
|
72
|
+
elif component_db is None and "db" in config:
|
|
73
|
+
# Explicitly set to None, remove the key
|
|
74
|
+
config.pop("db", None)
|
|
75
|
+
|
|
76
|
+
return config
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_components_router(
|
|
80
|
+
os_db: Union[BaseDb, AsyncBaseDb],
|
|
81
|
+
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
82
|
+
registry: Optional[Registry] = None,
|
|
83
|
+
) -> APIRouter:
|
|
84
|
+
"""Create components router."""
|
|
85
|
+
router = APIRouter(
|
|
86
|
+
dependencies=[Depends(get_authentication_dependency(settings))],
|
|
87
|
+
tags=["Components"],
|
|
88
|
+
responses={
|
|
89
|
+
400: {"description": "Bad Request", "model": BadRequestResponse},
|
|
90
|
+
401: {"description": "Unauthorized", "model": UnauthenticatedResponse},
|
|
91
|
+
404: {"description": "Not Found", "model": NotFoundResponse},
|
|
92
|
+
422: {"description": "Validation Error", "model": ValidationErrorResponse},
|
|
93
|
+
500: {"description": "Internal Server Error", "model": InternalServerErrorResponse},
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
return attach_routes(router=router, os_db=os_db, registry=registry)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def attach_routes(
|
|
100
|
+
router: APIRouter, os_db: Union[BaseDb, AsyncBaseDb], registry: Optional[Registry] = None
|
|
101
|
+
) -> APIRouter:
|
|
102
|
+
# Component routes require sync database
|
|
103
|
+
if not isinstance(os_db, BaseDb):
|
|
104
|
+
raise ValueError("Component routes require a sync database (BaseDb), not an async database.")
|
|
105
|
+
db: BaseDb = os_db # Type narrowed after isinstance check
|
|
106
|
+
|
|
107
|
+
@router.get(
|
|
108
|
+
"/components",
|
|
109
|
+
response_model=PaginatedResponse[ComponentResponse],
|
|
110
|
+
response_model_exclude_none=True,
|
|
111
|
+
status_code=200,
|
|
112
|
+
operation_id="list_components",
|
|
113
|
+
summary="List Components",
|
|
114
|
+
description="Retrieve a paginated list of components with optional filtering by type.",
|
|
115
|
+
)
|
|
116
|
+
async def list_components(
|
|
117
|
+
component_type: Optional[ComponentType] = Query(None, description="Filter by type: agent, team, workflow"),
|
|
118
|
+
page: int = Query(1, ge=1, description="Page number"),
|
|
119
|
+
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
|
120
|
+
) -> PaginatedResponse[ComponentResponse]:
|
|
121
|
+
try:
|
|
122
|
+
start_time_ms = time.time() * 1000
|
|
123
|
+
offset = (page - 1) * limit
|
|
124
|
+
|
|
125
|
+
components, total_count = db.list_components(
|
|
126
|
+
component_type=DbComponentType(component_type.value) if component_type else None,
|
|
127
|
+
limit=limit,
|
|
128
|
+
offset=offset,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
total_pages = (total_count + limit - 1) // limit if limit > 0 else 0
|
|
132
|
+
|
|
133
|
+
return PaginatedResponse(
|
|
134
|
+
data=[ComponentResponse(**c) for c in components],
|
|
135
|
+
meta=PaginationInfo(
|
|
136
|
+
page=page,
|
|
137
|
+
limit=limit,
|
|
138
|
+
total_pages=total_pages,
|
|
139
|
+
total_count=total_count,
|
|
140
|
+
search_time_ms=round(time.time() * 1000 - start_time_ms, 2),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
log_error(f"Error listing components: {e}")
|
|
145
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
146
|
+
|
|
147
|
+
@router.post(
|
|
148
|
+
"/components",
|
|
149
|
+
response_model=ComponentResponse,
|
|
150
|
+
response_model_exclude_none=True,
|
|
151
|
+
status_code=201,
|
|
152
|
+
operation_id="create_component",
|
|
153
|
+
summary="Create Component",
|
|
154
|
+
description="Create a new component (agent, team, or workflow) with initial config.",
|
|
155
|
+
)
|
|
156
|
+
async def create_component(
|
|
157
|
+
body: ComponentCreate,
|
|
158
|
+
) -> ComponentResponse:
|
|
159
|
+
try:
|
|
160
|
+
component_id = body.component_id
|
|
161
|
+
if component_id is None:
|
|
162
|
+
component_id = generate_id_from_name(body.name)
|
|
163
|
+
|
|
164
|
+
# TODO: Create links from config
|
|
165
|
+
|
|
166
|
+
# Prepare config - ensure it's a dict and resolve db reference
|
|
167
|
+
config = body.config or {}
|
|
168
|
+
config = _resolve_db_in_config(config, db, registry)
|
|
169
|
+
|
|
170
|
+
# Warn if creating a team without members
|
|
171
|
+
if body.component_type == ComponentType.TEAM:
|
|
172
|
+
members = config.get("members")
|
|
173
|
+
if not members or len(members) == 0:
|
|
174
|
+
log_warning(
|
|
175
|
+
f"Creating team '{body.name}' without members. "
|
|
176
|
+
"If this is unintended, add members to the config."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
component, _config = db.create_component_with_config(
|
|
180
|
+
component_id=component_id,
|
|
181
|
+
component_type=DbComponentType(body.component_type.value),
|
|
182
|
+
name=body.name,
|
|
183
|
+
description=body.description,
|
|
184
|
+
metadata=body.metadata,
|
|
185
|
+
config=config,
|
|
186
|
+
label=body.label,
|
|
187
|
+
stage=body.stage or "draft",
|
|
188
|
+
notes=body.notes,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return ComponentResponse(**component)
|
|
192
|
+
except ValueError as e:
|
|
193
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
194
|
+
except Exception as e:
|
|
195
|
+
log_error(f"Error creating component: {e}")
|
|
196
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
197
|
+
|
|
198
|
+
@router.get(
|
|
199
|
+
"/components/{component_id}",
|
|
200
|
+
response_model=ComponentResponse,
|
|
201
|
+
response_model_exclude_none=True,
|
|
202
|
+
status_code=200,
|
|
203
|
+
operation_id="get_component",
|
|
204
|
+
summary="Get Component",
|
|
205
|
+
description="Retrieve a component by ID.",
|
|
206
|
+
)
|
|
207
|
+
async def get_component(
|
|
208
|
+
component_id: str = Path(description="Component ID"),
|
|
209
|
+
) -> ComponentResponse:
|
|
210
|
+
try:
|
|
211
|
+
component = db.get_component(component_id)
|
|
212
|
+
if component is None:
|
|
213
|
+
raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
|
|
214
|
+
return ComponentResponse(**component)
|
|
215
|
+
except HTTPException:
|
|
216
|
+
raise
|
|
217
|
+
except Exception as e:
|
|
218
|
+
log_error(f"Error getting component: {e}")
|
|
219
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
220
|
+
|
|
221
|
+
@router.patch(
|
|
222
|
+
"/components/{component_id}",
|
|
223
|
+
response_model=ComponentResponse,
|
|
224
|
+
response_model_exclude_none=True,
|
|
225
|
+
status_code=200,
|
|
226
|
+
operation_id="update_component",
|
|
227
|
+
summary="Update Component",
|
|
228
|
+
description="Partially update a component by ID.",
|
|
229
|
+
)
|
|
230
|
+
async def update_component(
|
|
231
|
+
component_id: str = Path(description="Component ID"),
|
|
232
|
+
body: ComponentUpdate = Body(description="Component fields to update"),
|
|
233
|
+
) -> ComponentResponse:
|
|
234
|
+
try:
|
|
235
|
+
existing = db.get_component(component_id)
|
|
236
|
+
if existing is None:
|
|
237
|
+
raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
|
|
238
|
+
|
|
239
|
+
update_kwargs: Dict[str, Any] = {"component_id": component_id}
|
|
240
|
+
if body.name is not None:
|
|
241
|
+
update_kwargs["name"] = body.name
|
|
242
|
+
if body.description is not None:
|
|
243
|
+
update_kwargs["description"] = body.description
|
|
244
|
+
if body.metadata is not None:
|
|
245
|
+
update_kwargs["metadata"] = body.metadata
|
|
246
|
+
if body.component_type is not None:
|
|
247
|
+
update_kwargs["component_type"] = DbComponentType(body.component_type)
|
|
248
|
+
|
|
249
|
+
component = db.upsert_component(**update_kwargs)
|
|
250
|
+
return ComponentResponse(**component)
|
|
251
|
+
except HTTPException:
|
|
252
|
+
raise
|
|
253
|
+
except ValueError as e:
|
|
254
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
255
|
+
except Exception as e:
|
|
256
|
+
log_error(f"Error updating component: {e}")
|
|
257
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
258
|
+
|
|
259
|
+
@router.delete(
|
|
260
|
+
"/components/{component_id}",
|
|
261
|
+
status_code=204,
|
|
262
|
+
operation_id="delete_component",
|
|
263
|
+
summary="Delete Component",
|
|
264
|
+
description="Delete a component by ID.",
|
|
265
|
+
)
|
|
266
|
+
async def delete_component(
|
|
267
|
+
component_id: str = Path(description="Component ID"),
|
|
268
|
+
) -> None:
|
|
269
|
+
try:
|
|
270
|
+
deleted = db.delete_component(component_id)
|
|
271
|
+
if not deleted:
|
|
272
|
+
raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
|
|
273
|
+
except HTTPException:
|
|
274
|
+
raise
|
|
275
|
+
except Exception as e:
|
|
276
|
+
log_error(f"Error deleting component: {e}")
|
|
277
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
278
|
+
|
|
279
|
+
@router.get(
|
|
280
|
+
"/components/{component_id}/configs",
|
|
281
|
+
response_model=List[ComponentConfigResponse],
|
|
282
|
+
response_model_exclude_none=True,
|
|
283
|
+
status_code=200,
|
|
284
|
+
operation_id="list_configs",
|
|
285
|
+
summary="List Configs",
|
|
286
|
+
description="List all configs for a component.",
|
|
287
|
+
)
|
|
288
|
+
async def list_configs(
|
|
289
|
+
component_id: str = Path(description="Component ID"),
|
|
290
|
+
include_config: bool = Query(True, description="Include full config blob"),
|
|
291
|
+
) -> List[ComponentConfigResponse]:
|
|
292
|
+
try:
|
|
293
|
+
configs = db.list_configs(component_id, include_config=include_config)
|
|
294
|
+
return [ComponentConfigResponse(**c) for c in configs]
|
|
295
|
+
except Exception as e:
|
|
296
|
+
log_error(f"Error listing configs: {e}")
|
|
297
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
298
|
+
|
|
299
|
+
@router.post(
|
|
300
|
+
"/components/{component_id}/configs",
|
|
301
|
+
response_model=ComponentConfigResponse,
|
|
302
|
+
response_model_exclude_none=True,
|
|
303
|
+
status_code=201,
|
|
304
|
+
operation_id="create_config",
|
|
305
|
+
summary="Create Config Version",
|
|
306
|
+
description="Create a new config version for a component.",
|
|
307
|
+
)
|
|
308
|
+
async def create_config(
|
|
309
|
+
component_id: str = Path(description="Component ID"),
|
|
310
|
+
body: ConfigCreate = Body(description="Config data"),
|
|
311
|
+
) -> ComponentConfigResponse:
|
|
312
|
+
try:
|
|
313
|
+
# Resolve db from config if present
|
|
314
|
+
config_data = body.config or {}
|
|
315
|
+
config_data = _resolve_db_in_config(config_data, db, registry)
|
|
316
|
+
|
|
317
|
+
config = db.upsert_config(
|
|
318
|
+
component_id=component_id,
|
|
319
|
+
version=None, # Always create new
|
|
320
|
+
config=config_data,
|
|
321
|
+
label=body.label,
|
|
322
|
+
stage=body.stage,
|
|
323
|
+
notes=body.notes,
|
|
324
|
+
links=body.links,
|
|
325
|
+
)
|
|
326
|
+
return ComponentConfigResponse(**config)
|
|
327
|
+
except ValueError as e:
|
|
328
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
329
|
+
except Exception as e:
|
|
330
|
+
log_error(f"Error creating config: {e}")
|
|
331
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
332
|
+
|
|
333
|
+
@router.patch(
|
|
334
|
+
"/components/{component_id}/configs/{version}",
|
|
335
|
+
response_model=ComponentConfigResponse,
|
|
336
|
+
response_model_exclude_none=True,
|
|
337
|
+
status_code=200,
|
|
338
|
+
operation_id="update_config",
|
|
339
|
+
summary="Update Draft Config",
|
|
340
|
+
description="Update an existing draft config. Cannot update published configs.",
|
|
341
|
+
)
|
|
342
|
+
async def update_config(
|
|
343
|
+
component_id: str = Path(description="Component ID"),
|
|
344
|
+
version: int = Path(description="Version number"),
|
|
345
|
+
body: ConfigUpdate = Body(description="Config fields to update"),
|
|
346
|
+
) -> ComponentConfigResponse:
|
|
347
|
+
try:
|
|
348
|
+
# Resolve db from config if present
|
|
349
|
+
config_data = body.config
|
|
350
|
+
if config_data is not None:
|
|
351
|
+
config_data = _resolve_db_in_config(config_data, db, registry)
|
|
352
|
+
|
|
353
|
+
config = db.upsert_config(
|
|
354
|
+
component_id=component_id,
|
|
355
|
+
version=version, # Always update existing
|
|
356
|
+
config=config_data,
|
|
357
|
+
label=body.label,
|
|
358
|
+
stage=body.stage,
|
|
359
|
+
notes=body.notes,
|
|
360
|
+
links=body.links,
|
|
361
|
+
)
|
|
362
|
+
return ComponentConfigResponse(**config)
|
|
363
|
+
except ValueError as e:
|
|
364
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
365
|
+
except Exception as e:
|
|
366
|
+
log_error(f"Error updating config: {e}")
|
|
367
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
368
|
+
|
|
369
|
+
@router.get(
|
|
370
|
+
"/components/{component_id}/configs/current",
|
|
371
|
+
response_model=ComponentConfigResponse,
|
|
372
|
+
response_model_exclude_none=True,
|
|
373
|
+
status_code=200,
|
|
374
|
+
operation_id="get_current_config",
|
|
375
|
+
summary="Get Current Config",
|
|
376
|
+
description="Get the current config version for a component.",
|
|
377
|
+
)
|
|
378
|
+
async def get_current_config(
|
|
379
|
+
component_id: str = Path(description="Component ID"),
|
|
380
|
+
) -> ComponentConfigResponse:
|
|
381
|
+
try:
|
|
382
|
+
config = db.get_config(component_id)
|
|
383
|
+
if config is None:
|
|
384
|
+
raise HTTPException(status_code=404, detail=f"No current config for {component_id}")
|
|
385
|
+
return ComponentConfigResponse(**config)
|
|
386
|
+
except HTTPException:
|
|
387
|
+
raise
|
|
388
|
+
except Exception as e:
|
|
389
|
+
log_error(f"Error getting config: {e}")
|
|
390
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
391
|
+
|
|
392
|
+
@router.get(
|
|
393
|
+
"/components/{component_id}/configs/{version}",
|
|
394
|
+
response_model=ComponentConfigResponse,
|
|
395
|
+
response_model_exclude_none=True,
|
|
396
|
+
status_code=200,
|
|
397
|
+
operation_id="get_config",
|
|
398
|
+
summary="Get Config Version",
|
|
399
|
+
description="Get a specific config version by number.",
|
|
400
|
+
)
|
|
401
|
+
async def get_config_version(
|
|
402
|
+
component_id: str = Path(description="Component ID"),
|
|
403
|
+
version: int = Path(description="Version number"),
|
|
404
|
+
) -> ComponentConfigResponse:
|
|
405
|
+
try:
|
|
406
|
+
config = db.get_config(component_id, version=version)
|
|
407
|
+
|
|
408
|
+
if config is None:
|
|
409
|
+
raise HTTPException(status_code=404, detail=f"Config {component_id} v{version} not found")
|
|
410
|
+
return ComponentConfigResponse(**config)
|
|
411
|
+
except HTTPException:
|
|
412
|
+
raise
|
|
413
|
+
except Exception as e:
|
|
414
|
+
log_error(f"Error getting config: {e}")
|
|
415
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
416
|
+
|
|
417
|
+
@router.delete(
|
|
418
|
+
"/components/{component_id}/configs/{version}",
|
|
419
|
+
status_code=204,
|
|
420
|
+
operation_id="delete_config",
|
|
421
|
+
summary="Delete Config Version",
|
|
422
|
+
description="Delete a specific draft config version. Cannot delete published or current configs.",
|
|
423
|
+
)
|
|
424
|
+
async def delete_config_version(
|
|
425
|
+
component_id: str = Path(description="Component ID"),
|
|
426
|
+
version: int = Path(description="Version number"),
|
|
427
|
+
) -> None:
|
|
428
|
+
try:
|
|
429
|
+
# Resolve version number
|
|
430
|
+
deleted = db.delete_config(component_id, version=version)
|
|
431
|
+
if not deleted:
|
|
432
|
+
raise HTTPException(status_code=404, detail=f"Config {component_id} v{version} not found")
|
|
433
|
+
except HTTPException:
|
|
434
|
+
raise
|
|
435
|
+
except ValueError as e:
|
|
436
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
437
|
+
except Exception as e:
|
|
438
|
+
log_error(f"Error deleting config: {e}")
|
|
439
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
440
|
+
|
|
441
|
+
@router.post(
|
|
442
|
+
"/components/{component_id}/configs/{version}/set-current",
|
|
443
|
+
response_model=ComponentResponse,
|
|
444
|
+
response_model_exclude_none=True,
|
|
445
|
+
status_code=200,
|
|
446
|
+
operation_id="set_current_config",
|
|
447
|
+
summary="Set Current Config Version",
|
|
448
|
+
description="Set a published config version as current (for rollback).",
|
|
449
|
+
)
|
|
450
|
+
async def set_current_config(
|
|
451
|
+
component_id: str = Path(description="Component ID"),
|
|
452
|
+
version: int = Path(description="Version number"),
|
|
453
|
+
) -> ComponentResponse:
|
|
454
|
+
try:
|
|
455
|
+
success = db.set_current_version(component_id, version=version)
|
|
456
|
+
if not success:
|
|
457
|
+
raise HTTPException(
|
|
458
|
+
status_code=404, detail=f"Component {component_id} or config version {version} not found"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Fetch and return updated component
|
|
462
|
+
component = db.get_component(component_id)
|
|
463
|
+
if component is None:
|
|
464
|
+
raise HTTPException(status_code=404, detail=f"Component {component_id} not found")
|
|
465
|
+
|
|
466
|
+
return ComponentResponse(**component)
|
|
467
|
+
except HTTPException:
|
|
468
|
+
raise
|
|
469
|
+
except ValueError as e:
|
|
470
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
471
|
+
except Exception as e:
|
|
472
|
+
log_error(f"Error setting current config: {e}")
|
|
473
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
474
|
+
|
|
475
|
+
return router
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
|
|
3
|
+
from fastapi import (
|
|
4
|
+
APIRouter,
|
|
5
|
+
Depends,
|
|
6
|
+
HTTPException,
|
|
7
|
+
)
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from packaging import version
|
|
10
|
+
|
|
11
|
+
from agno.db.base import AsyncBaseDb
|
|
12
|
+
from agno.db.migrations.manager import MigrationManager
|
|
13
|
+
from agno.os.auth import get_authentication_dependency
|
|
14
|
+
from agno.os.schema import (
|
|
15
|
+
BadRequestResponse,
|
|
16
|
+
InternalServerErrorResponse,
|
|
17
|
+
NotFoundResponse,
|
|
18
|
+
UnauthenticatedResponse,
|
|
19
|
+
ValidationErrorResponse,
|
|
20
|
+
)
|
|
21
|
+
from agno.os.settings import AgnoAPISettings
|
|
22
|
+
from agno.os.utils import (
|
|
23
|
+
get_db,
|
|
24
|
+
)
|
|
25
|
+
from agno.remote.base import RemoteDb
|
|
26
|
+
from agno.utils.log import log_info
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from agno.os.app import AgentOS
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_database_router(
|
|
33
|
+
os: "AgentOS",
|
|
34
|
+
settings: AgnoAPISettings = AgnoAPISettings(),
|
|
35
|
+
) -> APIRouter:
|
|
36
|
+
"""Create the database router with comprehensive OpenAPI documentation."""
|
|
37
|
+
router = APIRouter(
|
|
38
|
+
dependencies=[Depends(get_authentication_dependency(settings))],
|
|
39
|
+
responses={
|
|
40
|
+
400: {"description": "Bad Request", "model": BadRequestResponse},
|
|
41
|
+
401: {"description": "Unauthorized", "model": UnauthenticatedResponse},
|
|
42
|
+
404: {"description": "Not Found", "model": NotFoundResponse},
|
|
43
|
+
422: {"description": "Validation Error", "model": ValidationErrorResponse},
|
|
44
|
+
500: {"description": "Internal Server Error", "model": InternalServerErrorResponse},
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def _migrate_single_db(db, target_version: Optional[str] = None) -> None:
|
|
49
|
+
"""Migrate a single database."""
|
|
50
|
+
if isinstance(db, RemoteDb):
|
|
51
|
+
log_info("Skipping logs for remote DB")
|
|
52
|
+
|
|
53
|
+
if target_version:
|
|
54
|
+
# Use the session table as proxy for the database schema version
|
|
55
|
+
if isinstance(db, AsyncBaseDb):
|
|
56
|
+
current_version = await db.get_latest_schema_version(db.session_table_name)
|
|
57
|
+
else:
|
|
58
|
+
current_version = db.get_latest_schema_version(db.session_table_name)
|
|
59
|
+
|
|
60
|
+
if version.parse(target_version) > version.parse(current_version): # type: ignore
|
|
61
|
+
await MigrationManager(db).up(target_version) # type: ignore
|
|
62
|
+
else:
|
|
63
|
+
await MigrationManager(db).down(target_version) # type: ignore
|
|
64
|
+
else:
|
|
65
|
+
# If the target version is not provided, migrate to the latest version
|
|
66
|
+
await MigrationManager(db).up() # type: ignore
|
|
67
|
+
|
|
68
|
+
@router.post(
|
|
69
|
+
"/databases/all/migrate",
|
|
70
|
+
tags=["Database"],
|
|
71
|
+
operation_id="migrate_all_databases",
|
|
72
|
+
summary="Migrate All Databases",
|
|
73
|
+
description=(
|
|
74
|
+
"Migrate all database schemas to the given target version. "
|
|
75
|
+
"If a target version is not provided, all databases will be migrated to the latest version."
|
|
76
|
+
),
|
|
77
|
+
responses={
|
|
78
|
+
200: {
|
|
79
|
+
"description": "All databases migrated successfully",
|
|
80
|
+
"content": {
|
|
81
|
+
"application/json": {
|
|
82
|
+
"example": {"message": "All databases migrated successfully to version 3.0.0"},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
500: {"description": "Failed to migrate databases", "model": InternalServerErrorResponse},
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
async def migrate_all_databases(target_version: Optional[str] = None):
|
|
90
|
+
"""Migrate all databases."""
|
|
91
|
+
all_dbs = {db.id: db for db_id, dbs in os.dbs.items() for db in dbs}
|
|
92
|
+
failed_dbs: dict[str, str] = {}
|
|
93
|
+
|
|
94
|
+
for db_id, db in all_dbs.items():
|
|
95
|
+
try:
|
|
96
|
+
await _migrate_single_db(db, target_version)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
failed_dbs[db_id] = str(e)
|
|
99
|
+
|
|
100
|
+
version_msg = f"version {target_version}" if target_version else "latest version"
|
|
101
|
+
migrated_count = len(all_dbs) - len(failed_dbs)
|
|
102
|
+
|
|
103
|
+
if failed_dbs:
|
|
104
|
+
return JSONResponse(
|
|
105
|
+
content={
|
|
106
|
+
"message": f"Migrated {migrated_count}/{len(all_dbs)} databases to {version_msg}",
|
|
107
|
+
"failed": failed_dbs,
|
|
108
|
+
},
|
|
109
|
+
status_code=207, # Multi-Status
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return JSONResponse(
|
|
113
|
+
content={"message": f"All databases migrated successfully to {version_msg}"}, status_code=200
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@router.post(
|
|
117
|
+
"/databases/{db_id}/migrate",
|
|
118
|
+
tags=["Database"],
|
|
119
|
+
operation_id="migrate_database",
|
|
120
|
+
summary="Migrate Database",
|
|
121
|
+
description=(
|
|
122
|
+
"Migrate the given database schema to the given target version. "
|
|
123
|
+
"If a target version is not provided, the database will be migrated to the latest version."
|
|
124
|
+
),
|
|
125
|
+
responses={
|
|
126
|
+
200: {
|
|
127
|
+
"description": "Database migrated successfully",
|
|
128
|
+
"content": {
|
|
129
|
+
"application/json": {
|
|
130
|
+
"example": {"message": "Database migrated successfully to version 3.0.0"},
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
404: {"description": "Database not found", "model": NotFoundResponse},
|
|
135
|
+
500: {"description": "Failed to migrate database", "model": InternalServerErrorResponse},
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
async def migrate_database(db_id: str, target_version: Optional[str] = None):
|
|
139
|
+
db = await get_db(os.dbs, db_id)
|
|
140
|
+
if not db:
|
|
141
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
await _migrate_single_db(db, target_version)
|
|
145
|
+
|
|
146
|
+
version_msg = f"version {target_version}" if target_version else "latest version"
|
|
147
|
+
return JSONResponse(
|
|
148
|
+
content={"message": f"Database migrated successfully to {version_msg}"}, status_code=200
|
|
149
|
+
)
|
|
150
|
+
except HTTPException:
|
|
151
|
+
raise
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise HTTPException(status_code=500, detail=f"Failed to migrate database: {str(e)}")
|
|
154
|
+
|
|
155
|
+
return router
|