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
@@ -1,5 +1,6 @@
1
1
  import json
2
- from dataclasses import dataclass
2
+ from dataclasses import dataclass, field
3
+ from os import getenv
3
4
  from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Type, Union
4
5
 
5
6
  from pydantic import BaseModel
@@ -10,6 +11,7 @@ from agno.models.message import Message
10
11
  from agno.models.metrics import Metrics
11
12
  from agno.models.response import ModelResponse
12
13
  from agno.utils.log import log_debug, log_warning
14
+ from agno.utils.reasoning import extract_thinking_content
13
15
 
14
16
  try:
15
17
  from ollama import AsyncClient as AsyncOllamaClient
@@ -43,6 +45,7 @@ class Ollama(Model):
43
45
  # Client parameters
44
46
  host: Optional[str] = None
45
47
  timeout: Optional[Any] = None
48
+ api_key: Optional[str] = field(default_factory=lambda: getenv("OLLAMA_API_KEY"))
46
49
  client_params: Optional[Dict[str, Any]] = None
47
50
 
48
51
  # Ollama clients
@@ -50,10 +53,23 @@ class Ollama(Model):
50
53
  async_client: Optional[AsyncOllamaClient] = None
51
54
 
52
55
  def _get_client_params(self) -> Dict[str, Any]:
56
+ host = self.host
57
+ headers = {}
58
+
59
+ if self.api_key:
60
+ if not host:
61
+ host = "https://ollama.com"
62
+ headers["authorization"] = f"Bearer {self.api_key}"
63
+ log_debug(f"Using Ollama cloud endpoint: {host}")
64
+
53
65
  base_params = {
54
- "host": self.host,
66
+ "host": host,
55
67
  "timeout": self.timeout,
56
68
  }
69
+
70
+ if headers:
71
+ base_params["headers"] = headers
72
+
57
73
  # Create client_params dict with non-None values
58
74
  client_params = {k: v for k, v in base_params.items() if v is not None}
59
75
  # Add additional client params if provided
@@ -84,7 +100,8 @@ class Ollama(Model):
84
100
  if self.async_client is not None:
85
101
  return self.async_client
86
102
 
87
- return AsyncOllamaClient(**self._get_client_params())
103
+ self.async_client = AsyncOllamaClient(**self._get_client_params())
104
+ return self.async_client
88
105
 
89
106
  def get_request_params(
90
107
  self,
@@ -144,12 +161,34 @@ class Ollama(Model):
144
161
  "role": message.role,
145
162
  "content": message.content,
146
163
  }
164
+
165
+ if message.role == "assistant" and message.tool_calls is not None:
166
+ # Format tool calls for assistant messages
167
+ formatted_tool_calls = []
168
+ for tool_call in message.tool_calls:
169
+ if "function" in tool_call:
170
+ function_data = tool_call["function"]
171
+ formatted_tool_call = {
172
+ "id": tool_call.get("id"),
173
+ "type": "function",
174
+ "function": {
175
+ "name": function_data["name"],
176
+ "arguments": json.loads(function_data["arguments"])
177
+ if isinstance(function_data["arguments"], str)
178
+ else function_data["arguments"],
179
+ },
180
+ }
181
+ formatted_tool_calls.append(formatted_tool_call)
182
+
183
+ if formatted_tool_calls:
184
+ _message["tool_calls"] = formatted_tool_calls
185
+
147
186
  if message.role == "user":
148
187
  if message.images is not None:
149
188
  message_images = []
150
189
  for image in message.images:
151
190
  if image.url is not None:
152
- message_images.append(image.image_url_content)
191
+ message_images.append(image.get_content_bytes())
153
192
  if image.filepath is not None:
154
193
  message_images.append(image.filepath) # type: ignore
155
194
  if image.content is not None and isinstance(image.content, bytes):
@@ -309,6 +348,16 @@ class Ollama(Model):
309
348
  if response_message.get("content") is not None:
310
349
  model_response.content = response_message.get("content")
311
350
 
