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.
Files changed (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {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