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/models/base.py CHANGED
@@ -1,77 +1,114 @@
1
+ import asyncio
1
2
  import collections.abc
3
+ import json
2
4
  from abc import ABC, abstractmethod
3
5
  from dataclasses import dataclass, field
6
+ from hashlib import md5
4
7
  from pathlib import Path
5
- from types import GeneratorType
6
- from typing import Any, Callable, Dict, Iterator, List, Optional, Sequence, Union
7
-
8
- from agno.exceptions import AgentRunException
9
- from agno.media import Audio, Image
10
- from agno.models.message import Message
11
- from agno.models.response import ModelResponse, ModelResponseEvent
12
- from agno.tools import Toolkit
13
- from agno.tools.function import Function, FunctionCall
14
- from agno.utils.log import logger
8
+ from time import sleep, time
9
+ from types import AsyncGeneratorType, GeneratorType
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Any,
13
+ AsyncIterator,
14
+ Dict,
15
+ Iterator,
16
+ List,
17
+ Literal,
18
+ Optional,
19
+ Sequence,
20
+ Tuple,
21
+ Type,
22
+ Union,
23
+ get_args,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from agno.compression.manager import CompressionManager
28
+ from uuid import uuid4
29
+
30
+ from pydantic import BaseModel
31
+
32
+ from agno.exceptions import AgentRunException, ModelProviderError, RetryableModelProviderError
33
+ from agno.media import Audio, File, Image, Video
34
+ from agno.models.message import Citations, Message
35
+ from agno.models.metrics import Metrics
36
+ from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution
37
+ from agno.run.agent import CustomEvent, RunContentEvent, RunOutput, RunOutputEvent
38
+ from agno.run.requirement import RunRequirement
39
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
40
+ from agno.run.team import TeamRunOutput, TeamRunOutputEvent
41
+ from agno.run.workflow import WorkflowRunOutputEvent
42
+ from agno.tools.function import Function, FunctionCall, FunctionExecutionResult, UserInputField
43
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
15
44
  from agno.utils.timer import Timer
16
- from agno.utils.tools import get_function_call_for_tool_call
45
+ from agno.utils.tools import get_function_call_for_tool_call, get_function_call_for_tool_execution
17
46
 
18
47
 
19
48
  @dataclass