351
+ # Extract thinking content between <think> tags if present
352
+ if model_response.content and model_response.content.find("<think>") != -1:
353
+ reasoning_content, clean_content = extract_thinking_content(model_response.content)
354
+
355
+ if reasoning_content:
356
+ # Store extracted thinking content separately
357
+ model_response.reasoning_content = reasoning_content
358
+ # Update main content with clean version
359
+ model_response.content = clean_content
360
+
312
361
  if response_message.get("tool_calls") is not None:
313
362
  if model_response.tool_calls is None:
314
363
  model_response.tool_calls = []
@@ -380,8 +429,13 @@ class Ollama(Model):
380
429
  """
381
430
  metrics = Metrics()
382
431
 
383
- metrics.input_tokens = response.get("prompt_eval_count", 0)
384
- metrics.output_tokens = response.get("eval_count", 0)
432
+ # Safely handle None values from Ollama Cloud responses
433
+ input_tokens = response.get("prompt_eval_count")
434
+ output_tokens = response.get("eval_count")
435
+
436
+ # Default to 0 if None
437
+ metrics.input_tokens = input_tokens if input_tokens is not None else 0
438
+ metrics.output_tokens = output_tokens if output_tokens is not None else 0
385
439
  metrics.total_tokens = metrics.input_tokens + metrics.output_tokens
386
440
 
387
441
  return metrics
@@ -2,32 +2,31 @@ from collections.abc import AsyncIterator
2
2
  from dataclasses import dataclass
3
3
  from os import getenv
4
4
  from typing import Any, Dict, Iterator, List, Literal, Optional, Type, Union
5
+ from uuid import uuid4
5
6
 
6
7
  import httpx
7
8
  from pydantic import BaseModel
8
9
 
9
10
  from agno.exceptions import ModelProviderError
10
- from agno.media import AudioResponse
11
+ from agno.media import Audio
11
12
  from agno.models.base import Model
12
13
  from agno.models.message import Message
13
14
  from agno.models.metrics import Metrics
14
15
  from agno.models.response import ModelResponse
15
16
  from agno.run.agent import RunOutput
17
+ from agno.run.team import TeamRunOutput
18
+ from agno.utils.http import get_default_async_client, get_default_sync_client
16
19
  from agno.utils.log import log_debug, log_error, log_warning
17
20
  from agno.utils.openai import _format_file_for_message, audio_to_message, images_to_message
21
+ from agno.utils.reasoning import extract_thinking_content
18
22
 
19
23
  try:
20
24
  from openai import APIConnectionError, APIStatusError, RateLimitError
21
25
  from openai import AsyncOpenAI as AsyncOpenAIClient
22
26
  from openai import OpenAI as OpenAIClient
23
27
  from openai.types import CompletionUsage
24
- from openai.types.chat import ChatCompletionAudio
25
- from openai.types.chat.chat_completion import ChatCompletion
26
- from openai.types.chat.chat_completion_chunk import (
27
- ChatCompletionChunk,
28
- ChoiceDelta,
29
- ChoiceDeltaToolCall,
30
- )
28
+ from openai.types.chat import ChatCompletion, ChatCompletionAudio, ChatCompletionChunk
29
+ from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall
31
30
  except (ImportError, ModuleNotFoundError):
32
31
  raise ImportError("`openai` not installed. Please install using `pip install openai`")
33
32
 
@@ -67,8 +66,10 @@ class OpenAIChat(Model):
67
66
  user: Optional[str] = None
68
67
  top_p: Optional[float] = None
69
68
  service_tier: Optional[str] = None # "auto" | "default" | "flex" | "priority", defaults to "auto" when not set
69
+ 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
70
70
  extra_headers: Optional[Any] = None
71
71
  extra_query: Optional[Any] = None
72
+ extra_body: Optional[Any] = None
72
73
  request_params: Optional[Dict[str, Any]] = None
73
74
  role_map: Optional[Dict[str, str]] = None
74
75
 
@@ -83,6 +84,10 @@ class OpenAIChat(Model):
83
84
  http_client: Optional[Union[httpx.Client, httpx.AsyncClient]] = None
84
85
  client_params: Optional[Dict[str, Any]] = None
85
86
 
87
+ # Cached clients to avoid recreating them on every request
88
+ client: Optional[OpenAIClient] = None
89
+ async_client: Optional[AsyncOpenAIClient] = None
90
+
86
91
  # The role to map the message role to.
87
92
  default_role_map = {
88
93
  "system": "developer",
@@ -120,48 +125,68 @@ class OpenAIChat(Model):
120
125
 
121
126
  def get_client(self) -> OpenAIClient:
122
127
  """
