agno 0.1.2__py3-none-any.whl → 2.3.13__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 (723) hide show
  1. agno/__init__.py +8 -0
  2. agno/agent/__init__.py +44 -5
  3. agno/agent/agent.py +10531 -2975
  4. agno/api/agent.py +14 -53
  5. agno/api/api.py +7 -46
  6. agno/api/evals.py +22 -0
  7. agno/api/os.py +17 -0
  8. agno/api/routes.py +6 -25
  9. agno/api/schemas/__init__.py +9 -0
  10. agno/api/schemas/agent.py +6 -9
  11. agno/api/schemas/evals.py +16 -0
  12. agno/api/schemas/os.py +14 -0
  13. agno/api/schemas/team.py +10 -10
  14. agno/api/schemas/utils.py +21 -0
  15. agno/api/schemas/workflows.py +16 -0
  16. agno/api/settings.py +53 -0
  17. agno/api/team.py +22 -26
  18. agno/api/workflow.py +28 -0
  19. agno/cloud/aws/base.py +214 -0
  20. agno/cloud/aws/s3/__init__.py +2 -0
  21. agno/cloud/aws/s3/api_client.py +43 -0
  22. agno/cloud/aws/s3/bucket.py +195 -0
  23. agno/cloud/aws/s3/object.py +57 -0
  24. agno/compression/__init__.py +3 -0
  25. agno/compression/manager.py +247 -0
  26. agno/culture/__init__.py +3 -0
  27. agno/culture/manager.py +956 -0
  28. agno/db/__init__.py +24 -0
  29. agno/db/async_postgres/__init__.py +3 -0
  30. agno/db/base.py +946 -0
  31. agno/db/dynamo/__init__.py +3 -0
  32. agno/db/dynamo/dynamo.py +2781 -0
  33. agno/db/dynamo/schemas.py +442 -0
  34. agno/db/dynamo/utils.py +743 -0
  35. agno/db/firestore/__init__.py +3 -0
  36. agno/db/firestore/firestore.py +2379 -0
  37. agno/db/firestore/schemas.py +181 -0
  38. agno/db/firestore/utils.py +376 -0
  39. agno/db/gcs_json/__init__.py +3 -0
  40. agno/db/gcs_json/gcs_json_db.py +1791 -0
  41. agno/db/gcs_json/utils.py +228 -0
  42. agno/db/in_memory/__init__.py +3 -0
  43. agno/db/in_memory/in_memory_db.py +1312 -0
  44. agno/db/in_memory/utils.py +230 -0
  45. agno/db/json/__init__.py +3 -0
  46. agno/db/json/json_db.py +1777 -0
  47. agno/db/json/utils.py +230 -0
  48. agno/db/migrations/manager.py +199 -0
  49. agno/db/migrations/v1_to_v2.py +635 -0
  50. agno/db/migrations/versions/v2_3_0.py +938 -0
  51. agno/db/mongo/__init__.py +17 -0
  52. agno/db/mongo/async_mongo.py +2760 -0
  53. agno/db/mongo/mongo.py +2597 -0
  54. agno/db/mongo/schemas.py +119 -0
  55. agno/db/mongo/utils.py +276 -0
  56. agno/db/mysql/__init__.py +4 -0
  57. agno/db/mysql/async_mysql.py +2912 -0
  58. agno/db/mysql/mysql.py +2923 -0
  59. agno/db/mysql/schemas.py +186 -0
  60. agno/db/mysql/utils.py +488 -0
  61. agno/db/postgres/__init__.py +4 -0
  62. agno/db/postgres/async_postgres.py +2579 -0
  63. agno/db/postgres/postgres.py +2870 -0
  64. agno/db/postgres/schemas.py +187 -0
  65. agno/db/postgres/utils.py +442 -0
  66. agno/db/redis/__init__.py +3 -0
  67. agno/db/redis/redis.py +2141 -0
  68. agno/db/redis/schemas.py +159 -0
  69. agno/db/redis/utils.py +346 -0
  70. agno/db/schemas/__init__.py +4 -0
  71. agno/db/schemas/culture.py +120 -0
  72. agno/db/schemas/evals.py +34 -0
  73. agno/db/schemas/knowledge.py +40 -0
  74. agno/db/schemas/memory.py +61 -0
  75. agno/db/singlestore/__init__.py +3 -0
  76. agno/db/singlestore/schemas.py +179 -0
  77. agno/db/singlestore/singlestore.py +2877 -0
  78. agno/db/singlestore/utils.py +384 -0
  79. agno/db/sqlite/__init__.py +4 -0
  80. agno/db/sqlite/async_sqlite.py +2911 -0
  81. agno/db/sqlite/schemas.py +181 -0
  82. agno/db/sqlite/sqlite.py +2908 -0
  83. agno/db/sqlite/utils.py +429 -0
  84. agno/db/surrealdb/__init__.py +3 -0
  85. agno/db/surrealdb/metrics.py +292 -0
  86. agno/db/surrealdb/models.py +334 -0
  87. agno/db/surrealdb/queries.py +71 -0
  88. agno/db/surrealdb/surrealdb.py +1908 -0
  89. agno/db/surrealdb/utils.py +147 -0
  90. agno/db/utils.py +118 -0
  91. agno/eval/__init__.py +24 -0
  92. agno/eval/accuracy.py +666 -276
  93. agno/eval/agent_as_judge.py +861 -0
  94. agno/eval/base.py +29 -0
  95. agno/eval/performance.py +779 -0
  96. agno/eval/reliability.py +241 -62
  97. agno/eval/utils.py +120 -0
  98. agno/exceptions.py +143 -1
  99. agno/filters.py +354 -0
  100. agno/guardrails/__init__.py +6 -0
  101. agno/guardrails/base.py +19 -0
  102. agno/guardrails/openai.py +144 -0
  103. agno/guardrails/pii.py +94 -0
  104. agno/guardrails/prompt_injection.py +52 -0
  105. agno/hooks/__init__.py +3 -0
  106. agno/hooks/decorator.py +164 -0
  107. agno/integrations/discord/__init__.py +3 -0
  108. agno/integrations/discord/client.py +203 -0
  109. agno/knowledge/__init__.py +5 -1
  110. agno/{document → knowledge}/chunking/agentic.py +22 -14
  111. agno/{document → knowledge}/chunking/document.py +2 -2
  112. agno/{document → knowledge}/chunking/fixed.py +7 -6
  113. agno/knowledge/chunking/markdown.py +151 -0
  114. agno/{document → knowledge}/chunking/recursive.py +15 -3
  115. agno/knowledge/chunking/row.py +39 -0
  116. agno/knowledge/chunking/semantic.py +91 -0
  117. agno/knowledge/chunking/strategy.py +165 -0
  118. agno/knowledge/content.py +74 -0
  119. agno/knowledge/document/__init__.py +5 -0
  120. agno/{document → knowledge/document}/base.py +12 -2
  121. agno/knowledge/embedder/__init__.py +5 -0
  122. agno/knowledge/embedder/aws_bedrock.py +343 -0
  123. agno/knowledge/embedder/azure_openai.py +210 -0
  124. agno/{embedder → knowledge/embedder}/base.py +8 -0
  125. agno/knowledge/embedder/cohere.py +323 -0
  126. agno/knowledge/embedder/fastembed.py +62 -0
  127. agno/{embedder → knowledge/embedder}/fireworks.py +1 -1
  128. agno/knowledge/embedder/google.py +258 -0
  129. agno/knowledge/embedder/huggingface.py +94 -0
  130. agno/knowledge/embedder/jina.py +182 -0
  131. agno/knowledge/embedder/langdb.py +22 -0
  132. agno/knowledge/embedder/mistral.py +206 -0
  133. agno/knowledge/embedder/nebius.py +13 -0
  134. agno/knowledge/embedder/ollama.py +154 -0
  135. agno/knowledge/embedder/openai.py +195 -0
  136. agno/knowledge/embedder/sentence_transformer.py +63 -0
  137. agno/{embedder → knowledge/embedder}/together.py +1 -1
  138. agno/knowledge/embedder/vllm.py +262 -0
  139. agno/knowledge/embedder/voyageai.py +165 -0
  140. agno/knowledge/knowledge.py +3006 -0
  141. agno/knowledge/reader/__init__.py +7 -0
  142. agno/knowledge/reader/arxiv_reader.py +81 -0
  143. agno/knowledge/reader/base.py +95 -0
  144. agno/knowledge/reader/csv_reader.py +164 -0
  145. agno/knowledge/reader/docx_reader.py +82 -0
  146. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  147. agno/knowledge/reader/firecrawl_reader.py +201 -0
  148. agno/knowledge/reader/json_reader.py +88 -0
  149. agno/knowledge/reader/markdown_reader.py +137 -0
  150. agno/knowledge/reader/pdf_reader.py +431 -0
  151. agno/knowledge/reader/pptx_reader.py +101 -0
  152. agno/knowledge/reader/reader_factory.py +313 -0
  153. agno/knowledge/reader/s3_reader.py +89 -0
  154. agno/knowledge/reader/tavily_reader.py +193 -0
  155. agno/knowledge/reader/text_reader.py +127 -0
  156. agno/knowledge/reader/web_search_reader.py +325 -0
  157. agno/knowledge/reader/website_reader.py +455 -0
  158. agno/knowledge/reader/wikipedia_reader.py +91 -0
  159. agno/knowledge/reader/youtube_reader.py +78 -0
  160. agno/knowledge/remote_content/remote_content.py +88 -0
  161. agno/knowledge/reranker/__init__.py +3 -0
  162. agno/{reranker → knowledge/reranker}/base.py +1 -1
  163. agno/{reranker → knowledge/reranker}/cohere.py +2 -2
  164. agno/knowledge/reranker/infinity.py +195 -0
  165. agno/knowledge/reranker/sentence_transformer.py +54 -0
  166. agno/knowledge/types.py +39 -0
  167. agno/knowledge/utils.py +234 -0
  168. agno/media.py +439 -95
  169. agno/memory/__init__.py +16 -3
  170. agno/memory/manager.py +1474 -123
  171. agno/memory/strategies/__init__.py +15 -0
  172. agno/memory/strategies/base.py +66 -0
  173. agno/memory/strategies/summarize.py +196 -0
  174. agno/memory/strategies/types.py +37 -0
  175. agno/models/aimlapi/__init__.py +5 -0
  176. agno/models/aimlapi/aimlapi.py +62 -0
  177. agno/models/anthropic/__init__.py +4 -0
  178. agno/models/anthropic/claude.py +960 -496
  179. agno/models/aws/__init__.py +15 -0
  180. agno/models/aws/bedrock.py +686 -451
  181. agno/models/aws/claude.py +190 -183
  182. agno/models/azure/__init__.py +18 -1
  183. agno/models/azure/ai_foundry.py +489 -0
  184. agno/models/azure/openai_chat.py +89 -40
  185. agno/models/base.py +2477 -550
  186. agno/models/cerebras/__init__.py +12 -0
  187. agno/models/cerebras/cerebras.py +565 -0
  188. agno/models/cerebras/cerebras_openai.py +131 -0
  189. agno/models/cohere/__init__.py +4 -0
  190. agno/models/cohere/chat.py +306 -492
  191. agno/models/cometapi/__init__.py +5 -0
  192. agno/models/cometapi/cometapi.py +74 -0
  193. agno/models/dashscope/__init__.py +5 -0
  194. agno/models/dashscope/dashscope.py +90 -0
  195. agno/models/deepinfra/__init__.py +5 -0
  196. agno/models/deepinfra/deepinfra.py +45 -0
  197. agno/models/deepseek/__init__.py +4 -0
  198. agno/models/deepseek/deepseek.py +110 -9
  199. agno/models/fireworks/__init__.py +4 -0
  200. agno/models/fireworks/fireworks.py +19 -22
  201. agno/models/google/__init__.py +3 -7
  202. agno/models/google/gemini.py +1717 -662
  203. agno/models/google/utils.py +22 -0
  204. agno/models/groq/__init__.py +4 -0
  205. agno/models/groq/groq.py +391 -666
  206. agno/models/huggingface/__init__.py +4 -0
  207. agno/models/huggingface/huggingface.py +266 -538
  208. agno/models/ibm/__init__.py +5 -0
  209. agno/models/ibm/watsonx.py +432 -0
  210. agno/models/internlm/__init__.py +3 -0
  211. agno/models/internlm/internlm.py +20 -3
  212. agno/models/langdb/__init__.py +1 -0
  213. agno/models/langdb/langdb.py +60 -0
  214. agno/models/litellm/__init__.py +14 -0
  215. agno/models/litellm/chat.py +503 -0
  216. agno/models/litellm/litellm_openai.py +42 -0
  217. agno/models/llama_cpp/__init__.py +5 -0
  218. agno/models/llama_cpp/llama_cpp.py +22 -0
  219. agno/models/lmstudio/__init__.py +5 -0
  220. agno/models/lmstudio/lmstudio.py +25 -0
  221. agno/models/message.py +361 -39
  222. agno/models/meta/__init__.py +12 -0
  223. agno/models/meta/llama.py +502 -0
  224. agno/models/meta/llama_openai.py +79 -0
  225. agno/models/metrics.py +120 -0
  226. agno/models/mistral/__init__.py +4 -0
  227. agno/models/mistral/mistral.py +293 -393
  228. agno/models/nebius/__init__.py +3 -0
  229. agno/models/nebius/nebius.py +53 -0
  230. agno/models/nexus/__init__.py +3 -0
  231. agno/models/nexus/nexus.py +22 -0
  232. agno/models/nvidia/__init__.py +4 -0
  233. agno/models/nvidia/nvidia.py +22 -3
  234. agno/models/ollama/__init__.py +4 -2
  235. agno/models/ollama/chat.py +257 -492
  236. agno/models/openai/__init__.py +7 -0
  237. agno/models/openai/chat.py +725 -770
  238. agno/models/openai/like.py +16 -2
  239. agno/models/openai/responses.py +1121 -0
  240. agno/models/openrouter/__init__.py +4 -0
  241. agno/models/openrouter/openrouter.py +62 -5
  242. agno/models/perplexity/__init__.py +5 -0
  243. agno/models/perplexity/perplexity.py +203 -0
  244. agno/models/portkey/__init__.py +3 -0
  245. agno/models/portkey/portkey.py +82 -0
  246. agno/models/requesty/__init__.py +5 -0
  247. agno/models/requesty/requesty.py +69 -0
  248. agno/models/response.py +177 -7
  249. agno/models/sambanova/__init__.py +4 -0
  250. agno/models/sambanova/sambanova.py +23 -4
  251. agno/models/siliconflow/__init__.py +5 -0
  252. agno/models/siliconflow/siliconflow.py +42 -0
  253. agno/models/together/__init__.py +4 -0
  254. agno/models/together/together.py +21 -164
  255. agno/models/utils.py +266 -0
  256. agno/models/vercel/__init__.py +3 -0
  257. agno/models/vercel/v0.py +43 -0
  258. agno/models/vertexai/__init__.py +0 -1
  259. agno/models/vertexai/claude.py +190 -0
  260. agno/models/vllm/__init__.py +3 -0
  261. agno/models/vllm/vllm.py +83 -0
  262. agno/models/xai/__init__.py +2 -0
  263. agno/models/xai/xai.py +111 -7
  264. agno/os/__init__.py +3 -0
  265. agno/os/app.py +1027 -0
  266. agno/os/auth.py +244 -0
  267. agno/os/config.py +126 -0
  268. agno/os/interfaces/__init__.py +1 -0
  269. agno/os/interfaces/a2a/__init__.py +3 -0
  270. agno/os/interfaces/a2a/a2a.py +42 -0
  271. agno/os/interfaces/a2a/router.py +249 -0
  272. agno/os/interfaces/a2a/utils.py +924 -0
  273. agno/os/interfaces/agui/__init__.py +3 -0
  274. agno/os/interfaces/agui/agui.py +47 -0
  275. agno/os/interfaces/agui/router.py +147 -0
  276. agno/os/interfaces/agui/utils.py +574 -0
  277. agno/os/interfaces/base.py +25 -0
  278. agno/os/interfaces/slack/__init__.py +3 -0
  279. agno/os/interfaces/slack/router.py +148 -0
  280. agno/os/interfaces/slack/security.py +30 -0
  281. agno/os/interfaces/slack/slack.py +47 -0
  282. agno/os/interfaces/whatsapp/__init__.py +3 -0
  283. agno/os/interfaces/whatsapp/router.py +210 -0
  284. agno/os/interfaces/whatsapp/security.py +55 -0
  285. agno/os/interfaces/whatsapp/whatsapp.py +36 -0
  286. agno/os/mcp.py +293 -0
  287. agno/os/middleware/__init__.py +9 -0
  288. agno/os/middleware/jwt.py +797 -0
  289. agno/os/router.py +258 -0
  290. agno/os/routers/__init__.py +3 -0
  291. agno/os/routers/agents/__init__.py +3 -0
  292. agno/os/routers/agents/router.py +599 -0
  293. agno/os/routers/agents/schema.py +261 -0
  294. agno/os/routers/evals/__init__.py +3 -0
  295. agno/os/routers/evals/evals.py +450 -0
  296. agno/os/routers/evals/schemas.py +174 -0
  297. agno/os/routers/evals/utils.py +231 -0
  298. agno/os/routers/health.py +31 -0
  299. agno/os/routers/home.py +52 -0
  300. agno/os/routers/knowledge/__init__.py +3 -0
  301. agno/os/routers/knowledge/knowledge.py +1008 -0
  302. agno/os/routers/knowledge/schemas.py +178 -0
  303. agno/os/routers/memory/__init__.py +3 -0
  304. agno/os/routers/memory/memory.py +661 -0
  305. agno/os/routers/memory/schemas.py +88 -0
  306. agno/os/routers/metrics/__init__.py +3 -0
  307. agno/os/routers/metrics/metrics.py +190 -0
  308. agno/os/routers/metrics/schemas.py +47 -0
  309. agno/os/routers/session/__init__.py +3 -0
  310. agno/os/routers/session/session.py +997 -0
  311. agno/os/routers/teams/__init__.py +3 -0
  312. agno/os/routers/teams/router.py +512 -0
  313. agno/os/routers/teams/schema.py +257 -0
  314. agno/os/routers/traces/__init__.py +3 -0
  315. agno/os/routers/traces/schemas.py +414 -0
  316. agno/os/routers/traces/traces.py +499 -0
  317. agno/os/routers/workflows/__init__.py +3 -0
  318. agno/os/routers/workflows/router.py +624 -0
  319. agno/os/routers/workflows/schema.py +75 -0
  320. agno/os/schema.py +534 -0
  321. agno/os/scopes.py +469 -0
  322. agno/{playground → os}/settings.py +7 -15
  323. agno/os/utils.py +973 -0
  324. agno/reasoning/anthropic.py +80 -0
  325. agno/reasoning/azure_ai_foundry.py +67 -0
  326. agno/reasoning/deepseek.py +63 -0
  327. agno/reasoning/default.py +97 -0
  328. agno/reasoning/gemini.py +73 -0
  329. agno/reasoning/groq.py +71 -0
  330. agno/reasoning/helpers.py +24 -1
  331. agno/reasoning/ollama.py +67 -0
  332. agno/reasoning/openai.py +86 -0
  333. agno/reasoning/step.py +2 -1
  334. agno/reasoning/vertexai.py +76 -0
  335. agno/run/__init__.py +6 -0
  336. agno/run/agent.py +822 -0
  337. agno/run/base.py +247 -0
  338. agno/run/cancel.py +81 -0
  339. agno/run/requirement.py +181 -0
  340. agno/run/team.py +767 -0
  341. agno/run/workflow.py +708 -0
  342. agno/session/__init__.py +10 -0
  343. agno/session/agent.py +260 -0
  344. agno/session/summary.py +265 -0
  345. agno/session/team.py +342 -0
  346. agno/session/workflow.py +501 -0
  347. agno/table.py +10 -0
  348. agno/team/__init__.py +37 -0
  349. agno/team/team.py +9536 -0
  350. agno/tools/__init__.py +7 -0
  351. agno/tools/agentql.py +120 -0
  352. agno/tools/airflow.py +22 -12
  353. agno/tools/api.py +122 -0
  354. agno/tools/apify.py +276 -83
  355. agno/tools/{arxiv_toolkit.py → arxiv.py} +20 -12
  356. agno/tools/aws_lambda.py +28 -7
  357. agno/tools/aws_ses.py +66 -0
  358. agno/tools/baidusearch.py +11 -4
  359. agno/tools/bitbucket.py +292 -0
  360. agno/tools/brandfetch.py +213 -0
  361. agno/tools/bravesearch.py +106 -0
  362. agno/tools/brightdata.py +367 -0
  363. agno/tools/browserbase.py +209 -0
  364. agno/tools/calcom.py +32 -23
  365. agno/tools/calculator.py +24 -37
  366. agno/tools/cartesia.py +187 -0
  367. agno/tools/{clickup_tool.py → clickup.py} +17 -28
  368. agno/tools/confluence.py +91 -26
  369. agno/tools/crawl4ai.py +139 -43
  370. agno/tools/csv_toolkit.py +28 -22
  371. agno/tools/dalle.py +36 -22
  372. agno/tools/daytona.py +475 -0
  373. agno/tools/decorator.py +169 -14
  374. agno/tools/desi_vocal.py +23 -11
  375. agno/tools/discord.py +32 -29
  376. agno/tools/docker.py +716 -0
  377. agno/tools/duckdb.py +76 -81
  378. agno/tools/duckduckgo.py +43 -40
  379. agno/tools/e2b.py +703 -0
  380. agno/tools/eleven_labs.py +65 -54
  381. agno/tools/email.py +13 -5
  382. agno/tools/evm.py +129 -0
  383. agno/tools/exa.py +324 -42
  384. agno/tools/fal.py +39 -35
  385. agno/tools/file.py +196 -30
  386. agno/tools/file_generation.py +356 -0
  387. agno/tools/financial_datasets.py +288 -0
  388. agno/tools/firecrawl.py +108 -33
  389. agno/tools/function.py +960 -122
  390. agno/tools/giphy.py +34 -12
  391. agno/tools/github.py +1294 -97
  392. agno/tools/gmail.py +922 -0
  393. agno/tools/google_bigquery.py +117 -0
  394. agno/tools/google_drive.py +271 -0
  395. agno/tools/google_maps.py +253 -0
  396. agno/tools/googlecalendar.py +607 -107
  397. agno/tools/googlesheets.py +377 -0
  398. agno/tools/hackernews.py +20 -12
  399. agno/tools/jina.py +24 -14
  400. agno/tools/jira.py +48 -19
  401. agno/tools/knowledge.py +218 -0
  402. agno/tools/linear.py +82 -43
  403. agno/tools/linkup.py +58 -0
  404. agno/tools/local_file_system.py +15 -7
  405. agno/tools/lumalab.py +41 -26
  406. agno/tools/mcp/__init__.py +10 -0
  407. agno/tools/mcp/mcp.py +331 -0
  408. agno/tools/mcp/multi_mcp.py +347 -0
  409. agno/tools/mcp/params.py +24 -0
  410. agno/tools/mcp_toolbox.py +284 -0
  411. agno/tools/mem0.py +193 -0
  412. agno/tools/memory.py +419 -0
  413. agno/tools/mlx_transcribe.py +11 -9
  414. agno/tools/models/azure_openai.py +190 -0
  415. agno/tools/models/gemini.py +203 -0
  416. agno/tools/models/groq.py +158 -0
  417. agno/tools/models/morph.py +186 -0
  418. agno/tools/models/nebius.py +124 -0
  419. agno/tools/models_labs.py +163 -82
  420. agno/tools/moviepy_video.py +18 -13
  421. agno/tools/nano_banana.py +151 -0
  422. agno/tools/neo4j.py +134 -0
  423. agno/tools/newspaper.py +15 -4
  424. agno/tools/newspaper4k.py +19 -6
  425. agno/tools/notion.py +204 -0
  426. agno/tools/openai.py +181 -17
  427. agno/tools/openbb.py +27 -20
  428. agno/tools/opencv.py +321 -0
  429. agno/tools/openweather.py +233 -0
  430. agno/tools/oxylabs.py +385 -0
  431. agno/tools/pandas.py +25 -15
  432. agno/tools/parallel.py +314 -0
  433. agno/tools/postgres.py +238 -185
  434. agno/tools/pubmed.py +125 -13
  435. agno/tools/python.py +48 -35
  436. agno/tools/reasoning.py +283 -0
  437. agno/tools/reddit.py +207 -29
  438. agno/tools/redshift.py +406 -0
  439. agno/tools/replicate.py +69 -26
  440. agno/tools/resend.py +11 -6
  441. agno/tools/scrapegraph.py +179 -19
  442. agno/tools/searxng.py +23 -31
  443. agno/tools/serpapi.py +15 -10
  444. agno/tools/serper.py +255 -0
  445. agno/tools/shell.py +23 -12
  446. agno/tools/shopify.py +1519 -0
  447. agno/tools/slack.py +56 -14
  448. agno/tools/sleep.py +8 -6
  449. agno/tools/spider.py +35 -11
  450. agno/tools/spotify.py +919 -0
  451. agno/tools/sql.py +34 -19
  452. agno/tools/tavily.py +158 -8
  453. agno/tools/telegram.py +18 -8
  454. agno/tools/todoist.py +218 -0
  455. agno/tools/toolkit.py +134 -9
  456. agno/tools/trafilatura.py +388 -0
  457. agno/tools/trello.py +25 -28
  458. agno/tools/twilio.py +18 -9
  459. agno/tools/user_control_flow.py +78 -0
  460. agno/tools/valyu.py +228 -0
  461. agno/tools/visualization.py +467 -0
  462. agno/tools/webbrowser.py +28 -0
  463. agno/tools/webex.py +76 -0
  464. agno/tools/website.py +23 -19
  465. agno/tools/webtools.py +45 -0
  466. agno/tools/whatsapp.py +286 -0
  467. agno/tools/wikipedia.py +28 -19
  468. agno/tools/workflow.py +285 -0
  469. agno/tools/{twitter.py → x.py} +142 -46
  470. agno/tools/yfinance.py +41 -39
  471. agno/tools/youtube.py +34 -17
  472. agno/tools/zendesk.py +15 -5
  473. agno/tools/zep.py +454 -0
  474. agno/tools/zoom.py +86 -37
  475. agno/tracing/__init__.py +12 -0
  476. agno/tracing/exporter.py +157 -0
  477. agno/tracing/schemas.py +276 -0
  478. agno/tracing/setup.py +111 -0
  479. agno/utils/agent.py +938 -0
  480. agno/utils/audio.py +37 -1
  481. agno/utils/certs.py +27 -0
  482. agno/utils/code_execution.py +11 -0
  483. agno/utils/common.py +103 -20
  484. agno/utils/cryptography.py +22 -0
  485. agno/utils/dttm.py +33 -0
  486. agno/utils/events.py +700 -0
  487. agno/utils/functions.py +107 -37
  488. agno/utils/gemini.py +426 -0
  489. agno/utils/hooks.py +171 -0
  490. agno/utils/http.py +185 -0
  491. agno/utils/json_schema.py +159 -37
  492. agno/utils/knowledge.py +36 -0
  493. agno/utils/location.py +19 -0
  494. agno/utils/log.py +221 -8
  495. agno/utils/mcp.py +214 -0
  496. agno/utils/media.py +335 -14
  497. agno/utils/merge_dict.py +22 -1
  498. agno/utils/message.py +77 -2
  499. agno/utils/models/ai_foundry.py +50 -0
  500. agno/utils/models/claude.py +373 -0
  501. agno/utils/models/cohere.py +94 -0
  502. agno/utils/models/llama.py +85 -0
  503. agno/utils/models/mistral.py +100 -0
  504. agno/utils/models/openai_responses.py +140 -0
  505. agno/utils/models/schema_utils.py +153 -0
  506. agno/utils/models/watsonx.py +41 -0
  507. agno/utils/openai.py +257 -0
  508. agno/utils/pickle.py +1 -1
  509. agno/utils/pprint.py +124 -8
  510. agno/utils/print_response/agent.py +930 -0
  511. agno/utils/print_response/team.py +1914 -0
  512. agno/utils/print_response/workflow.py +1668 -0
  513. agno/utils/prompts.py +111 -0
  514. agno/utils/reasoning.py +108 -0
  515. agno/utils/response.py +163 -0
  516. agno/utils/serialize.py +32 -0
  517. agno/utils/shell.py +4 -4
  518. agno/utils/streamlit.py +487 -0
  519. agno/utils/string.py +204 -51
  520. agno/utils/team.py +139 -0
  521. agno/utils/timer.py +9 -2
  522. agno/utils/tokens.py +657 -0
  523. agno/utils/tools.py +19 -1
  524. agno/utils/whatsapp.py +305 -0
  525. agno/utils/yaml_io.py +3 -3
  526. agno/vectordb/__init__.py +2 -0
  527. agno/vectordb/base.py +87 -9
  528. agno/vectordb/cassandra/__init__.py +5 -1
  529. agno/vectordb/cassandra/cassandra.py +383 -27
  530. agno/vectordb/chroma/__init__.py +4 -0
  531. agno/vectordb/chroma/chromadb.py +748 -83
  532. agno/vectordb/clickhouse/__init__.py +7 -1
  533. agno/vectordb/clickhouse/clickhousedb.py +554 -53
  534. agno/vectordb/couchbase/__init__.py +3 -0
  535. agno/vectordb/couchbase/couchbase.py +1446 -0
  536. agno/vectordb/lancedb/__init__.py +5 -0
  537. agno/vectordb/lancedb/lance_db.py +730 -98
  538. agno/vectordb/langchaindb/__init__.py +5 -0
  539. agno/vectordb/langchaindb/langchaindb.py +163 -0
  540. agno/vectordb/lightrag/__init__.py +5 -0
  541. agno/vectordb/lightrag/lightrag.py +388 -0
  542. agno/vectordb/llamaindex/__init__.py +3 -0
  543. agno/vectordb/llamaindex/llamaindexdb.py +166 -0
  544. agno/vectordb/milvus/__init__.py +3 -0
  545. agno/vectordb/milvus/milvus.py +966 -78
  546. agno/vectordb/mongodb/__init__.py +9 -1
  547. agno/vectordb/mongodb/mongodb.py +1175 -172
  548. agno/vectordb/pgvector/__init__.py +8 -0
  549. agno/vectordb/pgvector/pgvector.py +599 -115
  550. agno/vectordb/pineconedb/__init__.py +5 -1
  551. agno/vectordb/pineconedb/pineconedb.py +406 -43
  552. agno/vectordb/qdrant/__init__.py +4 -0
  553. agno/vectordb/qdrant/qdrant.py +914 -61
  554. agno/vectordb/redis/__init__.py +9 -0
  555. agno/vectordb/redis/redisdb.py +682 -0
  556. agno/vectordb/singlestore/__init__.py +8 -1
  557. agno/vectordb/singlestore/singlestore.py +771 -0
  558. agno/vectordb/surrealdb/__init__.py +3 -0
  559. agno/vectordb/surrealdb/surrealdb.py +663 -0
  560. agno/vectordb/upstashdb/__init__.py +5 -0
  561. agno/vectordb/upstashdb/upstashdb.py +718 -0
  562. agno/vectordb/weaviate/__init__.py +8 -0
  563. agno/vectordb/weaviate/index.py +15 -0
  564. agno/vectordb/weaviate/weaviate.py +1009 -0
  565. agno/workflow/__init__.py +23 -1
  566. agno/workflow/agent.py +299 -0
  567. agno/workflow/condition.py +759 -0
  568. agno/workflow/loop.py +756 -0
  569. agno/workflow/parallel.py +853 -0
  570. agno/workflow/router.py +723 -0
  571. agno/workflow/step.py +1564 -0
  572. agno/workflow/steps.py +613 -0
  573. agno/workflow/types.py +556 -0
  574. agno/workflow/workflow.py +4327 -514
  575. agno-2.3.13.dist-info/METADATA +639 -0
  576. agno-2.3.13.dist-info/RECORD +613 -0
  577. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +1 -1
  578. agno-2.3.13.dist-info/licenses/LICENSE +201 -0
  579. agno/api/playground.py +0 -91
  580. agno/api/schemas/playground.py +0 -22
  581. agno/api/schemas/user.py +0 -22
  582. agno/api/schemas/workspace.py +0 -46
  583. agno/api/user.py +0 -160
  584. agno/api/workspace.py +0 -151
  585. agno/cli/auth_server.py +0 -118
  586. agno/cli/config.py +0 -275
  587. agno/cli/console.py +0 -88
  588. agno/cli/credentials.py +0 -23
  589. agno/cli/entrypoint.py +0 -571
  590. agno/cli/operator.py +0 -355
  591. agno/cli/settings.py +0 -85
  592. agno/cli/ws/ws_cli.py +0 -817
  593. agno/constants.py +0 -13
  594. agno/document/__init__.py +0 -1
  595. agno/document/chunking/semantic.py +0 -47
  596. agno/document/chunking/strategy.py +0 -31
  597. agno/document/reader/__init__.py +0 -1
  598. agno/document/reader/arxiv_reader.py +0 -41
  599. agno/document/reader/base.py +0 -22
  600. agno/document/reader/csv_reader.py +0 -84
  601. agno/document/reader/docx_reader.py +0 -46
  602. agno/document/reader/firecrawl_reader.py +0 -99
  603. agno/document/reader/json_reader.py +0 -43
  604. agno/document/reader/pdf_reader.py +0 -219
  605. agno/document/reader/s3/pdf_reader.py +0 -46
  606. agno/document/reader/s3/text_reader.py +0 -51
  607. agno/document/reader/text_reader.py +0 -41
  608. agno/document/reader/website_reader.py +0 -175
  609. agno/document/reader/youtube_reader.py +0 -50
  610. agno/embedder/__init__.py +0 -1
  611. agno/embedder/azure_openai.py +0 -86
  612. agno/embedder/cohere.py +0 -72
  613. agno/embedder/fastembed.py +0 -37
  614. agno/embedder/google.py +0 -73
  615. agno/embedder/huggingface.py +0 -54
  616. agno/embedder/mistral.py +0 -80
  617. agno/embedder/ollama.py +0 -57
  618. agno/embedder/openai.py +0 -74
  619. agno/embedder/sentence_transformer.py +0 -38
  620. agno/embedder/voyageai.py +0 -64
  621. agno/eval/perf.py +0 -201
  622. agno/file/__init__.py +0 -1
  623. agno/file/file.py +0 -16
  624. agno/file/local/csv.py +0 -32
  625. agno/file/local/txt.py +0 -19
  626. agno/infra/app.py +0 -240
  627. agno/infra/base.py +0 -144
  628. agno/infra/context.py +0 -20
  629. agno/infra/db_app.py +0 -52
  630. agno/infra/resource.py +0 -205
  631. agno/infra/resources.py +0 -55
  632. agno/knowledge/agent.py +0 -230
  633. agno/knowledge/arxiv.py +0 -22
  634. agno/knowledge/combined.py +0 -22
  635. agno/knowledge/csv.py +0 -28
  636. agno/knowledge/csv_url.py +0 -19
  637. agno/knowledge/document.py +0 -20
  638. agno/knowledge/docx.py +0 -30
  639. agno/knowledge/json.py +0 -28
  640. agno/knowledge/langchain.py +0 -71
  641. agno/knowledge/llamaindex.py +0 -66
  642. agno/knowledge/pdf.py +0 -28
  643. agno/knowledge/pdf_url.py +0 -26
  644. agno/knowledge/s3/base.py +0 -60
  645. agno/knowledge/s3/pdf.py +0 -21
  646. agno/knowledge/s3/text.py +0 -23
  647. agno/knowledge/text.py +0 -30
  648. agno/knowledge/website.py +0 -88
  649. agno/knowledge/wikipedia.py +0 -31
  650. agno/knowledge/youtube.py +0 -22
  651. agno/memory/agent.py +0 -392
  652. agno/memory/classifier.py +0 -104
  653. agno/memory/db/__init__.py +0 -1
  654. agno/memory/db/base.py +0 -42
  655. agno/memory/db/mongodb.py +0 -189
  656. agno/memory/db/postgres.py +0 -203
  657. agno/memory/db/sqlite.py +0 -193
  658. agno/memory/memory.py +0 -15
  659. agno/memory/row.py +0 -36
  660. agno/memory/summarizer.py +0 -192
  661. agno/memory/summary.py +0 -19
  662. agno/memory/workflow.py +0 -38
  663. agno/models/google/gemini_openai.py +0 -26
  664. agno/models/ollama/hermes.py +0 -221
  665. agno/models/ollama/tools.py +0 -362
  666. agno/models/vertexai/gemini.py +0 -595
  667. agno/playground/__init__.py +0 -3
  668. agno/playground/async_router.py +0 -421
  669. agno/playground/deploy.py +0 -249
  670. agno/playground/operator.py +0 -92
  671. agno/playground/playground.py +0 -91
  672. agno/playground/schemas.py +0 -76
  673. agno/playground/serve.py +0 -55
  674. agno/playground/sync_router.py +0 -405
  675. agno/reasoning/agent.py +0 -68
  676. agno/run/response.py +0 -112
  677. agno/storage/agent/__init__.py +0 -0
  678. agno/storage/agent/base.py +0 -38
  679. agno/storage/agent/dynamodb.py +0 -350
  680. agno/storage/agent/json.py +0 -92
  681. agno/storage/agent/mongodb.py +0 -228
  682. agno/storage/agent/postgres.py +0 -367
  683. agno/storage/agent/session.py +0 -79
  684. agno/storage/agent/singlestore.py +0 -303
  685. agno/storage/agent/sqlite.py +0 -357
  686. agno/storage/agent/yaml.py +0 -93
  687. agno/storage/workflow/__init__.py +0 -0
  688. agno/storage/workflow/base.py +0 -40
  689. agno/storage/workflow/mongodb.py +0 -233
  690. agno/storage/workflow/postgres.py +0 -366
  691. agno/storage/workflow/session.py +0 -60
  692. agno/storage/workflow/sqlite.py +0 -359
  693. agno/tools/googlesearch.py +0 -88
  694. agno/utils/defaults.py +0 -57
  695. agno/utils/filesystem.py +0 -39
  696. agno/utils/git.py +0 -52
  697. agno/utils/json_io.py +0 -30
  698. agno/utils/load_env.py +0 -19
  699. agno/utils/py_io.py +0 -19
  700. agno/utils/pyproject.py +0 -18
  701. agno/utils/resource_filter.py +0 -31
  702. agno/vectordb/singlestore/s2vectordb.py +0 -390
  703. agno/vectordb/singlestore/s2vectordb2.py +0 -355
  704. agno/workspace/__init__.py +0 -0
  705. agno/workspace/config.py +0 -325
  706. agno/workspace/enums.py +0 -6
  707. agno/workspace/helpers.py +0 -48
  708. agno/workspace/operator.py +0 -758
  709. agno/workspace/settings.py +0 -63
  710. agno-0.1.2.dist-info/LICENSE +0 -375
  711. agno-0.1.2.dist-info/METADATA +0 -502
  712. agno-0.1.2.dist-info/RECORD +0 -352
  713. agno-0.1.2.dist-info/entry_points.txt +0 -3
  714. /agno/{cli → db/migrations}/__init__.py +0 -0
  715. /agno/{cli/ws → db/migrations/versions}/__init__.py +0 -0
  716. /agno/{document/chunking/__init__.py → db/schemas/metrics.py} +0 -0
  717. /agno/{document/reader/s3 → integrations}/__init__.py +0 -0
  718. /agno/{file/local → knowledge/chunking}/__init__.py +0 -0
  719. /agno/{infra → knowledge/remote_content}/__init__.py +0 -0
  720. /agno/{knowledge/s3 → tools/models}/__init__.py +0 -0
  721. /agno/{reranker → utils/models}/__init__.py +0 -0
  722. /agno/{storage → utils/print_response}/__init__.py +0 -0
  723. {agno-0.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/workflow/step.py ADDED
@@ -0,0 +1,1564 @@
1
+ import inspect
2
+ import warnings
3
+ from copy import copy
4
+ from dataclasses import dataclass
5
+ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union, cast
6
+ from uuid import uuid4
7
+
8
+ from pydantic import BaseModel
9
+ from typing_extensions import TypeGuard
10
+
11
+ from agno.agent import Agent
12
+ from agno.media import Audio, Image, Video
13
+ from agno.models.message import Message
14
+ from agno.models.metrics import Metrics
15
+ from agno.run import RunContext
16
+ from agno.run.agent import RunContentEvent, RunOutput
17
+ from agno.run.base import BaseRunOutputEvent
18
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
19
+ from agno.run.team import TeamRunOutput
20
+ from agno.run.workflow import (
21
+ StepCompletedEvent,
22
+ StepStartedEvent,
23
+ WorkflowRunOutput,
24
+ WorkflowRunOutputEvent,
25
+ )
26
+ from agno.session.agent import AgentSession
27
+ from agno.session.team import TeamSession
28
+ from agno.session.workflow import WorkflowSession
29
+ from agno.team import Team
30
+ from agno.utils.log import log_debug, log_warning, logger, use_agent_logger, use_team_logger, use_workflow_logger
31
+ from agno.utils.merge_dict import merge_dictionaries
32
+ from agno.workflow.types import StepInput, StepOutput, StepType
33
+
34
+ StepExecutor = Callable[
35
+ [StepInput],
36
+ Union[
37
+ StepOutput,
38
+ Iterator[StepOutput],
39
+ Iterator[Any],
40
+ Awaitable[StepOutput],
41
+ Awaitable[Any],
42
+ AsyncIterator[StepOutput],
43
+ AsyncIterator[Any],
44
+ ],
45
+ ]
46
+
47
+
48
+ @dataclass
49
+ class Step:
50
+ """A single unit of work in a workflow pipeline"""
51
+
52
+ name: Optional[str] = None
53
+
54
+ # Executor options - only one should be provided
55
+ agent: Optional[Agent] = None
56
+ team: Optional[Team] = None
57
+ executor: Optional[StepExecutor] = None
58
+
59
+ step_id: Optional[str] = None
60
+ description: Optional[str] = None
61
+
62
+ # Step configuration
63
+ max_retries: int = 3
64
+ timeout_seconds: Optional[int] = None
65
+
66
+ skip_on_failure: bool = False
67
+
68
+ # Input validation mode
69
+ # If False, only warn about missing inputs
70
+ strict_input_validation: bool = False
71
+
72
+ add_workflow_history: Optional[bool] = None
73
+ num_history_runs: int = 3
74
+
75
+ _retry_count: int = 0
76
+
77
+ def __init__(
78
+ self,
79
+ name: Optional[str] = None,
80
+ agent: Optional[Agent] = None,
81
+ team: Optional[Team] = None,
82
+ executor: Optional[StepExecutor] = None,
83
+ step_id: Optional[str] = None,
84
+ description: Optional[str] = None,
85
+ max_retries: int = 3,
86
+ timeout_seconds: Optional[int] = None,
87
+ skip_on_failure: bool = False,
88
+ strict_input_validation: bool = False,
89
+ add_workflow_history: Optional[bool] = None,
90
+ num_history_runs: int = 3,
91
+ ):
92
+ # Auto-detect name for function executors if not provided
93
+ if name is None and executor is not None:
94
+ name = getattr(executor, "__name__", None)
95
+
96
+ self.name = name
97
+ self.agent = agent
98
+ self.team = team
99
+ self.executor = executor
100
+
101
+ # Validate executor configuration
102
+ self._validate_executor_config()
103
+
104
+ self.step_id = step_id
105
+ self.description = description
106
+ self.max_retries = max_retries
107
+ self.timeout_seconds = timeout_seconds
108
+ self.skip_on_failure = skip_on_failure
109
+ self.strict_input_validation = strict_input_validation
110
+ self.add_workflow_history = add_workflow_history
111
+ self.num_history_runs = num_history_runs
112
+ self.step_id = step_id
113
+
114
+ if step_id is None:
115
+ self.step_id = str(uuid4())
116
+
117
+ # Set the active executor
118
+ self._set_active_executor()
119
+
120
+ @property
121
+ def executor_name(self) -> str:
122
+ """Get the name of the current executor"""
123
+ if hasattr(self.active_executor, "name"):
124
+ return self.active_executor.name or "unnamed_executor"
125
+ elif self._executor_type == "function":
126
+ return getattr(self.active_executor, "__name__", "anonymous_function")
127
+ else:
128
+ return f"{self._executor_type}_executor"
129
+
130
+ @property
131
+ def executor_type(self) -> str:
132
+ """Get the type of the current executor"""
133
+ return self._executor_type
134
+
135
+ def _validate_executor_config(self):
136
+ """Validate that only one executor type is provided"""
137
+ executor_count = sum(
138
+ [
139
+ self.agent is not None,
140
+ self.team is not None,
141
+ self.executor is not None,
142
+ ]
143
+ )
144
+
145
+ if executor_count == 0:
146
+ raise ValueError(f"Step '{self.name}' must have one executor: agent=, team=, or executor=")
147
+
148
+ if executor_count > 1:
149
+ provided_executors = []
150
+ if self.agent is not None:
151
+ provided_executors.append("agent")
152
+ if self.team is not None:
153
+ provided_executors.append("team")
154
+ if self.executor is not None:
155
+ provided_executors.append("executor")
156
+
157
+ raise ValueError(
158
+ f"Step '{self.name}' can only have one executor type. "
159
+ f"Provided: {', '.join(provided_executors)}. "
160
+ f"Please use only one of: agent=, team=, or executor="
161
+ )
162
+
163
+ def _set_active_executor(self) -> None:
164
+ """Set the active executor based on what was provided"""
165
+ if self.agent is not None:
166
+ self.active_executor = self.agent # type: ignore[assignment]
167
+ self._executor_type = "agent"
168
+ elif self.team is not None:
169
+ self.active_executor = self.team # type: ignore[assignment]
170
+ self._executor_type = "team"
171
+ elif self.executor is not None:
172
+ self.active_executor = self.executor # type: ignore[assignment]
173
+ self._executor_type = "function"
174
+ else:
175
+ raise ValueError("No executor configured")
176
+
177
+ def _extract_metrics_from_response(self, response: Union[RunOutput, TeamRunOutput]) -> Optional[Metrics]:
178
+ """Extract metrics from agent or team response"""
179
+ if hasattr(response, "metrics") and response.metrics:
180
+ return response.metrics
181
+ return None
182
+
183
+ def _call_custom_function(
184
+ self,
185
+ func: Callable,
186
+ step_input: StepInput,
187
+ session_state: Optional[Dict[str, Any]] = None,
188
+ run_context: Optional[RunContext] = None,
189
+ ) -> Any:
190
+ """Call custom function with session_state support if the function accepts it"""
191
+
192
+ kwargs: Dict[str, Any] = {}
193
+ if run_context is not None and self._function_has_run_context_param():
194
+ kwargs["run_context"] = run_context
195
+ if session_state is not None and self._function_has_session_state_param():
196
+ kwargs["session_state"] = session_state
197
+
198
+ return func(step_input, **kwargs)
199
+
200
+ async def _acall_custom_function(
201
+ self,
202
+ func: Callable,
203
+ step_input: StepInput,
204
+ session_state: Optional[Dict[str, Any]] = None,
205
+ run_context: Optional[RunContext] = None,
206
+ ) -> Any:
207
+ """Call custom async function with session_state support if the function accepts it"""
208
+
209
+ kwargs: Dict[str, Any] = {}
210
+ if run_context is not None and self._function_has_run_context_param():
211
+ kwargs["run_context"] = run_context
212
+ if session_state is not None and self._function_has_session_state_param():
213
+ kwargs["session_state"] = session_state
214
+
215
+ if _is_async_generator_function(func):
216
+ return func(step_input, **kwargs)
217
+ else:
218
+ return await func(step_input, **kwargs)
219
+
220
+ def execute(
221
+ self,
222
+ step_input: StepInput,
223
+ session_id: Optional[str] = None,
224
+ user_id: Optional[str] = None,
225
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
226
+ run_context: Optional[RunContext] = None,
227
+ session_state: Optional[Dict[str, Any]] = None,
228
+ store_executor_outputs: bool = True,
229
+ workflow_session: Optional[WorkflowSession] = None,
230
+ add_workflow_history_to_steps: Optional[bool] = False,
231
+ num_history_runs: int = 3,
232
+ background_tasks: Optional[Any] = None,
233
+ ) -> StepOutput:
234
+ """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
235
+ log_debug(f"Executing step: {self.name}")
236
+
237
+ if step_input.previous_step_outputs:
238
+ step_input.previous_step_content = step_input.get_last_step_content()
239
+
240
+ if workflow_session:
241
+ step_input.workflow_session = workflow_session
242
+
243
+ # Create session_state copy once to avoid duplication.
244
+ # Consider both run_context.session_state and session_state.
245
+ if run_context is not None and run_context.session_state is not None:
246
+ session_state_copy = run_context.session_state
247
+ else:
248
+ session_state_copy = copy(session_state) if session_state is not None else {}
249
+
250
+ # Execute with retries
251
+ for attempt in range(self.max_retries + 1):
252
+ try:
253
+ response: Union[RunOutput, TeamRunOutput, StepOutput]
254
+ if self._executor_type == "function":
255
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
256
+ raise ValueError("Cannot use async function with synchronous execution")
257
+ if _is_generator_function(self.active_executor):
258
+ content = ""
259
+ final_response = None
260
+ try:
261
+ for chunk in self._call_custom_function(
262
+ self.active_executor,
263
+ step_input,
264
+ session_state_copy, # type: ignore[arg-type]
265
+ run_context,
266
+ ): # type: ignore
267
+ if isinstance(chunk, (BaseRunOutputEvent)):
268
+ if (
269
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
270
+ and chunk.content is not None
271
+ ):
272
+ # Its a regular chunk of content
273
+ if isinstance(chunk.content, str):
274
+ content += chunk.content
275
+ # Its the BaseModel object, set it as the content. Replace any previous content.
276
+ # There should be no previous str content at this point
277
+ elif isinstance(chunk.content, BaseModel):
278
+ content = chunk.content # type: ignore[assignment]
279
+ else:
280
+ # Case when parse_response is False and the content is a dict
281
+ content += str(chunk.content)
282
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
283
+ # This is the final response from the agent/team
284
+ content = chunk.content # type: ignore[assignment]
285
+ # If the chunk is a StepOutput, use it as the final response
286
+ elif isinstance(chunk, StepOutput):
287
+ final_response = chunk
288
+ break
289
+ # Non Agent/Team data structure that was yielded
290
+ else:
291
+ content += str(chunk)
292
+
293
+ except StopIteration as e:
294
+ if hasattr(e, "value") and isinstance(e.value, StepOutput):
295
+ final_response = e.value
296
+
297
+ # Merge session_state changes back
298
+ if run_context is None and session_state is not None:
299
+ merge_dictionaries(session_state, session_state_copy)
300
+
301
+ if final_response is not None:
302
+ response = final_response
303
+ else:
304
+ response = StepOutput(content=content)
305
+ else:
306
+ # Execute function with signature inspection for session_state support
307
+ result = self._call_custom_function(
308
+ self.active_executor, # type: ignore[arg-type]
309
+ step_input,
310
+ session_state_copy,
311
+ run_context,
312
+ )
313
+
314
+ # Merge session_state changes back
315
+ if run_context is None and session_state is not None:
316
+ merge_dictionaries(session_state, session_state_copy)
317
+
318
+ # If function returns StepOutput, use it directly
319
+ if isinstance(result, StepOutput):
320
+ response = result
321
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
322
+ response = StepOutput(content=result.content)
323
+ else:
324
+ response = StepOutput(content=str(result))
325
+ else:
326
+ # For agents and teams, prepare message with context
327
+ message = self._prepare_message(
328
+ step_input.input,
329
+ step_input.previous_step_outputs,
330
+ )
331
+
332
+ # Execute agent or team with media
333
+ if self._executor_type in ["agent", "team"]:
334
+ # Switch to appropriate logger based on executor type
335
+ if self._executor_type == "agent":
336
+ use_agent_logger()
337
+ elif self._executor_type == "team":
338
+ use_team_logger()
339
+
340
+ images = (
341
+ self._convert_image_artifacts_to_images(step_input.images) if step_input.images else None
342
+ )
343
+ videos = (
344
+ self._convert_video_artifacts_to_videos(step_input.videos) if step_input.videos else None
345
+ )
346
+ audios = self._convert_audio_artifacts_to_audio(step_input.audio) if step_input.audio else None
347
+
348
+ kwargs: Dict[str, Any] = {}
349
+ if isinstance(self.active_executor, Team):
350
+ kwargs["store_member_responses"] = True
351
+
352
+ # Forward background_tasks if provided
353
+ if background_tasks is not None:
354
+ kwargs["background_tasks"] = background_tasks
355
+
356
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
357
+
358
+ use_history = (
359
+ self.add_workflow_history
360
+ if self.add_workflow_history is not None
361
+ else add_workflow_history_to_steps
362
+ )
363
+
364
+ final_message = message
365
+ if use_history and workflow_session:
366
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
367
+ if history_messages:
368
+ final_message = f"{history_messages}{message}"
369
+
370
+ response = self.active_executor.run( # type: ignore
371
+ input=final_message, # type: ignore
372
+ images=images,
373
+ videos=videos,
374
+ audio=audios,
375
+ files=step_input.files,
376
+ session_id=session_id,
377
+ user_id=user_id,
378
+ session_state=session_state_copy, # Send a copy to the executor
379
+ run_context=run_context,
380
+ **kwargs,
381
+ )
382
+
383
+ # Update workflow session state
384
+ if run_context is None and session_state is not None:
385
+ merge_dictionaries(session_state, session_state_copy)
386
+
387
+ if store_executor_outputs and workflow_run_response is not None:
388
+ self._store_executor_response(workflow_run_response, response) # type: ignore
389
+
390
+ # Switch back to workflow logger after execution
391
+ use_workflow_logger()
392
+ else:
393
+ raise ValueError(f"Unsupported executor type: {self._executor_type}")
394
+
395
+ # Create StepOutput from response
396
+ step_output = self._process_step_output(response) # type: ignore
397
+
398
+ return step_output
399
+
400
+ except Exception as e:
401
+ self.retry_count = attempt + 1
402
+ logger.warning(f"Step {self.name} failed (attempt {attempt + 1}): {e}")
403
+
404
+ if attempt == self.max_retries:
405
+ if self.skip_on_failure:
406
+ log_debug(f"Step {self.name} failed but continuing due to skip_on_failure=True")
407
+ # Create empty StepOutput for skipped step
408
+ return StepOutput(content=f"Step {self.name} failed but skipped", success=False, error=str(e))
409
+ else:
410
+ raise e
411
+
412
+ return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
413
+
414
+ def _function_has_run_context_param(self) -> bool:
415
+ """Check if the custom function has a run_context parameter"""
416
+ if self._executor_type != "function":
417
+ return False
418
+
419
+ try:
420
+ sig = inspect.signature(self.active_executor) # type: ignore
421
+ return "run_context" in sig.parameters
422
+ except Exception:
423
+ return False
424
+
425
+ def _function_has_session_state_param(self) -> bool:
426
+ """Check if the custom function has a session_state parameter"""
427
+ if self._executor_type != "function":
428
+ return False
429
+
430
+ try:
431
+ sig = inspect.signature(self.active_executor) # type: ignore
432
+ return "session_state" in sig.parameters
433
+ except Exception:
434
+ return False
435
+
436
+ def _enrich_event_with_context(
437
+ self,
438
+ event: Any,
439
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
440
+ step_index: Optional[Union[int, tuple]] = None,
441
+ ) -> Any:
442
+ """Enrich event with step and workflow context information"""
443
+ if workflow_run_response is None:
444
+ return event
445
+ if hasattr(event, "workflow_id"):
446
+ event.workflow_id = workflow_run_response.workflow_id
447
+ if hasattr(event, "workflow_run_id"):
448
+ event.workflow_run_id = workflow_run_response.run_id
449
+ if hasattr(event, "step_id"):
450
+ event.step_id = self.step_id
451
+ if hasattr(event, "step_name") and self.name is not None:
452
+ if getattr(event, "step_name", None) is None:
453
+ event.step_name = self.name
454
+ # Only set step_index if it's not already set (preserve parallel.py's tuples)
455
+ if hasattr(event, "step_index") and step_index is not None:
456
+ if event.step_index is None:
457
+ event.step_index = step_index
458
+
459
+ return event
460
+
461
+ def execute_stream(
462
+ self,
463
+ step_input: StepInput,
464
+ session_id: Optional[str] = None,
465
+ user_id: Optional[str] = None,
466
+ stream_events: bool = False,
467
+ stream_intermediate_steps: bool = False,
468
+ stream_executor_events: bool = True,
469
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
470
+ run_context: Optional[RunContext] = None,
471
+ session_state: Optional[Dict[str, Any]] = None,
472
+ step_index: Optional[Union[int, tuple]] = None,
473
+ store_executor_outputs: bool = True,
474
+ parent_step_id: Optional[str] = None,
475
+ workflow_session: Optional["WorkflowSession"] = None,
476
+ add_workflow_history_to_steps: Optional[bool] = False,
477
+ num_history_runs: int = 3,
478
+ background_tasks: Optional[Any] = None,
479
+ ) -> Iterator[Union[WorkflowRunOutputEvent, StepOutput]]:
480
+ """Execute the step with event-driven streaming support"""
481
+
482
+ if step_input.previous_step_outputs:
483
+ step_input.previous_step_content = step_input.get_last_step_content()
484
+
485
+ if workflow_session:
486
+ step_input.workflow_session = workflow_session
487
+
488
+ # Create session_state copy once to avoid duplication.
489
+ # Consider both run_context.session_state and session_state.
490
+ if run_context is not None and run_context.session_state is not None:
491
+ session_state_copy = run_context.session_state
492
+ else:
493
+ session_state_copy = copy(session_state) if session_state is not None else {}
494
+
495
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
496
+ if stream_intermediate_steps is not None:
497
+ warnings.warn(
498
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
499
+ DeprecationWarning,
500
+ stacklevel=2,
501
+ )
502
+ stream_events = stream_events or stream_intermediate_steps
503
+
504
+ # Emit StepStartedEvent
505
+ if stream_events and workflow_run_response:
506
+ yield StepStartedEvent(
507
+ run_id=workflow_run_response.run_id or "",
508
+ workflow_name=workflow_run_response.workflow_name or "",
509
+ workflow_id=workflow_run_response.workflow_id or "",
510
+ session_id=workflow_run_response.session_id or "",
511
+ step_name=self.name,
512
+ step_index=step_index,
513
+ step_id=self.step_id,
514
+ parent_step_id=parent_step_id,
515
+ )
516
+
517
+ # Execute with retries and streaming
518
+ for attempt in range(self.max_retries + 1):
519
+ try:
520
+ log_debug(f"Step {self.name} streaming attempt {attempt + 1}/{self.max_retries + 1}")
521
+ final_response = None
522
+
523
+ if self._executor_type == "function":
524
+ log_debug(f"Executing function executor for step: {self.name}")
525
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
526
+ raise ValueError("Cannot use async function with synchronous execution")
527
+ if _is_generator_function(self.active_executor):
528
+ log_debug("Function returned iterable, streaming events")
529
+ content = ""
530
+ try:
531
+ iterator = self._call_custom_function(
532
+ self.active_executor,
533
+ step_input,
534
+ session_state_copy,
535
+ run_context,
536
+ )
537
+ for event in iterator: # type: ignore
538
+ if isinstance(event, (BaseRunOutputEvent)):
539
+ if (
540
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
541
+ and event.content is not None
542
+ ):
543
+ if isinstance(event.content, str):
544
+ content += event.content
545
+ elif isinstance(event.content, BaseModel):
546
+ content = event.content # type: ignore[assignment]
547
+ else:
548
+ content = str(event.content)
549
+ # Only yield executor events if stream_executor_events is True
550
+ if stream_executor_events:
551
+ enriched_event = self._enrich_event_with_context(
552
+ event, workflow_run_response, step_index
553
+ )
554
+ yield enriched_event # type: ignore[misc]
555
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
556
+ content = event.content # type: ignore[assignment]
557
+ elif isinstance(event, StepOutput):
558
+ final_response = event
559
+ break
560
+ else:
561
+ content += str(event)
562
+
563
+ # Merge session_state changes back
564
+ if run_context is None and session_state is not None:
565
+ merge_dictionaries(session_state, session_state_copy)
566
+
567
+ if not final_response:
568
+ final_response = StepOutput(content=content)
569
+ except StopIteration as e:
570
+ if hasattr(e, "value") and isinstance(e.value, StepOutput):
571
+ final_response = e.value
572
+
573
+ else:
574
+ result = self._call_custom_function(
575
+ self.active_executor, # type: ignore[arg-type]
576
+ step_input,
577
+ session_state_copy,
578
+ run_context,
579
+ )
580
+
581
+ # Merge session_state changes back
582
+ if run_context is None and session_state is not None:
583
+ merge_dictionaries(session_state, session_state_copy)
584
+
585
+ if isinstance(result, StepOutput):
586
+ final_response = result
587
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
588
+ final_response = StepOutput(content=result.content)
589
+ else:
590
+ final_response = StepOutput(content=str(result))
591
+ log_debug("Function returned non-iterable, created StepOutput")
592
+ else:
593
+ # For agents and teams, prepare message with context
594
+ message = self._prepare_message(
595
+ step_input.input,
596
+ step_input.previous_step_outputs,
597
+ )
598
+
599
+ if self._executor_type in ["agent", "team"]:
600
+ # Switch to appropriate logger based on executor type
601
+ if self._executor_type == "agent":
602
+ use_agent_logger()
603
+ elif self._executor_type == "team":
604
+ use_team_logger()
605
+
606
+ images = (
607
+ self._convert_image_artifacts_to_images(step_input.images) if step_input.images else None
608
+ )
609
+ videos = (
610
+ self._convert_video_artifacts_to_videos(step_input.videos) if step_input.videos else None
611
+ )
612
+ audios = self._convert_audio_artifacts_to_audio(step_input.audio) if step_input.audio else None
613
+
614
+ kwargs: Dict[str, Any] = {}
615
+ if isinstance(self.active_executor, Team):
616
+ kwargs["store_member_responses"] = True
617
+
618
+ # Forward background_tasks if provided
619
+ if background_tasks is not None:
620
+ kwargs["background_tasks"] = background_tasks
621
+
622
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
623
+
624
+ use_history = (
625
+ self.add_workflow_history
626
+ if self.add_workflow_history is not None
627
+ else add_workflow_history_to_steps
628
+ )
629
+
630
+ final_message = message
631
+ if use_history and workflow_session:
632
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
633
+ if history_messages:
634
+ final_message = f"{history_messages}{message}"
635
+
636
+ response_stream = self.active_executor.run( # type: ignore[call-overload, misc]
637
+ input=final_message,
638
+ images=images,
639
+ videos=videos,
640
+ audio=audios,
641
+ files=step_input.files,
642
+ session_id=session_id,
643
+ user_id=user_id,
644
+ session_state=session_state_copy, # Send a copy to the executor
645
+ stream=True,
646
+ stream_events=stream_events,
647
+ yield_run_output=True,
648
+ run_context=run_context,
649
+ **kwargs,
650
+ )
651
+
652
+ active_executor_run_response = None
653
+ for event in response_stream:
654
+ if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
655
+ active_executor_run_response = event
656
+ continue
657
+ # Only yield executor events if stream_executor_events is True
658
+ if stream_executor_events:
659
+ enriched_event = self._enrich_event_with_context(
660
+ event, workflow_run_response, step_index
661
+ )
662
+ yield enriched_event # type: ignore[misc]
663
+
664
+ # Update workflow session state
665
+ if run_context is None and session_state is not None:
666
+ merge_dictionaries(session_state, session_state_copy)
667
+
668
+ if store_executor_outputs and workflow_run_response is not None:
669
+ self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
670
+
671
+ final_response = active_executor_run_response # type: ignore
672
+
673
+ else:
674
+ raise ValueError(f"Unsupported executor type: {self._executor_type}")
675
+
676
+ # If we didn't get a final response, create one
677
+ if final_response is None:
678
+ final_response = StepOutput(content="")
679
+ log_debug("Created empty StepOutput as fallback")
680
+
681
+ # Switch back to workflow logger after execution
682
+ use_workflow_logger()
683
+
684
+ # Yield the step output
685
+ final_response = self._process_step_output(final_response)
686
+ yield final_response
687
+
688
+ # Emit StepCompletedEvent
689
+ if stream_events and workflow_run_response:
690
+ yield StepCompletedEvent(
691
+ run_id=workflow_run_response.run_id or "",
692
+ workflow_name=workflow_run_response.workflow_name or "",
693
+ workflow_id=workflow_run_response.workflow_id or "",
694
+ session_id=workflow_run_response.session_id or "",
695
+ step_name=self.name,
696
+ step_index=step_index,
697
+ content=final_response.content,
698
+ step_response=final_response,
699
+ parent_step_id=parent_step_id,
700
+ )
701
+
702
+ return
703
+ except Exception as e:
704
+ self.retry_count = attempt + 1
705
+ logger.warning(f"Step {self.name} failed (attempt {attempt + 1}): {e}")
706
+
707
+ if attempt == self.max_retries:
708
+ if self.skip_on_failure:
709
+ log_debug(f"Step {self.name} failed but continuing due to skip_on_failure=True")
710
+ # Create empty StepOutput for skipped step
711
+ step_output = StepOutput(
712
+ content=f"Step {self.name} failed but skipped", success=False, error=str(e)
713
+ )
714
+ yield step_output
715
+ return
716
+ else:
717
+ raise e
718
+
719
+ return
720
+
721
+ async def aexecute(
722
+ self,
723
+ step_input: StepInput,
724
+ session_id: Optional[str] = None,
725
+ user_id: Optional[str] = None,
726
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
727
+ run_context: Optional[RunContext] = None,
728
+ session_state: Optional[Dict[str, Any]] = None,
729
+ store_executor_outputs: bool = True,
730
+ workflow_session: Optional["WorkflowSession"] = None,
731
+ add_workflow_history_to_steps: Optional[bool] = False,
732
+ num_history_runs: int = 3,
733
+ background_tasks: Optional[Any] = None,
734
+ ) -> StepOutput:
735
+ """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
736
+ logger.info(f"Executing async step (non-streaming): {self.name}")
737
+ log_debug(f"Executor type: {self._executor_type}")
738
+
739
+ if step_input.previous_step_outputs:
740
+ step_input.previous_step_content = step_input.get_last_step_content()
741
+
742
+ if workflow_session:
743
+ step_input.workflow_session = workflow_session
744
+
745
+ # Create session_state copy once to avoid duplication.
746
+ # Consider both run_context.session_state and session_state.
747
+ if run_context is not None and run_context.session_state is not None:
748
+ session_state_copy = run_context.session_state
749
+ else:
750
+ session_state_copy = copy(session_state) if session_state is not None else {}
751
+
752
+ # Execute with retries
753
+ for attempt in range(self.max_retries + 1):
754
+ try:
755
+ if self._executor_type == "function":
756
+ if _is_generator_function(self.active_executor) or _is_async_generator_function(
757
+ self.active_executor
758
+ ):
759
+ content = ""
760
+ final_response = None
761
+ try:
762
+ if _is_generator_function(self.active_executor):
763
+ iterator = self._call_custom_function(
764
+ self.active_executor,
765
+ step_input,
766
+ session_state_copy,
767
+ run_context,
768
+ )
769
+ for chunk in iterator: # type: ignore
770
+ if isinstance(chunk, (BaseRunOutputEvent)):
771
+ if (
772
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
773
+ and chunk.content is not None
774
+ ):
775
+ if isinstance(chunk.content, str):
776
+ content += chunk.content
777
+ elif isinstance(chunk.content, BaseModel):
778
+ content = chunk.content # type: ignore[assignment]
779
+ else:
780
+ content = str(chunk.content)
781
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
782
+ content = chunk.content # type: ignore[assignment]
783
+ elif isinstance(chunk, StepOutput):
784
+ final_response = chunk
785
+ break
786
+ else:
787
+ content += str(chunk)
788
+
789
+ else:
790
+ if _is_async_generator_function(self.active_executor):
791
+ iterator = await self._acall_custom_function(
792
+ self.active_executor,
793
+ step_input,
794
+ session_state_copy,
795
+ run_context,
796
+ )
797
+ async for chunk in iterator: # type: ignore
798
+ if isinstance(chunk, (BaseRunOutputEvent)):
799
+ if (
800
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
801
+ and chunk.content is not None
802
+ ):
803
+ if isinstance(chunk.content, str):
804
+ content += chunk.content
805
+ elif isinstance(chunk.content, BaseModel):
806
+ content = chunk.content # type: ignore[assignment]
807
+ else:
808
+ content = str(chunk.content)
809
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
810
+ content = chunk.content # type: ignore[assignment]
811
+ elif isinstance(chunk, StepOutput):
812
+ final_response = chunk
813
+ break
814
+ else:
815
+ content += str(chunk)
816
+
817
+ except StopIteration as e:
818
+ if hasattr(e, "value") and isinstance(e.value, StepOutput):
819
+ final_response = e.value
820
+
821
+ # Merge session_state changes back
822
+ if run_context is None and session_state is not None:
823
+ merge_dictionaries(session_state, session_state_copy)
824
+
825
+ if final_response is not None:
826
+ response = final_response
827
+ else:
828
+ response = StepOutput(content=content)
829
+ else:
830
+ if _is_async_callable(self.active_executor):
831
+ result = await self._acall_custom_function(
832
+ self.active_executor,
833
+ step_input,
834
+ session_state_copy,
835
+ run_context,
836
+ )
837
+ else:
838
+ result = self._call_custom_function(
839
+ self.active_executor, # type: ignore[arg-type]
840
+ step_input,
841
+ session_state_copy,
842
+ run_context,
843
+ )
844
+
845
+ # Merge session_state changes back
846
+ if run_context is None and session_state is not None:
847
+ merge_dictionaries(session_state, session_state_copy)
848
+
849
+ # If function returns StepOutput, use it directly
850
+ if isinstance(result, StepOutput):
851
+ response = result
852
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
853
+ response = StepOutput(content=result.content)
854
+ else:
855
+ response = StepOutput(content=str(result))
856
+
857
+ else:
858
+ # For agents and teams, prepare message with context
859
+ message = self._prepare_message(
860
+ step_input.input,
861
+ step_input.previous_step_outputs,
862
+ )
863
+
864
+ # Execute agent or team with media
865
+ if self._executor_type in ["agent", "team"]:
866
+ # Switch to appropriate logger based on executor type
867
+ if self._executor_type == "agent":
868
+ use_agent_logger()
869
+ elif self._executor_type == "team":
870
+ use_team_logger()
871
+
872
+ images = (
873
+ self._convert_image_artifacts_to_images(step_input.images) if step_input.images else None
874
+ )
875
+ videos = (
876
+ self._convert_video_artifacts_to_videos(step_input.videos) if step_input.videos else None
877
+ )
878
+ audios = self._convert_audio_artifacts_to_audio(step_input.audio) if step_input.audio else None
879
+
880
+ kwargs: Dict[str, Any] = {}
881
+ if isinstance(self.active_executor, Team):
882
+ kwargs["store_member_responses"] = True
883
+
884
+ # Forward background_tasks if provided
885
+ if background_tasks is not None:
886
+ kwargs["background_tasks"] = background_tasks
887
+
888
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
889
+
890
+ use_history = (
891
+ self.add_workflow_history
892
+ if self.add_workflow_history is not None
893
+ else add_workflow_history_to_steps
894
+ )
895
+
896
+ final_message = message
897
+ if use_history and workflow_session:
898
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
899
+ if history_messages:
900
+ final_message = f"{history_messages}{message}"
901
+
902
+ response = await self.active_executor.arun( # type: ignore
903
+ input=final_message, # type: ignore
904
+ images=images,
905
+ videos=videos,
906
+ audio=audios,
907
+ files=step_input.files,
908
+ session_id=session_id,
909
+ user_id=user_id,
910
+ session_state=session_state_copy,
911
+ run_context=run_context,
912
+ **kwargs,
913
+ )
914
+
915
+ # Update workflow session state
916
+ if run_context is None and session_state is not None:
917
+ merge_dictionaries(session_state, session_state_copy)
918
+
919
+ if store_executor_outputs and workflow_run_response is not None:
920
+ self._store_executor_response(workflow_run_response, response) # type: ignore
921
+
922
+ # Switch back to workflow logger after execution
923
+ use_workflow_logger()
924
+ else:
925
+ raise ValueError(f"Unsupported executor type: {self._executor_type}")
926
+
927
+ # Create StepOutput from response
928
+ step_output = self._process_step_output(response) # type: ignore
929
+
930
+ return step_output
931
+
932
+ except Exception as e:
933
+ self.retry_count = attempt + 1
934
+ logger.warning(f"Step {self.name} failed (attempt {attempt + 1}): {e}")
935
+
936
+ if attempt == self.max_retries:
937
+ if self.skip_on_failure:
938
+ log_debug(f"Step {self.name} failed but continuing due to skip_on_failure=True")
939
+ # Create empty StepOutput for skipped step
940
+ return StepOutput(content=f"Step {self.name} failed but skipped", success=False, error=str(e))
941
+ else:
942
+ raise e
943
+
944
+ return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
945
+
946
+ async def aexecute_stream(
947
+ self,
948
+ step_input: StepInput,
949
+ session_id: Optional[str] = None,
950
+ user_id: Optional[str] = None,
951
+ stream_events: bool = False,
952
+ stream_intermediate_steps: bool = False,
953
+ stream_executor_events: bool = True,
954
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
955
+ run_context: Optional[RunContext] = None,
956
+ session_state: Optional[Dict[str, Any]] = None,
957
+ step_index: Optional[Union[int, tuple]] = None,
958
+ store_executor_outputs: bool = True,
959
+ parent_step_id: Optional[str] = None,
960
+ workflow_session: Optional["WorkflowSession"] = None,
961
+ add_workflow_history_to_steps: Optional[bool] = False,
962
+ num_history_runs: int = 3,
963
+ background_tasks: Optional[Any] = None,
964
+ ) -> AsyncIterator[Union[WorkflowRunOutputEvent, StepOutput]]:
965
+ """Execute the step with event-driven streaming support"""
966
+
967
+ if step_input.previous_step_outputs:
968
+ step_input.previous_step_content = step_input.get_last_step_content()
969
+
970
+ if workflow_session:
971
+ step_input.workflow_session = workflow_session
972
+
973
+ # Create session_state copy once to avoid duplication.
974
+ # Consider both run_context.session_state and session_state.
975
+ if run_context is not None and run_context.session_state is not None:
976
+ session_state_copy = run_context.session_state
977
+ else:
978
+ session_state_copy = copy(session_state) if session_state is not None else {}
979
+
980
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
981
+ if stream_intermediate_steps is not None:
982
+ warnings.warn(
983
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
984
+ DeprecationWarning,
985
+ stacklevel=2,
986
+ )
987
+ stream_events = stream_events or stream_intermediate_steps
988
+
989
+ if stream_events and workflow_run_response:
990
+ # Emit StepStartedEvent
991
+ yield StepStartedEvent(
992
+ run_id=workflow_run_response.run_id or "",
993
+ workflow_name=workflow_run_response.workflow_name or "",
994
+ workflow_id=workflow_run_response.workflow_id or "",
995
+ session_id=workflow_run_response.session_id or "",
996
+ step_name=self.name,
997
+ step_index=step_index,
998
+ step_id=self.step_id,
999
+ parent_step_id=parent_step_id,
1000
+ )
1001
+
1002
+ # Execute with retries and streaming
1003
+ for attempt in range(self.max_retries + 1):
1004
+ try:
1005
+ log_debug(f"Async step {self.name} streaming attempt {attempt + 1}/{self.max_retries + 1}")
1006
+ final_response = None
1007
+
1008
+ if self._executor_type == "function":
1009
+ log_debug(f"Executing async function executor for step: {self.name}")
1010
+
1011
+ # Check if the function is an async generator
1012
+ if _is_async_generator_function(self.active_executor):
1013
+ content = ""
1014
+ # It's an async generator - iterate over it
1015
+ iterator = await self._acall_custom_function(
1016
+ self.active_executor,
1017
+ step_input,
1018
+ session_state_copy,
1019
+ run_context,
1020
+ )
1021
+ async for event in iterator: # type: ignore
1022
+ if isinstance(event, (BaseRunOutputEvent)):
1023
+ if (
1024
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1025
+ and event.content is not None
1026
+ ):
1027
+ if isinstance(event.content, str):
1028
+ content += event.content
1029
+ elif isinstance(event.content, BaseModel):
1030
+ content = event.content # type: ignore[assignment]
1031
+ else:
1032
+ content = str(event.content)
1033
+
1034
+ # Only yield executor events if stream_executor_events is True
1035
+ if stream_executor_events:
1036
+ enriched_event = self._enrich_event_with_context(
1037
+ event, workflow_run_response, step_index
1038
+ )
1039
+ yield enriched_event # type: ignore[misc]
1040
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1041
+ content = event.content # type: ignore[assignment]
1042
+ elif isinstance(event, StepOutput):
1043
+ final_response = event
1044
+ break
1045
+ else:
1046
+ content += str(event)
1047
+ if not final_response:
1048
+ final_response = StepOutput(content=content)
1049
+ elif _is_async_callable(self.active_executor):
1050
+ # It's a regular async function - await it
1051
+ result = await self._acall_custom_function(
1052
+ self.active_executor,
1053
+ step_input,
1054
+ session_state_copy,
1055
+ run_context,
1056
+ )
1057
+ if isinstance(result, StepOutput):
1058
+ final_response = result
1059
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1060
+ final_response = StepOutput(content=result.content)
1061
+ else:
1062
+ final_response = StepOutput(content=str(result))
1063
+ elif _is_generator_function(self.active_executor):
1064
+ content = ""
1065
+ # It's a regular generator function - iterate over it
1066
+ iterator = self._call_custom_function(
1067
+ self.active_executor,
1068
+ step_input,
1069
+ session_state_copy,
1070
+ run_context,
1071
+ )
1072
+ for event in iterator: # type: ignore
1073
+ if isinstance(event, (BaseRunOutputEvent)):
1074
+ if (
1075
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1076
+ and event.content is not None
1077
+ ):
1078
+ if isinstance(event.content, str):
1079
+ content += event.content
1080
+ elif isinstance(event.content, BaseModel):
1081
+ content = event.content # type: ignore[assignment]
1082
+ else:
1083
+ content = str(event.content)
1084
+
1085
+ # Only yield executor events if stream_executor_events is True
1086
+ if stream_executor_events:
1087
+ enriched_event = self._enrich_event_with_context(
1088
+ event, workflow_run_response, step_index
1089
+ )
1090
+ yield enriched_event # type: ignore[misc]
1091
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1092
+ content = event.content # type: ignore[assignment]
1093
+ elif isinstance(event, StepOutput):
1094
+ final_response = event
1095
+ break
1096
+ else:
1097
+ if isinstance(content, str):
1098
+ content += str(event)
1099
+ else:
1100
+ content = str(event)
1101
+ if not final_response:
1102
+ final_response = StepOutput(content=content)
1103
+ else:
1104
+ # It's a regular function - call it directly
1105
+ result = self._call_custom_function(
1106
+ self.active_executor, # type: ignore[arg-type]
1107
+ step_input,
1108
+ session_state_copy,
1109
+ run_context,
1110
+ )
1111
+ if isinstance(result, StepOutput):
1112
+ final_response = result
1113
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1114
+ final_response = StepOutput(content=result.content)
1115
+ else:
1116
+ final_response = StepOutput(content=str(result))
1117
+
1118
+ # Merge session_state changes back
1119
+ if run_context is None and session_state is not None:
1120
+ merge_dictionaries(session_state, session_state_copy)
1121
+ else:
1122
+ # For agents and teams, prepare message with context
1123
+ message = self._prepare_message(
1124
+ step_input.input,
1125
+ step_input.previous_step_outputs,
1126
+ )
1127
+
1128
+ if self._executor_type in ["agent", "team"]:
1129
+ # Switch to appropriate logger based on executor type
1130
+ if self._executor_type == "agent":
1131
+ use_agent_logger()
1132
+ elif self._executor_type == "team":
1133
+ use_team_logger()
1134
+
1135
+ images = (
1136
+ self._convert_image_artifacts_to_images(step_input.images) if step_input.images else None
1137
+ )
1138
+ videos = (
1139
+ self._convert_video_artifacts_to_videos(step_input.videos) if step_input.videos else None
1140
+ )
1141
+ audios = self._convert_audio_artifacts_to_audio(step_input.audio) if step_input.audio else None
1142
+
1143
+ kwargs: Dict[str, Any] = {}
1144
+ if isinstance(self.active_executor, Team):
1145
+ kwargs["store_member_responses"] = True
1146
+
1147
+ # Forward background_tasks if provided
1148
+ if background_tasks is not None:
1149
+ kwargs["background_tasks"] = background_tasks
1150
+
1151
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
1152
+
1153
+ use_history = (
1154
+ self.add_workflow_history
1155
+ if self.add_workflow_history is not None
1156
+ else add_workflow_history_to_steps
1157
+ )
1158
+
1159
+ final_message = message
1160
+ if use_history and workflow_session:
1161
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
1162
+ if history_messages:
1163
+ final_message = f"{history_messages}{message}"
1164
+
1165
+ response_stream = self.active_executor.arun( # type: ignore
1166
+ input=final_message,
1167
+ images=images,
1168
+ videos=videos,
1169
+ audio=audios,
1170
+ files=step_input.files,
1171
+ session_id=session_id,
1172
+ user_id=user_id,
1173
+ session_state=session_state_copy,
1174
+ stream=True,
1175
+ stream_events=stream_events,
1176
+ run_context=run_context,
1177
+ yield_run_output=True,
1178
+ **kwargs,
1179
+ )
1180
+
1181
+ active_executor_run_response = None
1182
+ async for event in response_stream:
1183
+ if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
1184
+ active_executor_run_response = event
1185
+ break
1186
+ # Only yield executor events if stream_executor_events is True
1187
+ if stream_executor_events:
1188
+ enriched_event = self._enrich_event_with_context(
1189
+ event, workflow_run_response, step_index
1190
+ )
1191
+ yield enriched_event # type: ignore[misc]
1192
+
1193
+ # Update workflow session state
1194
+ if run_context is None and session_state is not None:
1195
+ merge_dictionaries(session_state, session_state_copy)
1196
+
1197
+ if store_executor_outputs and workflow_run_response is not None:
1198
+ self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
1199
+
1200
+ final_response = active_executor_run_response # type: ignore
1201
+ else:
1202
+ raise ValueError(f"Unsupported executor type: {self._executor_type}")
1203
+
1204
+ # If we didn't get a final response, create one
1205
+ if final_response is None:
1206
+ final_response = StepOutput(content="")
1207
+
1208
+ # Switch back to workflow logger after execution
1209
+ use_workflow_logger()
1210
+
1211
+ # Yield the final response
1212
+ final_response = self._process_step_output(final_response)
1213
+ yield final_response
1214
+
1215
+ if stream_events and workflow_run_response:
1216
+ # Emit StepCompletedEvent
1217
+ yield StepCompletedEvent(
1218
+ run_id=workflow_run_response.run_id or "",
1219
+ workflow_name=workflow_run_response.workflow_name or "",
1220
+ workflow_id=workflow_run_response.workflow_id or "",
1221
+ session_id=workflow_run_response.session_id or "",
1222
+ step_name=self.name,
1223
+ step_index=step_index,
1224
+ step_id=self.step_id,
1225
+ content=final_response.content,
1226
+ step_response=final_response,
1227
+ parent_step_id=parent_step_id,
1228
+ )
1229
+ return
1230
+
1231
+ except Exception as e:
1232
+ self.retry_count = attempt + 1
1233
+ logger.warning(f"Step {self.name} failed (attempt {attempt + 1}): {e}")
1234
+
1235
+ if attempt == self.max_retries:
1236
+ if self.skip_on_failure:
1237
+ log_debug(f"Step {self.name} failed but continuing due to skip_on_failure=True")
1238
+ # Create empty StepOutput for skipped step
1239
+ step_output = StepOutput(
1240
+ content=f"Step {self.name} failed but skipped", success=False, error=str(e)
1241
+ )
1242
+ yield step_output
1243
+ else:
1244
+ raise e
1245
+
1246
+ return
1247
+
1248
+ def get_chat_history(self, session_id: str, last_n_runs: Optional[int] = None) -> List[Message]:
1249
+ """Return the step's Agent or Team chat history for the given session.
1250
+
1251
+ Args:
1252
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1253
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1254
+
1255
+ Returns:
1256
+ List[Message]: The step's Agent or Team chat history for the given session.
1257
+ """
1258
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1259
+
1260
+ if self.agent:
1261
+ session = self.agent.get_session(session_id=session_id)
1262
+ if not session:
1263
+ log_warning("Session not found")
1264
+ return []
1265
+
1266
+ if not isinstance(session, WorkflowSession):
1267
+ raise ValueError("The provided session is not a WorkflowSession")
1268
+
1269
+ session = cast(WorkflowSession, session)
1270
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1271
+
1272
+ elif self.team:
1273
+ session = self.team.get_session(session_id=session_id)
1274
+ if not session:
1275
+ log_warning("Session not found")
1276
+ return []
1277
+
1278
+ if not isinstance(session, WorkflowSession):
1279
+ raise ValueError("The provided session is not a WorkflowSession")
1280
+
1281
+ session = cast(WorkflowSession, session)
1282
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1283
+
1284
+ return []
1285
+
1286
+ async def aget_chat_history(
1287
+ self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
1288
+ ) -> List[Message]:
1289
+ """Return the step's Agent or Team chat history for the given session.
1290
+
1291
+ Args:
1292
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1293
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1294
+
1295
+ Returns:
1296
+ List[Message]: The step's Agent or Team chat history for the given session.
1297
+ """
1298
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1299
+
1300
+ if self.agent:
1301
+ session = await self.agent.aget_session(session_id=session_id)
1302
+ if not session:
1303
+ log_warning("Session not found")
1304
+ return []
1305
+
1306
+ if not isinstance(session, WorkflowSession):
1307
+ raise ValueError("The provided session is not a WorkflowSession")
1308
+
1309
+ session = cast(WorkflowSession, session)
1310
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1311
+
1312
+ elif self.team:
1313
+ session = await self.team.aget_session(session_id=session_id)
1314
+ if not session:
1315
+ log_warning("Session not found")
1316
+ return []
1317
+
1318
+ if not isinstance(session, WorkflowSession):
1319
+ raise ValueError("The provided session is not a WorkflowSession")
1320
+
1321
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1322
+
1323
+ return []
1324
+
1325
+ def _store_executor_response(
1326
+ self, workflow_run_response: "WorkflowRunOutput", executor_run_response: Union[RunOutput, TeamRunOutput]
1327
+ ) -> None:
1328
+ """Store agent/team responses in step_executor_runs if enabled"""
1329
+ if self._executor_type in ["agent", "team"]:
1330
+ # propogate the workflow run id as parent run id to the executor response
1331
+ executor_run_response.parent_run_id = workflow_run_response.run_id
1332
+ executor_run_response.workflow_step_id = self.step_id
1333
+
1334
+ # Scrub the executor response based on the executor's storage flags before storing
1335
+ if (
1336
+ not self.active_executor.store_media
1337
+ or not self.active_executor.store_tool_messages
1338
+ or not self.active_executor.store_history_messages
1339
+ ): # type: ignore
1340
+ self.active_executor._scrub_run_output_for_storage(executor_run_response) # type: ignore
1341
+
1342
+ # Get the raw response from the step's active executor
1343
+ raw_response = executor_run_response
1344
+ if raw_response and isinstance(raw_response, (RunOutput, TeamRunOutput)):
1345
+ if workflow_run_response.step_executor_runs is None:
1346
+ workflow_run_response.step_executor_runs = []
1347
+
1348
+ raw_response.workflow_step_id = self.step_id
1349
+ # Add the primary executor run
1350
+ workflow_run_response.step_executor_runs.append(raw_response)
1351
+
1352
+ # Add direct member agent runs (in case of a team we force store_member_responses=True here)
1353
+ if isinstance(raw_response, TeamRunOutput) and getattr(
1354
+ self.active_executor, "store_member_responses", False
1355
+ ):
1356
+ for mr in raw_response.member_responses or []:
1357
+ if isinstance(mr, RunOutput):
1358
+ workflow_run_response.step_executor_runs.append(mr)
1359
+
1360
+ def _get_deepest_content_from_step_output(self, step_output: "StepOutput") -> Optional[str]:
1361
+ """
1362
+ Extract the deepest content from a step output, handling nested structures like Steps, Router, Loop, etc.
1363
+
1364
+ For container steps (Steps, Router, Loop, etc.), this will recursively find the content from the
1365
+ last actual step rather than using the generic container message.
1366
+
1367
+ For Parallel steps, aggregates content from ALL inner steps (not just the last one).
1368
+ """
1369
+ # If this step has nested steps (like Steps, Condition, Router, Loop, Parallel, etc.)
1370
+ if hasattr(step_output, "steps") and step_output.steps and len(step_output.steps) > 0:
1371
+ # For Parallel steps, aggregate content from ALL inner steps
1372
+ if step_output.step_type == StepType.PARALLEL:
1373
+ aggregated_parts = []
1374
+ for i, inner_step in enumerate(step_output.steps):
1375
+ inner_content = self._get_deepest_content_from_step_output(inner_step)
1376
+ if inner_content:
1377
+ step_name = inner_step.step_name or f"Step {i + 1}"
1378
+ aggregated_parts.append(f"=== {step_name} ===\n{inner_content}")
1379
+ return "\n\n".join(aggregated_parts) if aggregated_parts else step_output.content # type: ignore
1380
+
1381
+ # For other nested step types, recursively get content from the last nested step
1382
+ return self._get_deepest_content_from_step_output(step_output.steps[-1])
1383
+
1384
+ # For regular steps, return their content
1385
+ return step_output.content # type: ignore
1386
+
1387
+ def _prepare_message(
1388
+ self,
1389
+ message: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]],
1390
+ previous_step_outputs: Optional[Dict[str, StepOutput]] = None,
1391
+ ) -> Optional[Union[str, List[Any], Dict[str, Any], BaseModel]]:
1392
+ """Prepare the primary input by combining message and previous step outputs"""
1393
+
1394
+ if previous_step_outputs and self._executor_type in ["agent", "team"]:
1395
+ last_output = list(previous_step_outputs.values())[-1] if previous_step_outputs else None
1396
+ if last_output:
1397
+ deepest_content = self._get_deepest_content_from_step_output(last_output)
1398
+ if deepest_content:
1399
+ return deepest_content
1400
+
1401
+ # If no previous step outputs, return the original message unchanged
1402
+ return message
1403
+
1404
+ def _process_step_output(self, response: Union[RunOutput, TeamRunOutput, StepOutput]) -> StepOutput:
1405
+ """Create StepOutput from execution response"""
1406
+ if isinstance(response, StepOutput):
1407
+ response.step_name = self.name or "unnamed_step"
1408
+ response.step_id = self.step_id
1409
+ response.step_type = StepType.STEP
1410
+ response.executor_type = self._executor_type
1411
+ response.executor_name = self.executor_name
1412
+ return response
1413
+
1414
+ # Extract media from response
1415
+ images = getattr(response, "images", None)
1416
+ videos = getattr(response, "videos", None)
1417
+ audio = getattr(response, "audio", None)
1418
+
1419
+ # Extract metrics from response
1420
+ metrics = self._extract_metrics_from_response(response)
1421
+
1422
+ return StepOutput(
1423
+ step_name=self.name or "unnamed_step",
1424
+ step_id=self.step_id,
1425
+ step_type=StepType.STEP,
1426
+ executor_type=self._executor_type,
1427
+ executor_name=self.executor_name,
1428
+ content=response.content,
1429
+ step_run_id=getattr(response, "run_id", None),
1430
+ images=images,
1431
+ videos=videos,
1432
+ audio=audio,
1433
+ metrics=metrics,
1434
+ )
1435
+
1436
+ def _convert_function_result_to_response(self, result: Any) -> RunOutput:
1437
+ """Convert function execution result to RunOutput"""
1438
+ if isinstance(result, RunOutput):
1439
+ return result
1440
+ elif isinstance(result, str):
1441
+ return RunOutput(content=result)
1442
+ elif isinstance(result, dict):
1443
+ # If it's a dict, try to extract content
1444
+ content = result.get("content", str(result))
1445
+ return RunOutput(content=content)
1446
+ else:
1447
+ # Convert any other type to string
1448
+ return RunOutput(content=str(result))
1449
+
1450
+ def _convert_audio_artifacts_to_audio(self, audio_artifacts: List[Audio]) -> List[Audio]:
1451
+ """Convert AudioArtifact objects to Audio objects"""
1452
+ audios = []
1453
+ for audio_artifact in audio_artifacts:
1454
+ if audio_artifact.url:
1455
+ audios.append(Audio(url=audio_artifact.url))
1456
+ elif audio_artifact.content:
1457
+ audios.append(Audio(content=audio_artifact.content))
1458
+ else:
1459
+ logger.warning(f"Skipping AudioArtifact with no URL or content: {audio_artifact}")
1460
+ continue
1461
+ return audios
1462
+
1463
+ def _convert_image_artifacts_to_images(self, image_artifacts: List[Image]) -> List[Image]:
1464
+ """
1465
+ Convert ImageArtifact objects to Image objects with proper content handling.
1466
+
1467
+ Args:
1468
+ image_artifacts: List of ImageArtifact objects to convert
1469
+
1470
+ Returns:
1471
+ List of Image objects ready for agent processing
1472
+ """
1473
+ import base64
1474
+
1475
+ images = []
1476
+ for i, img_artifact in enumerate(image_artifacts):
1477
+ # Create Image object with proper data from ImageArtifact
1478
+ if img_artifact.url:
1479
+ images.append(Image(url=img_artifact.url))
1480
+
1481
+ elif img_artifact.content:
1482
+ # Handle the case where content is base64-encoded bytes from OpenAI tools
1483
+ try:
1484
+ # Try to decode as base64 first (for images from OpenAI tools)
1485
+ if isinstance(img_artifact.content, bytes):
1486
+ # Decode bytes to string, then decode base64 to get actual image bytes
1487
+ base64_str: str = img_artifact.content.decode("utf-8")
1488
+ actual_image_bytes = base64.b64decode(base64_str)
1489
+ else:
1490
+ # If it's already actual image bytes
1491
+ actual_image_bytes = img_artifact.content
1492
+
1493
+ # Create Image object with proper format
1494
+ image_kwargs = {"content": actual_image_bytes}
1495
+ if img_artifact.mime_type:
1496
+ # Convert mime_type to format (e.g., "image/png" -> "png")
1497
+ if "/" in img_artifact.mime_type:
1498
+ format_from_mime = img_artifact.mime_type.split("/")[-1]
1499
+ image_kwargs["format"] = format_from_mime # type: ignore[assignment]
1500
+
1501
+ images.append(Image(**image_kwargs))
1502
+
1503
+ except Exception as e:
1504
+ logger.error(f"Failed to process image content: {e}")
1505
+ # Skip this image if we can't process it
1506
+ continue
1507
+
1508
+ else:
1509
+ # Skip images that have neither URL nor content
1510
+ logger.warning(f"Skipping ImageArtifact {i} with no URL or content: {img_artifact}")
1511
+ continue
1512
+
1513
+ return images
1514
+
1515
+ def _convert_video_artifacts_to_videos(self, video_artifacts: List[Video]) -> List[Video]:
1516
+ """
1517
+ Convert VideoArtifact objects to Video objects with proper content handling.
1518
+
1519
+ Args:
1520
+ video_artifacts: List of VideoArtifact objects to convert
1521
+
1522
+ Returns:
1523
+ List of Video objects ready for agent processing
1524
+ """
1525
+ videos = []
1526
+ for i, video_artifact in enumerate(video_artifacts):
1527
+ # Create Video object with proper data from VideoArtifact
1528
+ if video_artifact.url:
1529
+ videos.append(Video(url=video_artifact.url))
1530
+
1531
+ elif video_artifact.content:
1532
+ videos.append(Video(content=video_artifact.content))
1533
+
1534
+ else:
1535
+ # Skip videos that have neither URL nor content
1536
+ logger.warning(f"Skipping VideoArtifact {i} with no URL or content: {video_artifact}")
1537
+ continue
1538
+
1539
+ return videos
1540
+
1541
+
1542
+ def _is_async_callable(obj: Any) -> TypeGuard[Callable[..., Any]]:
1543
+ """Checks if obj is an async callable (coroutine function or callable with async __call__)"""
1544
+ return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))
1545
+
1546
+
1547
+ def _is_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1548
+ """Checks if obj is a generator function, including callable class instances with generator __call__ methods"""
1549
+ if inspect.isgeneratorfunction(obj):
1550
+ return True
1551
+ # Check if it's a callable class instance with a generator __call__ method
1552
+ if callable(obj) and hasattr(obj, "__call__"):
1553
+ return inspect.isgeneratorfunction(obj.__call__)
1554
+ return False
1555
+
1556
+
1557
+ def _is_async_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1558
+ """Checks if obj is an async generator function, including callable class instances"""
1559
+ if inspect.isasyncgenfunction(obj):
1560
+ return True
1561
+ # Check if it's a callable class instance with an async generator __call__ method
1562
+ if callable(obj) and hasattr(obj, "__call__"):
1563
+ return inspect.isasyncgenfunction(obj.__call__)
1564
+ return False