20
- class Metrics:
21
- input_tokens: int = 0
22
- output_tokens: int = 0
23
- total_tokens: int = 0
24
-
25
- prompt_tokens: int = 0
26
- completion_tokens: int = 0
27
- prompt_tokens_details: Optional[dict] = None
28
- completion_tokens_details: Optional[dict] = None
29
-
30
- time_to_first_token: Optional[float] = None
31
- response_timer: Timer = field(default_factory=Timer)
32
-
33
- def start_response_timer(self):
34
- self.response_timer.start()
35
-
36
- def stop_response_timer(self):
37
- self.response_timer.stop()
38
-
39
- def _log(self, metric_lines: list[str]):
40
- logger.debug("**************** METRICS START ****************")
41
- for line in metric_lines:
42
- logger.debug(line)
43
- logger.debug("**************** METRICS END ******************")
44
-
45
- def log(self):
46
- metric_lines = []
47
- if self.time_to_first_token is not None:
48
- metric_lines.append(f"* Time to first token: {self.time_to_first_token:.4f}s")
49
- metric_lines.extend(
50
- [
51
- f"* Time to generate response: {self.response_timer.elapsed:.4f}s",
52
- f"* Tokens per second: {self.output_tokens / self.response_timer.elapsed:.4f} tokens/s",
53
- f"* Input tokens: {self.input_tokens or self.prompt_tokens}",
54
- f"* Output tokens: {self.output_tokens or self.completion_tokens}",
55
- f"* Total tokens: {self.total_tokens}",
56
- ]
49
+ class MessageData:
50
+ response_role: Optional[Literal["system", "user", "assistant", "tool"]] = None
51
+ response_content: Any = ""
52
+ response_reasoning_content: Any = ""
53
+ response_redacted_reasoning_content: Any = ""
54
+ response_citations: Optional[Citations] = None
55
+ response_tool_calls: List[Dict[str, Any]] = field(default_factory=list)
56
+
57
+ response_audio: Optional[Audio] = None
58
+ response_image: Optional[Image] = None
59
+ response_video: Optional[Video] = None
60
+ response_file: Optional[File] = None
61
+
62
+ response_metrics: Optional[Metrics] = None
63
+
64
+ # Data from the provider that we might need on subsequent messages
65
+ response_provider_data: Optional[Dict[str, Any]] = None
66
+
67
+ extra: Optional[Dict[str, Any]] = None
68
+
69
+
70
+ def _log_messages(messages: List[Message]) -> None:
71
+ """
72
+ Log messages for debugging.
73
+ """
74
+ for m in messages:
75
+ # Don't log metrics for input messages
76
+ m.log(metrics=False)
77
+
78
+
79
+ def _handle_agent_exception(a_exc: AgentRunException, additional_input: Optional[List[Message]] = None) -> None:
80
+ """Handle AgentRunException and collect additional messages."""
81
+ if additional_input is None:
82
+ additional_input = []
83
+ if a_exc.user_message is not None:
84
+ msg = (
85
+ Message(role="user", content=a_exc.user_message)
86
+ if isinstance(a_exc.user_message, str)
87
+ else a_exc.user_message
57
88
  )
58
- if self.prompt_tokens_details is not None:
59
- metric_lines.append(f"* Prompt tokens details: {self.prompt_tokens_details}")
60
- if self.completion_tokens_details is not None:
61
- metric_lines.append(f"* Completion tokens details: {self.completion_tokens_details}")
62
- self._log(metric_lines=metric_lines)
89
+ additional_input.append(msg)
63
90
 
91
+ if a_exc.agent_message is not None:
92
+ msg = (
93
+ Message(role="assistant", content=a_exc.agent_message)
94
+ if isinstance(a_exc.agent_message, str)
95
+ else a_exc.agent_message
96
+ )
97
+ additional_input.append(msg)
64
98
 
65
- @dataclass
66
- class StreamData:
67
- response_content: str = ""
68
- response_tool_calls: Optional[List[Any]] = None
69
- completion_tokens: int = 0
70
- response_prompt_tokens: int = 0
71
- response_completion_tokens: int = 0
72
- response_total_tokens: int = 0
73
- time_to_first_token: Optional[float] = None
74
- response_timer: Timer = field(default_factory=Timer)
99
+ if a_exc.messages:
100
+ for m in a_exc.messages:
101
+ if isinstance(m, Message):
102
+ additional_input.append(m)
103
+ elif isinstance(m, dict):
104
+ try:
105
+ additional_input.append(Message(**m))
106
+ except Exception as e:
107
+ log_warning(f"Failed to convert dict to Message: {e}")
108
+
109
+ if a_exc.stop_execution:
110
+ for m in additional_input:
111
+ m.stop_after_tool_call = True
75
112
 
76
113
 
77
114
  @dataclass
@@ -82,16 +119,14 @@ class Model(ABC):
82
119
  name: Optional[str] = None
83
120
  # Provider for this Model. This is not sent to the Model API.
84
121
  provider: Optional[str] = None
85
- # Metrics collected for this Model. This is not sent to the Model API.
86
- metrics: Dict[str, Any] = field(default_factory=dict)
87
- # Used for structured_outputs
88
- response_format: Optional[Any] = None
89
122
 
90
- # A list of tools provided to the Model.
91
- # Tools are functions the model may generate JSON inputs for.
92
- # If you provide a dict, it is not called by the model.
93
- # Always add tools using the add_tool() method.
94
- tools: Optional[List[Dict]] = None
123
+ # -*- Do not set the following attributes directly -*-
124
+ # -*- Set them on the Agent instead -*-
125
+
126
+ # True if the Model supports structured outputs natively (e.g. OpenAI)
127
+ supports_native_structured_outputs: bool = False
128
+ # True if the Model requires a json_schema for structured outputs (e.g. LMStudio)
129
+ supports_json_schema_outputs: bool = False
95
130
 
96
131
  # Controls which (if any) function is called by the model.
97
132
  # "none" means the model will not call a function and instead generates a message.
@@ -99,588 +134,2472 @@ class Model(ABC):
99
134
  # Specifying a particular function via {"type: "function", "function": {"name": "my_function"}}
100
135
  # forces the model to call that function.
101
136
  # "none" is the default when no functions are present. "auto" is the default if functions are present.
102
- tool_choice: Optional[Union[str, Dict[str, Any]]] = None
103
-
104
- # If True, shows function calls in the response. Is not compatible with response_model
105
- show_tool_calls: Optional[bool] = None
106
-
107
- # Maximum number of tool calls allowed.
108
- tool_call_limit: Optional[int] = None
109
-
110
- # -*- Functions available to the Model to call -*-
111
- # Functions extracted from the tools.
112
- # Note: These are not sent to the Model API and are only used for execution + deduplication.
113
- _functions: Optional[Dict[str, Function]] = None
114
- # Function call stack.
115
- _function_call_stack: Optional[List[FunctionCall]] = None
137
+ _tool_choice: Optional[Union[str, Dict[str, Any]]] = None
116
138
 
117
139
  # System prompt from the model added to the Agent.
118
140
  system_prompt: Optional[str] = None
119
141
  # Instructions from the model added to the Agent.
120
142
  instructions: Optional[List[str]] = None
121
143
 
122
- # Session ID of the calling Agent or Workflow.
123
- session_id: Optional[str] = None
124
- # Whether to use the structured outputs with this Model.
125
- structured_outputs: Optional[bool] = None
126
- # Whether the Model supports native structured outputs.
127
- supports_structured_outputs: bool = False
128
- # Whether to override the system role.
129
- override_system_role: bool = False
130
- # The role to map the system message to.
131
- system_message_role: str = "system"
144
+ # The role of the tool message.
145
+ tool_message_role: str = "tool"
146
+ # The role of the assistant message.
147
+ assistant_message_role: str = "assistant"
148
+
149
+ # Cache model responses to avoid redundant API calls during development
150
+ cache_response: bool = False
151
+ cache_ttl: Optional[int] = None
152
+ cache_dir: Optional[str] = None
153
+
154
+ # Retry configuration for model provider errors
155
+ # Number of retries to attempt when a ModelProviderError occurs
156
+ retries: int = 0
157
+ # Delay between retries (in seconds)
158
+ delay_between_retries: int = 1
159
+ # Exponential backoff: if True, the delay between retries is doubled each time
160
+ exponential_backoff: bool = False
161
+ # Enable retrying a model invocation once with a guidance message.
162
+ # This is useful for known errors avoidable with extra instructions.
163
+ retry_with_guidance: bool = True
164
+ # Set the number of times to retry the model invocation with guidance.
165
+ retry_with_guidance_limit: int = 1
132
166
 
133
167
  def __post_init__(self):
134
168
  if self.provider is None and self.name is not None:
135
169
  self.provider = f"{self.name} ({self.id})"
136
170
 
171
+ def _get_retry_delay(self, attempt: int) -> float:
172
+ """Calculate the delay before the next retry attempt."""
173
+ if self.exponential_backoff:
174
+ return self.delay_between_retries * (2**attempt)
175
+ return self.delay_between_retries
176
+
177
+ def _invoke_with_retry(self, **kwargs) -> ModelResponse:
178
+ """
179
+ Invoke the model with retry logic for ModelProviderError.
180
+
181
+ This method wraps the invoke() call and retries on ModelProviderError
182
+ with optional exponential backoff.
183
+ """
184
+ last_exception: Optional[ModelProviderError] = None
185
+
186
+ for attempt in range(self.retries + 1):
187
+ try:
188
+ retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
189
+ return self.invoke(**kwargs)
190
+ except ModelProviderError as e:
191
+ last_exception = e
192
+ if attempt < self.retries:
193
+ delay = self._get_retry_delay(attempt)
194
+ log_warning(
195
+ f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
196
+ )
197
+ sleep(delay)
198
+ else:
199
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
200
+ except RetryableModelProviderError as e:
201
+ current_count = retries_with_guidance_count
202
+ if current_count >= self.retry_with_guidance_limit:
203
+ raise ModelProviderError(
204
+ message=f"Max retries with guidance reached. Error: {e.original_error}",
205
+ model_name=self.name,
206
+ model_id=self.id,
207
+ )
208
+ kwargs.pop("retry_with_guidance", None)
209
+ kwargs["retries_with_guidance_count"] = current_count + 1
210
+
211
+ # Append the guidance message to help the model avoid the error in the next invoke.
212
+ kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
213
+
214
+ return self._invoke_with_retry(**kwargs, retry_with_guidance=True)
215
+
216
+ # If we've exhausted all retries, raise the last exception
217
+ raise last_exception # type: ignore
218
+
219
+ async def _ainvoke_with_retry(self, **kwargs) -> ModelResponse:
220
+ """
221
+ Asynchronously invoke the model with retry logic for ModelProviderError.
222
+
223
+ This method wraps the ainvoke() call and retries on ModelProviderError
224
+ with optional exponential backoff.
225
+ """
226
+ last_exception: Optional[ModelProviderError] = None
227
+
228
+ for attempt in range(self.retries + 1):
229
+ try:
230
+ retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
231
+ return await self.ainvoke(**kwargs)
232
+ except ModelProviderError as e:
233
+ last_exception = e
234
+ if attempt < self.retries:
235
+ delay = self._get_retry_delay(attempt)
236
+ log_warning(
237
+ f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
238
+ )
239
+ await asyncio.sleep(delay)
240
+ else:
241
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
242
+ except RetryableModelProviderError as e:
243
+ current_count = retries_with_guidance_count
244
+ if current_count >= self.retry_with_guidance_limit:
245
+ raise ModelProviderError(
246
+ message=f"Max retries with guidance reached. Error: {e.original_error}",
247
+ model_name=self.name,
248
+ model_id=self.id,
249
+ )
250
+
251
+ kwargs.pop("retry_with_guidance", None)
252
+ kwargs["retries_with_guidance_count"] = current_count + 1
253
+
254
+ # Append the guidance message to help the model avoid the error in the next invoke.
255
+ kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
256
+
257
+ return await self._ainvoke_with_retry(**kwargs, retry_with_guidance=True)
258
+
259
+ # If we've exhausted all retries, raise the last exception
260
+ raise last_exception # type: ignore
261
+
262
+ def _invoke_stream_with_retry(self, **kwargs) -> Iterator[ModelResponse]:
263
+ """
264
+ Invoke the model stream with retry logic for ModelProviderError.
265
+
266
+ This method wraps the invoke_stream() call and retries on ModelProviderError
267
+ with optional exponential backoff. Note that retries restart the entire stream.
268
+ """
269
+ last_exception: Optional[ModelProviderError] = None
270
+
271
+ for attempt in range(self.retries + 1):
272
+ try:
273
+ retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
274
+ yield from self.invoke_stream(**kwargs)
275
+ return # Success, exit the retry loop
276
+ except ModelProviderError as e:
277
+ last_exception = e
278
+ if attempt < self.retries:
279
+ delay = self._get_retry_delay(attempt)
280
+ log_warning(
281
+ f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
282
+ f"Retrying in {delay}s..."
283
+ )
284
+ sleep(delay)
285
+ else:
286
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
287
+ except RetryableModelProviderError as e:
288
+ current_count = retries_with_guidance_count
289
+ if current_count >= self.retry_with_guidance_limit:
290
+ raise ModelProviderError(
291
+ message=f"Max retries with guidance reached. Error: {e.original_error}",
292
+ model_name=self.name,
293
+ model_id=self.id,
294
+ )
295
+
296
+ kwargs.pop("retry_with_guidance", None)
297
+ kwargs["retries_with_guidance_count"] = current_count + 1
298
+
299
+ # Append the guidance message to help the model avoid the error in the next invoke.
300
+ kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
301
+
302
+ yield from self._invoke_stream_with_retry(**kwargs, retry_with_guidance=True)
303
+ return # Success, exit after regeneration
304
+
305
+ # If we've exhausted all retries, raise the last exception
306
+ raise last_exception # type: ignore
307
+
308
+ async def _ainvoke_stream_with_retry(self, **kwargs) -> AsyncIterator[ModelResponse]:
309
+ """
310
+ Asynchronously invoke the model stream with retry logic for ModelProviderError.
311
+
312
+ This method wraps the ainvoke_stream() call and retries on ModelProviderError
313
+ with optional exponential backoff. Note that retries restart the entire stream.
314
+ """
315
+ last_exception: Optional[ModelProviderError] = None
316
+
317
+ for attempt in range(self.retries + 1):
318
+ try:
319
+ retries_with_guidance_count = kwargs.pop("retries_with_guidance_count", 0)
320
+ async for response in self.ainvoke_stream(**kwargs):
321
+ yield response
322
+ return # Success, exit the retry loop
323
+ except ModelProviderError as e:
324
+ last_exception = e
325
+ if attempt < self.retries:
326
+ delay = self._get_retry_delay(attempt)
327
+ log_warning(
328
+ f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
329
+ f"Retrying in {delay}s..."
330
+ )
331
+ await asyncio.sleep(delay)
332
+ else:
333
+ log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
334
+ except RetryableModelProviderError as e:
335
+ current_count = retries_with_guidance_count
336
+ if current_count >= self.retry_with_guidance_limit:
337
+ raise ModelProviderError(
338
+ message=f"Max retries with guidance reached. Error: {e.original_error}",
339
+ model_name=self.name,
340
+ model_id=self.id,
341
+ )
342
+
343
+ kwargs.pop("retry_with_guidance", None)
344
+ kwargs["retries_with_guidance_count"] = current_count + 1
345
+
346
+ # Append the guidance message to help the model avoid the error in the next invoke.
347
+ kwargs["messages"].append(Message(role="user", content=e.retry_guidance_message, temporary=True))
348
+
349
+ async for response in self._ainvoke_stream_with_retry(**kwargs, retry_with_guidance=True):
350
+ yield response
351
+ return # Success, exit after regeneration
352
+
353
+ # If we've exhausted all retries, raise the last exception
354
+ raise last_exception # type: ignore
355
+
137
356
  def to_dict(self) -> Dict[str, Any]:
138
- fields = {"name", "id", "provider", "metrics"}
357
+ fields = {"name", "id", "provider"}
139
358
  _dict = {field: getattr(self, field) for field in fields if getattr(self, field) is not None}
140
- # Add functions if they exist
141
- if self._functions:
142
- _dict["functions"] = {k: v.to_dict() for k, v in self._functions.items()}
143
- _dict["tool_call_limit"] = self.tool_call_limit
144
359
  return _dict
145
360
 
146
- @abstractmethod
147
- def invoke(self, *args, **kwargs) -> Any:
148
- pass
361
+ def _remove_temporary_messages(self, messages: List[Message]) -> None:
362
+ """Remove temporary messages from the given list.
149
363
 
150
- @abstractmethod
151
- async def ainvoke(self, *args, **kwargs) -> Any:
152
- pass
364
+ Args:
365
+ messages: The list of messages to filter (modified in place).
366
+ """
367
+ messages[:] = [m for m in messages if not m.temporary]
368
+
369
+ def get_provider(self) -> str:
370
+ return self.provider or self.name or self.__class__.__name__
371
+
372
+ def _get_model_cache_key(self, messages: List[Message], stream: bool, **kwargs: Any) -> str:
373
+ """Generate a cache key based on model messages and core parameters."""
374
+ message_data = []
375
+ for msg in messages:
376
+ msg_dict = {
377
+ "role": msg.role,
378
+ "content": msg.content,
379
+ }
380
+ message_data.append(msg_dict)
381
+
382
+ # Include tools parameter in cache key
383
+ has_tools = bool(kwargs.get("tools"))
384
+
385
+ cache_data = {
386
+ "model_id": self.id,
387
+ "messages": message_data,
388
+ "has_tools": has_tools,
389
+ "response_format": kwargs.get("response_format"),
390
+ "stream": stream,
391
+ }
392
+
393
+ cache_str = json.dumps(cache_data, sort_keys=True)
394
+ return md5(cache_str.encode()).hexdigest()
395
+
396
+ def _get_model_cache_file_path(self, cache_key: str) -> Path:
397
+ """Get the file path for a cache key."""
398
+ if self.cache_dir:
399
+ cache_dir = Path(self.cache_dir)
400
+ else:
401
+ cache_dir = Path.home() / ".agno" / "cache" / "model_responses"
402
+
403
+ cache_dir.mkdir(parents=True, exist_ok=True)
404
+ return cache_dir / f"{cache_key}.json"
405
+
406
+ def _get_cached_model_response(self, cache_key: str) -> Optional[Dict[str, Any]]:
407
+ """Retrieve a cached response if it exists and is not expired."""
408
+ cache_file = self._get_model_cache_file_path(cache_key)
409
+
410
+ if not cache_file.exists():
411
+ return None
412
+
413
+ try:
414
+ with open(cache_file, "r") as f:
415
+ cached_data = json.load(f)
416
+
417
+ # Check TTL if set (None means no expiration)
418
+ if self.cache_ttl is not None:
419
+ if time() - cached_data["timestamp"] > self.cache_ttl:
420
+ return None
421
+
422
+ return cached_data
423
+ except Exception:
424
+ return None
425
+
426
+ def _save_model_response_to_cache(self, cache_key: str, result: ModelResponse, is_streaming: bool = False) -> None:
427
+ """Save a model response to cache."""
428
+ try:
429
+ cache_file = self._get_model_cache_file_path(cache_key)
430
+
431
+ cache_data = {
432
+ "timestamp": int(time()),
433
+ "is_streaming": is_streaming,
434
+ "result": result.to_dict(),
435
+ }
436
+ with open(cache_file, "w") as f:
437
+ json.dump(cache_data, f)
438
+ except Exception:
439
+ pass
440
+
441
+ def _save_streaming_responses_to_cache(self, cache_key: str, responses: List[ModelResponse]) -> None:
442
+ """Save streaming responses to cache."""
443
+ cache_file = self._get_model_cache_file_path(cache_key)
444
+
445
+ cache_data = {
446
+ "timestamp": int(time()),
447
+ "is_streaming": True,
448
+ "streaming_responses": [r.to_dict() for r in responses],
449
+ }
450
+
451
+ try:
452
+ with open(cache_file, "w") as f:
453
+ json.dump(cache_data, f)
454
+ except Exception:
455
+ pass
456
+
457
+ def _model_response_from_cache(self, cached_data: Dict[str, Any]) -> ModelResponse:
458
+ """Reconstruct a ModelResponse from cached data."""
459
+ return ModelResponse.from_dict(cached_data["result"])
460
+
461
+ def _streaming_responses_from_cache(self, cached_data: list) -> Iterator[ModelResponse]:
462
+ """Reconstruct streaming responses from cached data."""
463
+ for cached_response in cached_data:
464
+ yield ModelResponse.from_dict(cached_response)
153
465
 
154
466
  @abstractmethod
155
- def invoke_stream(self, *args, **kwargs) -> Iterator[Any]:
467
+ def invoke(self, *args, **kwargs) -> ModelResponse:
156
468
  pass
157
469
 
158
470
  @abstractmethod
159
- async def ainvoke_stream(self, *args, **kwargs) -> Any:
471
+ async def ainvoke(self, *args, **kwargs) -> ModelResponse:
160
472
  pass
161
473
 
162
474
  @abstractmethod
163
- def response(self, messages: List[Message]) -> ModelResponse:
475
+ def invoke_stream(self, *args, **kwargs) -> Iterator[ModelResponse]:
164
476
  pass
165
477
 
166
478
  @abstractmethod
167
- async def aresponse(self, messages: List[Message]) -> ModelResponse:
479
+ def ainvoke_stream(self, *args, **kwargs) -> AsyncIterator[ModelResponse]:
168
480
  pass
169
481
 
170
482
  @abstractmethod
171
- def response_stream(self, messages: List[Message]) -> Iterator[ModelResponse]:
483
+ def _parse_provider_response(self, response: Any, **kwargs) -> ModelResponse:
484
+ """
485
+ Parse the raw response from the model provider into a ModelResponse.
486
+
487
+ Args:
488
+ response: Raw response from the model provider
489
+
490
+ Returns:
491
+ ModelResponse: Parsed response data
492
+ """
172
493
  pass
173
494
 
174
495
  @abstractmethod
175
- async def aresponse_stream(self, messages: List[Message]) -> Any:
496
+ def _parse_provider_response_delta(self, response: Any) -> ModelResponse:
497
+ """
498
+ Parse the streaming response from the model provider into ModelResponse objects.
499
+
500
+ Args:
501
+ response: Raw response chunk from the model provider
502
+
503
+ Returns:
504
+ ModelResponse: Parsed response delta
505
+ """
176
506
  pass
177
507
 
178
- def _log_messages(self, messages: List[Message]) -> None:
508
+ def _format_tools(self, tools: Optional[List[Union[Function, dict]]]) -> List[Dict[str, Any]]:
509
+ _tool_dicts = []
510
+ for tool in tools or []:
511
+ if isinstance(tool, Function):
512
+ _tool_dicts.append({"type": "function", "function": tool.to_dict()})
513
+ else:
514
+ # If a dict is passed, it is a builtin tool
515
+ _tool_dicts.append(tool)
516
+ return _tool_dicts
517
+
518
+ def count_tokens(
519
+ self,
520
+ messages: List[Message],
521
+ tools: Optional[Sequence[Union[Function, Dict[str, Any]]]] = None,
522
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
523
+ ) -> int:
524
+ from agno.utils.tokens import count_tokens
525
+
526
+ return count_tokens(
527
+ messages,
528
+ tools=list(tools) if tools else None,
529
+ model_id=self.id,
530
+ output_schema=output_schema,
531
+ )
532
+
533
+ async def acount_tokens(
534
+ self,
535
+ messages: List[Message],
536
+ tools: Optional[Sequence[Union[Function, Dict[str, Any]]]] = None,
537
+ output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
538
+ ) -> int:
539
+ return self.count_tokens(messages, tools, output_schema=output_schema)
540
+
541
+ def response(
542
+ self,
543
+ messages: List[Message],
544
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
545
+ tools: Optional[List[Union[Function, dict]]] = None,
546
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
547
+ tool_call_limit: Optional[int] = None,
548
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
549
+ send_media_to_model: bool = True,
550
+ compression_manager: Optional["CompressionManager"] = None,
551
+ ) -> ModelResponse:
552
+ """
553
+ Generate a response from the model.
554
+
555
+ Args:
556
+ messages: List of messages to send to the model
557
+ response_format: Response format to use
558
+ tools: List of tools to use. This includes the original Function objects and dicts for built-in tools.
559
+ tool_choice: Tool choice to use
560
+ tool_call_limit: Tool call limit
561
+ run_response: Run response to use
562
+ send_media_to_model: Whether to send media to the model
563
+ """
564
+ try:
565
+ # Check cache if enabled
566
+ if self.cache_response:
567
+ cache_key = self._get_model_cache_key(
568
+ messages, stream=False, response_format=response_format, tools=tools
569
+ )
570
+ cached_data = self._get_cached_model_response(cache_key)
571
+
572
+ if cached_data:
573
+ log_info("Cache hit for model response")
574
+ return self._model_response_from_cache(cached_data)
575
+
576
+ log_debug(f"{self.get_provider()} Response Start", center=True, symbol="-")
577
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
578
+
579
+ _log_messages(messages)
580
+ model_response = ModelResponse()
581
+
582
+ function_call_count = 0
583
+
584
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
585
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
586
+
587
+ _compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
588
+ _compression_manager = compression_manager if _compress_tool_results else None
589
+
590
+ while True:
591
+ # Compress tool results if compression is enabled and threshold is met
592
+ if _compression_manager is not None and _compression_manager.should_compress(
593
+ messages, tools, model=self, response_format=response_format
594
+ ):
595
+ _compression_manager.compress(messages)
596
+
597
+ # Get response from model
598
+ assistant_message = Message(role=self.assistant_message_role)
599
+ self._process_model_response(
600
+ messages=messages,
601
+ assistant_message=assistant_message,
602
+ model_response=model_response,
603
+ response_format=response_format,
604
+ tools=_tool_dicts,
605
+ tool_choice=tool_choice or self._tool_choice,
606
+ run_response=run_response,
607
+ compress_tool_results=_compress_tool_results,
608
+ )
609
+
610
+ # Add assistant message to messages
611
+ messages.append(assistant_message)
612
+
613
+ # Log response and metrics
614
+ assistant_message.log(metrics=True, use_compressed_content=_compress_tool_results)
615
+
616
+ # Handle tool calls if present
617
+ if assistant_message.tool_calls:
618
+ # Prepare function calls
619
+ function_calls_to_run = self._prepare_function_calls(
620
+ assistant_message=assistant_message,
621
+ messages=messages,
622
+ model_response=model_response,
623
+ functions=_functions,
624
+ )
625
+ function_call_results: List[Message] = []
626
+
627
+ # Execute function calls
628
+ for function_call_response in self.run_function_calls(
629
+ function_calls=function_calls_to_run,
630
+ function_call_results=function_call_results,
631
+ current_function_call_count=function_call_count,
632
+ function_call_limit=tool_call_limit,
633
+ ):
634
+ if isinstance(function_call_response, ModelResponse):
635
+ # The session state is updated by the function call
636
+ if function_call_response.updated_session_state is not None:
637
+ model_response.updated_session_state = function_call_response.updated_session_state
638
+
639
+ # Media artifacts are generated by the function call
640
+ if function_call_response.images is not None:
641
+ if model_response.images is None:
642
+ model_response.images = []
643
+ model_response.images.extend(function_call_response.images)
644
+
645
+ if function_call_response.audios is not None:
646
+ if model_response.audios is None:
647
+ model_response.audios = []
648
+ model_response.audios.extend(function_call_response.audios)
649
+
650
+ if function_call_response.videos is not None:
651
+ if model_response.videos is None:
652
+ model_response.videos = []
653
+ model_response.videos.extend(function_call_response.videos)
654
+
655
+ if function_call_response.files is not None:
656
+ if model_response.files is None:
657
+ model_response.files = []
658
+ model_response.files.extend(function_call_response.files)
659
+
660
+ if (
661
+ function_call_response.event
662
+ in [
663
+ ModelResponseEvent.tool_call_completed.value,
664
+ ModelResponseEvent.tool_call_paused.value,
665
+ ]
666
+ and function_call_response.tool_executions is not None
667
+ ):
668
+ # Record the tool execution in the model response
669
+ if model_response.tool_executions is None:
670
+ model_response.tool_executions = []
671
+ model_response.tool_executions.extend(function_call_response.tool_executions)
672
+
673
+ # If the tool is currently paused (HITL flow), add the requirement to the run response
674
+ if (
675
+ function_call_response.event == ModelResponseEvent.tool_call_paused.value
676
+ and run_response is not None
677
+ ):
678
+ current_tool_execution = function_call_response.tool_executions[-1]
679
+ if run_response.requirements is None:
680
+ run_response.requirements = []
681
+ run_response.requirements.append(
682
+ RunRequirement(tool_execution=current_tool_execution)
683
+ )
684
+
685
+ elif function_call_response.event not in [
686
+ ModelResponseEvent.tool_call_started.value,
687
+ ModelResponseEvent.tool_call_completed.value,
688
+ ]:
689
+ if function_call_response.content:
690
+ model_response.content += function_call_response.content # type: ignore
691
+
692
+ # Add a function call for each successful execution
693
+ function_call_count += len(function_call_results)
694
+
695
+ # Format and add results to messages
696
+ self.format_function_call_results(
697
+ messages=messages,
698
+ function_call_results=function_call_results,
699
+ compress_tool_results=_compress_tool_results,
700
+ **model_response.extra or {},
701
+ )
702
+
703
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
704
+ # Handle function call media
705
+ self._handle_function_call_media(
706
+ messages=messages,
707
+ function_call_results=function_call_results,
708
+ send_media_to_model=send_media_to_model,
709
+ )
710
+
711
+ for function_call_result in function_call_results:
712
+ function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
713
+
714
+ # Check if we should stop after tool calls
715
+ if any(m.stop_after_tool_call for m in function_call_results):
716
+ break
717
+
718
+ # If we have any tool calls that require confirmation, break the loop
719
+ if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
720
+ break
721
+
722
+ # If we have any tool calls that require external execution, break the loop
723
+ if any(tc.external_execution_required for tc in model_response.tool_executions or []):
724
+ break
725
+
726
+ # If we have any tool calls that require user input, break the loop
727
+ if any(tc.requires_user_input for tc in model_response.tool_executions or []):
728
+ break
729
+
730
+ # Continue loop to get next response
731
+ continue
732
+
733
+ # No tool calls or finished processing them
734
+ break
735
+
736
+ log_debug(f"{self.get_provider()} Response End", center=True, symbol="-")
737
+
738
+ # Save to cache if enabled
739
+ if self.cache_response:
740
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
741
+ finally:
742
+ # Close the Gemini client
743
+ if self.__class__.__name__ == "Gemini" and self.client is not None: # type: ignore
744
+ try:
745
+ self.client.close() # type: ignore
746
+ self.client = None
747
+ except AttributeError:
748
+ log_warning(
749
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
750
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
751
+ )
752
+
753
+ return model_response
754
+
755
+ async def aresponse(
756
+ self,
757
+ messages: List[Message],
758
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
759
+ tools: Optional[List[Union[Function, dict]]] = None,
760
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
761
+ tool_call_limit: Optional[int] = None,
762
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
763
+ send_media_to_model: bool = True,
764
+ compression_manager: Optional["CompressionManager"] = None,
765
+ ) -> ModelResponse:
179
766
  """
180
- Log messages for debugging.
767
+ Generate an asynchronous response from the model.
181
768
  """
182
- for m in messages:
183
- m.log()
184
769
 
185
- @staticmethod
186
- def _update_assistant_message_metrics(assistant_message: Message, metrics_for_run: Metrics = Metrics()) -> None:
187
- assistant_message.metrics["time"] = metrics_for_run.response_timer.elapsed
188
- if metrics_for_run.input_tokens is not None:
189
- assistant_message.metrics["input_tokens"] = metrics_for_run.input_tokens
190
- if metrics_for_run.output_tokens is not None:
191
- assistant_message.metrics["output_tokens"] = metrics_for_run.output_tokens
192
- if metrics_for_run.total_tokens is not None:
193
- assistant_message.metrics["total_tokens"] = metrics_for_run.total_tokens
194
- if metrics_for_run.time_to_first_token is not None:
195
- assistant_message.metrics["time_to_first_token"] = metrics_for_run.time_to_first_token
770
+ try:
771
+ # Check cache if enabled
772
+ if self.cache_response:
773
+ cache_key = self._get_model_cache_key(
774
+ messages, stream=False, response_format=response_format, tools=tools
775
+ )
776
+ cached_data = self._get_cached_model_response(cache_key)
777
+
778
+ if cached_data:
779
+ log_info("Cache hit for model response")
780
+ return self._model_response_from_cache(cached_data)
781
+
782
+ log_debug(f"{self.get_provider()} Async Response Start", center=True, symbol="-")
783
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
784
+ _log_messages(messages)
785
+ model_response = ModelResponse()
786
+
787
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
788
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
789
+
790
+ _compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
791
+ _compression_manager = compression_manager if _compress_tool_results else None
792
+
793
+ function_call_count = 0
794
+
795
+ while True:
796
+ # Compress existing tool results BEFORE making API call to avoid context overflow
797
+ if _compression_manager is not None and await _compression_manager.ashould_compress(
798
+ messages, tools, model=self, response_format=response_format
799
+ ):
800
+ await _compression_manager.acompress(messages)
801
+
802
+ # Get response from model
803
+ assistant_message = Message(role=self.assistant_message_role)
804
+ await self._aprocess_model_response(
805
+ messages=messages,
806
+ assistant_message=assistant_message,
807
+ model_response=model_response,
808
+ response_format=response_format,
809
+ tools=_tool_dicts,
810
+ tool_choice=tool_choice or self._tool_choice,
811
+ run_response=run_response,
812
+ compress_tool_results=_compress_tool_results,
813
+ )
814
+
815
+ # Add assistant message to messages
816
+ messages.append(assistant_message)
817
+
818
+ # Log response and metrics
819
+ assistant_message.log(metrics=True)
820
+
821
+ # Handle tool calls if present
822
+ if assistant_message.tool_calls:
823
+ # Prepare function calls
824
+ function_calls_to_run = self._prepare_function_calls(
825
+ assistant_message=assistant_message,
826
+ messages=messages,
827
+ model_response=model_response,
828
+ functions=_functions,
829
+ )
830
+ function_call_results: List[Message] = []
831
+
832
+ # Execute function calls
833
+ async for function_call_response in self.arun_function_calls(
834
+ function_calls=function_calls_to_run,
835
+ function_call_results=function_call_results,
836
+ current_function_call_count=function_call_count,
837
+ function_call_limit=tool_call_limit,
838
+ ):
839
+ if isinstance(function_call_response, ModelResponse):
840
+ # The session state is updated by the function call
841
+ if function_call_response.updated_session_state is not None:
842
+ model_response.updated_session_state = function_call_response.updated_session_state
843
+
844
+ # Media artifacts are generated by the function call
845
+ if function_call_response.images is not None:
846
+ if model_response.images is None:
847
+ model_response.images = []
848
+ model_response.images.extend(function_call_response.images)
849
+
850
+ if function_call_response.audios is not None:
851
+ if model_response.audios is None:
852
+ model_response.audios = []
853
+ model_response.audios.extend(function_call_response.audios)
854
+
855
+ if function_call_response.videos is not None:
856
+ if model_response.videos is None:
857
+ model_response.videos = []
858
+ model_response.videos.extend(function_call_response.videos)
859
+
860
+ if function_call_response.files is not None:
861
+ if model_response.files is None:
862
+ model_response.files = []
863
+ model_response.files.extend(function_call_response.files)
864
+
865
+ if (
866
+ function_call_response.event
867
+ in [
868
+ ModelResponseEvent.tool_call_completed.value,
869
+ ModelResponseEvent.tool_call_paused.value,
870
+ ]
871
+ and function_call_response.tool_executions is not None
872
+ ):
873
+ if model_response.tool_executions is None:
874
+ model_response.tool_executions = []
875
+ model_response.tool_executions.extend(function_call_response.tool_executions)
876
+
877
+ # If the tool is currently paused (HITL flow), add the requirement to the run response
878
+ if (
879
+ function_call_response.event == ModelResponseEvent.tool_call_paused.value
880
+ and run_response is not None
881
+ ):
882
+ current_tool_execution = function_call_response.tool_executions[-1]
883
+ if run_response.requirements is None:
884
+ run_response.requirements = []
885
+ run_response.requirements.append(
886
+ RunRequirement(tool_execution=current_tool_execution)
887
+ )
888
+
889
+ elif function_call_response.event not in [
890
+ ModelResponseEvent.tool_call_started.value,
891
+ ModelResponseEvent.tool_call_completed.value,
892
+ ]:
893
+ if function_call_response.content:
894
+ model_response.content += function_call_response.content # type: ignore
895
+
896
+ # Add a function call for each successful execution
897
+ function_call_count += len(function_call_results)
898
+
899
+ # Format and add results to messages
900
+ self.format_function_call_results(
901
+ messages=messages,
902
+ function_call_results=function_call_results,
903
+ compress_tool_results=_compress_tool_results,
904
+ **model_response.extra or {},
905
+ )
906
+
907
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
908
+ # Handle function call media
909
+ self._handle_function_call_media(
910
+ messages=messages,
911
+ function_call_results=function_call_results,
912
+ send_media_to_model=send_media_to_model,
913
+ )
914
+
915
+ for function_call_result in function_call_results:
916
+ function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
917
+
918
+ # Check if we should stop after tool calls
919
+ if any(m.stop_after_tool_call for m in function_call_results):
920
+ break
921
+
922
+ # If we have any tool calls that require confirmation, break the loop
923
+ if any(tc.requires_confirmation for tc in model_response.tool_executions or []):
924
+ break
925
+
926
+ # If we have any tool calls that require external execution, break the loop
927
+ if any(tc.external_execution_required for tc in model_response.tool_executions or []):
928
+ break
929
+
930
+ # If we have any tool calls that require user input, break the loop
931
+ if any(tc.requires_user_input for tc in model_response.tool_executions or []):
932
+ break
933
+
934
+ # Continue loop to get next response
935
+ continue
936
+
937
+ # No tool calls or finished processing them
938
+ break
939
+
940
+ log_debug(f"{self.get_provider()} Async Response End", center=True, symbol="-")
196
941
 
197
- def _update_model_metrics(
942
+ # Save to cache if enabled
943
+ if self.cache_response:
944
+ self._save_model_response_to_cache(cache_key, model_response, is_streaming=False)
945
+ finally:
946
+ # Close the Gemini client
947
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
948
+ try:
949
+ await self.client.aio.aclose() # type: ignore
950
+ self.client = None
951
+ except AttributeError:
952
+ log_warning(
953
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
954
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
955
+ )
956
+
957
+ return model_response
958
+
959
+ def _process_model_response(
198
960
  self,
199
- metrics_for_run: Metrics = Metrics(),
961
+ messages: List[Message],
962
+ assistant_message: Message,
963
+ model_response: ModelResponse,
964
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
965
+ tools: Optional[List[Dict[str, Any]]] = None,
966
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
967
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
968
+ compress_tool_results: bool = False,
200
969
  ) -> None:
201
- self.metrics.setdefault("response_times", []).append(metrics_for_run.response_timer.elapsed)
202
- if metrics_for_run.input_tokens is not None:
203
- self.metrics["input_tokens"] = self.metrics.get("input_tokens", 0) + metrics_for_run.input_tokens
204
- if metrics_for_run.output_tokens is not None:
205
- self.metrics["output_tokens"] = self.metrics.get("output_tokens", 0) + metrics_for_run.output_tokens
206
- if metrics_for_run.total_tokens is not None:
207
- self.metrics["total_tokens"] = self.metrics.get("total_tokens", 0) + metrics_for_run.total_tokens
208
- if metrics_for_run.time_to_first_token is not None:
209
- self.metrics.setdefault("time_to_first_token", []).append(metrics_for_run.time_to_first_token)
210
-
211
- def _get_function_calls_to_run(
212
- self, assistant_message: Message, messages: List[Message], error_response_role: str = "user"
213
- ) -> List[FunctionCall]:
214
970
  """
215
- Prepare function calls for the assistant message.
971
+ Process a single model response and return the assistant message and whether to continue.
972
+
973
+ Returns:
974
+ Tuple[Message, bool]: (assistant_message, should_continue)
975
+ """
976
+ # Generate response with retry logic for ModelProviderError
977
+ provider_response = self._invoke_with_retry(
978
+ assistant_message=assistant_message,
979
+ messages=messages,
980
+ response_format=response_format,
981
+ tools=tools,
982
+ tool_choice=tool_choice or self._tool_choice,
983
+ run_response=run_response,
984
+ compress_tool_results=compress_tool_results,
985
+ )
986
+
987
+ # Populate the assistant message
988
+ self._populate_assistant_message(assistant_message=assistant_message, provider_response=provider_response)
989
+
990
+ # Update model response with assistant message content and audio
991
+ if assistant_message.content is not None:
992
+ if model_response.content is None:
993
+ model_response.content = assistant_message.get_content_string()
994
+ else:
995
+ model_response.content += assistant_message.get_content_string()
996
+ if assistant_message.reasoning_content is not None:
997
+ model_response.reasoning_content = assistant_message.reasoning_content
998
+ if assistant_message.redacted_reasoning_content is not None:
999
+ model_response.redacted_reasoning_content = assistant_message.redacted_reasoning_content
1000
+ if assistant_message.citations is not None:
1001
+ model_response.citations = assistant_message.citations
1002
+ if assistant_message.audio_output is not None:
1003
+ if isinstance(assistant_message.audio_output, Audio):
1004
+ model_response.audio = assistant_message.audio_output
1005
+ if assistant_message.image_output is not None:
1006
+ model_response.images = [assistant_message.image_output]
1007
+ if assistant_message.video_output is not None:
1008
+ model_response.videos = [assistant_message.video_output]
1009
+ if provider_response.extra is not None:
1010
+ if model_response.extra is None:
1011
+ model_response.extra = {}
1012
+ model_response.extra.update(provider_response.extra)
1013
+ if provider_response.provider_data is not None:
1014
+ model_response.provider_data = provider_response.provider_data
1015
+
1016
+ async def _aprocess_model_response(
1017
+ self,
1018
+ messages: List[Message],
1019
+ assistant_message: Message,
1020
+ model_response: ModelResponse,
1021
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
1022
+ tools: Optional[List[Dict[str, Any]]] = None,
1023
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
1024
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
1025
+ compress_tool_results: bool = False,
1026
+ ) -> None:
1027
+ """
1028
+ Process a single async model response and return the assistant message and whether to continue.
1029
+
1030
+ Returns:
1031
+ Tuple[Message, bool]: (assistant_message, should_continue)
1032
+ """
1033
+ # Generate response with retry logic for ModelProviderError
1034
+ provider_response = await self._ainvoke_with_retry(
1035
+ messages=messages,
1036
+ response_format=response_format,
1037
+ tools=tools,
1038
+ tool_choice=tool_choice or self._tool_choice,
1039
+ assistant_message=assistant_message,
1040
+ run_response=run_response,
1041
+ compress_tool_results=compress_tool_results,
1042
+ )
1043
+
1044
+ # Populate the assistant message
1045
+ self._populate_assistant_message(assistant_message=assistant_message, provider_response=provider_response)
1046
+
1047
+ # Update model response with assistant message content and audio
1048
+ if assistant_message.content is not None:
1049
+ if model_response.content is None:
1050
+ model_response.content = assistant_message.get_content_string()
1051
+ else:
1052
+ model_response.content += assistant_message.get_content_string()
1053
+ if assistant_message.reasoning_content is not None:
1054
+ model_response.reasoning_content = assistant_message.reasoning_content
1055
+ if assistant_message.redacted_reasoning_content is not None:
1056
+ model_response.redacted_reasoning_content = assistant_message.redacted_reasoning_content
1057
+ if assistant_message.citations is not None:
1058
+ model_response.citations = assistant_message.citations
1059
+ if assistant_message.audio_output is not None:
1060
+ if isinstance(assistant_message.audio_output, Audio):
1061
+ model_response.audio = assistant_message.audio_output
1062
+ if assistant_message.image_output is not None:
1063
+ model_response.images = [assistant_message.image_output]
1064
+ if assistant_message.video_output is not None:
1065
+ model_response.videos = [assistant_message.video_output]
1066
+ if provider_response.extra is not None:
1067
+ if model_response.extra is None:
1068
+ model_response.extra = {}
1069
+ model_response.extra.update(provider_response.extra)
1070
+ if provider_response.provider_data is not None:
1071
+ model_response.provider_data = provider_response.provider_data
1072
+
1073
+ def _populate_assistant_message(
1074
+ self,
1075
+ assistant_message: Message,
1076
+ provider_response: ModelResponse,
1077
+ ) -> Message:
1078
+ """
1079
+ Populate an assistant message with the provider response data.
216
1080
 
217
1081
  Args:
218
- assistant_message (Message): The assistant message.
219
- messages (List[Message]): The list of conversation messages.
1082
+ assistant_message: The assistant message to populate
1083
+ provider_response: Parsed response from the model provider
220
1084
 
221
1085
  Returns:
222
- List[FunctionCall]: A list of function calls to run.
1086
+ Message: The populated assistant message
1087
+ """
1088
+ if provider_response.role is not None:
1089
+ assistant_message.role = provider_response.role
1090
+
1091
+ # Add content to assistant message
1092
+ if provider_response.content is not None:
1093
+ assistant_message.content = provider_response.content
1094
+
1095
+ # Add tool calls to assistant message
1096
+ if provider_response.tool_calls is not None and len(provider_response.tool_calls) > 0:
1097
+ assistant_message.tool_calls = provider_response.tool_calls
1098
+
1099
+ # Add audio to assistant message
1100
+ if provider_response.audio is not None:
1101
+ assistant_message.audio_output = provider_response.audio
1102
+
1103
+ # Add image to assistant message
1104
+ if provider_response.images is not None:
1105
+ if provider_response.images:
1106
+ assistant_message.image_output = provider_response.images[-1] # Taking last (most recent) image
1107
+
1108
+ # Add video to assistant message
1109
+ if provider_response.videos is not None:
1110
+ if provider_response.videos:
1111
+ assistant_message.video_output = provider_response.videos[-1] # Taking last (most recent) video
1112
+
1113
+ if provider_response.files is not None:
1114
+ if provider_response.files:
1115
+ assistant_message.file_output = provider_response.files[-1] # Taking last (most recent) file
1116
+
1117
+ if provider_response.audios is not None:
1118
+ if provider_response.audios:
1119
+ assistant_message.audio_output = provider_response.audios[-1] # Taking last (most recent) audio
1120
+
1121
+ # Add redacted thinking content to assistant message
1122
+ if provider_response.redacted_reasoning_content is not None:
1123
+ assistant_message.redacted_reasoning_content = provider_response.redacted_reasoning_content
1124
+
1125
+ # Add reasoning content to assistant message
1126
+ if provider_response.reasoning_content is not None:
1127
+ assistant_message.reasoning_content = provider_response.reasoning_content
1128
+
1129
+ # Add provider data to assistant message
1130
+ if provider_response.provider_data is not None:
1131
+ assistant_message.provider_data = provider_response.provider_data
1132
+
1133
+ # Add citations to assistant message
1134
+ if provider_response.citations is not None:
1135
+ assistant_message.citations = provider_response.citations
1136
+
1137
+ # Add usage metrics if provided
1138
+ if provider_response.response_usage is not None:
1139
+ assistant_message.metrics += provider_response.response_usage
1140
+
1141
+ return assistant_message
1142
+
1143
+ def process_response_stream(
1144
+ self,
1145
+ messages: List[Message],
1146
+ assistant_message: Message,
1147
+ stream_data: MessageData,
1148
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
1149
+ tools: Optional[List[Dict[str, Any]]] = None,
1150
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
1151
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
1152
+ compress_tool_results: bool = False,
1153
+ ) -> Iterator[ModelResponse]:
1154
+ """
1155
+ Process a streaming response from the model with retry logic for ModelProviderError.
1156
+ """
1157
+
1158
+ for response_delta in self._invoke_stream_with_retry(
1159
+ messages=messages,
1160
+ assistant_message=assistant_message,
1161
+ response_format=response_format,
1162
+ tools=tools,
1163
+ tool_choice=tool_choice or self._tool_choice,
1164
+ run_response=run_response,
1165
+ compress_tool_results=compress_tool_results,
1166
+ ):
1167
+ for model_response_delta in self._populate_stream_data(
1168
+ stream_data=stream_data,
1169
+ model_response_delta=response_delta,
1170
+ ):
1171
+ yield model_response_delta
1172
+
1173
+ # Populate assistant message from stream data after the stream ends
1174
+ self._populate_assistant_message_from_stream_data(assistant_message=assistant_message, stream_data=stream_data)
1175
+
1176
+ def response_stream(
1177
+ self,
1178
+ messages: List[Message],
1179
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
1180
+ tools: Optional[List[Union[Function, dict]]] = None,
1181
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
1182
+ tool_call_limit: Optional[int] = None,
1183
+ stream_model_response: bool = True,
1184
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
1185
+ send_media_to_model: bool = True,
1186
+ compression_manager: Optional["CompressionManager"] = None,
1187
+ ) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
1188
+ """
1189
+ Generate a streaming response from the model.
1190
+ """
1191
+ try:
1192
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
1193
+ cache_key = None
1194
+ if self.cache_response:
1195
+ cache_key = self._get_model_cache_key(
1196
+ messages, stream=True, response_format=response_format, tools=tools
1197
+ )
1198
+ cached_data = self._get_cached_model_response(cache_key)
1199
+
1200
+ if cached_data:
1201
+ log_info("Cache hit for streaming model response")
1202
+ # Yield cached responses
1203
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
1204
+ yield response
1205
+ return
1206
+
1207
+ log_info("Cache miss for streaming model response")
1208
+
1209
+ # Track streaming responses for caching
1210
+ streaming_responses: List[ModelResponse] = []
1211
+
1212
+ log_debug(f"{self.get_provider()} Response Stream Start", center=True, symbol="-")
1213
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
1214
+ _log_messages(messages)
1215
+
1216
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
1217
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
1218
+
1219
+ _compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
1220
+ _compression_manager = compression_manager if _compress_tool_results else None
1221
+
1222
+ function_call_count = 0
1223
+
1224
+ while True:
1225
+ # Compress existing tool results BEFORE invoke
1226
+ if _compression_manager is not None and _compression_manager.should_compress(
1227
+ messages, tools, model=self, response_format=response_format
1228
+ ):
1229
+ _compression_manager.compress(messages)
1230
+
1231
+ assistant_message = Message(role=self.assistant_message_role)
1232
+ # Create assistant message and stream data
1233
+ stream_data = MessageData()
1234
+ model_response = ModelResponse()
1235
+ if stream_model_response:
1236
+ # Generate response
1237
+ for response in self.process_response_stream(
1238
+ messages=messages,
1239
+ assistant_message=assistant_message,
1240
+ stream_data=stream_data,
1241
+ response_format=response_format,
1242
+ tools=_tool_dicts,
1243
+ tool_choice=tool_choice or self._tool_choice,
1244
+ run_response=run_response,
1245
+ compress_tool_results=_compress_tool_results,
1246
+ ):
1247
+ if self.cache_response and isinstance(response, ModelResponse):
1248
+ streaming_responses.append(response)
1249
+ yield response
1250
+
1251
+ else:
1252
+ self._process_model_response(
1253
+ messages=messages,
1254
+ assistant_message=assistant_message,
1255
+ model_response=model_response,
1256
+ response_format=response_format,
1257
+ tools=_tool_dicts,
1258
+ tool_choice=tool_choice or self._tool_choice,
1259
+ run_response=run_response,
1260
+ compress_tool_results=_compress_tool_results,
1261
+ )
1262
+ if self.cache_response:
1263
+ streaming_responses.append(model_response)
1264
+ yield model_response
1265
+
1266
+ # Add assistant message to messages
1267
+ messages.append(assistant_message)
1268
+ assistant_message.log(metrics=True)
1269
+
1270
+ # Handle tool calls if present
1271
+ if assistant_message.tool_calls is not None:
1272
+ # Prepare function calls
1273
+ function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
1274
+ assistant_message=assistant_message, messages=messages, functions=_functions
1275
+ )
1276
+ function_call_results: List[Message] = []
1277
+
1278
+ # Execute function calls
1279
+ for function_call_response in self.run_function_calls(
1280
+ function_calls=function_calls_to_run,
1281
+ function_call_results=function_call_results,
1282
+ current_function_call_count=function_call_count,
1283
+ function_call_limit=tool_call_limit,
1284
+ ):
1285
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
1286
+ streaming_responses.append(function_call_response)
1287
+ yield function_call_response
1288
+
1289
+ # Add a function call for each successful execution
1290
+ function_call_count += len(function_call_results)
1291
+
1292
+ # Format and add results to messages
1293
+ if stream_data and stream_data.extra is not None:
1294
+ self.format_function_call_results(
1295
+ messages=messages,
1296
+ function_call_results=function_call_results,
1297
+ compress_tool_results=_compress_tool_results,
1298
+ **stream_data.extra,
1299
+ )
1300
+ elif model_response and model_response.extra is not None:
1301
+ self.format_function_call_results(
1302
+ messages=messages,
1303
+ function_call_results=function_call_results,
1304
+ compress_tool_results=_compress_tool_results,
1305
+ **model_response.extra,
1306
+ )
1307
+ else:
1308
+ self.format_function_call_results(
1309
+ messages=messages,
1310
+ function_call_results=function_call_results,
1311
+ compress_tool_results=_compress_tool_results,
1312
+ )
1313
+
1314
+ # Handle function call media
1315
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
1316
+ self._handle_function_call_media(
1317
+ messages=messages,
1318
+ function_call_results=function_call_results,
1319
+ send_media_to_model=send_media_to_model,
1320
+ )
1321
+
1322
+ for function_call_result in function_call_results:
1323
+ function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
1324
+
1325
+ # Check if we should stop after tool calls
1326
+ if any(m.stop_after_tool_call for m in function_call_results):
1327
+ break
1328
+
1329
+ # If we have any tool calls that require confirmation, break the loop
1330
+ if any(fc.function.requires_confirmation for fc in function_calls_to_run):
1331
+ break
1332
+
1333
+ # If we have any tool calls that require external execution, break the loop
1334
+ if any(fc.function.external_execution for fc in function_calls_to_run):
1335
+ break
1336
+
1337
+ # If we have any tool calls that require user input, break the loop
1338
+ if any(fc.function.requires_user_input for fc in function_calls_to_run):
1339
+ break
1340
+
1341
+ # Continue loop to get next response
1342
+ continue
1343
+
1344
+ # No tool calls or finished processing them
1345
+ break
1346
+
1347
+ log_debug(f"{self.get_provider()} Response Stream End", center=True, symbol="-")
1348
+
1349
+ # Save streaming responses to cache if enabled
1350
+ if self.cache_response and cache_key and streaming_responses:
1351
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1352
+ finally:
1353
+ # Close the Gemini client
1354
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
1355
+ try:
1356
+ self.client.close() # type: ignore
1357
+ self.client = None
1358
+ except AttributeError:
1359
+ log_warning(
1360
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
1361
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
1362
+ )
1363
+
1364
+ async def aprocess_response_stream(
1365
+ self,
1366
+ messages: List[Message],
1367
+ assistant_message: Message,
1368
+ stream_data: MessageData,
1369
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
1370
+ tools: Optional[List[Dict[str, Any]]] = None,
1371
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
1372
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
1373
+ compress_tool_results: bool = False,
1374
+ ) -> AsyncIterator[ModelResponse]:
1375
+ """
1376
+ Process a streaming response from the model with retry logic for ModelProviderError.
1377
+ """
1378
+ async for response_delta in self._ainvoke_stream_with_retry(
1379
+ messages=messages,
1380
+ assistant_message=assistant_message,
1381
+ response_format=response_format,
1382
+ tools=tools,
1383
+ tool_choice=tool_choice or self._tool_choice,
1384
+ run_response=run_response,
1385
+ compress_tool_results=compress_tool_results,
1386
+ ):
1387
+ for model_response_delta in self._populate_stream_data(
1388
+ stream_data=stream_data,
1389
+ model_response_delta=response_delta,
1390
+ ):
1391
+ yield model_response_delta
1392
+
1393
+ # Populate assistant message from stream data after the stream ends
1394
+ self._populate_assistant_message_from_stream_data(assistant_message=assistant_message, stream_data=stream_data)
1395
+
1396
+ async def aresponse_stream(
1397
+ self,
1398
+ messages: List[Message],
1399
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
1400
+ tools: Optional[List[Union[Function, dict]]] = None,
1401
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
1402
+ tool_call_limit: Optional[int] = None,
1403
+ stream_model_response: bool = True,
1404
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
1405
+ send_media_to_model: bool = True,
1406
+ compression_manager: Optional["CompressionManager"] = None,
1407
+ ) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
1408
+ """
1409
+ Generate an asynchronous streaming response from the model.
1410
+ """
1411
+ try:
1412
+ # Check cache if enabled - capture key BEFORE streaming to avoid mismatch
1413
+ cache_key = None
1414
+ if self.cache_response:
1415
+ cache_key = self._get_model_cache_key(
1416
+ messages, stream=True, response_format=response_format, tools=tools
1417
+ )
1418
+ cached_data = self._get_cached_model_response(cache_key)
1419
+
1420
+ if cached_data:
1421
+ log_info("Cache hit for async streaming model response")
1422
+ # Yield cached responses
1423
+ for response in self._streaming_responses_from_cache(cached_data["streaming_responses"]):
1424
+ yield response
1425
+ return
1426
+
1427
+ log_info("Cache miss for async streaming model response")
1428
+
1429
+ # Track streaming responses for caching
1430
+ streaming_responses: List[ModelResponse] = []
1431
+
1432
+ log_debug(f"{self.get_provider()} Async Response Stream Start", center=True, symbol="-")
1433
+ log_debug(f"Model: {self.id}", center=True, symbol="-")
1434
+ _log_messages(messages)
1435
+
1436
+ _tool_dicts = self._format_tools(tools) if tools is not None else []
1437
+ _functions = {tool.name: tool for tool in tools if isinstance(tool, Function)} if tools is not None else {}
1438
+
1439
+ _compress_tool_results = compression_manager is not None and compression_manager.compress_tool_results
1440
+ _compression_manager = compression_manager if _compress_tool_results else None
1441
+
1442
+ function_call_count = 0
1443
+
1444
+ while True:
1445
+ # Compress existing tool results BEFORE making API call to avoid context overflow
1446
+ if _compression_manager is not None and await _compression_manager.ashould_compress(
1447
+ messages, tools, model=self, response_format=response_format
1448
+ ):
1449
+ await _compression_manager.acompress(messages)
1450
+
1451
+ # Create assistant message and stream data
1452
+ assistant_message = Message(role=self.assistant_message_role)
1453
+ stream_data = MessageData()
1454
+ model_response = ModelResponse()
1455
+ if stream_model_response:
1456
+ # Generate response
1457
+ async for model_response in self.aprocess_response_stream(
1458
+ messages=messages,
1459
+ assistant_message=assistant_message,
1460
+ stream_data=stream_data,
1461
+ response_format=response_format,
1462
+ tools=_tool_dicts,
1463
+ tool_choice=tool_choice or self._tool_choice,
1464
+ run_response=run_response,
1465
+ compress_tool_results=_compress_tool_results,
1466
+ ):
1467
+ if self.cache_response and isinstance(model_response, ModelResponse):
1468
+ streaming_responses.append(model_response)
1469
+ yield model_response
1470
+
1471
+ else:
1472
+ await self._aprocess_model_response(
1473
+ messages=messages,
1474
+ assistant_message=assistant_message,
1475
+ model_response=model_response,
1476
+ response_format=response_format,
1477
+ tools=_tool_dicts,
1478
+ tool_choice=tool_choice or self._tool_choice,
1479
+ run_response=run_response,
1480
+ compress_tool_results=_compress_tool_results,
1481
+ )
1482
+ if self.cache_response:
1483
+ streaming_responses.append(model_response)
1484
+ yield model_response
1485
+
1486
+ # Add assistant message to messages
1487
+ messages.append(assistant_message)
1488
+ assistant_message.log(metrics=True)
1489
+
1490
+ # Handle tool calls if present
1491
+ if assistant_message.tool_calls is not None:
1492
+ # Prepare function calls
1493
+ function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
1494
+ assistant_message=assistant_message, messages=messages, functions=_functions
1495
+ )
1496
+ function_call_results: List[Message] = []
1497
+
1498
+ # Execute function calls
1499
+ async for function_call_response in self.arun_function_calls(
1500
+ function_calls=function_calls_to_run,
1501
+ function_call_results=function_call_results,
1502
+ current_function_call_count=function_call_count,
1503
+ function_call_limit=tool_call_limit,
1504
+ ):
1505
+ if self.cache_response and isinstance(function_call_response, ModelResponse):
1506
+ streaming_responses.append(function_call_response)
1507
+ yield function_call_response
1508
+
1509
+ # Add a function call for each successful execution
1510
+ function_call_count += len(function_call_results)
1511
+
1512
+ # Format and add results to messages
1513
+ if stream_data and stream_data.extra is not None:
1514
+ self.format_function_call_results(
1515
+ messages=messages,
1516
+ function_call_results=function_call_results,
1517
+ compress_tool_results=_compress_tool_results,
1518
+ **stream_data.extra,
1519
+ )
1520
+ elif model_response and model_response.extra is not None:
1521
+ self.format_function_call_results(
1522
+ messages=messages,
1523
+ function_call_results=function_call_results,
1524
+ compress_tool_results=_compress_tool_results,
1525
+ **model_response.extra or {},
1526
+ )
1527
+ else:
1528
+ self.format_function_call_results(
1529
+ messages=messages,
1530
+ function_call_results=function_call_results,
1531
+ compress_tool_results=_compress_tool_results,
1532
+ )
1533
+
1534
+ # Handle function call media
1535
+ if any(msg.images or msg.videos or msg.audio or msg.files for msg in function_call_results):
1536
+ self._handle_function_call_media(
1537
+ messages=messages,
1538
+ function_call_results=function_call_results,
1539
+ send_media_to_model=send_media_to_model,
1540
+ )
1541
+
1542
+ for function_call_result in function_call_results:
1543
+ function_call_result.log(metrics=True, use_compressed_content=_compress_tool_results)
1544
+
1545
+ # Check if we should stop after tool calls
1546
+ if any(m.stop_after_tool_call for m in function_call_results):
1547
+ break
1548
+
1549
+ # If we have any tool calls that require confirmation, break the loop
1550
+ if any(fc.function.requires_confirmation for fc in function_calls_to_run):
1551
+ break
1552
+
1553
+ # If we have any tool calls that require external execution, break the loop
1554
+ if any(fc.function.external_execution for fc in function_calls_to_run):
1555
+ break
1556
+
1557
+ # If we have any tool calls that require user input, break the loop
1558
+ if any(fc.function.requires_user_input for fc in function_calls_to_run):
1559
+ break
1560
+
1561
+ # Continue loop to get next response
1562
+ continue
1563
+
1564
+ # No tool calls or finished processing them
1565
+ break
1566
+
1567
+ log_debug(f"{self.get_provider()} Async Response Stream End", center=True, symbol="-")
1568
+
1569
+ # Save streaming responses to cache if enabled
1570
+ if self.cache_response and cache_key and streaming_responses:
1571
+ self._save_streaming_responses_to_cache(cache_key, streaming_responses)
1572
+
1573
+ finally:
1574
+ # Close the Gemini client
1575
+ if self.__class__.__name__ == "Gemini" and self.client is not None:
1576
+ try:
1577
+ await self.client.aio.aclose() # type: ignore
1578
+ self.client = None
1579
+ except AttributeError:
1580
+ log_warning(
1581
+ "Your Gemini client is outdated. For Agno to properly handle the lifecycle of the client,"
1582
+ " please upgrade Gemini to the latest version: pip install -U google-genai"
1583
+ )
1584
+
1585
+ def _populate_assistant_message_from_stream_data(
1586
+ self, assistant_message: Message, stream_data: MessageData
1587
+ ) -> None:
1588
+ """
1589
+ Populate an assistant message with the stream data.
1590
+ """
1591
+ if stream_data.response_role is not None:
1592
+ assistant_message.role = stream_data.response_role
1593
+ if stream_data.response_metrics is not None:
1594
+ assistant_message.metrics = stream_data.response_metrics
1595
+ if stream_data.response_content:
1596
+ assistant_message.content = stream_data.response_content
1597
+ if stream_data.response_reasoning_content:
1598
+ assistant_message.reasoning_content = stream_data.response_reasoning_content
1599
+ if stream_data.response_redacted_reasoning_content:
1600
+ assistant_message.redacted_reasoning_content = stream_data.response_redacted_reasoning_content
1601
+ if stream_data.response_provider_data:
1602
+ assistant_message.provider_data = stream_data.response_provider_data
1603
+ if stream_data.response_citations:
1604
+ assistant_message.citations = stream_data.response_citations
1605
+ if stream_data.response_audio:
1606
+ assistant_message.audio_output = stream_data.response_audio
1607
+ if stream_data.response_image:
1608
+ assistant_message.image_output = stream_data.response_image
1609
+ if stream_data.response_video:
1610
+ assistant_message.video_output = stream_data.response_video
1611
+ if stream_data.response_file:
1612
+ assistant_message.file_output = stream_data.response_file
1613
+ if stream_data.response_tool_calls and len(stream_data.response_tool_calls) > 0:
1614
+ assistant_message.tool_calls = self.parse_tool_calls(stream_data.response_tool_calls)
1615
+
1616
+ def _populate_stream_data(
1617
+ self, stream_data: MessageData, model_response_delta: ModelResponse
1618
+ ) -> Iterator[ModelResponse]:
1619
+ """Update the stream data and assistant message with the model response."""
1620
+
1621
+ should_yield = False
1622
+ if model_response_delta.role is not None:
1623
+ stream_data.response_role = model_response_delta.role # type: ignore
1624
+
1625
+ if model_response_delta.response_usage is not None:
1626
+ if stream_data.response_metrics is None:
1627
+ stream_data.response_metrics = Metrics()
1628
+ stream_data.response_metrics += model_response_delta.response_usage
1629
+
1630
+ # Update stream_data content
1631
+ if model_response_delta.content is not None:
1632
+ stream_data.response_content += model_response_delta.content
1633
+ should_yield = True
1634
+
1635
+ if model_response_delta.reasoning_content is not None:
1636
+ stream_data.response_reasoning_content += model_response_delta.reasoning_content
1637
+ should_yield = True
1638
+
1639
+ if model_response_delta.redacted_reasoning_content is not None:
1640
+ stream_data.response_redacted_reasoning_content += model_response_delta.redacted_reasoning_content
1641
+ should_yield = True
1642
+
1643
+ if model_response_delta.citations is not None:
1644
+ stream_data.response_citations = model_response_delta.citations
1645
+ should_yield = True
1646
+
1647
+ if model_response_delta.provider_data:
1648
+ if stream_data.response_provider_data is None:
1649
+ stream_data.response_provider_data = {}
1650
+ stream_data.response_provider_data.update(model_response_delta.provider_data)
1651
+
1652
+ # Update stream_data tool calls
1653
+ if model_response_delta.tool_calls is not None:
1654
+ if stream_data.response_tool_calls is None:
1655
+ stream_data.response_tool_calls = []
1656
+ stream_data.response_tool_calls.extend(model_response_delta.tool_calls)
1657
+ should_yield = True
1658
+
1659
+ if model_response_delta.audio is not None and isinstance(model_response_delta.audio, Audio):
1660
+ if stream_data.response_audio is None:
1661
+ stream_data.response_audio = Audio(id=str(uuid4()), content="", transcript="")
1662
+
1663
+ from typing import cast
1664
+
1665
+ audio_response = cast(Audio, model_response_delta.audio)
1666
+
1667
+ # Update the stream data with audio information
1668
+ if audio_response.id is not None:
1669
+ stream_data.response_audio.id = audio_response.id # type: ignore
1670
+ if audio_response.content is not None:
1671
+ stream_data.response_audio.content += audio_response.content # type: ignore
1672
+ if audio_response.transcript is not None:
1673
+ stream_data.response_audio.transcript += audio_response.transcript # type: ignore
1674
+ if audio_response.expires_at is not None:
1675
+ stream_data.response_audio.expires_at = audio_response.expires_at
1676
+ if audio_response.mime_type is not None:
1677
+ stream_data.response_audio.mime_type = audio_response.mime_type
1678
+ stream_data.response_audio.sample_rate = audio_response.sample_rate
1679
+ stream_data.response_audio.channels = audio_response.channels
1680
+
1681
+ should_yield = True
1682
+
1683
+ if model_response_delta.images:
1684
+ if stream_data.response_image is None:
1685
+ stream_data.response_image = model_response_delta.images[-1]
1686
+ should_yield = True
1687
+
1688
+ if model_response_delta.videos:
1689
+ if stream_data.response_video is None:
1690
+ stream_data.response_video = model_response_delta.videos[-1]
1691
+ should_yield = True
1692
+
1693
+ if model_response_delta.extra is not None:
1694
+ if stream_data.extra is None:
1695
+ stream_data.extra = {}
1696
+ for key in model_response_delta.extra:
1697
+ if isinstance(model_response_delta.extra[key], list):
1698
+ if not stream_data.extra.get(key):
1699
+ stream_data.extra[key] = []
1700
+ stream_data.extra[key].extend(model_response_delta.extra[key])
1701
+ else:
1702
+ stream_data.extra[key] = model_response_delta.extra[key]
1703
+
1704
+ if should_yield:
1705
+ yield model_response_delta
1706
+
1707
+ def parse_tool_calls(self, tool_calls_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
1708
+ """
1709
+ Parse the tool calls from the model provider into a list of tool calls.
1710
+ """
1711
+ return tool_calls_data
1712
+
1713
+ def get_function_call_to_run_from_tool_execution(
1714
+ self,
1715
+ tool_execution: ToolExecution,
1716
+ functions: Optional[Dict[str, Function]] = None,
1717
+ ) -> FunctionCall:
1718
+ function_call = get_function_call_for_tool_execution(
1719
+ tool_execution=tool_execution,
1720
+ functions=functions,
1721
+ )
1722
+ if function_call is None:
1723
+ raise ValueError("Function call not found")
1724
+ return function_call
1725
+
1726
+ def get_function_calls_to_run(
1727
+ self,
1728
+ assistant_message: Message,
1729
+ messages: List[Message],
1730
+ functions: Optional[Dict[str, Function]] = None,
1731
+ ) -> List[FunctionCall]:
1732
+ """
1733
+ Prepare function calls for the assistant message.
223
1734
  """
224
1735
  function_calls_to_run: List[FunctionCall] = []
225
1736
  if assistant_message.tool_calls is not None:
226
1737
  for tool_call in assistant_message.tool_calls:
227
- _function_call = get_function_call_for_tool_call(tool_call, self._functions)
1738
+ _tool_call_id = tool_call.get("id")
1739
+ _function_call = get_function_call_for_tool_call(tool_call, functions)
228
1740
  if _function_call is None:
229
- messages.append(Message(role=error_response_role, content="Could not find function to call."))
1741
+ messages.append(
1742
+ Message(
1743
+ role=self.tool_message_role,
1744
+ tool_call_id=_tool_call_id,
1745
+ content="Error: The requested tool does not exist or is not available.",
1746
+ )
1747
+ )
230
1748
  continue
231
1749
  if _function_call.error is not None:
232
- messages.append(Message(role=error_response_role, content=_function_call.error))
1750
+ messages.append(
1751
+ Message(role=self.tool_message_role, tool_call_id=_tool_call_id, content=_function_call.error)
1752
+ )
233
1753
  continue
234
1754
  function_calls_to_run.append(_function_call)
235
1755
  return function_calls_to_run
236
1756
 
237
- def add_tool(
238
- self, tool: Union[Toolkit, Callable, Dict, Function], strict: bool = False, agent: Optional[Any] = None
239
- ) -> None:
240
- if self.tools is None:
241
- self.tools = []
242
-
243
- # If the tool is a Dict, add it directly to the Model
244
- if isinstance(tool, Dict):
245
- if tool not in self.tools:
246
- self.tools.append(tool)
247
- logger.debug(f"Added tool {tool} to model.")
248
-
249
- # If the tool is a Callable or Toolkit, process and add to the Model
250
- elif callable(tool) or isinstance(tool, Toolkit) or isinstance(tool, Function):
251
- if self._functions is None:
252
- self._functions = {}
253
-
254
- if isinstance(tool, Toolkit):
255
- # For each function in the toolkit, process entrypoint and add to self.tools
256
- for name, func in tool.functions.items():
257
- # If the function does not exist in self.functions, add to self.tools
258
- if name not in self._functions:
259
- func._agent = agent
260
- func.process_entrypoint(strict=strict)
261
- if strict and self.supports_structured_outputs:
262
- func.strict = True
263
- self._functions[name] = func
264
- self.tools.append({"type": "function", "function": func.to_dict()})
265
- logger.debug(f"Function {name} from {tool.name} added to model.")
266
-
267
- elif isinstance(tool, Function):
268
- if tool.name not in self._functions:
269
- tool._agent = agent
270
- tool.process_entrypoint(strict=strict)
271
- if strict and self.supports_structured_outputs:
272
- tool.strict = True
273
- self._functions[tool.name] = tool
274
- self.tools.append({"type": "function", "function": tool.to_dict()})
275
- logger.debug(f"Function {tool.name} added to model.")
276
-
277
- elif callable(tool):
278
- try:
279
- function_name = tool.__name__
280
- if function_name not in self._functions:
281
- func = Function.from_callable(tool, strict=strict)
282
- func._agent = agent
283
- if strict and self.supports_structured_outputs:
284
- func.strict = True
285
- self._functions[func.name] = func
286
- self.tools.append({"type": "function", "function": func.to_dict()})
287
- logger.debug(f"Function {func.name} added to model.")
288
- except Exception as e:
289
- logger.warning(f"Could not add function {tool}: {e}")
290
-
291
- def run_function_calls(
292
- self, function_calls: List[FunctionCall], function_call_results: List[Message], tool_role: str = "tool"
293
- ) -> Iterator[ModelResponse]:
294
- for function_call in function_calls:
295
- if self._function_call_stack is None:
296
- self._function_call_stack = []
1757
+ def create_function_call_result(
1758
+ self,
1759
+ function_call: FunctionCall,
1760
+ success: bool,
1761
+ output: Optional[Union[List[Any], str]] = None,
1762
+ timer: Optional[Timer] = None,
1763
+ function_execution_result: Optional[FunctionExecutionResult] = None,
1764
+ ) -> Message:
1765
+ """Create a function call result message."""
1766
+ kwargs = {}
1767
+ if timer is not None:
1768
+ kwargs["metrics"] = Metrics(duration=timer.elapsed)
1769
+
1770
+ # Include media artifacts from function execution result in the tool message
1771
+ images = None
1772
+ videos = None
1773
+ audios = None
1774
+ files = None
1775
+
1776
+ if success and function_execution_result:
1777
+ # With unified classes, no conversion needed - use directly
1778
+ images = function_execution_result.images
1779
+ videos = function_execution_result.videos
1780
+ audios = function_execution_result.audios
1781
+ files = function_execution_result.files
1782
+
1783
+ return Message(
1784
+ role=self.tool_message_role,
1785
+ content=output if success else function_call.error,
1786
+ tool_call_id=function_call.call_id,
1787
+ tool_name=function_call.function.name,
1788
+ tool_args=function_call.arguments,
1789
+ tool_call_error=not success,
1790
+ stop_after_tool_call=function_call.function.stop_after_tool_call,
1791
+ images=images,
1792
+ videos=videos,
1793
+ audio=audios,
1794
+ files=files,
1795
+ **kwargs, # type: ignore
1796
+ )
297
1797
 
298
- # -*- Start function call
299
- function_call_timer = Timer()
300
- function_call_timer.start()
301
- yield ModelResponse(
302
- content=function_call.get_call_str(),
303
- tool_calls=[
304
- {
305
- "role": tool_role,
306
- "tool_call_id": function_call.call_id,
307
- "tool_name": function_call.function.name,
308
- "tool_args": function_call.arguments,
309
- }
310
- ],
311
- event=ModelResponseEvent.tool_call_started.value,
312
- )
1798
+ def create_tool_call_limit_error_result(self, function_call: FunctionCall) -> Message:
1799
+ return Message(
1800
+ role=self.tool_message_role,
1801
+ content=f"Tool call limit reached. Tool call {function_call.function.name} not executed. Don't try to execute it again.",
1802
+ tool_call_id=function_call.call_id,
1803
+ tool_name=function_call.function.name,
1804
+ tool_args=function_call.arguments,
1805
+ tool_call_error=True,
1806
+ )
313
1807
 
314
- # Track if the function call was successful
315
- function_call_success = False
316
- # If True, stop execution after this function call
317
- stop_execution_after_tool_call = False
318
- # Additional messages from the function call that will be added to the function call results
319
- additional_messages_from_function_call = []
1808
+ def run_function_call(
1809
+ self,
1810
+ function_call: FunctionCall,
1811
+ function_call_results: List[Message],
1812
+ additional_input: Optional[List[Message]] = None,
1813
+ ) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
1814
+ # Start function call
1815
+ function_call_timer = Timer()
1816
+ function_call_timer.start()
1817
+ # Yield a tool_call_started event
1818
+ yield ModelResponse(
1819
+ content=function_call.get_call_str(),
1820
+ tool_executions=[
1821
+ ToolExecution(
1822
+ tool_call_id=function_call.call_id,
1823
+ tool_name=function_call.function.name,
1824
+ tool_args=function_call.arguments,
1825
+ )
1826
+ ],
1827
+ event=ModelResponseEvent.tool_call_started.value,
1828
+ )
320
1829
 
321
- # -*- Run function call
1830
+ # Run function calls sequentially
1831
+ function_execution_result: FunctionExecutionResult = FunctionExecutionResult(status="failure")
1832
+ stop_after_tool_call_from_exception = False
1833
+ try:
1834
+ function_execution_result = function_call.execute()
1835
+ except AgentRunException as a_exc:
1836
+ # Update additional messages from function call
1837
+ _handle_agent_exception(a_exc, additional_input)
1838
+ # If stop_execution is True, mark that we should stop after this tool call
1839
+ if a_exc.stop_execution:
1840
+ stop_after_tool_call_from_exception = True
1841
+ # Set function call success to False if an exception occurred
1842
+ except Exception as e:
1843
+ log_error(f"Error executing function {function_call.function.name}: {e}")
1844
+ raise e
1845
+
1846
+ function_call_success = function_execution_result.status == "success"
1847
+
1848
+ # Stop function call timer
1849
+ function_call_timer.stop()
1850
+
1851
+ # Process function call output
1852
+ function_call_output: str = ""
1853
+
1854
+ if isinstance(function_execution_result.result, (GeneratorType, collections.abc.Iterator)):
322
1855
  try:
323
- function_call_success = function_call.execute()
324
- except AgentRunException as a_exc:
325
- if a_exc.user_message is not None:
326
- if isinstance(a_exc.user_message, str):
327
- additional_messages_from_function_call.append(Message(role="user", content=a_exc.user_message))
328
- else:
329
- additional_messages_from_function_call.append(a_exc.user_message)
330
- if a_exc.agent_message is not None:
331
- if isinstance(a_exc.agent_message, str):
332
- additional_messages_from_function_call.append(
333
- Message(role="assistant", content=a_exc.agent_message)
334
- )
1856
+ for item in function_execution_result.result:
1857
+ # This function yields agent/team/workflow run events
1858
+ if (
1859
+ isinstance(item, tuple(get_args(RunOutputEvent)))
1860
+ or isinstance(item, tuple(get_args(TeamRunOutputEvent)))
1861
+ or isinstance(item, tuple(get_args(WorkflowRunOutputEvent)))
1862
+ ):
1863
+ # We only capture content events for output accumulation
1864
+ if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
1865
+ if item.content is not None and isinstance(item.content, BaseModel):
1866
+ function_call_output += item.content.model_dump_json()
1867
+ else:
1868
+ # Capture output
1869
+ function_call_output += item.content or ""
1870
+
1871
+ if function_call.function.show_result and item.content is not None:
1872
+ yield ModelResponse(content=item.content)
1873
+
1874
+ if isinstance(item, CustomEvent):
1875
+ function_call_output += str(item)
1876
+
1877
+ # For WorkflowCompletedEvent, extract content for final output
1878
+ from agno.run.workflow import WorkflowCompletedEvent
1879
+
1880
+ if isinstance(item, WorkflowCompletedEvent):
1881
+ if item.content is not None:
1882
+ if isinstance(item.content, BaseModel):
1883
+ function_call_output += item.content.model_dump_json()
1884
+ else:
1885
+ function_call_output += str(item.content)
1886
+
1887
+ # Yield the event itself to bubble it up
1888
+ yield item
1889
+
335
1890
  else:
336
- additional_messages_from_function_call.append(a_exc.agent_message)
337
- if a_exc.messages is not None and len(a_exc.messages) > 0:
338
- for m in a_exc.messages:
339
- if isinstance(m, Message):
340
- additional_messages_from_function_call.append(m)
341
- elif isinstance(m, dict):
342
- try:
343
- additional_messages_from_function_call.append(Message(**m))
344
- except Exception as e:
345
- logger.warning(f"Failed to convert dict to Message: {e}")
346
- if a_exc.stop_execution:
347
- stop_execution_after_tool_call = True
348
- if len(additional_messages_from_function_call) > 0:
349
- for m in additional_messages_from_function_call:
350
- m.stop_after_tool_call = True
351
-
352
- function_call_output: Optional[Union[List[Any], str]] = ""
353
- if isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
354
- for item in function_call.result:
355
- function_call_output += item
356
- if function_call.function.show_result:
357
- yield ModelResponse(content=item)
1891
+ function_call_output += str(item)
1892
+ if function_call.function.show_result and item is not None:
1893
+ yield ModelResponse(content=str(item))
1894
+ except Exception as e:
1895
+ log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
1896
+ function_call.error = str(e)
1897
+ function_call_success = False
1898
+
1899
+ # For generators, re-capture updated_session_state after consumption
1900
+ # since session_state modifications were made during iteration
1901
+ if function_execution_result.updated_session_state is None:
1902
+ if (
1903
+ function_call.function._run_context is not None
1904
+ and function_call.function._run_context.session_state is not None
1905
+ ):
1906
+ function_execution_result.updated_session_state = function_call.function._run_context.session_state
1907
+ elif function_call.function._session_state is not None:
1908
+ function_execution_result.updated_session_state = function_call.function._session_state
1909
+ else:
1910
+ from agno.tools.function import ToolResult
1911
+
1912
+ if isinstance(function_execution_result.result, ToolResult):
1913
+ # Extract content and media from ToolResult
1914
+ tool_result = function_execution_result.result
1915
+ function_call_output = tool_result.content
1916
+
1917
+ # Transfer media from ToolResult to FunctionExecutionResult
1918
+ if tool_result.images:
1919
+ function_execution_result.images = tool_result.images
1920
+ if tool_result.videos:
1921
+ function_execution_result.videos = tool_result.videos
1922
+ if tool_result.audios:
1923
+ function_execution_result.audios = tool_result.audios
1924
+ if tool_result.files:
1925
+ function_execution_result.files = tool_result.files
358
1926
  else:
359
- function_call_output = function_call.result
360
- if function_call.function.show_result:
361
- yield ModelResponse(content=function_call_output)
1927
+ function_call_output = str(function_execution_result.result) if function_execution_result.result else ""
1928
+
1929
+ if function_call.function.show_result and function_call_output is not None:
1930
+ yield ModelResponse(content=function_call_output)
1931
+
1932
+ # Create and yield function call result
1933
+ function_call_result = self.create_function_call_result(
1934
+ function_call,
1935
+ success=function_call_success,
1936
+ output=function_call_output,
1937
+ timer=function_call_timer,
1938
+ function_execution_result=function_execution_result,
1939
+ )
1940
+ # Override stop_after_tool_call if set by exception
1941
+ if stop_after_tool_call_from_exception:
1942
+ function_call_result.stop_after_tool_call = True
1943
+ yield ModelResponse(
1944
+ content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
1945
+ tool_executions=[
1946
+ ToolExecution(
1947
+ tool_call_id=function_call_result.tool_call_id,
1948
+ tool_name=function_call_result.tool_name,
1949
+ tool_args=function_call_result.tool_args,
1950
+ tool_call_error=function_call_result.tool_call_error,
1951
+ result=str(function_call_result.content),
1952
+ stop_after_tool_call=function_call_result.stop_after_tool_call,
1953
+ metrics=function_call_result.metrics,
1954
+ )
1955
+ ],
1956
+ event=ModelResponseEvent.tool_call_completed.value,
1957
+ updated_session_state=function_execution_result.updated_session_state,
1958
+ # Add media artifacts from function execution
1959
+ images=function_execution_result.images,
1960
+ videos=function_execution_result.videos,
1961
+ audios=function_execution_result.audios,
1962
+ files=function_execution_result.files,
1963
+ )
362
1964
 
363
- # -*- Stop function call timer
364
- function_call_timer.stop()
365
-
366
- # -*- Create function call result message
367
- function_call_result = Message(
368
- role=tool_role,
369
- content=function_call_output if function_call_success else function_call.error,
370
- tool_call_id=function_call.call_id,
371
- tool_name=function_call.function.name,
372
- tool_args=function_call.arguments,
373
- tool_call_error=not function_call_success,
374
- stop_after_tool_call=function_call.function.stop_after_tool_call or stop_execution_after_tool_call,
375
- metrics={"time": function_call_timer.elapsed},
376
- )
1965
+ # Add function call to function call results
1966
+ function_call_results.append(function_call_result)
377
1967
 
378
- # -*- Yield function call result
379
- yield ModelResponse(
380
- content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s.",
381
- tool_calls=[
382
- function_call_result.model_dump(
383
- include={
384
- "content",
385
- "tool_call_id",
386
- "tool_name",
387
- "tool_args",
388
- "tool_call_error",
389
- "metrics",
390
- "created_at",
391
- }
1968
+ def run_function_calls(
1969
+ self,
1970
+ function_calls: List[FunctionCall],
1971
+ function_call_results: List[Message],
1972
+ additional_input: Optional[List[Message]] = None,
1973
+ current_function_call_count: int = 0,
1974
+ function_call_limit: Optional[int] = None,
1975
+ ) -> Iterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
1976
+ # Additional messages from function calls that will be added to the function call results
1977
+ if additional_input is None:
1978
+ additional_input = []
1979
+
1980
+ for fc in function_calls:
1981
+ if function_call_limit is not None:
1982
+ current_function_call_count += 1
1983
+ # We have reached the function call limit, so we add an error result to the function call results
1984
+ if current_function_call_count > function_call_limit:
1985
+ function_call_results.append(self.create_tool_call_limit_error_result(fc))
1986
+ continue
1987
+
1988
+ paused_tool_executions = []
1989
+
1990
+ # The function requires user confirmation (HITL)
1991
+ if fc.function.requires_confirmation:
1992
+ paused_tool_executions.append(
1993
+ ToolExecution(
1994
+ tool_call_id=fc.call_id,
1995
+ tool_name=fc.function.name,
1996
+ tool_args=fc.arguments,
1997
+ requires_confirmation=True,
392
1998
  )
393
- ],
394
- event=ModelResponseEvent.tool_call_completed.value,
395
- )
1999
+ )
396
2000
 
397
- # Add metrics to the model
398
- if "tool_call_times" not in self.metrics:
399
- self.metrics["tool_call_times"] = {}
400
- if function_call.function.name not in self.metrics["tool_call_times"]:
401
- self.metrics["tool_call_times"][function_call.function.name] = []
402
- self.metrics["tool_call_times"][function_call.function.name].append(function_call_timer.elapsed)
2001
+ # The function requires user input (HITL)
2002
+ if fc.function.requires_user_input:
2003
+ user_input_schema = fc.function.user_input_schema
2004
+ if fc.arguments and user_input_schema:
2005
+ for name, value in fc.arguments.items():
2006
+ for user_input_field in user_input_schema:
2007
+ if user_input_field.name == name:
2008
+ user_input_field.value = value
2009
+
2010
+ paused_tool_executions.append(
2011
+ ToolExecution(
2012
+ tool_call_id=fc.call_id,
2013
+ tool_name=fc.function.name,
2014
+ tool_args=fc.arguments,
2015
+ requires_user_input=True,
2016
+ user_input_schema=user_input_schema,
2017
+ )
2018
+ )
403
2019
 
404
- # Add the function call result to the function call results
405
- function_call_results.append(function_call_result)
406
- if len(additional_messages_from_function_call) > 0:
407
- function_call_results.extend(additional_messages_from_function_call)
408
- self._function_call_stack.append(function_call)
409
-
410
- # -*- Check function call limit
411
- if self.tool_call_limit and len(self._function_call_stack) >= self.tool_call_limit:
412
- # Deactivate tool calls by setting future tool calls to "none"
413
- self.tool_choice = "none"
414
- break # Exit early if we reach the function call limit
415
-
416
- def _handle_response_after_tool_calls(
417
- self, response_after_tool_calls: ModelResponse, model_response: ModelResponse
418
- ):
419
- if response_after_tool_calls.content is not None:
420
- if model_response.content is None:
421
- model_response.content = ""
422
- model_response.content += response_after_tool_calls.content
423
- if response_after_tool_calls.parsed is not None:
424
- # bubble up the parsed object, so that the final response has the parsed object
425
- # that is visible to the agent
426
- model_response.parsed = response_after_tool_calls.parsed
427
- if response_after_tool_calls.audio is not None:
428
- # bubble up the audio, so that the final response has the audio
429
- # that is visible to the agent
430
- model_response.audio = response_after_tool_calls.audio
431
-
432
- def _handle_stop_after_tool_calls(self, last_message: Message, model_response: ModelResponse):
433
- logger.debug("Stopping execution as stop_after_tool_call=True")
434
- if (
435
- last_message.role == "assistant"
436
- and last_message.content is not None
437
- and isinstance(last_message.content, str)
438
- ):
439
- if model_response.content is None:
440
- model_response.content = ""
441
- model_response.content += last_message.content
2020
+ # If the function is from the user control flow (HITL) tools, we handle it here
2021
+ if fc.function.name == "get_user_input" and fc.arguments and fc.arguments.get("user_input_fields"):
2022
+ user_input_schema = []
2023
+ for input_field in fc.arguments.get("user_input_fields", []):
2024
+ field_type = input_field.get("field_type")
2025
+ try:
2026
+ python_type = eval(field_type) if isinstance(field_type, str) else field_type
2027
+ except (NameError, SyntaxError):
2028
+ python_type = str # Default to str if type is invalid
2029
+ user_input_schema.append(
2030
+ UserInputField(
2031
+ name=input_field.get("field_name"),
2032
+ field_type=python_type,
2033
+ description=input_field.get("field_description"),
2034
+ )
2035
+ )
442
2036
 
443
- def handle_post_tool_call_messages(self, messages: List[Message], model_response: ModelResponse) -> ModelResponse:
444
- last_message = messages[-1]
445
- if last_message.stop_after_tool_call:
446
- self._handle_stop_after_tool_calls(last_message, model_response)
447
- else:
448
- response_after_tool_calls = self.response(messages=messages)
449
- self._handle_response_after_tool_calls(response_after_tool_calls, model_response)
450
- return model_response
2037
+ paused_tool_executions.append(
2038
+ ToolExecution(
2039
+ tool_call_id=fc.call_id,
2040
+ tool_name=fc.function.name,
2041
+ tool_args=fc.arguments,
2042
+ requires_user_input=True,
2043
+ user_input_schema=user_input_schema,
2044
+ )
2045
+ )
451
2046
 
452
- async def ahandle_post_tool_call_messages(
453
- self, messages: List[Message], model_response: ModelResponse
454
- ) -> ModelResponse:
455
- last_message = messages[-1]
456
- if last_message.stop_after_tool_call:
457
- self._handle_stop_after_tool_calls(last_message, model_response)
458
- else:
459
- response_after_tool_calls = await self.aresponse(messages=messages)
460
- self._handle_response_after_tool_calls(response_after_tool_calls, model_response)
461
- return model_response
2047
+ # The function requires external execution (HITL)
2048
+ if fc.function.external_execution:
2049
+ paused_tool_executions.append(
2050
+ ToolExecution(
2051
+ tool_call_id=fc.call_id,
2052
+ tool_name=fc.function.name,
2053
+ tool_args=fc.arguments,
2054
+ external_execution_required=True,
2055
+ )
2056
+ )
2057
+
2058
+ if paused_tool_executions:
2059
+ yield ModelResponse(
2060
+ tool_executions=paused_tool_executions,
2061
+ event=ModelResponseEvent.tool_call_paused.value,
2062
+ )
2063
+ # We don't execute the function calls here
2064
+ continue
2065
+
2066
+ yield from self.run_function_call(
2067
+ function_call=fc, function_call_results=function_call_results, additional_input=additional_input
2068
+ )
2069
+
2070
+ # Add any additional messages at the end
2071
+ if additional_input:
2072
+ function_call_results.extend(additional_input)
2073
+
2074
+ async def arun_function_call(
2075
+ self,
2076
+ function_call: FunctionCall,
2077
+ ) -> Tuple[Union[bool, AgentRunException], Timer, FunctionCall, FunctionExecutionResult]:
2078
+ """Run a single function call and return its success status, timer, and the FunctionCall object."""
2079
+ from inspect import isasyncgenfunction, iscoroutine, iscoroutinefunction
462
2080
 
463
- def handle_post_tool_call_messages_stream(self, messages: List[Message]) -> Iterator[ModelResponse]:
464
- last_message = messages[-1]
465
- if last_message.stop_after_tool_call:
466
- logger.debug("Stopping execution as stop_after_tool_call=True")
2081
+ function_call_timer = Timer()
2082
+ function_call_timer.start()
2083
+ success: Union[bool, AgentRunException] = False
2084
+ result: FunctionExecutionResult = FunctionExecutionResult(status="failure")
2085
+
2086
+ try:
467
2087
  if (
468
- last_message.role == "assistant"
469
- and last_message.content is not None
470
- and isinstance(last_message.content, str)
2088
+ iscoroutinefunction(function_call.function.entrypoint)
2089
+ or isasyncgenfunction(function_call.function.entrypoint)
2090
+ or iscoroutine(function_call.function.entrypoint)
471
2091
  ):
472
- yield ModelResponse(content=last_message.content)
473
- else:
474
- yield from self.response_stream(messages=messages)
2092
+ result = await function_call.aexecute()
2093
+ success = result.status == "success"
475
2094
 
476
- async def ahandle_post_tool_call_messages_stream(self, messages: List[Message]) -> Any:
477
- last_message = messages[-1]
478
- if last_message.stop_after_tool_call:
479
- logger.debug("Stopping execution as stop_after_tool_call=True")
2095
+ # If any of the hooks are async, we need to run the function call asynchronously
2096
+ elif function_call.function.tool_hooks is not None and any(
2097
+ iscoroutinefunction(f) for f in function_call.function.tool_hooks
2098
+ ):
2099
+ result = await function_call.aexecute()
2100
+ success = result.status == "success"
2101
+ else:
2102
+ result = await asyncio.to_thread(function_call.execute)
2103
+ success = result.status == "success"
2104
+ except AgentRunException as e:
2105
+ success = e
2106
+ except Exception as e:
2107
+ log_error(f"Error executing function {function_call.function.name}: {e}")
2108
+ success = False
2109
+ raise e
2110
+
2111
+ function_call_timer.stop()
2112
+ return success, function_call_timer, function_call, result
2113
+
2114
+ async def arun_function_calls(
2115
+ self,
2116
+ function_calls: List[FunctionCall],
2117
+ function_call_results: List[Message],
2118
+ additional_input: Optional[List[Message]] = None,
2119
+ current_function_call_count: int = 0,
2120
+ function_call_limit: Optional[int] = None,
2121
+ skip_pause_check: bool = False,
2122
+ ) -> AsyncIterator[Union[ModelResponse, RunOutputEvent, TeamRunOutputEvent]]:
2123
+ # Additional messages from function calls that will be added to the function call results
2124
+ if additional_input is None:
2125
+ additional_input = []
2126
+
2127
+ function_calls_to_run = []
2128
+ for fc in function_calls:
2129
+ if function_call_limit is not None:
2130
+ current_function_call_count += 1
2131
+ # We have reached the function call limit, so we add an error result to the function call results
2132
+ if current_function_call_count > function_call_limit:
2133
+ function_call_results.append(self.create_tool_call_limit_error_result(fc))
2134
+ # Skip this function call
2135
+ continue
2136
+ function_calls_to_run.append(fc)
2137
+
2138
+ # Yield tool_call_started events for all function calls or pause them
2139
+ for fc in function_calls_to_run:
2140
+ paused_tool_executions = []
2141
+ # The function cannot be executed without user confirmation
2142
+ if fc.function.requires_confirmation and not skip_pause_check:
2143
+ paused_tool_executions.append(
2144
+ ToolExecution(
2145
+ tool_call_id=fc.call_id,
2146
+ tool_name=fc.function.name,
2147
+ tool_args=fc.arguments,
2148
+ requires_confirmation=True,
2149
+ )
2150
+ )
2151
+ # If the function requires user input, we yield a message to the user
2152
+ if fc.function.requires_user_input and not skip_pause_check:
2153
+ user_input_schema = fc.function.user_input_schema
2154
+ if fc.arguments and user_input_schema:
2155
+ for name, value in fc.arguments.items():
2156
+ for user_input_field in user_input_schema:
2157
+ if user_input_field.name == name:
2158
+ user_input_field.value = value
2159
+
2160
+ paused_tool_executions.append(
2161
+ ToolExecution(
2162
+ tool_call_id=fc.call_id,
2163
+ tool_name=fc.function.name,
2164
+ tool_args=fc.arguments,
2165
+ requires_user_input=True,
2166
+ user_input_schema=user_input_schema,
2167
+ )
2168
+ )
2169
+ # If the function is from the user control flow tools, we handle it here
480
2170
  if (
481
- last_message.role == "assistant"
482
- and last_message.content is not None
483
- and isinstance(last_message.content, str)
2171
+ fc.function.name == "get_user_input"
2172
+ and fc.arguments
2173
+ and fc.arguments.get("user_input_fields")
2174
+ and not skip_pause_check
484
2175
  ):
485
- yield ModelResponse(content=last_message.content)
486
- else:
487
- async for model_response in self.aresponse_stream(messages=messages): # type: ignore
488
- yield model_response
2176
+ fc.function.requires_user_input = True
2177
+ user_input_schema = []
2178
+ for input_field in fc.arguments.get("user_input_fields", []):
2179
+ field_type = input_field.get("field_type")
2180
+ try:
2181
+ python_type = eval(field_type) if isinstance(field_type, str) else field_type
2182
+ except (NameError, SyntaxError):
2183
+ python_type = str # Default to str if type is invalid
2184
+ user_input_schema.append(
2185
+ UserInputField(
2186
+ name=input_field.get("field_name"),
2187
+ field_type=python_type,
2188
+ description=input_field.get("field_description"),
2189
+ )
2190
+ )
489
2191
 
490
- def _process_image_url(self, image_url: str) -> Dict[str, Any]:
491
- """Process image (base64 or URL)."""
2192
+ paused_tool_executions.append(
2193
+ ToolExecution(
2194
+ tool_call_id=fc.call_id,
2195
+ tool_name=fc.function.name,
2196
+ tool_args=fc.arguments,
2197
+ requires_user_input=True,
2198
+ user_input_schema=user_input_schema,
2199
+ )
2200
+ )
2201
+ # If the function requires external execution, we yield a message to the user
2202
+ if fc.function.external_execution and not skip_pause_check:
2203
+ paused_tool_executions.append(
2204
+ ToolExecution(
2205
+ tool_call_id=fc.call_id,
2206
+ tool_name=fc.function.name,
2207
+ tool_args=fc.arguments,
2208
+ external_execution_required=True,
2209
+ )
2210
+ )
492
2211
 
493
- if image_url.startswith("data:image") or image_url.startswith(("http://", "https://")):
494
- return {"type": "image_url", "image_url": {"url": image_url}}
495
- else:
496
- raise ValueError("Image URL must start with 'data:image' or 'http(s)://'.")
2212
+ if paused_tool_executions:
2213
+ yield ModelResponse(
2214
+ tool_executions=paused_tool_executions,
2215
+ event=ModelResponseEvent.tool_call_paused.value,
2216
+ )
2217
+ # We don't execute the function calls here
2218
+ continue
497
2219
 
498
- def _process_image_path(self, image_path: Union[Path, str]) -> Dict[str, Any]:
499
- """Process image ( file path)."""
500
- # Process local file image
501
- import base64
502
- import mimetypes
2220
+ yield ModelResponse(
2221
+ content=fc.get_call_str(),
2222
+ tool_executions=[
2223
+ ToolExecution(
2224
+ tool_call_id=fc.call_id,
2225
+ tool_name=fc.function.name,
2226
+ tool_args=fc.arguments,
2227
+ )
2228
+ ],
2229
+ event=ModelResponseEvent.tool_call_started.value,
2230
+ )
503
2231
 
504
- path = image_path if isinstance(image_path, Path) else Path(image_path)
505
- if not path.exists():
506
- raise FileNotFoundError(f"Image file not found: {image_path}")
2232
+ # Create and run all function calls in parallel (skip ones that need confirmation)
2233
+ if skip_pause_check:
2234
+ function_calls_to_run = function_calls_to_run
2235
+ else:
2236
+ function_calls_to_run = [
2237
+ fc
2238
+ for fc in function_calls_to_run
2239
+ if not (
2240
+ fc.function.requires_confirmation
2241
+ or fc.function.external_execution
2242
+ or fc.function.requires_user_input
2243
+ )
2244
+ ]
507
2245
 
508
- mime_type = mimetypes.guess_type(image_path)[0] or "image/jpeg"
509
- with open(path, "rb") as image_file:
510
- base64_image = base64.b64encode(image_file.read()).decode("utf-8")
511
- image_url = f"data:{mime_type};base64,{base64_image}"
512
- return {"type": "image_url", "image_url": {"url": image_url}}
2246
+ results = await asyncio.gather(
2247
+ *(self.arun_function_call(fc) for fc in function_calls_to_run), return_exceptions=True
2248
+ )
513
2249
 
514
- def _process_bytes_image(self, image: bytes) -> Dict[str, Any]:
515
- """Process bytes image data."""
516
- import base64
2250
+ # Separate async generators from other results for concurrent processing
2251
+ async_generator_results: List[Any] = []
2252
+ non_async_generator_results: List[Any] = []
517
2253
 
518
- base64_image = base64.b64encode(image).decode("utf-8")
519
- image_url = f"data:image/jpeg;base64,{base64_image}"
520
- return {"type": "image_url", "image_url": {"url": image_url}}
2254
+ for result in results:
2255
+ if isinstance(result, BaseException):
2256
+ non_async_generator_results.append(result)
2257
+ continue
521
2258
 
522
- def _process_image(self, image: Image) -> Optional[Dict[str, Any]]:
523
- """Process an image based on the format."""
2259
+ function_call_success, function_call_timer, function_call, function_execution_result = result
524
2260
 
525
- if image.url is not None:
526
- image_payload = self._process_image_url(image.url)
2261
+ # Check if this result contains an async generator
2262
+ if isinstance(function_call.result, (AsyncGeneratorType, AsyncIterator)):
2263
+ async_generator_results.append(result)
2264
+ else:
2265
+ non_async_generator_results.append(result)
527
2266
 
528
- elif image.filepath is not None:
529
- image_payload = self._process_image_path(image.filepath)
2267
+ # Process async generators with real-time event streaming using asyncio.Queue
2268
+ async_generator_outputs: Dict[int, Tuple[Any, str, Optional[BaseException]]] = {}
2269
+ event_queue: asyncio.Queue = asyncio.Queue()
2270
+ active_generators_count: int = len(async_generator_results)
530
2271
 
531
- elif image.content is not None:
532
- image_payload = self._process_bytes_image(image.content)
2272
+ # Create background tasks for each async generator
2273
+ async def process_async_generator(result, generator_id):
2274
+ function_call_success, function_call_timer, function_call, function_execution_result = result
2275
+ function_call_output = ""
533
2276
 
534
- else:
535
- logger.warning(f"Unsupported image type: {type(image)}")
536
- return None
2277
+ try:
2278
+ async for item in function_call.result:
2279
+ # This function yields agent/team/workflow run events
2280
+ if isinstance(
2281
+ item,
2282
+ tuple(get_args(RunOutputEvent))
2283
+ + tuple(get_args(TeamRunOutputEvent))
2284
+ + tuple(get_args(WorkflowRunOutputEvent)),
2285
+ ):
2286
+ # We only capture content events
2287
+ if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
2288
+ if item.content is not None and isinstance(item.content, BaseModel):
2289
+ function_call_output += item.content.model_dump_json()
2290
+ else:
2291
+ # Capture output
2292
+ function_call_output += item.content or ""
2293
+
2294
+ if function_call.function.show_result and item.content is not None:
2295
+ await event_queue.put(ModelResponse(content=item.content))
2296
+ continue
2297
+
2298
+ if isinstance(item, CustomEvent):
2299
+ function_call_output += str(item)
2300
+
2301
+ # For WorkflowCompletedEvent, extract content for final output
2302
+ from agno.run.workflow import WorkflowCompletedEvent
2303
+
2304
+ if isinstance(item, WorkflowCompletedEvent):
2305
+ if item.content is not None:
2306
+ if isinstance(item.content, BaseModel):
2307
+ function_call_output += item.content.model_dump_json()
2308
+ else:
2309
+ function_call_output += str(item.content)
2310
+
2311
+ # Put the event into the queue to be yielded
2312
+ await event_queue.put(item)
2313
+
2314
+ # Yield custom events emitted by the tool
2315
+ else:
2316
+ function_call_output += str(item)
2317
+ if function_call.function.show_result and item is not None:
2318
+ await event_queue.put(ModelResponse(content=str(item)))
537
2319
 
538
- if image.detail:
539
- image_payload["image_url"]["detail"] = image.detail
2320
+ # Store the final output for this generator
2321
+ async_generator_outputs[generator_id] = (result, function_call_output, None)
540
2322
 
541
- return image_payload
2323
+ except Exception as e:
2324
+ # Store the exception
2325
+ async_generator_outputs[generator_id] = (result, "", e)
542
2326
 
543
- def add_images_to_message(self, message: Message, images: Sequence[Image]) -> Message:
544
- """
545
- Add images to a message for the model. By default, we use the OpenAI image format but other Models
546
- can override this method to use a different image format.
2327
+ # Signal that this generator is done
2328
+ await event_queue.put(("GENERATOR_DONE", generator_id))
547
2329
 
548
- Args:
549
- message: The message for the Model
550
- images: Sequence of images in various formats:
551
- - str: base64 encoded image, URL, or file path
552
- - Dict: pre-formatted image data
553
- - bytes: raw image data
2330
+ # Start all async generator tasks
2331
+ generator_tasks = []
2332
+ for i, result in enumerate(async_generator_results):
2333
+ task = asyncio.create_task(process_async_generator(result, i))
2334
+ generator_tasks.append(task)
554
2335
 
555
- Returns:
556
- Message content with images added in the format expected by the model
557
- """
558
- # If no images are provided, return the message as is
559
- if len(images) == 0:
560
- return message
2336
+ # Stream events from the queue as they arrive
2337
+ completed_generators_count = 0
2338
+ while completed_generators_count < active_generators_count:
2339
+ try:
2340
+ event = await event_queue.get()
561
2341
 
562
- # Ignore non-string message content
563
- # because we assume that the images/audio are already added to the message
564
- if not isinstance(message.content, str):
565
- return message
2342
+ # Check if this is a completion signal
2343
+ if isinstance(event, tuple) and event[0] == "GENERATOR_DONE":
2344
+ completed_generators_count += 1
2345
+ continue
566
2346
 
567
- # Create a default message content with text
568
- message_content_with_image: List[Dict[str, Any]] = [{"type": "text", "text": message.content}]
2347
+ # Yield the actual event
2348
+ yield event
569
2349
 
570
- # Add images to the message content
571
- for image in images:
572
- try:
573
- image_data = self._process_image(image)
574
- if image_data:
575
- message_content_with_image.append(image_data)
576
2350
  except Exception as e:
577
- logger.error(f"Failed to process image: {str(e)}")
578
- continue
579
-
580
- # Update the message content with the images
581
- message.content = message_content_with_image
582
- return message
2351
+ log_error(f"Error processing async generator event: {e}")
2352
+ break
2353
+
2354
+ # Now process all results (non-async generators and completed async generators)
2355
+ for i, original_result in enumerate(results):
2356
+ # If result is an exception, skip processing it
2357
+ if isinstance(original_result, BaseException):
2358
+ log_error(f"Error during function call: {original_result}")
2359
+ raise original_result
2360
+
2361
+ # Unpack result
2362
+ function_call_success, function_call_timer, function_call, function_execution_result = original_result
2363
+
2364
+ # Check if this was an async generator that was already processed
2365
+ async_function_call_output = None
2366
+ if isinstance(function_call.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
2367
+ # Find the corresponding processed result
2368
+ async_gen_index = 0
2369
+ for j, result in enumerate(results[: i + 1]):
2370
+ if not isinstance(result, BaseException):
2371
+ _, _, fc, _ = result
2372
+ if isinstance(fc.result, (AsyncGeneratorType, collections.abc.AsyncIterator)):
2373
+ if j == i: # This is our async generator
2374
+ if async_gen_index in async_generator_outputs:
2375
+ _, async_function_call_output, error = async_generator_outputs[async_gen_index]
2376
+ if error:
2377
+ log_error(f"Error in async generator: {error}")
2378
+ raise error
2379
+ break
2380
+ async_gen_index += 1
2381
+
2382
+ updated_session_state = function_execution_result.updated_session_state
2383
+
2384
+ # Handle AgentRunException
2385
+ stop_after_tool_call_from_exception = False
2386
+ if isinstance(function_call_success, AgentRunException):
2387
+ a_exc = function_call_success
2388
+ # Update additional messages from function call
2389
+ _handle_agent_exception(a_exc, additional_input)
2390
+ # If stop_execution is True, mark that we should stop after this tool call
2391
+ if a_exc.stop_execution:
2392
+ stop_after_tool_call_from_exception = True
2393
+ # Set function call success to False if an exception occurred
2394
+ function_call_success = False
2395
+
2396
+ # Process function call output
2397
+ function_call_output: str = ""
2398
+
2399
+ # Check if this was an async generator that was already processed
2400
+ if async_function_call_output is not None:
2401
+ function_call_output = async_function_call_output
2402
+ # Events from async generators were already yielded in real-time above
2403
+ elif isinstance(function_call.result, (GeneratorType, collections.abc.Iterator)):
2404
+ try:
2405
+ for item in function_call.result:
2406
+ # This function yields agent/team/workflow run events
2407
+ if isinstance(
2408
+ item,
2409
+ tuple(get_args(RunOutputEvent))
2410
+ + tuple(get_args(TeamRunOutputEvent))
2411
+ + tuple(get_args(WorkflowRunOutputEvent)),
2412
+ ):
2413
+ # We only capture content events
2414
+ if isinstance(item, RunContentEvent) or isinstance(item, TeamRunContentEvent):
2415
+ if item.content is not None and isinstance(item.content, BaseModel):
2416
+ function_call_output += item.content.model_dump_json()
2417
+ else:
2418
+ # Capture output
2419
+ function_call_output += item.content or ""
2420
+
2421
+ if function_call.function.show_result and item.content is not None:
2422
+ yield ModelResponse(content=item.content)
2423
+ continue
2424
+
2425
+ # Yield the event itself to bubble it up
2426
+ yield item
2427
+ else:
2428
+ function_call_output += str(item)
2429
+ if function_call.function.show_result and item is not None:
2430
+ yield ModelResponse(content=str(item))
2431
+ except Exception as e:
2432
+ log_error(f"Error while iterating function result generator for {function_call.function.name}: {e}")
2433
+ function_call.error = str(e)
2434
+ function_call_success = False
2435
+
2436
+ # For generators (sync or async), re-capture updated_session_state after consumption
2437
+ # since session_state modifications were made during iteration
2438
+ if async_function_call_output is not None or isinstance(
2439
+ function_call.result,
2440
+ (GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
2441
+ ):
2442
+ if updated_session_state is None:
2443
+ if (
2444
+ function_call.function._run_context is not None
2445
+ and function_call.function._run_context.session_state is not None
2446
+ ):
2447
+ updated_session_state = function_call.function._run_context.session_state
2448
+ elif function_call.function._session_state is not None:
2449
+ updated_session_state = function_call.function._session_state
2450
+
2451
+ if not (
2452
+ async_function_call_output is not None
2453
+ or isinstance(
2454
+ function_call.result,
2455
+ (GeneratorType, collections.abc.Iterator, AsyncGeneratorType, collections.abc.AsyncIterator),
2456
+ )
2457
+ ):
2458
+ from agno.tools.function import ToolResult
2459
+
2460
+ if isinstance(function_execution_result.result, ToolResult):
2461
+ tool_result = function_execution_result.result
2462
+ function_call_output = tool_result.content
2463
+
2464
+ if tool_result.images:
2465
+ function_execution_result.images = tool_result.images
2466
+ if tool_result.videos:
2467
+ function_execution_result.videos = tool_result.videos
2468
+ if tool_result.audios:
2469
+ function_execution_result.audios = tool_result.audios
2470
+ if tool_result.files:
2471
+ function_execution_result.files = tool_result.files
2472
+ else:
2473
+ function_call_output = str(function_call.result)
2474
+
2475
+ if function_call.function.show_result and function_call_output is not None:
2476
+ yield ModelResponse(content=function_call_output)
583
2477
 
584
- @staticmethod
585
- def add_audio_to_message(message: Message, audio: Sequence[Audio]) -> Message:
586
- """
587
- Add audio to a message for the model. By default, we use the OpenAI audio format but other Models
588
- can override this method to use a different audio format.
2478
+ # Create and yield function call result
2479
+ function_call_result = self.create_function_call_result(
2480
+ function_call,
2481
+ success=function_call_success,
2482
+ output=function_call_output,
2483
+ timer=function_call_timer,
2484
+ function_execution_result=function_execution_result,
2485
+ )
2486
+ # Override stop_after_tool_call if set by exception
2487
+ if stop_after_tool_call_from_exception:
2488
+ function_call_result.stop_after_tool_call = True
2489
+ yield ModelResponse(
2490
+ content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
2491
+ tool_executions=[
2492
+ ToolExecution(
2493
+ tool_call_id=function_call_result.tool_call_id,
2494
+ tool_name=function_call_result.tool_name,
2495
+ tool_args=function_call_result.tool_args,
2496
+ tool_call_error=function_call_result.tool_call_error,
2497
+ result=str(function_call_result.content),
2498
+ stop_after_tool_call=function_call_result.stop_after_tool_call,
2499
+ metrics=function_call_result.metrics,
2500
+ )
2501
+ ],
2502
+ event=ModelResponseEvent.tool_call_completed.value,
2503
+ updated_session_state=updated_session_state,
2504
+ images=function_execution_result.images,
2505
+ videos=function_execution_result.videos,
2506
+ audios=function_execution_result.audios,
2507
+ files=function_execution_result.files,
2508
+ )
589
2509
 
590
- Args:
591
- message: The message for the Model
592
- audio: Pre-formatted audio data like {
593
- "content": encoded_string,
594
- "format": "wav"
595
- }
2510
+ # Add function call result to function call results
2511
+ function_call_results.append(function_call_result)
596
2512
 
597
- Returns:
598
- Message content with audio added in the format expected by the model
599
- """
600
- if len(audio) == 0:
601
- return message
602
-
603
- # Create a default message content with text
604
- message_content_with_audio: List[Dict[str, Any]] = [{"type": "text", "text": message.content}]
605
-
606
- for audio_snippet in audio:
607
- # This means the audio is raw data
608
- if audio_snippet.content:
609
- import base64
610
-
611
- encoded_string = base64.b64encode(audio_snippet.content).decode("utf-8")
612
-
613
- # Create a message with audio
614
- message_content_with_audio.append(
615
- {
616
- "type": "input_audio",
617
- "input_audio": {
618
- "data": encoded_string,
619
- "format": audio_snippet.format,
620
- },
621
- },
622
- )
2513
+ # Add any additional messages at the end
2514
+ if additional_input:
2515
+ function_call_results.extend(additional_input)
623
2516
 
624
- # Update the message content with the audio
625
- message.content = message_content_with_audio
626
- message.audio = None # The message should not have an audio component after this
2517
+ def _prepare_function_calls(
2518
+ self,
2519
+ assistant_message: Message,
2520
+ messages: List[Message],
2521
+ model_response: ModelResponse,
2522
+ functions: Optional[Dict[str, Function]] = None,
2523
+ ) -> List[FunctionCall]:
2524
+ """
2525
+ Prepare function calls from tool calls in the assistant message.
2526
+ """
2527
+ if model_response.content is None:
2528
+ model_response.content = ""
2529
+ if model_response.tool_calls is None:
2530
+ model_response.tool_calls = []
627
2531
 
628
- return message
2532
+ function_calls_to_run: List[FunctionCall] = self.get_function_calls_to_run(
2533
+ assistant_message=assistant_message, messages=messages, functions=functions
2534
+ )
2535
+ return function_calls_to_run
629
2536
 
630
- @staticmethod
631
- def _build_tool_calls(tool_calls_data: List[Any]) -> List[Dict[str, Any]]:
2537
+ def format_function_call_results(
2538
+ self,
2539
+ messages: List[Message],
2540
+ function_call_results: List[Message],
2541
+ compress_tool_results: bool = False,
2542
+ **kwargs,
2543
+ ) -> None:
632
2544
  """
633
- Build tool calls from tool call data.
2545
+ Format function call results.
2546
+ """
2547
+ if len(function_call_results) > 0:
2548
+ messages.extend(function_call_results)
634
2549
 
635
- Args:
636
- tool_calls_data (List[ChoiceDeltaToolCall]): The tool call data to build from.
2550
+ def _handle_function_call_media(
2551
+ self, messages: List[Message], function_call_results: List[Message], send_media_to_model: bool = True
2552
+ ) -> None:
2553
+ """
2554
+ Handle media artifacts from function calls by adding follow-up user messages for generated media if needed.
2555
+ """
2556
+ if not function_call_results:
2557
+ return
2558
+
2559
+ # Collect all media artifacts from function calls
2560
+ all_images: List[Image] = []
2561
+ all_videos: List[Video] = []
2562
+ all_audio: List[Audio] = []
2563
+ all_files: List[File] = []
2564
+
2565
+ for result_message in function_call_results:
2566
+ if result_message.images:
2567
+ all_images.extend(result_message.images)
2568
+ # Remove images from tool message to avoid errors on the LLMs
2569
+ result_message.images = None
2570
+
2571
+ if result_message.videos:
2572
+ all_videos.extend(result_message.videos)
2573
+ result_message.videos = None
2574
+
2575
+ if result_message.audio:
2576
+ all_audio.extend(result_message.audio)
2577
+ result_message.audio = None
2578
+
2579
+ if result_message.files:
2580
+ all_files.extend(result_message.files)
2581
+ result_message.files = None
2582
+
2583
+ # Only add media message if we should send media to model
2584
+ if send_media_to_model and (all_images or all_videos or all_audio or all_files):
2585
+ # If we have media artifacts, add a follow-up "user" message instead of a "tool"
2586
+ # message with the media artifacts which throws error for some models
2587
+ media_message = Message(
2588
+ role="user",
2589
+ content="Take note of the following content",
2590
+ images=all_images if all_images else None,
2591
+ videos=all_videos if all_videos else None,
2592
+ audio=all_audio if all_audio else None,
2593
+ files=all_files if all_files else None,
2594
+ )
2595
+ messages.append(media_message)
637
2596
 
638
- Returns:
639
- List[Dict[str, Any]]: The built tool calls.
640
- """
641
- tool_calls: List[Dict[str, Any]] = []
642
- for _tool_call in tool_calls_data:
643
- _index = _tool_call.index
644
- _tool_call_id = _tool_call.id
645
- _tool_call_type = _tool_call.type
646
- _function_name = _tool_call.function.name if _tool_call.function else None
647
- _function_arguments = _tool_call.function.arguments if _tool_call.function else None
648
-
649
- if len(tool_calls) <= _index:
650
- tool_calls.extend([{}] * (_index - len(tool_calls) + 1))
651
- tool_call_entry = tool_calls[_index]
652
- if not tool_call_entry:
653
- tool_call_entry["id"] = _tool_call_id
654
- tool_call_entry["type"] = _tool_call_type
655
- tool_call_entry["function"] = {
656
- "name": _function_name or "",
657
- "arguments": _function_arguments or "",
658
- }
659
- else:
660
- if _function_name:
661
- tool_call_entry["function"]["name"] += _function_name
662
- if _function_arguments:
663
- tool_call_entry["function"]["arguments"] += _function_arguments
664
- if _tool_call_id:
665
- tool_call_entry["id"] = _tool_call_id
666
- if _tool_call_type:
667
- tool_call_entry["type"] = _tool_call_type
668
- return tool_calls
669
-
670
- def get_system_message_for_model(self) -> Optional[str]:
2597
+ def get_system_message_for_model(self, tools: Optional[List[Any]] = None) -> Optional[str]:
671
2598
  return self.system_prompt
672
2599
 
673
- def get_instructions_for_model(self) -> Optional[List[str]]:
2600
+ def get_instructions_for_model(self, tools: Optional[List[Any]] = None) -> Optional[List[str]]:
674
2601
  return self.instructions
675
2602
 
676
- def clear(self) -> None:
677
- """Clears the Model's state."""
678
-
679
- self.metrics = {}
680
- self._functions = None
681
- self._function_call_stack = None
682
- self.session_id = None
683
-
684
2603
  def __deepcopy__(self, memo):
685
2604
  """Create a deep copy of the Model instance.
686
2605
 
@@ -690,19 +2609,27 @@ class Model(ABC):
690
2609
  Returns:
691
2610
  Model: A new Model instance with deeply copied attributes.
692
2611
  """
693
- from copy import deepcopy
2612
+ from copy import copy, deepcopy
694
2613
 
695
2614
  # Create a new instance without calling __init__
696
2615
  cls = self.__class__
697
2616
  new_model = cls.__new__(cls)
698
2617
  memo[id(self)] = new_model
699
2618
 
700
- # Deep copy all attributes
2619
+ # Deep copy all attributes except client objects
701
2620
  for k, v in self.__dict__.items():
702
- if k in {"metrics", "_functions", "_function_call_stack", "session_id"}:
2621
+ if k in {"response_format", "_tools", "_functions"}:
703
2622
  continue
704
- setattr(new_model, k, deepcopy(v, memo))
2623
+ # Skip client objects
2624
+ if k in {"client", "async_client", "http_client", "mistral_client", "model_client"}:
2625
+ setattr(new_model, k, None)
2626
+ continue
2627
+ try:
2628
+ setattr(new_model, k, deepcopy(v, memo))
2629
+ except Exception:
2630
+ try:
2631
+ setattr(new_model, k, copy(v))
2632
+ except Exception:
2633
+ setattr(new_model, k, v)
705
2634
 
706
- # Clear the new model to remove any references to the old model
707
- new_model.clear()
708
2635
  return new_model