123
- Returns an OpenAI client.
128
+ Returns an OpenAI client. Caches the client to avoid recreating it on every request.
124
129
 
125
130
  Returns:
126
131
  OpenAIClient: An instance of the OpenAI client.
127
132
  """
133
+ # Return cached client if it exists and is not closed
134
+ if self.client is not None and not self.client.is_closed():
135
+ return self.client
136
+
137
+ log_debug(f"Creating new sync OpenAI client for model {self.id}")
128
138
  client_params: Dict[str, Any] = self._get_client_params()
129
139
  if self.http_client:
130
140
  if isinstance(self.http_client, httpx.Client):
131
141
  client_params["http_client"] = self.http_client
132
142
  else:
133
- log_warning("http_client is not an instance of httpx.Client.")
134
- return OpenAIClient(**client_params)
143
+ log_warning("http_client is not an instance of httpx.Client. Using default global httpx.Client.")
144
+ # Use global sync client when user http_client is invalid
145
+ client_params["http_client"] = get_default_sync_client()
146
+ else:
147
+ # Use global sync client when no custom http_client is provided
148
+ client_params["http_client"] = get_default_sync_client()
149
+
150
+ # Create and cache the client
151
+ self.client = OpenAIClient(**client_params)
152
+ return self.client
135
153
 
136
154
  def get_async_client(self) -> AsyncOpenAIClient:
137
155
  """
138
- Returns an asynchronous OpenAI client.
156
+ Returns an asynchronous OpenAI client. Caches the client to avoid recreating it on every request.
139
157
 
140
158
  Returns:
141
159
  AsyncOpenAIClient: An instance of the asynchronous OpenAI client.
142
160
  """
161
+ # Return cached client if it exists and is not closed
162
+ if self.async_client is not None and not self.async_client.is_closed():
163
+ return self.async_client
164
+
165
+ log_debug(f"Creating new async OpenAI client for model {self.id}")
143
166
  client_params: Dict[str, Any] = self._get_client_params()
144
167
  if self.http_client:
145
168
  if isinstance(self.http_client, httpx.AsyncClient):
146
169
  client_params["http_client"] = self.http_client
147
170
  else:
148
- log_warning("http_client is not an instance of httpx.AsyncClient. Using default httpx.AsyncClient.")
149
- # Create a new async HTTP client with custom limits
150
- client_params["http_client"] = httpx.AsyncClient(
151
- limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
171
+ log_warning(
172
+ "http_client is not an instance of httpx.AsyncClient. Using default global httpx.AsyncClient."
152
173
  )
174
+ # Use global async client when user http_client is invalid
175
+ client_params["http_client"] = get_default_async_client()
153
176
  else:
154
- # Create a new async HTTP client with custom limits
155
- client_params["http_client"] = httpx.AsyncClient(
156
- limits=httpx.Limits(max_connections=1000, max_keepalive_connections=100)
157
- )
158
- return AsyncOpenAIClient(**client_params)
177
+ # Use global async client when no custom http_client is provided
178
+ client_params["http_client"] = get_default_async_client()
179
+
180
+ # Create and cache the client
181
+ self.async_client = AsyncOpenAIClient(**client_params)
182
+ return self.async_client
159
183
 
160
184
  def get_request_params(
161
185
  self,
162
186
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
163
187
  tools: Optional[List[Dict[str, Any]]] = None,
164
188
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
189
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
165
190
  ) -> Dict[str, Any]:
166
191
  """
167
192
  Returns keyword arguments for API requests.
@@ -190,6 +215,7 @@ class OpenAIChat(Model):
190
215
  "top_p": self.top_p,
