agno 2.0.2__py3-none-any.whl → 2.0.4__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 +164 -87
- agno/db/dynamo/dynamo.py +8 -0
- agno/db/firestore/firestore.py +8 -1
- agno/db/gcs_json/gcs_json_db.py +9 -0
- agno/db/json/json_db.py +8 -0
- agno/db/mongo/mongo.py +10 -1
- agno/db/mysql/mysql.py +10 -0
- agno/db/postgres/postgres.py +16 -8
- agno/db/redis/redis.py +6 -0
- agno/db/singlestore/schemas.py +1 -1
- agno/db/singlestore/singlestore.py +8 -1
- agno/db/sqlite/sqlite.py +9 -1
- agno/db/utils.py +14 -0
- agno/knowledge/chunking/fixed.py +1 -1
- agno/knowledge/knowledge.py +91 -65
- agno/knowledge/reader/base.py +3 -0
- agno/knowledge/reader/csv_reader.py +1 -1
- agno/knowledge/reader/json_reader.py +1 -1
- agno/knowledge/reader/markdown_reader.py +5 -5
- agno/knowledge/reader/s3_reader.py +0 -12
- agno/knowledge/reader/text_reader.py +5 -5
- agno/models/base.py +2 -2
- agno/models/cerebras/cerebras.py +5 -3
- agno/models/cerebras/cerebras_openai.py +5 -3
- agno/models/google/gemini.py +33 -11
- agno/models/litellm/chat.py +1 -1
- agno/models/openai/chat.py +3 -0
- agno/models/openai/responses.py +81 -40
- agno/models/response.py +5 -0
- agno/models/siliconflow/__init__.py +5 -0
- agno/models/siliconflow/siliconflow.py +25 -0
- agno/os/app.py +4 -1
- agno/os/auth.py +24 -14
- agno/os/interfaces/slack/router.py +1 -1
- agno/os/interfaces/whatsapp/router.py +2 -0
- agno/os/router.py +187 -76
- agno/os/routers/evals/utils.py +9 -9
- agno/os/routers/health.py +26 -0
- agno/os/routers/knowledge/knowledge.py +11 -11
- agno/os/routers/session/session.py +24 -8
- agno/os/schema.py +8 -2
- agno/run/agent.py +5 -2
- agno/run/base.py +6 -3
- agno/run/team.py +11 -3
- agno/run/workflow.py +69 -12
- agno/session/team.py +1 -0
- agno/team/team.py +196 -93
- agno/tools/mcp.py +1 -0
- agno/tools/mem0.py +11 -17
- agno/tools/memory.py +419 -0
- agno/tools/workflow.py +279 -0
- agno/utils/audio.py +27 -0
- agno/utils/common.py +90 -1
- agno/utils/print_response/agent.py +6 -2
- agno/utils/streamlit.py +14 -8
- agno/vectordb/chroma/chromadb.py +8 -2
- agno/workflow/step.py +111 -13
- agno/workflow/workflow.py +16 -13
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/RECORD +63 -58
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/top_level.txt +0 -0
|
@@ -67,12 +67,12 @@ class MarkdownReader(Reader):
|
|
|
67
67
|
raise FileNotFoundError(f"Could not find file: {file}")
|
|
68
68
|
log_info(f"Reading: {file}")
|
|
69
69
|
file_name = name or file.stem
|
|
70
|
-
file_contents = file.read_text("utf-8")
|
|
70
|
+
file_contents = file.read_text(encoding=self.encoding or "utf-8")
|
|
71
71
|
else:
|
|
72
72
|
log_info(f"Reading uploaded file: {file.name}")
|
|
73
73
|
file_name = name or file.name.split(".")[0]
|
|
74
74
|
file.seek(0)
|
|
75
|
-
file_contents = file.read().decode("utf-8")
|
|
75
|
+
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
76
76
|
|
|
77
77
|
documents = [Document(name=file_name, id=str(uuid.uuid4()), content=file_contents)]
|
|
78
78
|
if self.chunk:
|
|
@@ -97,16 +97,16 @@ class MarkdownReader(Reader):
|
|
|
97
97
|
try:
|
|
98
98
|
import aiofiles
|
|
99
99
|
|
|
100
|
-
async with aiofiles.open(file, "r", encoding="utf-8") as f:
|
|
100
|
+
async with aiofiles.open(file, "r", encoding=self.encoding or "utf-8") as f:
|
|
101
101
|
file_contents = await f.read()
|
|
102
102
|
except ImportError:
|
|
103
103
|
logger.warning("aiofiles not installed, using synchronous file I/O")
|
|
104
|
-
file_contents = file.read_text("utf-8")
|
|
104
|
+
file_contents = file.read_text(self.encoding or "utf-8")
|
|
105
105
|
else:
|
|
106
106
|
log_info(f"Reading uploaded file asynchronously: {file.name}")
|
|
107
107
|
file_name = name or file.name.split(".")[0]
|
|
108
108
|
file.seek(0)
|
|
109
|
-
file_contents = file.read().decode("utf-8")
|
|
109
|
+
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
110
110
|
|
|
111
111
|
document = Document(
|
|
112
112
|
name=file_name,
|
|
@@ -74,18 +74,6 @@ class S3Reader(Reader):
|
|
|
74
74
|
obj_name = s3_object.name.split("/")[-1]
|
|
75
75
|
temporary_file = Path("storage").joinpath(obj_name)
|
|
76
76
|
s3_object.download(temporary_file)
|
|
77
|
-
|
|
78
|
-
# TODO: Before we were using textract here. Needed?
|
|
79
|
-
# s3_object.download(temporary_file)
|
|
80
|
-
# doc_content = textract.process(temporary_file)
|
|
81
|
-
# documents = [
|
|
82
|
-
# Document(
|
|
83
|
-
# name=doc_name,
|
|
84
|
-
# id=doc_name,
|
|
85
|
-
# content=doc_content.decode("utf-8"),
|
|
86
|
-
# )
|
|
87
|
-
# ]
|
|
88
|
-
|
|
89
77
|
documents = TextReader().read(file=temporary_file, name=doc_name)
|
|
90
78
|
|
|
91
79
|
temporary_file.unlink()
|
|
@@ -39,12 +39,12 @@ class TextReader(Reader):
|
|
|
39
39
|
raise FileNotFoundError(f"Could not find file: {file}")
|
|
40
40
|
log_info(f"Reading: {file}")
|
|
41
41
|
file_name = name or file.stem
|
|
42
|
-
file_contents = file.read_text("utf-8")
|
|
42
|
+
file_contents = file.read_text(self.encoding or "utf-8")
|
|
43
43
|
else:
|
|
44
44
|
file_name = name or file.name.split(".")[0]
|
|
45
45
|
log_info(f"Reading uploaded file: {file_name}")
|
|
46
46
|
file.seek(0)
|
|
47
|
-
file_contents = file.read().decode("utf-8")
|
|
47
|
+
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
48
48
|
|
|
49
49
|
documents = [
|
|
50
50
|
Document(
|
|
@@ -75,16 +75,16 @@ class TextReader(Reader):
|
|
|
75
75
|
try:
|
|
76
76
|
import aiofiles
|
|
77
77
|
|
|
78
|
-
async with aiofiles.open(file, "r", encoding="utf-8") as f:
|
|
78
|
+
async with aiofiles.open(file, "r", encoding=self.encoding or "utf-8") as f:
|
|
79
79
|
file_contents = await f.read()
|
|
80
80
|
except ImportError:
|
|
81
81
|
logger.warning("aiofiles not installed, using synchronous file I/O")
|
|
82
|
-
file_contents = file.read_text("utf-8")
|
|
82
|
+
file_contents = file.read_text(self.encoding or "utf-8")
|
|
83
83
|
else:
|
|
84
84
|
log_info(f"Reading uploaded file asynchronously: {file.name}")
|
|
85
85
|
file_name = name or file.name.split(".")[0]
|
|
86
86
|
file.seek(0)
|
|
87
|
-
file_contents = file.read().decode("utf-8")
|
|
87
|
+
file_contents = file.read().decode(self.encoding or "utf-8")
|
|
88
88
|
|
|
89
89
|
document = Document(
|
|
90
90
|
name=file_name,
|
agno/models/base.py
CHANGED
|
@@ -1228,7 +1228,7 @@ class Model(ABC):
|
|
|
1228
1228
|
function_execution_result=function_execution_result,
|
|
1229
1229
|
)
|
|
1230
1230
|
yield ModelResponse(
|
|
1231
|
-
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s.",
|
|
1231
|
+
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
|
|
1232
1232
|
tool_executions=[
|
|
1233
1233
|
ToolExecution(
|
|
1234
1234
|
tool_call_id=function_call_result.tool_call_id,
|
|
@@ -1632,7 +1632,7 @@ class Model(ABC):
|
|
|
1632
1632
|
function_execution_result=function_execution_result,
|
|
1633
1633
|
)
|
|
1634
1634
|
yield ModelResponse(
|
|
1635
|
-
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s.",
|
|
1635
|
+
content=f"{function_call.get_call_str()} completed in {function_call_timer.elapsed:.4f}s. ",
|
|
1636
1636
|
tool_executions=[
|
|
1637
1637
|
ToolExecution(
|
|
1638
1638
|
tool_call_id=function_call_result.tool_call_id,
|
agno/models/cerebras/cerebras.py
CHANGED
|
@@ -45,7 +45,7 @@ class Cerebras(Model):
|
|
|
45
45
|
supports_json_schema_outputs: bool = True
|
|
46
46
|
|
|
47
47
|
# Request parameters
|
|
48
|
-
parallel_tool_calls: bool =
|
|
48
|
+
parallel_tool_calls: Optional[bool] = None
|
|
49
49
|
max_completion_tokens: Optional[int] = None
|
|
50
50
|
repetition_penalty: Optional[float] = None
|
|
51
51
|
temperature: Optional[float] = None
|
|
@@ -166,7 +166,6 @@ class Cerebras(Model):
|
|
|
166
166
|
"type": "function",
|
|
167
167
|
"function": {
|
|
168
168
|
"name": tool["function"]["name"],
|
|
169
|
-
"strict": True, # Ensure strict adherence to expected outputs
|
|
170
169
|
"description": tool["function"]["description"],
|
|
171
170
|
"parameters": tool["function"]["parameters"],
|
|
172
171
|
},
|
|
@@ -174,7 +173,10 @@ class Cerebras(Model):
|
|
|
174
173
|
for tool in tools
|
|
175
174
|
]
|
|
176
175
|
# Cerebras requires parallel_tool_calls=False for llama-4-scout-17b-16e-instruct
|
|
177
|
-
|
|
176
|
+
if self.id == "llama-4-scout-17b-16e-instruct":
|
|
177
|
+
request_params["parallel_tool_calls"] = False
|
|
178
|
+
elif self.parallel_tool_calls is not None:
|
|
179
|
+
request_params["parallel_tool_calls"] = self.parallel_tool_calls
|
|
178
180
|
|
|
179
181
|
# Handle response format for structured outputs
|
|
180
182
|
if response_format is not None:
|
|
@@ -16,7 +16,7 @@ class CerebrasOpenAI(OpenAILike):
|
|
|
16
16
|
name: str = "CerebrasOpenAI"
|
|
17
17
|
provider: str = "CerebrasOpenAI"
|
|
18
18
|
|
|
19
|
-
parallel_tool_calls: bool =
|
|
19
|
+
parallel_tool_calls: Optional[bool] = None
|
|
20
20
|
base_url: str = "https://api.cerebras.ai/v1"
|
|
21
21
|
api_key: Optional[str] = getenv("CEREBRAS_API_KEY", None)
|
|
22
22
|
|
|
@@ -44,7 +44,6 @@ class CerebrasOpenAI(OpenAILike):
|
|
|
44
44
|
"type": "function",
|
|
45
45
|
"function": {
|
|
46
46
|
"name": tool["function"]["name"],
|
|
47
|
-
"strict": True, # Ensure strict adherence to expected outputs
|
|
48
47
|
"description": tool["function"]["description"],
|
|
49
48
|
"parameters": tool["function"]["parameters"],
|
|
50
49
|
},
|
|
@@ -52,7 +51,10 @@ class CerebrasOpenAI(OpenAILike):
|
|
|
52
51
|
for tool in tools
|
|
53
52
|
]
|
|
54
53
|
# Cerebras requires parallel_tool_calls=False for llama-4-scout-17b-16e-instruct
|
|
55
|
-
|
|
54
|
+
if self.id == "llama-4-scout-17b-16e-instruct":
|
|
55
|
+
request_params["parallel_tool_calls"] = False
|
|
56
|
+
elif self.parallel_tool_calls is not None:
|
|
57
|
+
request_params["parallel_tool_calls"] = self.parallel_tool_calls
|
|
56
58
|
|
|
57
59
|
if request_params:
|
|
58
60
|
log_debug(f"Calling {self.provider} with request parameters: {request_params}", log_level=2)
|
agno/models/google/gemini.py
CHANGED
|
@@ -87,7 +87,7 @@ class Gemini(Model):
|
|
|
87
87
|
presence_penalty: Optional[float] = None
|
|
88
88
|
frequency_penalty: Optional[float] = None
|
|
89
89
|
seed: Optional[int] = None
|
|
90
|
-
response_modalities: Optional[list[str]] = None # "
|
|
90
|
+
response_modalities: Optional[list[str]] = None # "TEXT", "IMAGE", and/or "AUDIO"
|
|
91
91
|
speech_config: Optional[dict[str, Any]] = None
|
|
92
92
|
cached_content: Optional[Any] = None
|
|
93
93
|
thinking_budget: Optional[int] = None # Thinking budget for Gemini 2.5 models
|
|
@@ -817,11 +817,21 @@ class Gemini(Model):
|
|
|
817
817
|
model_response.content += content_str
|
|
818
818
|
|
|
819
819
|
if hasattr(part, "inline_data") and part.inline_data is not None:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
820
|
+
# Handle audio responses (for TTS models)
|
|
821
|
+
if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
|
|
822
|
+
# Store raw bytes data
|
|
823
|
+
model_response.audio = Audio(
|
|
824
|
+
id=str(uuid4()),
|
|
825
|
+
content=part.inline_data.data,
|
|
826
|
+
mime_type=part.inline_data.mime_type,
|
|
827
|
+
)
|
|
828
|
+
# Image responses
|
|
829
|
+
else:
|
|
830
|
+
if model_response.images is None:
|
|
831
|
+
model_response.images = []
|
|
832
|
+
model_response.images.append(
|
|
833
|
+
Image(id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type)
|
|
834
|
+
)
|
|
825
835
|
|
|
826
836
|
# Extract function call if present
|
|
827
837
|
if hasattr(part, "function_call") and part.function_call is not None:
|
|
@@ -929,11 +939,23 @@ class Gemini(Model):
|
|
|
929
939
|
model_response.content += text_content
|
|
930
940
|
|
|
931
941
|
if hasattr(part, "inline_data") and part.inline_data is not None:
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
942
|
+
# Audio responses
|
|
943
|
+
if part.inline_data.mime_type and part.inline_data.mime_type.startswith("audio/"):
|
|
944
|
+
# Store raw bytes audio data
|
|
945
|
+
model_response.audio = Audio(
|
|
946
|
+
id=str(uuid4()),
|
|
947
|
+
content=part.inline_data.data,
|
|
948
|
+
mime_type=part.inline_data.mime_type,
|
|
949
|
+
)
|
|
950
|
+
# Image responses
|
|
951
|
+
else:
|
|
952
|
+
if model_response.images is None:
|
|
953
|
+
model_response.images = []
|
|
954
|
+
model_response.images.append(
|
|
955
|
+
Image(
|
|
956
|
+
id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type
|
|
957
|
+
)
|
|
958
|
+
)
|
|
937
959
|
|
|
938
960
|
# Extract function call if present
|
|
939
961
|
if hasattr(part, "function_call") and part.function_call is not None:
|
agno/models/litellm/chat.py
CHANGED
|
@@ -47,7 +47,7 @@ class LiteLLM(Model):
|
|
|
47
47
|
super().__post_init__()
|
|
48
48
|
|
|
49
49
|
# Set up API key from environment variable if not already set
|
|
50
|
-
if not self.api_key:
|
|
50
|
+
if not self.client and not self.api_key:
|
|
51
51
|
self.api_key = getenv("LITELLM_API_KEY")
|
|
52
52
|
if not self.api_key:
|
|
53
53
|
# Check for other present valid keys, e.g. OPENAI_API_KEY if self.id is an OpenAI model
|
agno/models/openai/chat.py
CHANGED
|
@@ -70,6 +70,7 @@ class OpenAIChat(Model):
|
|
|
70
70
|
service_tier: Optional[str] = None # "auto" | "default" | "flex" | "priority", defaults to "auto" when not set
|
|
71
71
|
extra_headers: Optional[Any] = None
|
|
72
72
|
extra_query: Optional[Any] = None
|
|
73
|
+
extra_body: Optional[Any] = None
|
|
73
74
|
request_params: Optional[Dict[str, Any]] = None
|
|
74
75
|
role_map: Optional[Dict[str, str]] = None
|
|
75
76
|
|
|
@@ -191,6 +192,7 @@ class OpenAIChat(Model):
|
|
|
191
192
|
"top_p": self.top_p,
|
|
192
193
|
"extra_headers": self.extra_headers,
|
|
193
194
|
"extra_query": self.extra_query,
|
|
195
|
+
"extra_body": self.extra_body,
|
|
194
196
|
"metadata": self.metadata,
|
|
195
197
|
"service_tier": self.service_tier,
|
|
196
198
|
}
|
|
@@ -270,6 +272,7 @@ class OpenAIChat(Model):
|
|
|
270
272
|
"user": self.user,
|
|
271
273
|
"extra_headers": self.extra_headers,
|
|
272
274
|
"extra_query": self.extra_query,
|
|
275
|
+
"extra_body": self.extra_body,
|
|
273
276
|
"service_tier": self.service_tier,
|
|
274
277
|
}
|
|
275
278
|
)
|
agno/models/openai/responses.py
CHANGED
|
@@ -20,9 +20,10 @@ from agno.utils.models.schema_utils import get_response_schema_for_provider
|
|
|
20
20
|
try:
|
|
21
21
|
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
|
|
22
22
|
from openai.types.responses.response import Response
|
|
23
|
+
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
23
24
|
from openai.types.responses.response_stream_event import ResponseStreamEvent
|
|
24
25
|
from openai.types.responses.response_usage import ResponseUsage
|
|
25
|
-
except
|
|
26
|
+
except ImportError as e:
|
|
26
27
|
raise ImportError("`openai` not installed. Please install using `pip install openai -U`") from e
|
|
27
28
|
|
|
28
29
|
|
|
@@ -55,6 +56,9 @@ class OpenAIResponses(Model):
|
|
|
55
56
|
truncation: Optional[Literal["auto", "disabled"]] = None
|
|
56
57
|
user: Optional[str] = None
|
|
57
58
|
service_tier: Optional[Literal["auto", "default", "flex", "priority"]] = None
|
|
59
|
+
extra_headers: Optional[Any] = None
|
|
60
|
+
extra_query: Optional[Any] = None
|
|
61
|
+
extra_body: Optional[Any] = None
|
|
58
62
|
request_params: Optional[Dict[str, Any]] = None
|
|
59
63
|
|
|
60
64
|
# Client parameters
|
|
@@ -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)
|
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
275
|
-
request_params["
|
|
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,22 +405,23 @@ 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]] = []
|
|
389
409
|
|
|
390
410
|
if self._using_reasoning_model():
|
|
391
411
|
# Detect whether we're chaining via previous_response_id. If so, we should NOT
|
|
392
412
|
# re-send prior function_call items; the Responses API already has the state and
|
|
393
413
|
# expects only the corresponding function_call_output items.
|
|
394
414
|
previous_response_id: Optional[str] = None
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
415
|
+
if self.store is not False:
|
|
416
|
+
for msg in reversed(messages):
|
|
417
|
+
if (
|
|
418
|
+
msg.role == "assistant"
|
|
419
|
+
and hasattr(msg, "provider_data")
|
|
420
|
+
and msg.provider_data
|
|
421
|
+
and "response_id" in msg.provider_data
|
|
422
|
+
):
|
|
423
|
+
previous_response_id = msg.provider_data["response_id"]
|
|
424
|
+
break
|
|
404
425
|
|
|
405
426
|
# Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
|
|
406
427
|
fc_id_to_call_id: Dict[str, str] = {}
|
|
@@ -475,6 +496,12 @@ class OpenAIResponses(Model):
|
|
|
475
496
|
content = message.content if message.content is not None else ""
|
|
476
497
|
formatted_messages.append({"role": self.role_map[message.role], "content": content})
|
|
477
498
|
|
|
499
|
+
if self.store is False and hasattr(message, "provider_data") and message.provider_data is not None:
|
|
500
|
+
if message.provider_data.get("reasoning_output") is not None:
|
|
501
|
+
reasoning_output = ResponseReasoningItem.model_validate(
|
|
502
|
+
message.provider_data["reasoning_output"]
|
|
503
|
+
)
|
|
504
|
+
formatted_messages.append(reasoning_output)
|
|
478
505
|
return formatted_messages
|
|
479
506
|
|
|
480
507
|
def invoke(
|
|
@@ -858,7 +885,7 @@ class OpenAIResponses(Model):
|
|
|
858
885
|
|
|
859
886
|
# Add role
|
|
860
887
|
model_response.role = "assistant"
|
|
861
|
-
reasoning_summary: str =
|
|
888
|
+
reasoning_summary: Optional[str] = None
|
|
862
889
|
|
|
863
890
|
for output in response.output:
|
|
864
891
|
# Add content
|
|
@@ -898,8 +925,14 @@ class OpenAIResponses(Model):
|
|
|
898
925
|
model_response.extra = model_response.extra or {}
|
|
899
926
|
model_response.extra.setdefault("tool_call_ids", []).append(output.call_id)
|
|
900
927
|
|
|
901
|
-
#
|
|
928
|
+
# Handle reasoning output items
|
|
902
929
|
elif output.type == "reasoning":
|
|
930
|
+
# Save encrypted reasoning content for ZDR mode
|
|
931
|
+
if self.store is False:
|
|
932
|
+
if model_response.provider_data is None:
|
|
933
|
+
model_response.provider_data = {}
|
|
934
|
+
model_response.provider_data["reasoning_output"] = output.model_dump(exclude_none=True)
|
|
935
|
+
|
|
903
936
|
if reasoning_summaries := getattr(output, "summary", None):
|
|
904
937
|
for summary in reasoning_summaries:
|
|
905
938
|
if isinstance(summary, dict):
|
|
@@ -1009,19 +1042,27 @@ class OpenAIResponses(Model):
|
|
|
1009
1042
|
elif stream_event.type == "response.completed":
|
|
1010
1043
|
model_response = ModelResponse()
|
|
1011
1044
|
|
|
1012
|
-
#
|
|
1013
|
-
if self.reasoning_summary is not None:
|
|
1045
|
+
# Handle reasoning output items
|
|
1046
|
+
if self.reasoning_summary is not None or self.store is False:
|
|
1014
1047
|
summary_text: str = ""
|
|
1015
1048
|
for out in getattr(stream_event.response, "output", []) or []:
|
|
1016
1049
|
if getattr(out, "type", None) == "reasoning":
|
|
1017
|
-
|
|
1018
|
-
if
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1050
|
+
# In ZDR mode (store=False), store reasoning data for next request
|
|
1051
|
+
if self.store is False and hasattr(out, "encrypted_content"):
|
|
1052
|
+
if model_response.provider_data is None:
|
|
1053
|
+
model_response.provider_data = {}
|
|
1054
|
+
# Store the complete output item
|
|
1055
|
+
model_response.provider_data["reasoning_output"] = out.model_dump(exclude_none=True)
|
|
1056
|
+
if self.reasoning_summary is not None:
|
|
1057
|
+
summaries = getattr(out, "summary", None)
|
|
1058
|
+
if summaries:
|
|
1059
|
+
for s in summaries:
|
|
1060
|
+
text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
|
|
1061
|
+
if text_val:
|
|
1062
|
+
if summary_text:
|
|
1063
|
+
summary_text += "\n\n"
|
|
1064
|
+
summary_text += text_val
|
|
1065
|
+
|
|
1025
1066
|
if summary_text:
|
|
1026
1067
|
model_response.reasoning_content = summary_text
|
|
1027
1068
|
|
agno/models/response.py
CHANGED
|
@@ -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"),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from os import getenv
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from agno.models.openai.like import OpenAILike
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Siliconflow(OpenAILike):
|
|
10
|
+
"""
|
|
11
|
+
A class for interacting with Siliconflow API.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
id (str): The id of the Siliconflow model to use. Default is "Qwen/QwQ-32B".
|
|
15
|
+
name (str): The name of this chat model instance. Default is "Siliconflow".
|
|
16
|
+
provider (str): The provider of the model. Default is "Siliconflow".
|
|
17
|
+
api_key (str): The api key to authorize request to Siliconflow.
|
|
18
|
+
base_url (str): The base url to which the requests are sent. Defaults to "https://api.siliconflow.cn/v1".
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
id: str = "Qwen/QwQ-32B"
|
|
22
|
+
name: str = "Siliconflow"
|
|
23
|
+
provider: str = "Siliconflow"
|
|
24
|
+
api_key: Optional[str] = getenv("SILICONFLOW_API_KEY")
|
|
25
|
+
base_url: str = "https://api.siliconflow.com/v1"
|
agno/os/app.py
CHANGED
|
@@ -27,8 +27,9 @@ from agno.os.config import (
|
|
|
27
27
|
SessionDomainConfig,
|
|
28
28
|
)
|
|
29
29
|
from agno.os.interfaces.base import BaseInterface
|
|
30
|
-
from agno.os.router import get_base_router
|
|
30
|
+
from agno.os.router import get_base_router, get_websocket_router
|
|
31
31
|
from agno.os.routers.evals import get_eval_router
|
|
32
|
+
from agno.os.routers.health import get_health_router
|
|
32
33
|
from agno.os.routers.knowledge import get_knowledge_router
|
|
33
34
|
from agno.os.routers.memory import get_memory_router
|
|
34
35
|
from agno.os.routers.metrics import get_metrics_router
|
|
@@ -208,6 +209,8 @@ class AgentOS:
|
|
|
208
209
|
|
|
209
210
|
# Add routes
|
|
210
211
|
self.fastapi_app.include_router(get_base_router(self, settings=self.settings))
|
|
212
|
+
self.fastapi_app.include_router(get_websocket_router(self, settings=self.settings))
|
|
213
|
+
self.fastapi_app.include_router(get_health_router())
|
|
211
214
|
|
|
212
215
|
for interface in self.interfaces:
|
|
213
216
|
interface_router = interface.get_router()
|
agno/os/auth.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from fastapi import Header, HTTPException
|
|
4
|
-
from fastapi.security import HTTPBearer
|
|
1
|
+
from fastapi import Depends, HTTPException
|
|
2
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
5
3
|
|
|
6
4
|
from agno.os.settings import AgnoAPISettings
|
|
7
5
|
|
|
@@ -20,23 +18,16 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
20
18
|
A dependency function that can be used with FastAPI's Depends()
|
|
21
19
|
"""
|
|
22
20
|
|
|
23
|
-
def auth_dependency(
|
|
21
|
+
def auth_dependency(credentials: HTTPAuthorizationCredentials = Depends(security)) -> bool:
|
|
24
22
|
# If no security key is set, skip authentication entirely
|
|
25
23
|
if not settings or not settings.os_security_key:
|
|
26
24
|
return True
|
|
27
25
|
|
|
28
26
|
# If security is enabled but no authorization header provided, fail
|
|
29
|
-
if not
|
|
27
|
+
if not credentials:
|
|
30
28
|
raise HTTPException(status_code=401, detail="Authorization header required")
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
if not authorization.startswith("Bearer "):
|
|
34
|
-
raise HTTPException(
|
|
35
|
-
status_code=401, detail="Invalid authorization header format. Expected 'Bearer <token>'"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
# Extract the token from the authorization header
|
|
39
|
-
token = authorization[7:] # Remove "Bearer " prefix
|
|
30
|
+
token = credentials.credentials
|
|
40
31
|
|
|
41
32
|
# Verify the token
|
|
42
33
|
if token != settings.os_security_key:
|
|
@@ -45,3 +36,22 @@ def get_authentication_dependency(settings: AgnoAPISettings):
|
|
|
45
36
|
return True
|
|
46
37
|
|
|
47
38
|
return auth_dependency
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_websocket_token(token: str, settings: AgnoAPISettings) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Validate a bearer token for WebSocket authentication.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
token: The bearer token to validate
|
|
47
|
+
settings: The API settings containing the security key
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
True if the token is valid or authentication is disabled, False otherwise
|
|
51
|
+
"""
|
|
52
|
+
# If no security key is set, skip authentication entirely
|
|
53
|
+
if not settings or not settings.os_security_key:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
# Verify the token matches the configured security key
|
|
57
|
+
return token == settings.os_security_key
|
|
@@ -10,7 +10,7 @@ from agno.utils.log import log_info
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Optional[Team] = None) -> APIRouter:
|
|
13
|
-
@router.post("/
|
|
13
|
+
@router.post("/events")
|
|
14
14
|
async def slack_events(request: Request, background_tasks: BackgroundTasks):
|
|
15
15
|
body = await request.body()
|
|
16
16
|
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
|
@@ -167,6 +167,8 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
167
167
|
)
|
|
168
168
|
await _send_whatsapp_message(phone_number, response.content) # type: ignore
|
|
169
169
|
await _send_whatsapp_message(phone_number, response.content) # type: ignore
|
|
170
|
+
else:
|
|
171
|
+
await _send_whatsapp_message(phone_number, response.content) # type: ignore
|
|
170
172
|
|
|
171
173
|
except Exception as e:
|
|
172
174
|
log_error(f"Error processing message: {str(e)}")
|