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,757 @@
1
+ import json
2
+ from typing import TYPE_CHECKING, Any, AsyncGenerator, Dict, List, Optional, Union
3
+ from uuid import uuid4
4
+
5
+ from fastapi import (
6
+ APIRouter,
7
+ BackgroundTasks,
8
+ Depends,
9
+ Form,
10
+ HTTPException,
11
+ Request,
12
+ WebSocket,
13
+ )
14
+ from fastapi.responses import JSONResponse, StreamingResponse
15
+ from pydantic import BaseModel
16
+
17
+ from agno.db.base import BaseDb
18
+ from agno.exceptions import InputCheckError, OutputCheckError
19
+ from agno.os.auth import (
20
+ get_auth_token_from_request,
21
+ get_authentication_dependency,
22
+ require_resource_access,
23
+ validate_websocket_token,
24
+ )
25
+ from agno.os.managers import event_buffer, websocket_manager
26
+ from agno.os.routers.workflows.schema import WorkflowResponse
27
+ from agno.os.schema import (
28
+ BadRequestResponse,
29
+ InternalServerErrorResponse,
30
+ NotFoundResponse,
31
+ UnauthenticatedResponse,
32
+ ValidationErrorResponse,
33
+ WorkflowSummaryResponse,
34
+ )
35
+ from agno.os.settings import AgnoAPISettings
36
+ from agno.os.utils import (
37
+ format_sse_event,
38
+ get_request_kwargs,
39
+ get_workflow_by_id,
40
+ )
41
+ from agno.run.base import RunStatus
42
+ from agno.run.workflow import WorkflowErrorEvent
43
+ from agno.utils.log import log_debug, log_warning, logger
44
+ from agno.utils.serialize import json_serializer
45
+ from agno.workflow.remote import RemoteWorkflow
46
+ from agno.workflow.workflow import Workflow
47
+
48
+ if TYPE_CHECKING:
49
+ from agno.os.app import AgentOS
50
+
51
+
52
+ async def handle_workflow_via_websocket(websocket: WebSocket, message: dict, os: "AgentOS"):
53
+ """Handle workflow execution directly via WebSocket"""
54
+ try:
55
+ workflow_id = message.get("workflow_id")
56
+ session_id = message.get("session_id")
57
+ user_message = message.get("message", "")
58
+ user_id = message.get("user_id")
59
+
60
+ if not workflow_id:
61
+ await websocket.send_text(json.dumps({"event": "error", "error": "workflow_id is required"}))
62
+ return
63
+
64
+ # Get workflow from OS
65
+ workflow = get_workflow_by_id(
66
+ workflow_id=workflow_id, workflows=os.workflows, db=os.db, registry=os.registry, create_fresh=True
67
+ )
68
+ if not workflow:
69
+ await websocket.send_text(json.dumps({"event": "error", "error": f"Workflow {workflow_id} not found"}))
70
+ return
71
+
72
+ if isinstance(workflow, RemoteWorkflow):
73
+ await websocket.send_text(
74
+ json.dumps({"event": "error", "error": "Remote workflows are not supported via WebSocket"})
75
+ )
76
+ return
77
+
78
+ # Generate session_id if not provided
79
+ # Use workflow's default session_id if not provided in message
80
+ if not session_id:
81
+ if workflow.session_id:
82
+ session_id = workflow.session_id
83
+ else:
84
+ session_id = str(uuid4())
85
+
86
+ # Execute workflow in background with streaming
87
+ await workflow.arun( # type: ignore
88
+ input=user_message,
89
+ session_id=session_id,
90
+ user_id=user_id,
91
+ stream=True,
92
+ stream_events=True,
93
+ background=True,
94
+ websocket=websocket,
95
+ )
96
+
97
+ # NOTE: Don't register the original websocket in the manager
98
+ # It's already handled by the WebSocketHandler passed to the workflow
99
+ # The manager is ONLY for reconnected clients (see handle_workflow_subscription)
100
+
101
+ except (InputCheckError, OutputCheckError) as e:
102
+ await websocket.send_text(
103
+ json.dumps(
104
+ {
105
+ "event": "error",
106
+ "error": str(e),
107
+ "error_type": e.type,
108
+ "error_id": e.error_id,
109
+ "additional_data": e.additional_data,
110
+ }
111
+ )
112
+ )
113
+ except Exception as e:
114
+ logger.error(f"Error executing workflow via WebSocket: {e}")
115
+ error_payload = {
116
+ "event": "error",
117
+ "error": str(e),
118
+ "error_type": e.type if hasattr(e, "type") else None,
119
+ "error_id": e.error_id if hasattr(e, "error_id") else None,
120
+ }
121
+ error_payload = {k: v for k, v in error_payload.items() if v is not None}
122
+ await websocket.send_text(json.dumps(error_payload))
123
+
124
+
125
+ async def handle_workflow_subscription(websocket: WebSocket, message: dict, os: "AgentOS"):
126
+ """
127
+ Handle subscription/reconnection to an existing workflow run.
128
+
129
+ Allows clients to reconnect after page refresh or disconnection and catch up on missed events.
130
+ """
131
+ try:
132
+ run_id = message.get("run_id")
133
+ workflow_id = message.get("workflow_id")
134
+ session_id = message.get("session_id")
135
+ last_event_index = message.get("last_event_index") # 0-based index of last received event
136
+
137
+ if not run_id:
138
+ await websocket.send_text(json.dumps({"event": "error", "error": "run_id is required for subscription"}))
139
+ return
140
+
141
+ # Check if run exists in event buffer
142
+ buffer_status = event_buffer.get_run_status(run_id)
143
+
144
+ if buffer_status is None:
145
+ # Run not in buffer - check database
146
+ if workflow_id and session_id:
147
+ workflow = get_workflow_by_id(
148
+ workflow_id=workflow_id, workflows=os.workflows, db=os.db, registry=os.registry, create_fresh=True
149
+ )
150
+ if workflow and isinstance(workflow, Workflow):
151
+ workflow_run = await workflow.aget_run_output(run_id, session_id)
152
+
153
+ if workflow_run:
154
+ # Run exists in DB - send all events from DB
155
+ if workflow_run.events:
156
+ await websocket.send_text(
157
+ json.dumps(
158
+ {
159
+ "event": "replay",
160
+ "run_id": run_id,
161
+ "status": workflow_run.status.value if workflow_run.status else "unknown",
162
+ "total_events": len(workflow_run.events),
163
+ "message": "Run completed. Replaying all events from database.",
164
+ }
165
+ )
166
+ )
167
+
168
+ # Send events one by one
169
+ for idx, event in enumerate(workflow_run.events):
170
+ # Convert event to dict and add event_index
171
+ event_dict = event.model_dump() if hasattr(event, "model_dump") else event.to_dict()
172
+ event_dict["event_index"] = idx
173
+ if "run_id" not in event_dict:
174
+ event_dict["run_id"] = run_id
175
+
176
+ await websocket.send_text(json.dumps(event_dict, default=json_serializer))
177
+ else:
178
+ await websocket.send_text(
179
+ json.dumps(
180
+ {
181
+ "event": "replay",
182
+ "run_id": run_id,
183
+ "status": workflow_run.status.value if workflow_run.status else "unknown",
184
+ "total_events": 0,
185
+ "message": "Run completed but no events stored.",
186
+ }
187
+ )
188
+ )
189
+ return
190
+
191
+ # Run not found anywhere
192
+ await websocket.send_text(
193
+ json.dumps({"event": "error", "error": f"Run {run_id} not found in buffer or database"})
194
+ )
195
+ return
196
+
197
+ # Run is in buffer (still active or recently completed)
198
+ if buffer_status in [RunStatus.completed, RunStatus.error, RunStatus.cancelled]:
199
+ # Run finished - send all events from buffer
200
+ all_events = event_buffer.get_events(run_id, last_event_index=None)
201
+
202
+ await websocket.send_text(
203
+ json.dumps(
204
+ {
205
+ "event": "replay",
206
+ "run_id": run_id,
207
+ "status": buffer_status.value,
208
+ "total_events": len(all_events),
209
+ "message": f"Run {buffer_status.value}. Replaying all events.",
210
+ }
211
+ )
212
+ )
213
+
214
+ # Send all events
215
+ for idx, buffered_event in enumerate(all_events):
216
+ # Convert event to dict and add event_index
217
+ event_dict = (
218
+ buffered_event.model_dump() if hasattr(buffered_event, "model_dump") else buffered_event.to_dict()
219
+ )
220
+ event_dict["event_index"] = idx
221
+ if "run_id" not in event_dict:
222
+ event_dict["run_id"] = run_id
223
+
224
+ await websocket.send_text(json.dumps(event_dict))
225
+ return
226
+
227
+ # Run is still active - send missed events and subscribe to new ones
228
+ missed_events = event_buffer.get_events(run_id, last_event_index)
229
+ current_event_count = event_buffer.get_event_count(run_id)
230
+
231
+ if missed_events:
232
+ # Send catch-up notification
233
+ await websocket.send_text(
234
+ json.dumps(
235
+ {
236
+ "event": "catch_up",
237
+ "run_id": run_id,
238
+ "status": "running",
239
+ "missed_events": len(missed_events),
240
+ "current_event_count": current_event_count,
241
+ "message": f"Catching up on {len(missed_events)} missed events.",
242
+ }
243
+ )
244
+ )
245
+
246
+ # Send missed events
247
+ start_index = (last_event_index + 1) if last_event_index is not None else 0
248
+ for idx, buffered_event in enumerate(missed_events):
249
+ # Convert event to dict and add event_index
250
+ event_dict = (
251
+ buffered_event.model_dump() if hasattr(buffered_event, "model_dump") else buffered_event.to_dict()
252
+ )
253
+ event_dict["event_index"] = start_index + idx
254
+ if "run_id" not in event_dict:
255
+ event_dict["run_id"] = run_id
256
+
257
+ await websocket.send_text(json.dumps(event_dict))
258
+
259
+ # Register websocket for future events
260
+ await websocket_manager.register_websocket(run_id, websocket)
261
+
262
+ # Send subscription confirmation
263
+ await websocket.send_text(
264
+ json.dumps(
265
+ {
266
+ "event": "subscribed",
267
+ "run_id": run_id,
268
+ "status": "running",
269
+ "current_event_count": current_event_count,
270
+ "message": "Subscribed to workflow run. You will receive new events as they occur.",
271
+ }
272
+ )
273
+ )
274
+
275
+ log_debug(f"Client subscribed to workflow run {run_id} (last_event_index: {last_event_index})")
276
+
277
+ except Exception as e:
278
+ logger.error(f"Error handling workflow subscription: {e}")
279
+ await websocket.send_text(
280
+ json.dumps(
281
+ {
282
+ "event": "error",
283
+ "error": f"Subscription failed: {str(e)}",
284
+ }
285
+ )
286
+ )
287
+
288
+
289
+ async def workflow_response_streamer(
290
+ workflow: Union[Workflow, RemoteWorkflow],
291
+ input: Union[str, Dict[str, Any], List[Any], BaseModel],
292
+ session_id: Optional[str] = None,
293
+ user_id: Optional[str] = None,
294
+ background_tasks: Optional[BackgroundTasks] = None,
295
+ auth_token: Optional[str] = None,
296
+ **kwargs: Any,
297
+ ) -> AsyncGenerator:
298
+ try:
299
+ # Pass background_tasks if provided
300
+ if background_tasks is not None:
301
+ kwargs["background_tasks"] = background_tasks
302
+
303
+ if "stream_events" in kwargs:
304
+ stream_events = kwargs.pop("stream_events")
305
+ else:
306
+ stream_events = True
307
+
308
+ # Pass auth_token for remote workflows
309
+ if auth_token and isinstance(workflow, RemoteWorkflow):
310
+ kwargs["auth_token"] = auth_token
311
+
312
+ run_response = workflow.arun( # type: ignore
313
+ input=input,
314
+ session_id=session_id,
315
+ user_id=user_id,
316
+ stream=True,
317
+ stream_events=stream_events,
318
+ **kwargs,
319
+ )
320
+
321
+ async for run_response_chunk in run_response:
322
+ yield format_sse_event(run_response_chunk) # type: ignore
323
+
324
+ except (InputCheckError, OutputCheckError) as e:
325
+ error_response = WorkflowErrorEvent(
326
+ error=str(e),
327
+ error_type=e.type,
328
+ error_id=e.error_id,
329
+ additional_data=e.additional_data,
330
+ )
331
+ yield format_sse_event(error_response)
332
+
333
+ except Exception as e:
334
+ import traceback
335
+
336
+ traceback.print_exc()
337
+ error_response = WorkflowErrorEvent(
338
+ error=str(e),
339
+ error_type=e.type if hasattr(e, "type") else None,
340
+ error_id=e.error_id if hasattr(e, "error_id") else None,
341
+ )
342
+ yield format_sse_event(error_response)
343
+ return
344
+
345
+
346
+ def get_websocket_router(
347
+ os: "AgentOS",
348
+ settings: AgnoAPISettings = AgnoAPISettings(),
349
+ ) -> APIRouter:
350
+ """
351
+ Create WebSocket router with support for both legacy (os_security_key) and JWT authentication.
352
+
353
+ WebSocket endpoints handle authentication internally via message-based auth.
354
+ Authentication methods (in order of precedence):
355
+ 1. JWT tokens - if JWTMiddleware is configured (via app.state.jwt_middleware)
356
+ 2. Legacy bearer token - if settings.os_security_key is set
357
+ 3. No authentication - if neither is configured
358
+
359
+ The JWT middleware instance is accessed from app.state.jwt_middleware, which is set
360
+ by AgentOS when authorization is enabled. This allows reusing the same validation
361
+ logic and loaded keys as the HTTP middleware.
362
+
363
+ Args:
364
+ os: The AgentOS instance
365
+ settings: API settings (includes os_security_key for legacy auth)
366
+ """
367
+ ws_router = APIRouter()
368
+
369
+ @ws_router.websocket(
370
+ "/workflows/ws",
371
+ name="workflow_websocket",
372
+ )
373
+ async def workflow_websocket_endpoint(websocket: WebSocket):
374
+ """WebSocket endpoint for receiving real-time workflow events"""
375
+ # Check if JWT validator is configured (set by AgentOS when authorization=True)
376
+ jwt_validator = getattr(websocket.app.state, "jwt_validator", None)
377
+ jwt_auth_enabled = jwt_validator is not None
378
+
379
+ # Determine auth requirements - JWT takes precedence over legacy
380
+ requires_auth = jwt_auth_enabled or bool(settings.os_security_key)
381
+
382
+ await websocket_manager.connect(websocket, requires_auth=requires_auth)
383
+
384
+ # Store user context from JWT auth
385
+ websocket_user_context: Dict[str, Any] = {}
386
+
387
+ try:
388
+ while True:
389
+ data = await websocket.receive_text()
390
+ message = json.loads(data)
391
+ action = message.get("action")
392
+
393
+ # Handle authentication first
394
+ if action == "authenticate":
395
+ token = message.get("token")
396
+ if not token:
397
+ await websocket.send_text(json.dumps({"event": "auth_error", "error": "Token is required"}))
398
+ continue
399
+
400
+ if jwt_auth_enabled and jwt_validator:
401
+ # Use JWT validator for token validation
402
+ try:
403
+ payload = jwt_validator.validate_token(token)
404
+ claims = jwt_validator.extract_claims(payload)
405
+ await websocket_manager.authenticate_websocket(websocket)
406
+
407
+ # Store user context from JWT
408
+ websocket_user_context["user_id"] = claims["user_id"]
409
+ websocket_user_context["scopes"] = claims["scopes"]
410
+ websocket_user_context["payload"] = payload
411
+
412
+ # Include user info in auth success message
413
+ await websocket.send_text(
414
+ json.dumps(
415
+ {
416
+ "event": "authenticated",
417
+ "message": "JWT authentication successful.",
418
+ "user_id": claims["user_id"],
419
+ }
420
+ )
421
+ )
422
+ except Exception as e:
423
+ error_msg = str(e) if str(e) else "Invalid token"
424
+ error_type = "expired" if "expired" in error_msg.lower() else "invalid_token"
425
+ await websocket.send_text(
426
+ json.dumps(
427
+ {
428
+ "event": "auth_error",
429
+ "error": error_msg,
430
+ "error_type": error_type,
431
+ }
432
+ )
433
+ )
434
+ continue
435
+ elif validate_websocket_token(token, settings):
436
+ # Legacy os_security_key authentication
437
+ await websocket_manager.authenticate_websocket(websocket)
438
+ else:
439
+ await websocket.send_text(json.dumps({"event": "auth_error", "error": "Invalid token"}))
440
+ continue
441
+
442
+ # Check authentication for all other actions (only when required)
443
+ elif requires_auth and not websocket_manager.is_authenticated(websocket):
444
+ auth_type = "JWT" if jwt_auth_enabled else "bearer token"
445
+ await websocket.send_text(
446
+ json.dumps(
447
+ {
448
+ "event": "auth_required",
449
+ "error": f"Authentication required. Send authenticate action with valid {auth_type}.",
450
+ }
451
+ )
452
+ )
453
+ continue
454
+
455
+ # Handle authenticated actions
456
+ elif action == "ping":
457
+ await websocket.send_text(json.dumps({"event": "pong"}))
458
+
459
+ elif action == "start-workflow":
460
+ # Add user context to message if available from JWT auth
461
+ if websocket_user_context:
462
+ if "user_id" not in message and websocket_user_context.get("user_id"):
463
+ message["user_id"] = websocket_user_context["user_id"]
464
+ # Handle workflow execution directly via WebSocket
465
+ await handle_workflow_via_websocket(websocket, message, os)
466
+
467
+ elif action == "reconnect":
468
+ # Subscribe/reconnect to an existing workflow run
469
+ await handle_workflow_subscription(websocket, message, os)
470
+
471
+ else:
472
+ await websocket.send_text(json.dumps({"event": "error", "error": f"Unknown action: {action}"}))
473
+
474
+ except Exception as e:
475
+ if "1012" not in str(e) and "1001" not in str(e):
476
+ logger.error(f"WebSocket error: {e}")
477
+ finally:
478
+ # Clean up the websocket connection
479
+ await websocket_manager.disconnect_websocket(websocket)
480
+
481
+ return ws_router
482
+
483
+
484
+ def get_workflow_router(
485
+ os: "AgentOS",
486
+ settings: AgnoAPISettings = AgnoAPISettings(),
487
+ ) -> APIRouter:
488
+ """Create the workflow router with comprehensive OpenAPI documentation."""
489
+ router = APIRouter(
490
+ dependencies=[Depends(get_authentication_dependency(settings))],
491
+ responses={
492
+ 400: {"description": "Bad Request", "model": BadRequestResponse},
493
+ 401: {"description": "Unauthorized", "model": UnauthenticatedResponse},
494
+ 404: {"description": "Not Found", "model": NotFoundResponse},
495
+ 422: {"description": "Validation Error", "model": ValidationErrorResponse},
496
+ 500: {"description": "Internal Server Error", "model": InternalServerErrorResponse},
497
+ },
498
+ )
499
+
500
+ @router.get(
501
+ "/workflows",
502
+ response_model=List[WorkflowSummaryResponse],
503
+ response_model_exclude_none=True,
504
+ tags=["Workflows"],
505
+ operation_id="get_workflows",
506
+ summary="List All Workflows",
507
+ description=(
508
+ "Retrieve a comprehensive list of all workflows configured in this OS instance.\n\n"
509
+ "**Return Information:**\n"
510
+ "- Workflow metadata (ID, name, description)\n"
511
+ "- Input schema requirements\n"
512
+ "- Step sequence and execution flow\n"
513
+ "- Associated agents and teams"
514
+ ),
515
+ responses={
516
+ 200: {
517
+ "description": "List of workflows retrieved successfully",
518
+ "content": {
519
+ "application/json": {
520
+ "example": [
521
+ {
522
+ "id": "content-creation-workflow",
523
+ "name": "Content Creation Workflow",
524
+ "description": "Automated content creation from blog posts to social media",
525
+ "db_id": "123",
526
+ }
527
+ ]
528
+ }
529
+ },
530
+ }
531
+ },
532
+ )
533
+ async def get_workflows(request: Request) -> List[WorkflowSummaryResponse]:
534
+ # Filter workflows based on user's scopes (only if authorization is enabled)
535
+ if getattr(request.state, "authorization_enabled", False):
536
+ from agno.os.auth import filter_resources_by_access, get_accessible_resources
537
+
538
+ # Check if user has any workflow scopes at all
539
+ accessible_ids = get_accessible_resources(request, "workflows")
540
+ if not accessible_ids:
541
+ raise HTTPException(status_code=403, detail="Insufficient permissions")
542
+
543
+ accessible_workflows = filter_resources_by_access(request, os.workflows or [], "workflows")
544
+ else:
545
+ accessible_workflows = os.workflows or []
546
+
547
+ workflows: List[WorkflowSummaryResponse] = []
548
+ if accessible_workflows:
549
+ for workflow in accessible_workflows:
550
+ workflows.append(WorkflowSummaryResponse.from_workflow(workflow=workflow))
551
+
552
+ if os.db and isinstance(os.db, BaseDb):
553
+ from agno.workflow.workflow import get_workflows
554
+
555
+ db_workflows = get_workflows(db=os.db, registry=os.registry)
556
+ if db_workflows:
557
+ for db_workflow in db_workflows:
558
+ workflows.append(WorkflowSummaryResponse.from_workflow(workflow=db_workflow))
559
+
560
+ return workflows
561
+
562
+ @router.get(
563
+ "/workflows/{workflow_id}",
564
+ response_model=WorkflowResponse,
565
+ response_model_exclude_none=True,
566
+ tags=["Workflows"],
567
+ operation_id="get_workflow",
568
+ summary="Get Workflow Details",
569
+ description=("Retrieve detailed configuration and step information for a specific workflow."),
570
+ responses={
571
+ 200: {
572
+ "description": "Workflow details retrieved successfully",
573
+ "content": {
574
+ "application/json": {
575
+ "example": {
576
+ "id": "content-creation-workflow",
577
+ "name": "Content Creation Workflow",
578
+ "description": "Automated content creation from blog posts to social media",
579
+ "db_id": "123",
580
+ }
581
+ }
582
+ },
583
+ },
584
+ 404: {"description": "Workflow not found", "model": NotFoundResponse},
585
+ },
586
+ dependencies=[Depends(require_resource_access("workflows", "read", "workflow_id"))],
587
+ )
588
+ async def get_workflow(workflow_id: str, request: Request) -> WorkflowResponse:
589
+ workflow = get_workflow_by_id(
590
+ workflow_id=workflow_id, workflows=os.workflows, db=os.db, registry=os.registry, create_fresh=True
591
+ )
592
+ if workflow is None:
593
+ raise HTTPException(status_code=404, detail="Workflow not found")
594
+ if isinstance(workflow, RemoteWorkflow):
595
+ return await workflow.get_workflow_config()
596
+ else:
597
+ return await WorkflowResponse.from_workflow(workflow=workflow)
598
+
599
+ @router.post(
600
+ "/workflows/{workflow_id}/runs",
601
+ tags=["Workflows"],
602
+ operation_id="create_workflow_run",
603
+ response_model_exclude_none=True,
604
+ summary="Execute Workflow",
605
+ description=(
606
+ "Execute a workflow with the provided input data. Workflows can run in streaming or batch mode.\n\n"
607
+ "**Execution Modes:**\n"
608
+ "- **Streaming (`stream=true`)**: Real-time step-by-step execution updates via SSE\n"
609
+ "- **Non-Streaming (`stream=false`)**: Complete workflow execution with final result\n\n"
610
+ "**Workflow Execution Process:**\n"
611
+ "1. Input validation against workflow schema\n"
612
+ "2. Sequential or parallel step execution based on workflow design\n"
613
+ "3. Data flow between steps with transformation\n"
614
+ "4. Error handling and automatic retries where configured\n"
615
+ "5. Final result compilation and response\n\n"
616
+ "**Session Management:**\n"
617
+ "Workflows support session continuity for stateful execution across multiple runs."
618
+ ),
619
+ responses={
620
+ 200: {
621
+ "description": "Workflow executed successfully",
622
+ "content": {
623
+ "text/event-stream": {
624
+ "example": 'event: RunStarted\ndata: {"content": "Hello!", "run_id": "123..."}\n\n'
625
+ },
626
+ },
627
+ },
628
+ 400: {"description": "Invalid input data or workflow configuration", "model": BadRequestResponse},
629
+ 404: {"description": "Workflow not found", "model": NotFoundResponse},
630
+ 500: {"description": "Workflow execution error", "model": InternalServerErrorResponse},
631
+ },
632
+ dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
633
+ )
634
+ async def create_workflow_run(
635
+ workflow_id: str,
636
+ request: Request,
637
+ background_tasks: BackgroundTasks,
638
+ message: str = Form(...),
639
+ stream: bool = Form(True),
640
+ session_id: Optional[str] = Form(None),
641
+ user_id: Optional[str] = Form(None),
642
+ version: Optional[int] = Form(None),
643
+ ):
644
+ kwargs = await get_request_kwargs(request, create_workflow_run)
645
+
646
+ if hasattr(request.state, "user_id") and request.state.user_id is not None:
647
+ if user_id and user_id != request.state.user_id:
648
+ log_warning("User ID parameter passed in both request state and kwargs, using request state")
649
+ user_id = request.state.user_id
650
+ if hasattr(request.state, "session_id") and request.state.session_id is not None:
651
+ if session_id and session_id != request.state.session_id:
652
+ log_warning("Session ID parameter passed in both request state and kwargs, using request state")
653
+ session_id = request.state.session_id
654
+ if hasattr(request.state, "session_state") and request.state.session_state is not None:
655
+ session_state = request.state.session_state
656
+ if "session_state" in kwargs:
657
+ log_warning("Session state parameter passed in both request state and kwargs, using request state")
658
+ kwargs["session_state"] = session_state
659
+ if hasattr(request.state, "dependencies") and request.state.dependencies is not None:
660
+ dependencies = request.state.dependencies
661
+ if "dependencies" in kwargs:
662
+ log_warning("Dependencies parameter passed in both request state and kwargs, using request state")
663
+ kwargs["dependencies"] = dependencies
664
+ if hasattr(request.state, "metadata") and request.state.metadata is not None:
665
+ metadata = request.state.metadata
666
+ if "metadata" in kwargs:
667
+ log_warning("Metadata parameter passed in both request state and kwargs, using request state")
668
+ kwargs["metadata"] = metadata
669
+
670
+ # Retrieve the workflow by ID
671
+ workflow = get_workflow_by_id(
672
+ workflow_id=workflow_id,
673
+ workflows=os.workflows,
674
+ db=os.db,
675
+ version=version,
676
+ registry=os.registry,
677
+ create_fresh=True,
678
+ )
679
+ if workflow is None:
680
+ raise HTTPException(status_code=404, detail="Workflow not found")
681
+
682
+ if session_id:
683
+ logger.debug(f"Continuing session: {session_id}")
684
+ else:
685
+ logger.debug("Creating new session")
686
+ session_id = str(uuid4())
687
+
688
+ # Extract auth token for remote workflows
689
+ auth_token = get_auth_token_from_request(request)
690
+
691
+ # Return based on stream parameter
692
+ try:
693
+ if stream:
694
+ return StreamingResponse(
695
+ workflow_response_streamer(
696
+ workflow,
697
+ input=message,
698
+ session_id=session_id,
699
+ user_id=user_id,
700
+ background_tasks=background_tasks,
701
+ auth_token=auth_token,
702
+ **kwargs,
703
+ ),
704
+ media_type="text/event-stream",
705
+ )
706
+ else:
707
+ # Pass auth_token for remote workflows
708
+ if auth_token and isinstance(workflow, RemoteWorkflow):
709
+ kwargs["auth_token"] = auth_token
710
+
711
+ run_response = await workflow.arun(
712
+ input=message,
713
+ session_id=session_id,
714
+ user_id=user_id,
715
+ stream=False,
716
+ background_tasks=background_tasks,
717
+ **kwargs,
718
+ )
719
+ return run_response.to_dict()
720
+
721
+ except InputCheckError as e:
722
+ raise HTTPException(status_code=400, detail=str(e))
723
+ except Exception as e:
724
+ # Handle unexpected runtime errors
725
+ raise HTTPException(status_code=500, detail=f"Error running workflow: {str(e)}")
726
+
727
+ @router.post(
728
+ "/workflows/{workflow_id}/runs/{run_id}/cancel",
729
+ tags=["Workflows"],
730
+ operation_id="cancel_workflow_run",
731
+ summary="Cancel Workflow Run",
732
+ description=(
733
+ "Cancel a currently executing workflow run, stopping all active steps and cleanup.\n"
734
+ "**Note:** Complex workflows with multiple parallel steps may take time to fully cancel."
735
+ ),
736
+ responses={
737
+ 200: {},
738
+ 404: {"description": "Workflow or run not found", "model": NotFoundResponse},
739
+ 500: {"description": "Failed to cancel workflow run", "model": InternalServerErrorResponse},
740
+ },
741
+ dependencies=[Depends(require_resource_access("workflows", "run", "workflow_id"))],
742
+ )
743
+ async def cancel_workflow_run(workflow_id: str, run_id: str):
744
+ workflow = get_workflow_by_id(
745
+ workflow_id=workflow_id, workflows=os.workflows, db=os.db, registry=os.registry, create_fresh=True
746
+ )
747
+
748
+ if workflow is None:
749
+ raise HTTPException(status_code=404, detail="Workflow not found")
750
+
751
+ cancelled = await workflow.acancel_run(run_id=run_id)
752
+ if not cancelled:
753
+ raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
754
+
755
+ return JSONResponse(content={}, status_code=200)
756
+
757
+ return router