191
216
  "extra_headers": self.extra_headers,
192
217
  "extra_query": self.extra_query,
218
+ "extra_body": self.extra_body,
193
219
  "metadata": self.metadata,
194
220
  "service_tier": self.service_tier,
195
221
  }
@@ -206,7 +232,7 @@ class OpenAIChat(Model):
206
232
  "json_schema": {
207
233
  "name": response_format.__name__,
208
234
  "schema": schema,
209
- "strict": True,
235
+ "strict": self.strict_output,
210
236
  },
211
237
  }
212
238
  else:
@@ -269,6 +295,7 @@ class OpenAIChat(Model):
269
295
  "user": self.user,
270
296
  "extra_headers": self.extra_headers,
271
297
  "extra_query": self.extra_query,
298
+ "extra_body": self.extra_body,
272
299
  "service_tier": self.service_tier,
273
300
  }
274
301
  )
@@ -346,7 +373,7 @@ class OpenAIChat(Model):
346
373
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
347
374
  tools: Optional[List[Dict[str, Any]]] = None,
348
375
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
349
- run_response: Optional[RunOutput] = None,
376
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
350
377
  ) -> ModelResponse:
351
378
  """
352
379
  Send a chat completion request to the OpenAI API and parse the response.
@@ -370,7 +397,9 @@ class OpenAIChat(Model):
370
397
  provider_response = self.get_client().chat.completions.create(
371
398
  model=self.id,
372
399
  messages=[self._format_message(m) for m in messages], # type: ignore
373
- **self.get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice),
400
+ **self.get_request_params(
401
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
402
+ ),
374
403
  )
375
404
  assistant_message.metrics.stop_timer()
376
405
 
@@ -424,7 +453,7 @@ class OpenAIChat(Model):
424
453
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
425
454
  tools: Optional[List[Dict[str, Any]]] = None,
426
455
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
427
- run_response: Optional[RunOutput] = None,
456
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
428
457
  ) -> ModelResponse:
429
458
  """
430
459
  Sends an asynchronous chat completion request to the OpenAI API.
@@ -447,7 +476,9 @@ class OpenAIChat(Model):
447
476
  response = await self.get_async_client().chat.completions.create(
448
477
  model=self.id,
449
478
  messages=[self._format_message(m) for m in messages], # type: ignore
450
- **self.get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice),
479
+ **self.get_request_params(
480
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
481
+ ),
451
482
  )
452
483
  assistant_message.metrics.stop_timer()
453
484
 
@@ -501,7 +532,7 @@ class OpenAIChat(Model):
501
532
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
502
533
  tools: Optional[List[Dict[str, Any]]] = None,
503
534
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
504
- run_response: Optional[RunOutput] = None,
535
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
505
536
  ) -> Iterator[ModelResponse]:
506
537
  """
507
538
  Send a streaming chat completion request to the OpenAI API.
@@ -524,7 +555,9 @@ class OpenAIChat(Model):
524
555
  messages=[self._format_message(m) for m in messages], # type: ignore
525
556
  stream=True,
526
557
  stream_options={"include_usage": True},
527
- **self.get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice),
558
+ **self.get_request_params(
559
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
560
+ ),
528
561
  ):
529
562
  yield self._parse_provider_response_delta(chunk)
530
563
 
@@ -575,7 +608,7 @@ class OpenAIChat(Model):
575
608
  response_format: Optional[Union[Dict, Type[BaseModel]]] = None,
576
609
  tools: Optional[List[Dict[str, Any]]] = None,
577
610
  tool_choice: Optional[Union[str, Dict[str, Any]]] = None,
578
- run_response: Optional[RunOutput] = None,
611
+ run_response: Optional[Union[RunOutput, TeamRunOutput]] = None,
579
612
  ) -> AsyncIterator[ModelResponse]:
580
613
  """
581
614
  Sends an asynchronous streaming chat completion request to the OpenAI API.
@@ -598,7 +631,9 @@ class OpenAIChat(Model):
598
631
  messages=[self._format_message(m) for m in messages], # type: ignore
