agno 2.0.0rc2__py3-none-any.whl → 2.3.0__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 (331) hide show
  1. agno/agent/agent.py +6009 -2874
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +595 -187
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +3 -0
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +339 -266
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +1011 -566
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +110 -37
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +143 -4
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +60 -6
  142. agno/models/openai/chat.py +102 -43
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +81 -5
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -175
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +266 -112
  205. agno/run/base.py +53 -24
  206. agno/run/team.py +252 -111
  207. agno/run/workflow.py +156 -45
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1692
  213. agno/tools/brightdata.py +3 -3
  214. agno/tools/cartesia.py +3 -5
  215. agno/tools/dalle.py +9 -8
  216. agno/tools/decorator.py +4 -2
  217. agno/tools/desi_vocal.py +2 -2
  218. agno/tools/duckduckgo.py +15 -11
  219. agno/tools/e2b.py +20 -13
  220. agno/tools/eleven_labs.py +26 -28
  221. agno/tools/exa.py +21 -16
  222. agno/tools/fal.py +4 -4
  223. agno/tools/file.py +153 -23
  224. agno/tools/file_generation.py +350 -0
  225. agno/tools/firecrawl.py +4 -4
  226. agno/tools/function.py +257 -37
  227. agno/tools/giphy.py +2 -2
  228. agno/tools/gmail.py +238 -14
  229. agno/tools/google_drive.py +270 -0
  230. agno/tools/googlecalendar.py +36 -8
  231. agno/tools/googlesheets.py +20 -5
  232. agno/tools/jira.py +20 -0
  233. agno/tools/knowledge.py +3 -3
  234. agno/tools/lumalab.py +3 -3
  235. agno/tools/mcp/__init__.py +10 -0
  236. agno/tools/mcp/mcp.py +331 -0
  237. agno/tools/mcp/multi_mcp.py +347 -0
  238. agno/tools/mcp/params.py +24 -0
  239. agno/tools/mcp_toolbox.py +284 -0
  240. agno/tools/mem0.py +11 -17
  241. agno/tools/memori.py +1 -53
  242. agno/tools/memory.py +419 -0
  243. agno/tools/models/azure_openai.py +2 -2
  244. agno/tools/models/gemini.py +3 -3
  245. agno/tools/models/groq.py +3 -5
  246. agno/tools/models/nebius.py +7 -7
  247. agno/tools/models_labs.py +25 -15
  248. agno/tools/notion.py +204 -0
  249. agno/tools/openai.py +4 -9
  250. agno/tools/opencv.py +3 -3
  251. agno/tools/parallel.py +314 -0
  252. agno/tools/replicate.py +7 -7
  253. agno/tools/scrapegraph.py +58 -31
  254. agno/tools/searxng.py +2 -2
  255. agno/tools/serper.py +2 -2
  256. agno/tools/slack.py +18 -3
  257. agno/tools/spider.py +2 -2
  258. agno/tools/tavily.py +146 -0
  259. agno/tools/whatsapp.py +1 -1
  260. agno/tools/workflow.py +278 -0
  261. agno/tools/yfinance.py +12 -11
  262. agno/utils/agent.py +820 -0
  263. agno/utils/audio.py +27 -0
  264. agno/utils/common.py +90 -1
  265. agno/utils/events.py +222 -7
  266. agno/utils/gemini.py +181 -23
  267. agno/utils/hooks.py +57 -0
  268. agno/utils/http.py +111 -0
  269. agno/utils/knowledge.py +12 -5
  270. agno/utils/log.py +1 -0
  271. agno/utils/mcp.py +95 -5
  272. agno/utils/media.py +188 -10
  273. agno/utils/merge_dict.py +22 -1
  274. agno/utils/message.py +60 -0
  275. agno/utils/models/claude.py +40 -11
  276. agno/utils/models/cohere.py +1 -1
  277. agno/utils/models/watsonx.py +1 -1
  278. agno/utils/openai.py +1 -1
  279. agno/utils/print_response/agent.py +105 -21
  280. agno/utils/print_response/team.py +103 -38
  281. agno/utils/print_response/workflow.py +251 -34
  282. agno/utils/reasoning.py +22 -1
  283. agno/utils/serialize.py +32 -0
  284. agno/utils/streamlit.py +16 -10
  285. agno/utils/string.py +41 -0
  286. agno/utils/team.py +98 -9
  287. agno/utils/tools.py +1 -1
  288. agno/vectordb/base.py +23 -4
  289. agno/vectordb/cassandra/cassandra.py +65 -9
  290. agno/vectordb/chroma/chromadb.py +182 -38
  291. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  292. agno/vectordb/couchbase/couchbase.py +105 -10
  293. agno/vectordb/lancedb/lance_db.py +183 -135
  294. agno/vectordb/langchaindb/langchaindb.py +25 -7
  295. agno/vectordb/lightrag/lightrag.py +17 -3
  296. agno/vectordb/llamaindex/__init__.py +3 -0
  297. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  298. agno/vectordb/milvus/milvus.py +126 -9
  299. agno/vectordb/mongodb/__init__.py +7 -1
  300. agno/vectordb/mongodb/mongodb.py +112 -7
  301. agno/vectordb/pgvector/pgvector.py +142 -21
  302. agno/vectordb/pineconedb/pineconedb.py +80 -8
  303. agno/vectordb/qdrant/qdrant.py +125 -39
  304. agno/vectordb/redis/__init__.py +9 -0
  305. agno/vectordb/redis/redisdb.py +694 -0
  306. agno/vectordb/singlestore/singlestore.py +111 -25
  307. agno/vectordb/surrealdb/surrealdb.py +31 -5
  308. agno/vectordb/upstashdb/upstashdb.py +76 -8
  309. agno/vectordb/weaviate/weaviate.py +86 -15
  310. agno/workflow/__init__.py +2 -0
  311. agno/workflow/agent.py +299 -0
  312. agno/workflow/condition.py +112 -18
  313. agno/workflow/loop.py +69 -10
  314. agno/workflow/parallel.py +266 -118
  315. agno/workflow/router.py +110 -17
  316. agno/workflow/step.py +645 -136
  317. agno/workflow/steps.py +65 -6
  318. agno/workflow/types.py +71 -33
  319. agno/workflow/workflow.py +2113 -300
  320. agno-2.3.0.dist-info/METADATA +618 -0
  321. agno-2.3.0.dist-info/RECORD +577 -0
  322. agno-2.3.0.dist-info/licenses/LICENSE +201 -0
  323. agno/knowledge/reader/url_reader.py +0 -128
  324. agno/tools/googlesearch.py +0 -98
  325. agno/tools/mcp.py +0 -610
  326. agno/utils/models/aws_claude.py +0 -170
  327. agno-2.0.0rc2.dist-info/METADATA +0 -355
  328. agno-2.0.0rc2.dist-info/RECORD +0 -515
  329. agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
  330. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  331. {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
@@ -8,21 +8,20 @@ from typing_extensions import Literal
8
8
 
9
9
  from agno.exceptions import ModelProviderError
10
10
  from agno.media import File
11
- from agno.models.base import MessageData, Model
11
+ from agno.models.base import Model
12
12
  from agno.models.message import Citations, Message, UrlCitation
13
13
  from agno.models.metrics import Metrics
14
14
  from agno.models.response import ModelResponse
15
15
  from agno.run.agent import RunOutput
16
+ from agno.utils.http import get_default_async_client, get_default_sync_client
16
17
  from agno.utils.log import log_debug, log_error, log_warning
17
18
  from agno.utils.models.openai_responses import images_to_message
18
19
  from agno.utils.models.schema_utils import get_response_schema_for_provider
19
20
 
20
21
  try:
21
22
  from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
22
- from openai.types.responses.response import Response
23
- from openai.types.responses.response_stream_event import ResponseStreamEvent
24
- from openai.types.responses.response_usage import ResponseUsage
25
- except (ImportError, ModuleNotFoundError) as e:
23
+ from openai.types.responses import Response, ResponseReasoningItem, ResponseStreamEvent, ResponseUsage
24
+ except ImportError as e:
26
25
  raise ImportError("`openai` not installed. Please install using `pip install openai -U`") from e
27
26
 
28
27
 
@@ -47,7 +46,7 @@ class OpenAIResponses(Model):
47
46
  parallel_tool_calls: Optional[bool] = None
48
47
  reasoning: Optional[Dict[str, Any]] = None
49
48
  verbosity: Optional[Literal["low", "medium", "high"]] = None
50
- reasoning_effort: Optional[Literal["minimal", "medium", "high"]] = None
49
+ reasoning_effort: Optional[Literal["minimal", "low", "medium", "high"]] = None
51
50
  reasoning_summary: Optional[Literal["auto", "concise", "detailed"]] = None
52
51
  store: Optional[bool] = None
53
52
  temperature: Optional[float] = None
@@ -55,6 +54,10 @@ class OpenAIResponses(Model):
55
54
  truncation: Optional[Literal["auto", "disabled"]] = None
56
55
  user: Optional[str] = None
57
56
  service_tier: Optional[Literal["auto", "default", "flex", "priority"]] = None
57
+ strict_output: bool = True # When True, guarantees schema adherence for structured outputs. When False, attempts to follow schema as a guide but may occasionally deviate
58
+ extra_headers: Optional[Any] = None
59
+ extra_query: Optional[Any] = None
60
+ extra_body: Optional[Any] = None
58
61
  request_params: Optional[Dict[str, Any]] = None
59
62
 
60
63
  # Client parameters
@@ -65,7 +68,7 @@ class OpenAIResponses(Model):
65
68
  max_retries: Optional[int] = None
66
69
  default_headers: Optional[Dict[str, str]] = None
67
70
  default_query: Optional[Dict[str, str]] = None
68
- http_client: Optional[httpx.Client] = None
71
+ http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
69
72
  client_params: Optional[Dict[str, Any]] = None
70
73
 
71
74
  # Parameters affecting built-in tools
@@ -138,7 +141,7 @@ class OpenAIResponses(Model):
138
141
 
139
142
  def get_client(self) -> OpenAI:
140
143
  """
141
- Returns an OpenAI client.
144
+ Returns an OpenAI client. Caches the client to avoid recreating it on every request.
142
145
 
143
146
  Returns:
144
147
  OpenAI: An instance of the OpenAI client.
@@ -149,28 +152,29 @@ class OpenAIResponses(Model):
149
152
  client_params: Dict[str, Any] = self._get_client_params()
150
153
  if self.http_client is not None:
151
154
  client_params["http_client"] = self.http_client
155
+ else:
156
+ # Use global sync client when no custom http_client is provided
157
+ client_params["http_client"] = get_default_sync_client()
152
158
 
153
159
  self.client = OpenAI(**client_params)
154
160
  return self.client
155
161
 
156
162
  def get_async_client(self) -> AsyncOpenAI:
157
163
  """
158
- Returns an asynchronous OpenAI client.
164
+ Returns an asynchronous OpenAI client. Caches the client to avoid recreating it on every request.
159
165
 
160
166
  Returns:
161
167
  AsyncOpenAI: An instance of the asynchronous OpenAI client.
162
168
  """
163
- if self.async_client:
169
+ if self.async_client and not self.async_client.is_closed():
164
170
  return self.async_client
165
171
 
166
172
  client_params: Dict[str, Any] = self._get_client_params()
167
- if self.http_client:
173
+ if self.http_client and isinstance(self.http_client, httpx.AsyncClient):
168
174
  client_params["http_client"] = self.http_client
169
175
  else:
170
- # Create a new async HTTP client with custom limits
171
- client_params["http_client"] = httpx.AsyncClient(
172
- limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
173
- )
176
+ # Use global async client when no custom http_client is provided
177
+ client_params["http_client"] = get_default_async_client()
174
178
 
175
179
  self.async_client = AsyncOpenAI(**client_params)
176
180
  return self.async_client
@@ -201,6 +205,9 @@ class OpenAIResponses(Model):
201
205
  "truncation": self.truncation,
202
206
  "user": self.user,
203
207
  "service_tier": self.service_tier,
208
+ "extra_headers": self.extra_headers,
209
+ "extra_query": self.extra_query,
210
+ "extra_body": self.extra_body,
204
211
  }
205
212
  # Populate the reasoning parameter
206
213
  base_params = self._set_reasoning_request_param(base_params)
@@ -220,7 +227,7 @@ class OpenAIResponses(Model):
220
227
  "type": "json_schema",
221
228
  "name": response_format.__name__,
222
229
  "schema": schema,
223
- "strict": True,
230
+ "strict": self.strict_output,
224
231
  }
225
232
  else:
226
233
  # JSON mode
@@ -256,23 +263,36 @@ class OpenAIResponses(Model):
256
263
 
257
264
  # Handle reasoning tools for o3 and o4-mini models
258
265
  if self._using_reasoning_model() and messages is not None:
259
- request_params["store"] = True
260
-
261
- # Check if the last assistant message has a previous_response_id to continue from
262
- previous_response_id = None
263
- for msg in reversed(messages):
264
- if (
265
- msg.role == "assistant"
266
- and hasattr(msg, "provider_data")
267
- and msg.provider_data
268
- and "response_id" in msg.provider_data
269
- ):
270
- previous_response_id = msg.provider_data["response_id"]
271
- log_debug(f"Using previous_response_id: {previous_response_id}")
272
- break
266
+ if self.store is False:
267
+ request_params["store"] = False
268
+
269
+ # Add encrypted reasoning content to include if not already present
270
+ include_list = request_params.get("include", []) or []
271
+ if "reasoning.encrypted_content" not in include_list:
272
+ include_list.append("reasoning.encrypted_content")
273
+ if request_params.get("include") is None:
274
+ request_params["include"] = include_list
275
+ elif isinstance(request_params["include"], list):
276
+ request_params["include"].extend(include_list)
273
277
 
274
- if previous_response_id:
275
- request_params["previous_response_id"] = previous_response_id
278
+ else:
279
+ request_params["store"] = True
280
+
281
+ # Check if the last assistant message has a previous_response_id to continue from
282
+ previous_response_id = None
283
+ for msg in reversed(messages):
284
+ if (
285
+ msg.role == "assistant"
286
+ and hasattr(msg, "provider_data")
287
+ and msg.provider_data
288
+ and "response_id" in msg.provider_data
289
+ ):
290
+ previous_response_id = msg.provider_data["response_id"]
291
+ log_debug(f"Using previous_response_id: {previous_response_id}")
292
+ break
293
+
294
+ if previous_response_id:
295
+ request_params["previous_response_id"] = previous_response_id
276
296
 
277
297
  # Add additional request params if provided
278
298
  if self.request_params:
@@ -375,7 +395,7 @@ class OpenAIResponses(Model):
375
395
 
376
396
  return formatted_tools
377
397
 
378
- def _format_messages(self, messages: List[Message]) -> List[Dict[str, Any]]:
398
+ def _format_messages(self, messages: List[Message]) -> List[Union[Dict[str, Any], ResponseReasoningItem]]:
379
399
  """
380
400
  Format a message into the format expected by OpenAI.
381
401
 
@@ -385,13 +405,16 @@ class OpenAIResponses(Model):
385
405
  Returns:
386
406
  Dict[str, Any]: The formatted message.
387
407
  """
388
- formatted_messages: List[Dict[str, Any]] = []
408
+ formatted_messages: List[Union[Dict[str, Any], ResponseReasoningItem]] = []
409
+
410
+ messages_to_format = messages
411
+ previous_response_id: Optional[str] = None
389
412
 
390
- if self._using_reasoning_model():
413
+ if self._using_reasoning_model() and self.store is not False:
391
414
  # Detect whether we're chaining via previous_response_id. If so, we should NOT
392
415
  # re-send prior function_call items; the Responses API already has the state and
393
416
  # expects only the corresponding function_call_output items.
394
- previous_response_id: Optional[str] = None
417
+
395
418
  for msg in reversed(messages):
396
419
  if (
397
420
  msg.role == "assistant"
@@ -400,6 +423,11 @@ class OpenAIResponses(Model):
400
423
  and "response_id" in msg.provider_data
401
424
  ):
402
425
  previous_response_id = msg.provider_data["response_id"]
426
+ msg_index = messages.index(msg)
427
+
428
+ # Include messages after this assistant message
429
+ messages_to_format = messages[msg_index + 1 :]
430
+
403
431
  break
404
432
 
405
433
  # Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
@@ -413,7 +441,7 @@ class OpenAIResponses(Model):
413
441
  if isinstance(fc_id, str) and isinstance(call_id, str):
414
442
  fc_id_to_call_id[fc_id] = call_id
415
443
 
416
- for message in messages:
444
+ for message in messages_to_format:
417
445
  if message.role in ["user", "system"]:
418
446
  message_dict: Dict[str, Any] = {
419
447
  "role": self.role_map[message.role],
@@ -475,6 +503,12 @@ class OpenAIResponses(Model):
475
503
  content = message.content if message.content is not None else ""
476
504
  formatted_messages.append({"role": self.role_map[message.role], "content": content})
477
505
 
506
+ if self.store is False and hasattr(message, "provider_data") and message.provider_data is not None:
507
+ if message.provider_data.get("reasoning_output") is not None:
508
+ reasoning_output = ResponseReasoningItem.model_validate(
509
+ message.provider_data["reasoning_output"]
510
+ )
511
+ formatted_messages.append(reasoning_output)
478
512
  return formatted_messages
479
513
 
480
514
  def invoke(
@@ -774,63 +808,6 @@ class OpenAIResponses(Model):
774
808
  _fc_message.tool_call_id = tool_call_ids[_fc_message_index]
775
809
  messages.append(_fc_message)
776
810
 
777
- def process_response_stream(
778
- self,
779
- messages: List[Message],
780
- assistant_message: Message,
781
- stream_data: MessageData,
782
- response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
783
- tools: Optional[List[Dict[str, Any]]] = None,
784
- tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
785
- run_response: Optional[RunOutput] = None,
786
- ) -> Iterator[ModelResponse]:
787
- """Process the synchronous response stream."""
788
- for model_response_delta in self.invoke_stream(
789
- messages=messages,
790
- assistant_message=assistant_message,
791
- tools=tools,
792
- response_format=response_format,
793
- tool_choice=tool_choice,
794
- run_response=run_response,
795
- ):
796
- yield from self._populate_stream_data_and_assistant_message(
797
- stream_data=stream_data,
798
- assistant_message=assistant_message,
799
- model_response_delta=model_response_delta,
800
- )
801
-
802
- # Add final metrics to assistant message
803
- self._populate_assistant_message(assistant_message=assistant_message, provider_response=model_response_delta)
804
-
805
- async def aprocess_response_stream(
806
- self,
807
- messages: List[Message],
808
- assistant_message: Message,
809
- stream_data: MessageData,
810
- response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
811
- tools: Optional[List[Dict[str, Any]]] = None,
812
- tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
813
- run_response: Optional[RunOutput] = None,
814
- ) -> AsyncIterator[ModelResponse]:
815
- """Process the asynchronous response stream."""
816
- async for model_response_delta in self.ainvoke_stream(
817
- messages=messages,
818
- assistant_message=assistant_message,
819
- tools=tools,
820
- response_format=response_format,
821
- tool_choice=tool_choice,
822
- run_response=run_response,
823
- ):
824
- for model_response in self._populate_stream_data_and_assistant_message(
825
- stream_data=stream_data,
826
- assistant_message=assistant_message,
827
- model_response_delta=model_response_delta,
828
- ):
829
- yield model_response
830
-
831
- # Add final metrics to assistant message
832
- self._populate_assistant_message(assistant_message=assistant_message, provider_response=model_response_delta)
833
-
834
811
  def _parse_provider_response(self, response: Response, **kwargs) -> ModelResponse:
835
812
  """
836
813
  Parse the OpenAI response into a ModelResponse.
@@ -858,7 +835,7 @@ class OpenAIResponses(Model):
858
835
 
859
836
  # Add role
860
837
  model_response.role = "assistant"
861
- reasoning_summary: str = ""
838
+ reasoning_summary: Optional[str] = None
862
839
 
863
840
  for output in response.output:
864
841
  # Add content
@@ -898,8 +875,14 @@ class OpenAIResponses(Model):
898
875
  model_response.extra = model_response.extra or {}
899
876
  model_response.extra.setdefault("tool_call_ids", []).append(output.call_id)
900
877
 
901
- # Add reasoning summary
878
+ # Handle reasoning output items
902
879
  elif output.type == "reasoning":
880
+ # Save encrypted reasoning content for ZDR mode
881
+ if self.store is False:
882
+ if model_response.provider_data is None:
883
+ model_response.provider_data = {}
884
+ model_response.provider_data["reasoning_output"] = output.model_dump(exclude_none=True)
885
+
903
886
  if reasoning_summaries := getattr(output, "summary", None):
904
887
  for summary in reasoning_summaries:
905
888
  if isinstance(summary, dict):
@@ -1009,19 +992,27 @@ class OpenAIResponses(Model):
1009
992
  elif stream_event.type == "response.completed":
1010
993
  model_response = ModelResponse()
1011
994
 
1012
- # Add reasoning summary
1013
- if self.reasoning_summary is not None:
995
+ # Handle reasoning output items
996
+ if self.reasoning_summary is not None or self.store is False:
1014
997
  summary_text: str = ""
1015
998
  for out in getattr(stream_event.response, "output", []) or []:
1016
999
  if getattr(out, "type", None) == "reasoning":
1017
- summaries = getattr(out, "summary", None)
1018
- if summaries:
1019
- for s in summaries:
1020
- text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
1021
- if text_val:
1022
- if summary_text:
1023
- summary_text += "\n\n"
1024
- summary_text += text_val
1000
+ # In ZDR mode (store=False), store reasoning data for next request
1001
+ if self.store is False and hasattr(out, "encrypted_content"):
1002
+ if model_response.provider_data is None:
1003
+ model_response.provider_data = {}
1004
+ # Store the complete output item
1005
+ model_response.provider_data["reasoning_output"] = out.model_dump(exclude_none=True)
1006
+ if self.reasoning_summary is not None:
1007
+ summaries = getattr(out, "summary", None)
1008
+ if summaries:
1009
+ for s in summaries:
1010
+ text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
1011
+ if text_val:
1012
+ if summary_text:
1013
+ summary_text += "\n\n"
1014
+ summary_text += text_val
1015
+
1025
1016
  if summary_text:
1026
1017
  model_response.reasoning_content = summary_text
1027
1018
 
@@ -1047,4 +1038,10 @@ class OpenAIResponses(Model):
1047
1038
  metrics.output_tokens = response_usage.output_tokens or 0
1048
1039
  metrics.total_tokens = response_usage.total_tokens or 0
1049
1040
 
1041
+ if input_tokens_details := response_usage.input_tokens_details:
1042
+ metrics.cache_read_tokens = input_tokens_details.cached_tokens
1043
+
1044
+ if output_tokens_details := response_usage.output_tokens_details:
1045
+ metrics.reasoning_tokens = output_tokens_details.reasoning_tokens
1046
+
1050
1047
  return metrics
@@ -1,8 +1,11 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from os import getenv
3
- from typing import Optional
3
+ from typing import Any, Dict, List, Optional, Type, Union
4
+
5
+ from pydantic import BaseModel
4
6
 
5
7
  from agno.models.openai.like import OpenAILike
8
+ from agno.run.agent import RunOutput
6
9
 
7
10
 
8
11
  @dataclass
@@ -17,12 +20,47 @@ class OpenRouter(OpenAILike):
17
20
  api_key (Optional[str]): The API key.
18
21
  base_url (str): The base URL. Defaults to "https://openrouter.ai/api/v1".
19
22
  max_tokens (int): The maximum number of tokens. Defaults to 1024.
23
+ fallback_models (Optional[List[str]]): List of fallback model IDs to use if the primary model
24
+ fails due to rate limits, timeouts, or unavailability. OpenRouter will automatically try
25
+ these models in order. Example: ["anthropic/claude-sonnet-4", "deepseek/deepseek-r1"]
20
26
  """
21
27
 
22
28
  id: str = "gpt-4o"
23
29
  name: str = "OpenRouter"
24
30
  provider: str = "OpenRouter"
25
31
 
26
- api_key: Optional[str] = getenv("OPENROUTER_API_KEY")
32
+ api_key: Optional[str] = field(default_factory=lambda: getenv("OPENROUTER_API_KEY"))
27
33
  base_url: str = "https://openrouter.ai/api/v1"
28
34
  max_tokens: int = 1024
35
+ models: Optional[List[str]] = None # Dynamic model routing https://openrouter.ai/docs/features/model-routing
36
+
37
+ def get_request_params(
38
+ self,
39
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
40
+ tools: Optional[List[Dict[str, Any]]] = None,
41
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
42
+ run_response: Optional[RunOutput] = None,
43
+ ) -> Dict[str, Any]:
44
+ """
45
+ Returns keyword arguments for API requests, including fallback models configuration.
46
+
47
+ Returns:
48
+ Dict[str, Any]: A dictionary of keyword arguments for API requests.
49
+ """
50
+ # Get base request params from parent class
51
+ request_params = super().get_request_params(
52
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
53
+ )
54
+
55
+ # Add fallback models to extra_body if specified
56
+ if self.models:
57
+ # Get existing extra_body or create new dict
58
+ extra_body = request_params.get("extra_body") or {}
59
+
60
+ # Merge fallback models into extra_body
61
+ extra_body["models"] = self.models
62
+
63
+ # Update request params
64
+ request_params["extra_body"] = extra_body
65
+
66
+ return request_params
@@ -1,6 +1,6 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from os import getenv
3
- from typing import Any, Dict, List, Optional, Type, Union
3
+ from typing import Any, Dict, Optional, Type, Union
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
@@ -42,7 +42,7 @@ class Perplexity(OpenAILike):
42
42
  name: str = "Perplexity"
43
43
  provider: str = "Perplexity"
44
44
 
45
- api_key: Optional[str] = getenv("PERPLEXITY_API_KEY")
45
+ api_key: Optional[str] = field(default_factory=lambda: getenv("PERPLEXITY_API_KEY"))
46
46
  base_url: str = "https://api.perplexity.ai/"
47
47
  max_tokens: int = 1024
48
48
  top_k: Optional[float] = None
@@ -53,8 +53,7 @@ class Perplexity(OpenAILike):
53
53
  def get_request_params(
54
54
  self,
55
55
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
56
- tools: Optional[List[Dict[str, Any]]] = None,
57
- tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
56
+ **kwargs: Any,
58
57
  ) -> Dict[str, Any]:
59
58
  """
60
59
  Returns keyword arguments for API requests.
@@ -1,4 +1,4 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
2
  from os import getenv
3
3
  from typing import Any, Dict, Optional, cast
4
4
 
@@ -30,8 +30,8 @@ class Portkey(OpenAILike):
30
30
  name: str = "Portkey"
31
31
  provider: str = "Portkey"
32
32
 
33
- portkey_api_key: Optional[str] = getenv("PORTKEY_API_KEY")
34
- virtual_key: Optional[str] = getenv("PORTKEY_VIRTUAL_KEY")
33
+ portkey_api_key: Optional[str] = field(default_factory=lambda: getenv("PORTKEY_API_KEY"))
34
+ virtual_key: Optional[str] = field(default_factory=lambda: getenv("PORTKEY_VIRTUAL_KEY"))
35
35
  config: Optional[Dict[str, Any]] = None
36
36
  base_url: str = PORTKEY_GATEWAY_URL
37
37
 
@@ -0,0 +1,5 @@
1
+ from agno.models.requesty.requesty import Requesty
2
+
3
+ __all__ = [
4
+ "Requesty",
5
+ ]
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass, field
2
+ from os import getenv
3
+ from typing import Any, Dict, List, Optional, Type, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from agno.models.openai.like import OpenAILike
8
+ from agno.run.agent import RunOutput
9
+ from agno.run.team import TeamRunOutput
10
+
11
+
12
+ @dataclass
13
+ class Requesty(OpenAILike):
14
+ """
15
+ A class for using models hosted on Requesty.
16
+
17
+ Attributes:
18
+ id (str): The model id. Defaults to "openai/gpt-4.1".
19
+ provider (str): The provider name. Defaults to "Requesty".
20
+ api_key (Optional[str]): The API key.
21
+ base_url (str): The base URL. Defaults to "https://router.requesty.ai/v1".
22
+ max_tokens (int): The maximum number of tokens. Defaults to 1024.
23
+ """
24
+
25
+ id: str = "openai/gpt-4.1"
26
+ name: str = "Requesty"
27
+ provider: str = "Requesty"
28
+
29
+ api_key: Optional[str] = field(default_factory=lambda: getenv("REQUESTY_API_KEY"))
30
+ base_url: str = "https://router.requesty.ai/v1"
31
+ max_tokens: int = 1024
32
+
33
+ def get_request_params(
34
+ self,
35
+ response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
36
+ tools: Optional[List[Dict[str, Any]]] = None,
37
+ tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
38
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
39
+ ) -> Dict[str, Any]:
40
+ params = super().get_request_params(
41
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
42
+ )
43
+
44
+ if "extra_body" not in params:
45
+ params["extra_body"] = {}
46
+ params["extra_body"]["requesty"] = {}
47
+ if run_response and run_response.user_id:
48
+ params["extra_body"]["requesty"]["user_id"] = run_response.user_id
49
+ if run_response and run_response.session_id:
50
+ params["extra_body"]["requesty"]["trace_id"] = run_response.session_id
51
+
52
+ return params
agno/models/response.py CHANGED
@@ -3,7 +3,7 @@ from enum import Enum
3
3
  from time import time
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
- from agno.media import AudioArtifact, AudioResponse, ImageArtifact, VideoArtifact
6
+ from agno.media import Audio, File, Image, Video
7
7
  from agno.models.message import Citations
8
8
  from agno.models.metrics import Metrics
9
9
  from agno.tools.function import UserInputField
@@ -29,11 +29,15 @@ class ToolExecution:
29
29
  result: Optional[str] = None
30
30
  metrics: Optional[Metrics] = None
31
31
 
32
+ # In the case where a tool call creates a run of an agent/team/workflow
33
+ child_run_id: Optional[str] = None
34
+
32
35
  # If True, the agent will stop executing after this tool call.
33
36
  stop_after_tool_call: bool = False
34
37
 
35
38
  created_at: int = int(time())
36
39
 
40
+ # User control flow requirements
37
41
  requires_confirmation: Optional[bool] = None
38
42
  confirmed: Optional[bool] = None
39
43
  confirmation_note: Optional[str] = None
@@ -66,6 +70,7 @@ class ToolExecution:
66
70
  tool_args=data.get("tool_args"),
67
71
  tool_call_error=data.get("tool_call_error"),
68
72
  result=data.get("result"),
73
+ child_run_id=data.get("child_run_id"),
69
74
  stop_after_tool_call=data.get("stop_after_tool_call", False),
70
75
  requires_confirmation=data.get("requires_confirmation"),
71
76
  confirmed=data.get("confirmed"),
@@ -87,12 +92,13 @@ class ModelResponse:
87
92
 
88
93
  content: Optional[Any] = None
89
94
  parsed: Optional[Any] = None
90
- audio: Optional[AudioResponse] = None
95
+ audio: Optional[Audio] = None
91
96
 
92
97
  # Unified media fields for LLM-generated and tool-generated media artifacts
93
- images: Optional[List[ImageArtifact]] = None
94
- videos: Optional[List[VideoArtifact]] = None
95
- audios: Optional[List[AudioArtifact]] = None
98
+ images: Optional[List[Image]] = None
99
+ videos: Optional[List[Video]] = None
100
+ audios: Optional[List[Audio]] = None
101
+ files: Optional[List[File]] = None
96
102
 
97
103
  # Model tool calls
98
104
  tool_calls: List[Dict[str, Any]] = field(default_factory=list)
@@ -117,8 +123,78 @@ class ModelResponse:
117
123
 
118
124
  updated_session_state: Optional[Dict[str, Any]] = None
119
125
 
126
+ def to_dict(self) -> Dict[str, Any]:
127
+ """Serialize ModelResponse to dictionary for caching."""
128
+ _dict = asdict(self)
129
+
130
+ # Handle special serialization for audio
131
+ if self.audio is not None:
132
+ _dict["audio"] = self.audio.to_dict()
133
+
134
+ # Handle lists of media objects
135
+ if self.images is not None:
136
+ _dict["images"] = [img.to_dict() for img in self.images]
137
+ if self.videos is not None:
138
+ _dict["videos"] = [vid.to_dict() for vid in self.videos]
139
+ if self.audios is not None:
140
+ _dict["audios"] = [aud.to_dict() for aud in self.audios]
141
+ if self.files is not None:
142
+ _dict["files"] = [f.to_dict() for f in self.files]
143
+
144
+ # Handle tool executions
145
+ if self.tool_executions is not None:
146
+ _dict["tool_executions"] = [tool_execution.to_dict() for tool_execution in self.tool_executions]
147
+
148
+ # Handle response usage which might be a Pydantic BaseModel
149
+ response_usage = _dict.pop("response_usage", None)
150
+ if response_usage is not None:
151
+ try:
152
+ from pydantic import BaseModel
153
+
154
+ if isinstance(response_usage, BaseModel):
155
+ _dict["response_usage"] = response_usage.model_dump()
156
+ else:
157
+ _dict["response_usage"] = response_usage
158
+ except ImportError:
159
+ _dict["response_usage"] = response_usage
160
+
161
+ return _dict
162
+
163
+ @classmethod
164
+ def from_dict(cls, data: Dict[str, Any]) -> "ModelResponse":
165
+ """Reconstruct ModelResponse from cached dictionary."""
166
+ # Reconstruct media objects
167
+ if data.get("audio"):
168
+ data["audio"] = Audio(**data["audio"])
169
+
170
+ if data.get("images"):
171
+ data["images"] = [Image(**img) for img in data["images"]]
172
+ if data.get("videos"):
173
+ data["videos"] = [Video(**vid) for vid in data["videos"]]
174
+ if data.get("audios"):
175
+ data["audios"] = [Audio(**aud) for aud in data["audios"]]
176
+ if data.get("files"):
177
+ data["files"] = [File(**f) for f in data["files"]]
178
+
179
+ # Reconstruct tool executions
180
+ if data.get("tool_executions"):
181
+ data["tool_executions"] = [ToolExecution.from_dict(te) for te in data["tool_executions"]]
182
+
183
+ # Reconstruct citations
184
+ if data.get("citations") and isinstance(data["citations"], dict):
185
+ data["citations"] = Citations(**data["citations"])
186
+
187
+ # Reconstruct response usage (Metrics)
188
+ if data.get("response_usage") and isinstance(data["response_usage"], dict):
189
+ from agno.models.metrics import Metrics
190
+
191
+ data["response_usage"] = Metrics(**data["response_usage"])
192
+
193
+ return cls(**data)
194
+
120
195
 
121
196
  class FileType(str, Enum):
122
197
  MP4 = "mp4"
123
198
  GIF = "gif"
124
199
  MP3 = "mp3"
200
+ WAV = "wav"