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.
- agno/agent/agent.py +6009 -2874
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +385 -6
- agno/db/dynamo/dynamo.py +388 -81
- agno/db/dynamo/schemas.py +47 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +435 -64
- agno/db/firestore/schemas.py +11 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +384 -42
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +351 -66
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +339 -48
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +510 -37
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2036 -0
- agno/db/mongo/mongo.py +653 -76
- agno/db/mongo/schemas.py +13 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/mysql.py +687 -25
- agno/db/mysql/schemas.py +61 -37
- agno/db/mysql/utils.py +60 -2
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2001 -0
- agno/db/postgres/postgres.py +676 -57
- agno/db/postgres/schemas.py +43 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +344 -38
- agno/db/redis/schemas.py +18 -0
- agno/db/redis/utils.py +60 -2
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/memory.py +13 -0
- agno/db/singlestore/schemas.py +26 -1
- agno/db/singlestore/singlestore.py +687 -53
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2371 -0
- agno/db/sqlite/schemas.py +24 -0
- agno/db/sqlite/sqlite.py +774 -85
- agno/db/sqlite/utils.py +168 -5
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +309 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1361 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +50 -22
- agno/eval/accuracy.py +50 -43
- agno/eval/performance.py +6 -3
- agno/eval/reliability.py +6 -3
- agno/eval/utils.py +33 -16
- agno/exceptions.py +68 -1
- agno/filters.py +354 -0
- agno/guardrails/__init__.py +6 -0
- agno/guardrails/base.py +19 -0
- agno/guardrails/openai.py +144 -0
- agno/guardrails/pii.py +94 -0
- agno/guardrails/prompt_injection.py +52 -0
- agno/integrations/discord/client.py +1 -0
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/chunking/semantic.py +40 -8
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/aws_bedrock.py +9 -4
- agno/knowledge/embedder/azure_openai.py +54 -0
- agno/knowledge/embedder/base.py +2 -0
- agno/knowledge/embedder/cohere.py +184 -5
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/google.py +79 -1
- agno/knowledge/embedder/huggingface.py +9 -4
- agno/knowledge/embedder/jina.py +63 -0
- agno/knowledge/embedder/mistral.py +78 -11
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +13 -0
- agno/knowledge/embedder/openai.py +37 -65
- agno/knowledge/embedder/sentence_transformer.py +8 -4
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/embedder/voyageai.py +69 -16
- agno/knowledge/knowledge.py +595 -187
- agno/knowledge/reader/base.py +9 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
- agno/knowledge/reader/json_reader.py +6 -5
- agno/knowledge/reader/markdown_reader.py +13 -13
- agno/knowledge/reader/pdf_reader.py +43 -68
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +51 -6
- agno/knowledge/reader/s3_reader.py +3 -15
- agno/knowledge/reader/tavily_reader.py +194 -0
- agno/knowledge/reader/text_reader.py +13 -13
- agno/knowledge/reader/web_search_reader.py +2 -43
- agno/knowledge/reader/website_reader.py +43 -25
- agno/knowledge/reranker/__init__.py +3 -0
- agno/knowledge/types.py +9 -0
- agno/knowledge/utils.py +20 -0
- agno/media.py +339 -266
- agno/memory/manager.py +336 -82
- agno/models/aimlapi/aimlapi.py +2 -2
- agno/models/anthropic/claude.py +183 -37
- agno/models/aws/bedrock.py +52 -112
- agno/models/aws/claude.py +33 -1
- agno/models/azure/ai_foundry.py +33 -15
- agno/models/azure/openai_chat.py +25 -8
- agno/models/base.py +1011 -566
- agno/models/cerebras/cerebras.py +19 -13
- agno/models/cerebras/cerebras_openai.py +8 -5
- agno/models/cohere/chat.py +27 -1
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/dashscope/dashscope.py +1 -0
- agno/models/deepinfra/deepinfra.py +2 -2
- agno/models/deepseek/deepseek.py +2 -2
- agno/models/fireworks/fireworks.py +2 -2
- agno/models/google/gemini.py +110 -37
- agno/models/groq/groq.py +28 -11
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/internlm/internlm.py +2 -2
- agno/models/langdb/langdb.py +4 -4
- agno/models/litellm/chat.py +18 -1
- agno/models/litellm/litellm_openai.py +2 -2
- agno/models/llama_cpp/__init__.py +5 -0
- agno/models/llama_cpp/llama_cpp.py +22 -0
- agno/models/message.py +143 -4
- agno/models/meta/llama.py +27 -10
- agno/models/meta/llama_openai.py +5 -17
- agno/models/nebius/nebius.py +6 -6
- agno/models/nexus/__init__.py +3 -0
- agno/models/nexus/nexus.py +22 -0
- agno/models/nvidia/nvidia.py +2 -2
- agno/models/ollama/chat.py +60 -6
- agno/models/openai/chat.py +102 -43
- agno/models/openai/responses.py +103 -106
- agno/models/openrouter/openrouter.py +41 -3
- agno/models/perplexity/perplexity.py +4 -5
- agno/models/portkey/portkey.py +3 -3
- agno/models/requesty/__init__.py +5 -0
- agno/models/requesty/requesty.py +52 -0
- agno/models/response.py +81 -5
- agno/models/sambanova/sambanova.py +2 -2
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/models/together/together.py +2 -2
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +2 -2
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +96 -0
- agno/models/vllm/vllm.py +1 -0
- agno/models/xai/xai.py +3 -2
- agno/os/app.py +543 -175
- agno/os/auth.py +24 -14
- agno/os/config.py +1 -0
- agno/os/interfaces/__init__.py +1 -0
- agno/os/interfaces/a2a/__init__.py +3 -0
- agno/os/interfaces/a2a/a2a.py +42 -0
- agno/os/interfaces/a2a/router.py +250 -0
- agno/os/interfaces/a2a/utils.py +924 -0
- agno/os/interfaces/agui/agui.py +23 -7
- agno/os/interfaces/agui/router.py +27 -3
- agno/os/interfaces/agui/utils.py +242 -142
- agno/os/interfaces/base.py +6 -2
- agno/os/interfaces/slack/router.py +81 -23
- agno/os/interfaces/slack/slack.py +29 -14
- agno/os/interfaces/whatsapp/router.py +11 -4
- agno/os/interfaces/whatsapp/whatsapp.py +14 -7
- agno/os/mcp.py +111 -54
- agno/os/middleware/__init__.py +7 -0
- agno/os/middleware/jwt.py +233 -0
- agno/os/router.py +556 -139
- agno/os/routers/evals/evals.py +71 -34
- agno/os/routers/evals/schemas.py +31 -31
- agno/os/routers/evals/utils.py +6 -5
- agno/os/routers/health.py +31 -0
- agno/os/routers/home.py +52 -0
- agno/os/routers/knowledge/knowledge.py +185 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +158 -53
- agno/os/routers/memory/schemas.py +20 -16
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +499 -38
- agno/os/schema.py +308 -198
- agno/os/utils.py +401 -41
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +7 -2
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +266 -112
- agno/run/base.py +53 -24
- agno/run/team.py +252 -111
- agno/run/workflow.py +156 -45
- agno/session/agent.py +105 -89
- agno/session/summary.py +65 -25
- agno/session/team.py +176 -96
- agno/session/workflow.py +406 -40
- agno/team/team.py +3854 -1692
- agno/tools/brightdata.py +3 -3
- agno/tools/cartesia.py +3 -5
- agno/tools/dalle.py +9 -8
- agno/tools/decorator.py +4 -2
- agno/tools/desi_vocal.py +2 -2
- agno/tools/duckduckgo.py +15 -11
- agno/tools/e2b.py +20 -13
- agno/tools/eleven_labs.py +26 -28
- agno/tools/exa.py +21 -16
- agno/tools/fal.py +4 -4
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +350 -0
- agno/tools/firecrawl.py +4 -4
- agno/tools/function.py +257 -37
- agno/tools/giphy.py +2 -2
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +270 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/knowledge.py +3 -3
- agno/tools/lumalab.py +3 -3
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memori.py +1 -53
- agno/tools/memory.py +419 -0
- agno/tools/models/azure_openai.py +2 -2
- agno/tools/models/gemini.py +3 -3
- agno/tools/models/groq.py +3 -5
- agno/tools/models/nebius.py +7 -7
- agno/tools/models_labs.py +25 -15
- agno/tools/notion.py +204 -0
- agno/tools/openai.py +4 -9
- agno/tools/opencv.py +3 -3
- agno/tools/parallel.py +314 -0
- agno/tools/replicate.py +7 -7
- agno/tools/scrapegraph.py +58 -31
- agno/tools/searxng.py +2 -2
- agno/tools/serper.py +2 -2
- agno/tools/slack.py +18 -3
- agno/tools/spider.py +2 -2
- agno/tools/tavily.py +146 -0
- agno/tools/whatsapp.py +1 -1
- agno/tools/workflow.py +278 -0
- agno/tools/yfinance.py +12 -11
- agno/utils/agent.py +820 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/events.py +222 -7
- agno/utils/gemini.py +181 -23
- agno/utils/hooks.py +57 -0
- agno/utils/http.py +111 -0
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +95 -5
- agno/utils/media.py +188 -10
- agno/utils/merge_dict.py +22 -1
- agno/utils/message.py +60 -0
- agno/utils/models/claude.py +40 -11
- agno/utils/models/cohere.py +1 -1
- agno/utils/models/watsonx.py +1 -1
- agno/utils/openai.py +1 -1
- agno/utils/print_response/agent.py +105 -21
- agno/utils/print_response/team.py +103 -38
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/reasoning.py +22 -1
- agno/utils/serialize.py +32 -0
- agno/utils/streamlit.py +16 -10
- agno/utils/string.py +41 -0
- agno/utils/team.py +98 -9
- agno/utils/tools.py +1 -1
- agno/vectordb/base.py +23 -4
- agno/vectordb/cassandra/cassandra.py +65 -9
- agno/vectordb/chroma/chromadb.py +182 -38
- agno/vectordb/clickhouse/clickhousedb.py +64 -11
- agno/vectordb/couchbase/couchbase.py +105 -10
- agno/vectordb/lancedb/lance_db.py +183 -135
- agno/vectordb/langchaindb/langchaindb.py +25 -7
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/__init__.py +3 -0
- agno/vectordb/llamaindex/llamaindexdb.py +46 -7
- agno/vectordb/milvus/milvus.py +126 -9
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +112 -7
- agno/vectordb/pgvector/pgvector.py +142 -21
- agno/vectordb/pineconedb/pineconedb.py +80 -8
- agno/vectordb/qdrant/qdrant.py +125 -39
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +694 -0
- agno/vectordb/singlestore/singlestore.py +111 -25
- agno/vectordb/surrealdb/surrealdb.py +31 -5
- agno/vectordb/upstashdb/upstashdb.py +76 -8
- agno/vectordb/weaviate/weaviate.py +86 -15
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +112 -18
- agno/workflow/loop.py +69 -10
- agno/workflow/parallel.py +266 -118
- agno/workflow/router.py +110 -17
- agno/workflow/step.py +645 -136
- agno/workflow/steps.py +65 -6
- agno/workflow/types.py +71 -33
- agno/workflow/workflow.py +2113 -300
- agno-2.3.0.dist-info/METADATA +618 -0
- agno-2.3.0.dist-info/RECORD +577 -0
- agno-2.3.0.dist-info/licenses/LICENSE +201 -0
- agno/knowledge/reader/url_reader.py +0 -128
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -610
- agno/utils/models/aws_claude.py +0 -170
- agno-2.0.0rc2.dist-info/METADATA +0 -355
- agno-2.0.0rc2.dist-info/RECORD +0 -515
- agno-2.0.0rc2.dist-info/licenses/LICENSE +0 -375
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
- {agno-2.0.0rc2.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
agno/models/ollama/chat.py
CHANGED
|
@@ -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":
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
384
|
-
|
|
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
|
agno/models/openai/chat.py
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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(
|
|
149
|
-
|
|
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
|
-
#
|
|
155
|
-
client_params["http_client"] =
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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":
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
)
|