599
632
  stream=True,
600
633
  stream_options={"include_usage": True},
601
- **self.get_request_params(response_format=response_format, tools=tools, tool_choice=tool_choice),
634
+ **self.get_request_params(
635
+ response_format=response_format, tools=tools, tool_choice=tool_choice, run_response=run_response
636
+ ),
602
637
  )
603
638
 
604
639
  async for chunk in async_stream:
@@ -712,6 +747,12 @@ class OpenAIChat(Model):
712
747
  if response_message.content is not None:
713
748
  model_response.content = response_message.content
714
749
 
750
+ # Extract thinking content before any structured parsing
751
+ if model_response.content:
752
+ reasoning_content, output_content = extract_thinking_content(model_response.content)
753
+ if reasoning_content:
754
+ model_response.reasoning_content = reasoning_content
755
+ model_response.content = output_content
715
756
  # Add tool calls
716
757
  if response_message.tool_calls is not None and len(response_message.tool_calls) > 0:
717
758
  try:
@@ -729,14 +770,14 @@ class OpenAIChat(Model):
729
770
  # If the audio output modality is requested, we can extract an audio response
730
771
  try:
731
772
  if isinstance(response_message.audio, dict):
732
- model_response.audio = AudioResponse(
773
+ model_response.audio = Audio(
733
774
  id=response_message.audio.get("id"),
734
775
  content=response_message.audio.get("data"),
735
776
  expires_at=response_message.audio.get("expires_at"),
736
777
  transcript=response_message.audio.get("transcript"),
737
778
  )
738
779
  else:
739
- model_response.audio = AudioResponse(
780
+ model_response.audio = Audio(
740
781
  id=response_message.audio.id,
741
782
  content=response_message.audio.data,
742
783
  expires_at=response_message.audio.expires_at,
@@ -783,21 +824,39 @@ class OpenAIChat(Model):
783
824
  # Add audio if present
784
825
  if hasattr(choice_delta, "audio") and choice_delta.audio is not None:
785
826
  try:
827
+ audio_data = None
828
+ audio_id = None
829
+ audio_expires_at = None
830
+ audio_transcript = None
831
+
786
832
  if isinstance(choice_delta.audio, dict):
787
- model_response.audio = AudioResponse(
788
- id=choice_delta.audio.get("id"),
789
- content=choice_delta.audio.get("data"),
790
- expires_at=choice_delta.audio.get("expires_at"),
791
- transcript=choice_delta.audio.get("transcript"),
833
+ audio_data = choice_delta.audio.get("data")
834
+ audio_id = choice_delta.audio.get("id")
835
+ audio_expires_at = choice_delta.audio.get("expires_at")
836
+ audio_transcript = choice_delta.audio.get("transcript")
837
+ else:
838
+ audio_data = choice_delta.audio.data
839
+ audio_id = choice_delta.audio.id
840
+ audio_expires_at = choice_delta.audio.expires_at
841
+ audio_transcript = choice_delta.audio.transcript
842
+
843
+ # Only create Audio object if there's actual content
844
+ if audio_data is not None:
845
+ model_response.audio = Audio(
846
+ id=audio_id,
847
+ content=audio_data,
848
+ expires_at=audio_expires_at,
849
+ transcript=audio_transcript,
792
850
  sample_rate=24000,
793
851
  mime_type="pcm16",
794
852
  )
795
- else:
796
- model_response.audio = AudioResponse(
797
- id=choice_delta.audio.id,
798
- content=choice_delta.audio.data,
799
- expires_at=choice_delta.audio.expires_at,
800
- transcript=choice_delta.audio.transcript,
853
+ # If no content but there's transcript/metadata, create minimal Audio object
854
+ elif audio_transcript is not None or audio_id is not None:
855
+ model_response.audio = Audio(
856
+ id=audio_id or str(uuid4()),
857
+ content=b"",
858
+ expires_at=audio_expires_at,
859
+ transcript=audio_transcript,
801
860
  sample_rate=24000,
802
861
  mime_type="pcm16",
803
862
  )