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.
Files changed (63) hide show
  1. agno/agent/agent.py +164 -87
  2. agno/db/dynamo/dynamo.py +8 -0
  3. agno/db/firestore/firestore.py +8 -1
  4. agno/db/gcs_json/gcs_json_db.py +9 -0
  5. agno/db/json/json_db.py +8 -0
  6. agno/db/mongo/mongo.py +10 -1
  7. agno/db/mysql/mysql.py +10 -0
  8. agno/db/postgres/postgres.py +16 -8
  9. agno/db/redis/redis.py +6 -0
  10. agno/db/singlestore/schemas.py +1 -1
  11. agno/db/singlestore/singlestore.py +8 -1
  12. agno/db/sqlite/sqlite.py +9 -1
  13. agno/db/utils.py +14 -0
  14. agno/knowledge/chunking/fixed.py +1 -1
  15. agno/knowledge/knowledge.py +91 -65
  16. agno/knowledge/reader/base.py +3 -0
  17. agno/knowledge/reader/csv_reader.py +1 -1
  18. agno/knowledge/reader/json_reader.py +1 -1
  19. agno/knowledge/reader/markdown_reader.py +5 -5
  20. agno/knowledge/reader/s3_reader.py +0 -12
  21. agno/knowledge/reader/text_reader.py +5 -5
  22. agno/models/base.py +2 -2
  23. agno/models/cerebras/cerebras.py +5 -3
  24. agno/models/cerebras/cerebras_openai.py +5 -3
  25. agno/models/google/gemini.py +33 -11
  26. agno/models/litellm/chat.py +1 -1
  27. agno/models/openai/chat.py +3 -0
  28. agno/models/openai/responses.py +81 -40
  29. agno/models/response.py +5 -0
  30. agno/models/siliconflow/__init__.py +5 -0
  31. agno/models/siliconflow/siliconflow.py +25 -0
  32. agno/os/app.py +4 -1
  33. agno/os/auth.py +24 -14
  34. agno/os/interfaces/slack/router.py +1 -1
  35. agno/os/interfaces/whatsapp/router.py +2 -0
  36. agno/os/router.py +187 -76
  37. agno/os/routers/evals/utils.py +9 -9
  38. agno/os/routers/health.py +26 -0
  39. agno/os/routers/knowledge/knowledge.py +11 -11
  40. agno/os/routers/session/session.py +24 -8
  41. agno/os/schema.py +8 -2
  42. agno/run/agent.py +5 -2
  43. agno/run/base.py +6 -3
  44. agno/run/team.py +11 -3
  45. agno/run/workflow.py +69 -12
  46. agno/session/team.py +1 -0
  47. agno/team/team.py +196 -93
  48. agno/tools/mcp.py +1 -0
  49. agno/tools/mem0.py +11 -17
  50. agno/tools/memory.py +419 -0
  51. agno/tools/workflow.py +279 -0
  52. agno/utils/audio.py +27 -0
  53. agno/utils/common.py +90 -1
  54. agno/utils/print_response/agent.py +6 -2
  55. agno/utils/streamlit.py +14 -8
  56. agno/vectordb/chroma/chromadb.py +8 -2
  57. agno/workflow/step.py +111 -13
  58. agno/workflow/workflow.py +16 -13
  59. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/METADATA +1 -1
  60. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/RECORD +63 -58
  61. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/WHEEL +0 -0
  62. {agno-2.0.2.dist-info → agno-2.0.4.dist-info}/licenses/LICENSE +0 -0
  63. {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,
@@ -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 = False
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
- request_params["parallel_tool_calls"] = self.parallel_tool_calls
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 = False
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
- request_params["parallel_tool_calls"] = self.parallel_tool_calls
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)
@@ -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 # "Text" and/or "Image"
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
- if model_response.images is None:
821
- model_response.images = []
822
- model_response.images.append(
823
- Image(id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type)
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
- if model_response.images is None:
933
- model_response.images = []
934
- model_response.images.append(
935
- Image(id=str(uuid4()), content=part.inline_data.data, mime_type=part.inline_data.mime_type)
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:
@@ -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
@@ -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
  )
@@ -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 (ImportError, ModuleNotFoundError) as e:
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
- request_params["store"] = True
260
-
261
- # Check if the last assistant message has a previous_response_id to continue from
262
- previous_response_id = None
263
- for msg in reversed(messages):
264
- if (
265
- msg.role == "assistant"
266
- and hasattr(msg, "provider_data")
267
- and msg.provider_data
268
- and "response_id" in msg.provider_data
269
- ):
270
- previous_response_id = msg.provider_data["response_id"]
271
- log_debug(f"Using previous_response_id: {previous_response_id}")
272
- break
266
+ if self.store is False:
267
+ request_params["store"] = False
268
+
269
+ # Add encrypted reasoning content to include if not already present
270
+ include_list = request_params.get("include", []) or []
271
+ if "reasoning.encrypted_content" not in include_list:
272
+ include_list.append("reasoning.encrypted_content")
273
+ if request_params.get("include") is None:
274
+ request_params["include"] = include_list
275
+ elif isinstance(request_params["include"], list):
276
+ request_params["include"].extend(include_list)
273
277
 
274
- if previous_response_id:
275
- request_params["previous_response_id"] = previous_response_id
278
+ else:
279
+ request_params["store"] = True
280
+
281
+ # Check if the last assistant message has a previous_response_id to continue from
282
+ previous_response_id = None
283
+ for msg in reversed(messages):
284
+ if (
285
+ msg.role == "assistant"
286
+ and hasattr(msg, "provider_data")
287
+ and msg.provider_data
288
+ and "response_id" in msg.provider_data
289
+ ):
290
+ previous_response_id = msg.provider_data["response_id"]
291
+ log_debug(f"Using previous_response_id: {previous_response_id}")
292
+ break
293
+
294
+ if previous_response_id:
295
+ request_params["previous_response_id"] = previous_response_id
276
296
 
277
297
  # Add additional request params if provided
278
298
  if self.request_params:
@@ -375,7 +395,7 @@ class OpenAIResponses(Model):
375
395
 
376
396
  return formatted_tools
377
397
 
378
- def _format_messages(self, messages: List[Message]) -> List[Dict[str, Any]]:
398
+ def _format_messages(self, messages: List[Message]) -> List[Union[Dict[str, Any], ResponseReasoningItem]]:
379
399
  """
380
400
  Format a message into the format expected by OpenAI.
381
401
 
@@ -385,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
- for msg in reversed(messages):
396
- if (
397
- msg.role == "assistant"
398
- and hasattr(msg, "provider_data")
399
- and msg.provider_data
400
- and "response_id" in msg.provider_data
401
- ):
402
- previous_response_id = msg.provider_data["response_id"]
403
- break
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
- # Add reasoning summary
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
- # Add reasoning summary
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
- summaries = getattr(out, "summary", None)
1018
- if summaries:
1019
- for s in summaries:
1020
- text_val = s.get("text") if isinstance(s, dict) else getattr(s, "text", None)
1021
- if text_val:
1022
- if summary_text:
1023
- summary_text += "\n\n"
1024
- summary_text += text_val
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,5 @@
1
+ from agno.models.siliconflow.siliconflow import Siliconflow
2
+
3
+ __all__ = [
4
+ "Siliconflow",
5
+ ]
@@ -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 typing import Optional
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(authorization: Optional[str] = Header(None)) -> bool:
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 authorization:
27
+ if not credentials:
30
28
  raise HTTPException(status_code=401, detail="Authorization header required")
31
29
 
32
- # Check if the authorization header starts with "Bearer "
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("/slack/events")
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)}")