agno 2.0.7__py3-none-any.whl → 2.0.9__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 +83 -51
- agno/db/base.py +14 -0
- agno/db/dynamo/dynamo.py +107 -27
- agno/db/firestore/firestore.py +109 -33
- agno/db/gcs_json/gcs_json_db.py +100 -20
- agno/db/in_memory/in_memory_db.py +95 -20
- agno/db/json/json_db.py +101 -21
- agno/db/migrations/v1_to_v2.py +322 -47
- agno/db/mongo/mongo.py +251 -26
- agno/db/mysql/mysql.py +307 -6
- agno/db/postgres/postgres.py +279 -33
- agno/db/redis/redis.py +99 -22
- agno/db/singlestore/singlestore.py +319 -38
- agno/db/sqlite/sqlite.py +339 -23
- agno/knowledge/embedder/sentence_transformer.py +3 -3
- agno/knowledge/knowledge.py +152 -31
- agno/knowledge/types.py +8 -0
- agno/models/anthropic/claude.py +0 -20
- agno/models/cometapi/__init__.py +5 -0
- agno/models/cometapi/cometapi.py +57 -0
- agno/models/google/gemini.py +4 -8
- agno/models/huggingface/huggingface.py +2 -1
- agno/models/ollama/chat.py +52 -3
- agno/models/openai/chat.py +9 -7
- agno/models/openai/responses.py +21 -17
- agno/os/interfaces/agui/agui.py +2 -2
- agno/os/interfaces/agui/utils.py +81 -18
- agno/os/interfaces/base.py +2 -0
- agno/os/interfaces/slack/router.py +50 -10
- agno/os/interfaces/slack/slack.py +6 -4
- agno/os/interfaces/whatsapp/router.py +7 -4
- agno/os/interfaces/whatsapp/whatsapp.py +2 -2
- agno/os/router.py +18 -0
- agno/os/utils.py +10 -2
- agno/reasoning/azure_ai_foundry.py +2 -2
- agno/reasoning/deepseek.py +2 -2
- agno/reasoning/default.py +3 -1
- agno/reasoning/groq.py +2 -2
- agno/reasoning/ollama.py +2 -2
- agno/reasoning/openai.py +2 -2
- agno/run/base.py +15 -2
- agno/session/agent.py +8 -5
- agno/session/team.py +14 -10
- agno/team/team.py +218 -111
- agno/tools/function.py +43 -4
- agno/tools/mcp.py +60 -37
- agno/tools/mcp_toolbox.py +284 -0
- agno/tools/scrapegraph.py +58 -31
- agno/tools/whatsapp.py +1 -1
- agno/utils/gemini.py +147 -19
- agno/utils/models/claude.py +9 -0
- agno/utils/print_response/agent.py +18 -2
- agno/utils/print_response/team.py +22 -6
- agno/utils/reasoning.py +22 -1
- agno/utils/string.py +9 -0
- agno/vectordb/base.py +2 -2
- agno/vectordb/langchaindb/langchaindb.py +5 -7
- agno/vectordb/llamaindex/llamaindexdb.py +25 -6
- agno/workflow/workflow.py +30 -15
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/METADATA +4 -1
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/RECORD +64 -61
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/WHEEL +0 -0
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/licenses/LICENSE +0 -0
- {agno-2.0.7.dist-info → agno-2.0.9.dist-info}/top_level.txt +0 -0
agno/models/openai/chat.py
CHANGED
|
@@ -16,19 +16,15 @@ from agno.models.response import ModelResponse
|
|
|
16
16
|
from agno.run.agent import RunOutput
|
|
17
17
|
from agno.utils.log import log_debug, log_error, log_warning
|
|
18
18
|
from agno.utils.openai import _format_file_for_message, audio_to_message, images_to_message
|
|
19
|
+
from agno.utils.reasoning import extract_thinking_content
|
|
19
20
|
|
|
20
21
|
try:
|
|
21
22
|
from openai import APIConnectionError, APIStatusError, RateLimitError
|
|
22
23
|
from openai import AsyncOpenAI as AsyncOpenAIClient
|
|
23
24
|
from openai import OpenAI as OpenAIClient
|
|
24
25
|
from openai.types import CompletionUsage
|
|
25
|
-
from openai.types.chat import ChatCompletionAudio
|
|
26
|
-
from openai.types.chat.
|
|
27
|
-
from openai.types.chat.chat_completion_chunk import (
|
|
28
|
-
ChatCompletionChunk,
|
|
29
|
-
ChoiceDelta,
|
|
30
|
-
ChoiceDeltaToolCall,
|
|
31
|
-
)
|
|
26
|
+
from openai.types.chat import ChatCompletion, ChatCompletionAudio, ChatCompletionChunk
|
|
27
|
+
from openai.types.chat.chat_completion_chunk import ChoiceDelta, ChoiceDeltaToolCall
|
|
32
28
|
except (ImportError, ModuleNotFoundError):
|
|
33
29
|
raise ImportError("`openai` not installed. Please install using `pip install openai`")
|
|
34
30
|
|
|
@@ -716,6 +712,12 @@ class OpenAIChat(Model):
|
|
|
716
712
|
if response_message.content is not None:
|
|
717
713
|
model_response.content = response_message.content
|
|
718
714
|
|
|
715
|
+
# Extract thinking content before any structured parsing
|
|
716
|
+
if model_response.content:
|
|
717
|
+
reasoning_content, output_content = extract_thinking_content(model_response.content)
|
|
718
|
+
if reasoning_content:
|
|
719
|
+
model_response.reasoning_content = reasoning_content
|
|
720
|
+
model_response.content = output_content
|
|
719
721
|
# Add tool calls
|
|
720
722
|
if response_message.tool_calls is not None and len(response_message.tool_calls) > 0:
|
|
721
723
|
try:
|
agno/models/openai/responses.py
CHANGED
|
@@ -19,10 +19,7 @@ from agno.utils.models.schema_utils import get_response_schema_for_provider
|
|
|
19
19
|
|
|
20
20
|
try:
|
|
21
21
|
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, OpenAI, RateLimitError
|
|
22
|
-
from openai.types.responses
|
|
23
|
-
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
|
24
|
-
from openai.types.responses.response_stream_event import ResponseStreamEvent
|
|
25
|
-
from openai.types.responses.response_usage import ResponseUsage
|
|
22
|
+
from openai.types.responses import Response, ResponseReasoningItem, ResponseStreamEvent, ResponseUsage
|
|
26
23
|
except ImportError as e:
|
|
27
24
|
raise ImportError("`openai` not installed. Please install using `pip install openai -U`") from e
|
|
28
25
|
|
|
@@ -407,21 +404,28 @@ class OpenAIResponses(Model):
|
|
|
407
404
|
"""
|
|
408
405
|
formatted_messages: List[Union[Dict[str, Any], ResponseReasoningItem]] = []
|
|
409
406
|
|
|
410
|
-
|
|
407
|
+
messages_to_format = messages
|
|
408
|
+
previous_response_id: Optional[str] = None
|
|
409
|
+
|
|
410
|
+
if self._using_reasoning_model() and self.store is not False:
|
|
411
411
|
# Detect whether we're chaining via previous_response_id. If so, we should NOT
|
|
412
412
|
# re-send prior function_call items; the Responses API already has the state and
|
|
413
413
|
# expects only the corresponding function_call_output items.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
414
|
+
|
|
415
|
+
for msg in reversed(messages):
|
|
416
|
+
if (
|
|
417
|
+
msg.role == "assistant"
|
|
418
|
+
and hasattr(msg, "provider_data")
|
|
419
|
+
and msg.provider_data
|
|
420
|
+
and "response_id" in msg.provider_data
|
|
421
|
+
):
|
|
422
|
+
previous_response_id = msg.provider_data["response_id"]
|
|
423
|
+
msg_index = messages.index(msg)
|
|
424
|
+
|
|
425
|
+
# Include messages after this assistant message
|
|
426
|
+
messages_to_format = messages[msg_index + 1 :]
|
|
427
|
+
|
|
428
|
+
break
|
|
425
429
|
|
|
426
430
|
# Build a mapping from function_call id (fc_*) → call_id (call_*) from prior assistant tool_calls
|
|
427
431
|
fc_id_to_call_id: Dict[str, str] = {}
|
|
@@ -434,7 +438,7 @@ class OpenAIResponses(Model):
|
|
|
434
438
|
if isinstance(fc_id, str) and isinstance(call_id, str):
|
|
435
439
|
fc_id_to_call_id[fc_id] = call_id
|
|
436
440
|
|
|
437
|
-
for message in
|
|
441
|
+
for message in messages_to_format:
|
|
438
442
|
if message.role in ["user", "system"]:
|
|
439
443
|
message_dict: Dict[str, Any] = {
|
|
440
444
|
"role": self.role_map[message.role],
|
agno/os/interfaces/agui/agui.py
CHANGED
|
@@ -19,8 +19,8 @@ class AGUI(BaseInterface):
|
|
|
19
19
|
self.agent = agent
|
|
20
20
|
self.team = team
|
|
21
21
|
|
|
22
|
-
if not self.agent
|
|
23
|
-
raise ValueError("AGUI requires an agent
|
|
22
|
+
if not (self.agent or self.team):
|
|
23
|
+
raise ValueError("AGUI requires an agent or a team")
|
|
24
24
|
|
|
25
25
|
def get_router(self, **kwargs) -> APIRouter:
|
|
26
26
|
# Cannot be overridden
|
agno/os/interfaces/agui/utils.py
CHANGED
|
@@ -35,10 +35,16 @@ class EventBuffer:
|
|
|
35
35
|
|
|
36
36
|
active_tool_call_ids: Set[str] # All currently active tool calls
|
|
37
37
|
ended_tool_call_ids: Set[str] # All tool calls that have ended
|
|
38
|
+
current_text_message_id: str = "" # ID of the current text message context (for tool call parenting)
|
|
39
|
+
next_text_message_id: str = "" # Pre-generated ID for the next text message
|
|
40
|
+
pending_tool_calls_parent_id: str = "" # Parent message ID for pending tool calls
|
|
38
41
|
|
|
39
42
|
def __init__(self):
|
|
40
43
|
self.active_tool_call_ids = set()
|
|
41
44
|
self.ended_tool_call_ids = set()
|
|
45
|
+
self.current_text_message_id = ""
|
|
46
|
+
self.next_text_message_id = str(uuid.uuid4())
|
|
47
|
+
self.pending_tool_calls_parent_id = ""
|
|
42
48
|
|
|
43
49
|
def start_tool_call(self, tool_call_id: str) -> None:
|
|
44
50
|
"""Start a new tool call."""
|
|
@@ -49,6 +55,29 @@ class EventBuffer:
|
|
|
49
55
|
self.active_tool_call_ids.discard(tool_call_id)
|
|
50
56
|
self.ended_tool_call_ids.add(tool_call_id)
|
|
51
57
|
|
|
58
|
+
def start_text_message(self) -> str:
|
|
59
|
+
"""Start a new text message and return its ID."""
|
|
60
|
+
# Use the pre-generated next ID as current, and generate a new next ID
|
|
61
|
+
self.current_text_message_id = self.next_text_message_id
|
|
62
|
+
self.next_text_message_id = str(uuid.uuid4())
|
|
63
|
+
return self.current_text_message_id
|
|
64
|
+
|
|
65
|
+
def get_parent_message_id_for_tool_call(self) -> str:
|
|
66
|
+
"""Get the message ID to use as parent for tool calls."""
|
|
67
|
+
# If we have a pending parent ID set (from text message end), use that
|
|
68
|
+
if self.pending_tool_calls_parent_id:
|
|
69
|
+
return self.pending_tool_calls_parent_id
|
|
70
|
+
# Otherwise use current text message ID
|
|
71
|
+
return self.current_text_message_id
|
|
72
|
+
|
|
73
|
+
def set_pending_tool_calls_parent_id(self, parent_id: str) -> None:
|
|
74
|
+
"""Set the parent message ID for upcoming tool calls."""
|
|
75
|
+
self.pending_tool_calls_parent_id = parent_id
|
|
76
|
+
|
|
77
|
+
def clear_pending_tool_calls_parent_id(self) -> None:
|
|
78
|
+
"""Clear the pending parent ID when a new text message starts."""
|
|
79
|
+
self.pending_tool_calls_parent_id = ""
|
|
80
|
+
|
|
52
81
|
|
|
53
82
|
def convert_agui_messages_to_agno_messages(messages: List[AGUIMessage]) -> List[Message]:
|
|
54
83
|
"""Convert AG-UI messages to Agno messages."""
|
|
@@ -113,10 +142,18 @@ def _create_events_from_chunk(
|
|
|
113
142
|
message_id: str,
|
|
114
143
|
message_started: bool,
|
|
115
144
|
event_buffer: EventBuffer,
|
|
116
|
-
) -> Tuple[List[BaseEvent], bool]:
|
|
145
|
+
) -> Tuple[List[BaseEvent], bool, str]:
|
|
117
146
|
"""
|
|
118
147
|
Process a single chunk and return events to emit + updated message_started state.
|
|
119
|
-
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
chunk: The event chunk to process
|
|
151
|
+
message_id: Current message identifier
|
|
152
|
+
message_started: Whether a message is currently active
|
|
153
|
+
event_buffer: Event buffer for tracking tool call state
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Tuple of (events_to_emit, new_message_started_state, message_id)
|
|
120
157
|
"""
|
|
121
158
|
events_to_emit: List[BaseEvent] = []
|
|
122
159
|
|
|
@@ -133,6 +170,11 @@ def _create_events_from_chunk(
|
|
|
133
170
|
# Handle the message start event, emitted once per message
|
|
134
171
|
if not message_started:
|
|
135
172
|
message_started = True
|
|
173
|
+
message_id = event_buffer.start_text_message()
|
|
174
|
+
|
|
175
|
+
# Clear pending tool calls parent ID when starting new text message
|
|
176
|
+
event_buffer.clear_pending_tool_calls_parent_id()
|
|
177
|
+
|
|
136
178
|
start_event = TextMessageStartEvent(
|
|
137
179
|
type=EventType.TEXT_MESSAGE_START,
|
|
138
180
|
message_id=message_id,
|
|
@@ -149,21 +191,37 @@ def _create_events_from_chunk(
|
|
|
149
191
|
)
|
|
150
192
|
events_to_emit.append(content_event) # type: ignore
|
|
151
193
|
|
|
152
|
-
# Handle starting a new tool
|
|
153
|
-
elif chunk.event == RunEvent.tool_call_started:
|
|
154
|
-
# End the current text message if one is active before starting tool calls
|
|
155
|
-
if message_started:
|
|
156
|
-
end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=message_id)
|
|
157
|
-
events_to_emit.append(end_message_event)
|
|
158
|
-
message_started = False # Reset message_started state
|
|
159
|
-
|
|
194
|
+
# Handle starting a new tool
|
|
195
|
+
elif chunk.event == RunEvent.tool_call_started or chunk.event == TeamRunEvent.tool_call_started:
|
|
160
196
|
if chunk.tool is not None: # type: ignore
|
|
161
197
|
tool_call = chunk.tool # type: ignore
|
|
198
|
+
|
|
199
|
+
# End current text message and handle for tool calls
|
|
200
|
+
current_message_id = message_id
|
|
201
|
+
if message_started:
|
|
202
|
+
# End the current text message
|
|
203
|
+
end_message_event = TextMessageEndEvent(type=EventType.TEXT_MESSAGE_END, message_id=current_message_id)
|
|
204
|
+
events_to_emit.append(end_message_event)
|
|
205
|
+
|
|
206
|
+
# Set this message as the parent for any upcoming tool calls
|
|
207
|
+
# This ensures multiple sequential tool calls all use the same parent
|
|
208
|
+
event_buffer.set_pending_tool_calls_parent_id(current_message_id)
|
|
209
|
+
|
|
210
|
+
# Reset message started state and generate new message_id for future messages
|
|
211
|
+
message_started = False
|
|
212
|
+
message_id = str(uuid.uuid4())
|
|
213
|
+
|
|
214
|
+
# Get the parent message ID - this will use pending parent if set, ensuring multiple tool calls in sequence have the same parent
|
|
215
|
+
parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
|
|
216
|
+
|
|
217
|
+
if not parent_message_id:
|
|
218
|
+
parent_message_id = current_message_id
|
|
219
|
+
|
|
162
220
|
start_event = ToolCallStartEvent(
|
|
163
221
|
type=EventType.TOOL_CALL_START,
|
|
164
222
|
tool_call_id=tool_call.tool_call_id, # type: ignore
|
|
165
223
|
tool_call_name=tool_call.tool_name, # type: ignore
|
|
166
|
-
parent_message_id=
|
|
224
|
+
parent_message_id=parent_message_id,
|
|
167
225
|
)
|
|
168
226
|
events_to_emit.append(start_event)
|
|
169
227
|
|
|
@@ -175,7 +233,7 @@ def _create_events_from_chunk(
|
|
|
175
233
|
events_to_emit.append(args_event) # type: ignore
|
|
176
234
|
|
|
177
235
|
# Handle tool call completion
|
|
178
|
-
elif chunk.event == RunEvent.tool_call_completed:
|
|
236
|
+
elif chunk.event == RunEvent.tool_call_completed or chunk.event == TeamRunEvent.tool_call_completed:
|
|
179
237
|
if chunk.tool is not None: # type: ignore
|
|
180
238
|
tool_call = chunk.tool # type: ignore
|
|
181
239
|
if tool_call.tool_call_id not in event_buffer.ended_tool_call_ids:
|
|
@@ -203,7 +261,7 @@ def _create_events_from_chunk(
|
|
|
203
261
|
step_finished_event = StepFinishedEvent(type=EventType.STEP_FINISHED, step_name="reasoning")
|
|
204
262
|
events_to_emit.append(step_finished_event)
|
|
205
263
|
|
|
206
|
-
return events_to_emit, message_started
|
|
264
|
+
return events_to_emit, message_started, message_id
|
|
207
265
|
|
|
208
266
|
|
|
209
267
|
def _create_completion_events(
|
|
@@ -237,11 +295,16 @@ def _create_completion_events(
|
|
|
237
295
|
if tool.tool_call_id is None or tool.tool_name is None:
|
|
238
296
|
continue
|
|
239
297
|
|
|
298
|
+
# Use the current text message ID from event buffer as parent
|
|
299
|
+
parent_message_id = event_buffer.get_parent_message_id_for_tool_call()
|
|
300
|
+
if not parent_message_id:
|
|
301
|
+
parent_message_id = message_id # Fallback to the passed message_id
|
|
302
|
+
|
|
240
303
|
start_event = ToolCallStartEvent(
|
|
241
304
|
type=EventType.TOOL_CALL_START,
|
|
242
305
|
tool_call_id=tool.tool_call_id,
|
|
243
306
|
tool_call_name=tool.tool_name,
|
|
244
|
-
parent_message_id=
|
|
307
|
+
parent_message_id=parent_message_id,
|
|
245
308
|
)
|
|
246
309
|
events_to_emit.append(start_event)
|
|
247
310
|
|
|
@@ -285,7 +348,7 @@ def stream_agno_response_as_agui_events(
|
|
|
285
348
|
response_stream: Iterator[Union[RunOutputEvent, TeamRunOutputEvent]], thread_id: str, run_id: str
|
|
286
349
|
) -> Iterator[BaseEvent]:
|
|
287
350
|
"""Map the Agno response stream to AG-UI format, handling event ordering constraints."""
|
|
288
|
-
message_id =
|
|
351
|
+
message_id = "" # Will be set by EventBuffer when text message starts
|
|
289
352
|
message_started = False
|
|
290
353
|
event_buffer = EventBuffer()
|
|
291
354
|
stream_completed = False
|
|
@@ -304,7 +367,7 @@ def stream_agno_response_as_agui_events(
|
|
|
304
367
|
stream_completed = True
|
|
305
368
|
else:
|
|
306
369
|
# Process regular chunk immediately
|
|
307
|
-
events_from_chunk, message_started = _create_events_from_chunk(
|
|
370
|
+
events_from_chunk, message_started, message_id = _create_events_from_chunk(
|
|
308
371
|
chunk, message_id, message_started, event_buffer
|
|
309
372
|
)
|
|
310
373
|
|
|
@@ -345,7 +408,7 @@ async def async_stream_agno_response_as_agui_events(
|
|
|
345
408
|
run_id: str,
|
|
346
409
|
) -> AsyncIterator[BaseEvent]:
|
|
347
410
|
"""Map the Agno response stream to AG-UI format, handling event ordering constraints."""
|
|
348
|
-
message_id =
|
|
411
|
+
message_id = "" # Will be set by EventBuffer when text message starts
|
|
349
412
|
message_started = False
|
|
350
413
|
event_buffer = EventBuffer()
|
|
351
414
|
stream_completed = False
|
|
@@ -364,7 +427,7 @@ async def async_stream_agno_response_as_agui_events(
|
|
|
364
427
|
stream_completed = True
|
|
365
428
|
else:
|
|
366
429
|
# Process regular chunk immediately
|
|
367
|
-
events_from_chunk, message_started = _create_events_from_chunk(
|
|
430
|
+
events_from_chunk, message_started, message_id = _create_events_from_chunk(
|
|
368
431
|
chunk, message_id, message_started, event_buffer
|
|
369
432
|
)
|
|
370
433
|
|
agno/os/interfaces/base.py
CHANGED
|
@@ -5,6 +5,7 @@ from fastapi import APIRouter
|
|
|
5
5
|
|
|
6
6
|
from agno.agent import Agent
|
|
7
7
|
from agno.team import Team
|
|
8
|
+
from agno.workflow.workflow import Workflow
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class BaseInterface(ABC):
|
|
@@ -13,6 +14,7 @@ class BaseInterface(ABC):
|
|
|
13
14
|
router_prefix: str = ""
|
|
14
15
|
agent: Optional[Agent] = None
|
|
15
16
|
team: Optional[Team] = None
|
|
17
|
+
workflow: Optional[Workflow] = None
|
|
16
18
|
|
|
17
19
|
router: APIRouter
|
|
18
20
|
|
|
@@ -1,16 +1,49 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
3
|
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
4
5
|
|
|
5
6
|
from agno.agent.agent import Agent
|
|
6
7
|
from agno.os.interfaces.slack.security import verify_slack_signature
|
|
7
8
|
from agno.team.team import Team
|
|
8
9
|
from agno.tools.slack import SlackTools
|
|
9
10
|
from agno.utils.log import log_info
|
|
11
|
+
from agno.workflow.workflow import Workflow
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
class SlackEventResponse(BaseModel):
|
|
15
|
+
"""Response model for Slack event processing"""
|
|
16
|
+
|
|
17
|
+
status: str = Field(default="ok", description="Processing status")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SlackChallengeResponse(BaseModel):
|
|
21
|
+
"""Response model for Slack URL verification challenge"""
|
|
22
|
+
|
|
23
|
+
challenge: str = Field(description="Challenge string to echo back to Slack")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def attach_routes(
|
|
27
|
+
router: APIRouter, agent: Optional[Agent] = None, team: Optional[Team] = None, workflow: Optional[Workflow] = None
|
|
28
|
+
) -> APIRouter:
|
|
29
|
+
# Determine entity type for documentation
|
|
30
|
+
entity_type = "agent" if agent else "team" if team else "workflow" if workflow else "unknown"
|
|
31
|
+
entity_name = getattr(agent or team or workflow, "name", f"Unnamed {entity_type}")
|
|
32
|
+
|
|
33
|
+
@router.post(
|
|
34
|
+
"/events",
|
|
35
|
+
operation_id=f"slack_events_{entity_type}",
|
|
36
|
+
summary=f"Process Slack Events for {entity_type.title()}",
|
|
37
|
+
description=f"Process incoming Slack events and route them to the configured {entity_type}: {entity_name}",
|
|
38
|
+
tags=["Slack", f"Slack-{entity_type.title()}"],
|
|
39
|
+
response_model=SlackEventResponse,
|
|
40
|
+
response_model_exclude_none=True,
|
|
41
|
+
responses={
|
|
42
|
+
200: {"description": "Event processed successfully"},
|
|
43
|
+
400: {"description": "Missing Slack headers"},
|
|
44
|
+
403: {"description": "Invalid Slack signature"},
|
|
45
|
+
},
|
|
46
|
+
)
|
|
14
47
|
async def slack_events(request: Request, background_tasks: BackgroundTasks):
|
|
15
48
|
body = await request.body()
|
|
16
49
|
timestamp = request.headers.get("X-Slack-Request-Timestamp")
|
|
@@ -26,7 +59,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
26
59
|
|
|
27
60
|
# Handle URL verification
|
|
28
61
|
if data.get("type") == "url_verification":
|
|
29
|
-
return
|
|
62
|
+
return SlackChallengeResponse(challenge=data.get("challenge"))
|
|
30
63
|
|
|
31
64
|
# Process other event types (e.g., message events) asynchronously
|
|
32
65
|
if "event" in data:
|
|
@@ -37,7 +70,7 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
37
70
|
else:
|
|
38
71
|
background_tasks.add_task(_process_slack_event, event)
|
|
39
72
|
|
|
40
|
-
return
|
|
73
|
+
return SlackEventResponse(status="ok")
|
|
41
74
|
|
|
42
75
|
async def _process_slack_event(event: dict):
|
|
43
76
|
if event.get("type") == "message":
|
|
@@ -57,12 +90,19 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
57
90
|
response = await agent.arun(message_text, user_id=user if user else None, session_id=session_id)
|
|
58
91
|
elif team:
|
|
59
92
|
response = await team.arun(message_text, user_id=user if user else None, session_id=session_id) # type: ignore
|
|
93
|
+
elif workflow:
|
|
94
|
+
response = await workflow.arun(message_text, user_id=user if user else None, session_id=session_id) # type: ignore
|
|
60
95
|
|
|
61
|
-
if response
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
if response:
|
|
97
|
+
if hasattr(response, "reasoning_content") and response.reasoning_content:
|
|
98
|
+
_send_slack_message(
|
|
99
|
+
channel=channel_id,
|
|
100
|
+
message=f"Reasoning: \n{response.reasoning_content}",
|
|
101
|
+
thread_ts=ts,
|
|
102
|
+
italics=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
_send_slack_message(channel=channel_id, message=response.content or "", thread_ts=ts)
|
|
66
106
|
|
|
67
107
|
def _send_slack_message(channel: str, thread_ts: str, message: str, italics: bool = False):
|
|
68
108
|
if len(message) <= 40000:
|
|
@@ -85,6 +125,6 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
85
125
|
formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
|
|
86
126
|
SlackTools().send_message_thread(channel=channel, text=formatted_batch or "", thread_ts=thread_ts)
|
|
87
127
|
else:
|
|
88
|
-
SlackTools().send_message_thread(channel=channel, text=
|
|
128
|
+
SlackTools().send_message_thread(channel=channel, text=batch_message or "", thread_ts=thread_ts)
|
|
89
129
|
|
|
90
130
|
return router
|
|
@@ -7,6 +7,7 @@ from agno.agent.agent import Agent
|
|
|
7
7
|
from agno.os.interfaces.base import BaseInterface
|
|
8
8
|
from agno.os.interfaces.slack.router import attach_routes
|
|
9
9
|
from agno.team.team import Team
|
|
10
|
+
from agno.workflow.workflow import Workflow
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
@@ -16,17 +17,18 @@ class Slack(BaseInterface):
|
|
|
16
17
|
|
|
17
18
|
router: APIRouter
|
|
18
19
|
|
|
19
|
-
def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None):
|
|
20
|
+
def __init__(self, agent: Optional[Agent] = None, team: Optional[Team] = None, workflow: Optional[Workflow] = None):
|
|
20
21
|
self.agent = agent
|
|
21
22
|
self.team = team
|
|
23
|
+
self.workflow = workflow
|
|
22
24
|
|
|
23
|
-
if not self.agent
|
|
24
|
-
raise ValueError("Slack requires an agent
|
|
25
|
+
if not (self.agent or self.team or self.workflow):
|
|
26
|
+
raise ValueError("Slack requires an agent, team or workflow")
|
|
25
27
|
|
|
26
28
|
def get_router(self, **kwargs) -> APIRouter:
|
|
27
29
|
# Cannot be overridden
|
|
28
30
|
self.router = APIRouter(prefix="/slack", tags=["Slack"])
|
|
29
31
|
|
|
30
|
-
self.router = attach_routes(router=self.router, agent=self.agent, team=self.team)
|
|
32
|
+
self.router = attach_routes(router=self.router, agent=self.agent, team=self.team, workflow=self.workflow)
|
|
31
33
|
|
|
32
34
|
return self.router
|
|
@@ -19,6 +19,9 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
19
19
|
if agent is None and team is None:
|
|
20
20
|
raise ValueError("Either agent or team must be provided.")
|
|
21
21
|
|
|
22
|
+
# Create WhatsApp tools instance once for reuse
|
|
23
|
+
whatsapp_tools = WhatsAppTools(async_mode=True)
|
|
24
|
+
|
|
22
25
|
@router.get("/status")
|
|
23
26
|
async def status():
|
|
24
27
|
return {"status": "available"}
|
|
@@ -185,9 +188,9 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
185
188
|
if italics:
|
|
186
189
|
# Handle multi-line messages by making each line italic
|
|
187
190
|
formatted_message = "\n".join([f"_{line}_" for line in message.split("\n")])
|
|
188
|
-
await
|
|
191
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_message)
|
|
189
192
|
else:
|
|
190
|
-
await
|
|
193
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=message)
|
|
191
194
|
return
|
|
192
195
|
|
|
193
196
|
# Split message into batches of 4000 characters (WhatsApp message limit is 4096)
|
|
@@ -199,8 +202,8 @@ def attach_routes(router: APIRouter, agent: Optional[Agent] = None, team: Option
|
|
|
199
202
|
if italics:
|
|
200
203
|
# Handle multi-line messages by making each line italic
|
|
201
204
|
formatted_batch = "\n".join([f"_{line}_" for line in batch_message.split("\n")])
|
|
202
|
-
await
|
|
205
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=formatted_batch)
|
|
203
206
|
else:
|
|
204
|
-
await
|
|
207
|
+
await whatsapp_tools.send_text_message_async(recipient=recipient, text=batch_message)
|
|
205
208
|
|
|
206
209
|
return router
|
|
@@ -17,8 +17,8 @@ class Whatsapp(BaseInterface):
|
|
|
17
17
|
self.agent = agent
|
|
18
18
|
self.team = team
|
|
19
19
|
|
|
20
|
-
if not self.agent
|
|
21
|
-
raise ValueError("Whatsapp requires an agent
|
|
20
|
+
if not (self.agent or self.team):
|
|
21
|
+
raise ValueError("Whatsapp requires an agent or a team")
|
|
22
22
|
|
|
23
23
|
def get_router(self, **kwargs) -> APIRouter:
|
|
24
24
|
# Cannot be overridden
|
agno/os/router.py
CHANGED
|
@@ -72,6 +72,24 @@ async def _get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict
|
|
|
72
72
|
sig = inspect.signature(endpoint_func)
|
|
73
73
|
known_fields = set(sig.parameters.keys())
|
|
74
74
|
kwargs = {key: value for key, value in form_data.items() if key not in known_fields}
|
|
75
|
+
|
|
76
|
+
# Handle JSON parameters. They are passed as strings and need to be deserialized.
|
|
77
|
+
|
|
78
|
+
if session_state := kwargs.get("session_state"):
|
|
79
|
+
try:
|
|
80
|
+
session_state_dict = json.loads(session_state) # type: ignore
|
|
81
|
+
kwargs["session_state"] = session_state_dict
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
kwargs.pop("session_state")
|
|
84
|
+
log_warning(f"Invalid session_state parameter couldn't be loaded: {session_state}")
|
|
85
|
+
if dependencies := kwargs.get("dependencies"):
|
|
86
|
+
try:
|
|
87
|
+
dependencies_dict = json.loads(dependencies) # type: ignore
|
|
88
|
+
kwargs["dependencies"] = dependencies_dict
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
kwargs.pop("dependencies")
|
|
91
|
+
log_warning(f"Invalid dependencies parameter couldn't be loaded: {dependencies}")
|
|
92
|
+
|
|
75
93
|
return kwargs
|
|
76
94
|
|
|
77
95
|
|
agno/os/utils.py
CHANGED
|
@@ -57,12 +57,20 @@ def get_run_input(run_dict: Dict[str, Any], is_workflow_run: bool = False) -> st
|
|
|
57
57
|
if is_workflow_run:
|
|
58
58
|
step_executor_runs = run_dict.get("step_executor_runs", [])
|
|
59
59
|
if step_executor_runs:
|
|
60
|
-
for message in step_executor_runs[0].get("messages", []):
|
|
60
|
+
for message in reversed(step_executor_runs[0].get("messages", [])):
|
|
61
61
|
if message.get("role") == "user":
|
|
62
62
|
return message.get("content", "")
|
|
63
63
|
|
|
64
|
+
# Check the input field directly as final fallback
|
|
65
|
+
if run_dict.get("input") is not None:
|
|
66
|
+
input_value = run_dict.get("input")
|
|
67
|
+
if isinstance(input_value, str):
|
|
68
|
+
return input_value
|
|
69
|
+
else:
|
|
70
|
+
return str(input_value)
|
|
71
|
+
|
|
64
72
|
if run_dict.get("messages") is not None:
|
|
65
|
-
for message in run_dict["messages"]:
|
|
73
|
+
for message in reversed(run_dict["messages"]):
|
|
66
74
|
if message.get("role") == "user":
|
|
67
75
|
return message.get("content", "")
|
|
68
76
|
|
|
@@ -20,7 +20,7 @@ def get_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Message])
|
|
|
20
20
|
from agno.run.agent import RunOutput
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
reasoning_agent_response: RunOutput = reasoning_agent.run(
|
|
23
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
24
24
|
except Exception as e:
|
|
25
25
|
logger.warning(f"Reasoning error: {e}")
|
|
26
26
|
return None
|
|
@@ -46,7 +46,7 @@ async def aget_ai_foundry_reasoning(reasoning_agent: "Agent", messages: List[Mes
|
|
|
46
46
|
from agno.run.agent import RunOutput
|
|
47
47
|
|
|
48
48
|
try:
|
|
49
|
-
reasoning_agent_response: RunOutput = await reasoning_agent.arun(
|
|
49
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
50
50
|
except Exception as e:
|
|
51
51
|
logger.warning(f"Reasoning error: {e}")
|
|
52
52
|
return None
|
agno/reasoning/deepseek.py
CHANGED
|
@@ -20,7 +20,7 @@ def get_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Message]) ->
|
|
|
20
20
|
message.role = "system"
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
reasoning_agent_response: RunOutput = reasoning_agent.run(
|
|
23
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
24
24
|
except Exception as e:
|
|
25
25
|
logger.warning(f"Reasoning error: {e}")
|
|
26
26
|
return None
|
|
@@ -46,7 +46,7 @@ async def aget_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Messa
|
|
|
46
46
|
message.role = "system"
|
|
47
47
|
|
|
48
48
|
try:
|
|
49
|
-
reasoning_agent_response: RunOutput = await reasoning_agent.arun(
|
|
49
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
50
50
|
except Exception as e:
|
|
51
51
|
logger.warning(f"Reasoning error: {e}")
|
|
52
52
|
return None
|
agno/reasoning/default.py
CHANGED
|
@@ -14,6 +14,7 @@ def get_default_reasoning_agent(
|
|
|
14
14
|
min_steps: int,
|
|
15
15
|
max_steps: int,
|
|
16
16
|
tools: Optional[List[Union[Toolkit, Callable, Function, Dict]]] = None,
|
|
17
|
+
tool_call_limit: Optional[int] = None,
|
|
17
18
|
use_json_mode: bool = False,
|
|
18
19
|
telemetry: bool = True,
|
|
19
20
|
debug_mode: bool = False,
|
|
@@ -56,7 +57,7 @@ def get_default_reasoning_agent(
|
|
|
56
57
|
- **validate**: When you reach a potential answer, signaling it's ready for validation.
|
|
57
58
|
- **final_answer**: Only if you have confidently validated the solution.
|
|
58
59
|
- **reset**: Immediately restart analysis if a critical error or incorrect result is identified.
|
|
59
|
-
6. **Confidence Score**: Provide a numeric confidence score (0.0–1.0) indicating your certainty in the step
|
|
60
|
+
6. **Confidence Score**: Provide a numeric confidence score (0.0–1.0) indicating your certainty in the step's correctness and its outcome.
|
|
60
61
|
|
|
61
62
|
Step 5 - Validation (mandatory before finalizing an answer):
|
|
62
63
|
- Explicitly validate your solution by:
|
|
@@ -82,6 +83,7 @@ def get_default_reasoning_agent(
|
|
|
82
83
|
- Only create a single instance of ReasoningSteps for your response.\
|
|
83
84
|
"""),
|
|
84
85
|
tools=tools,
|
|
86
|
+
tool_call_limit=tool_call_limit,
|
|
85
87
|
output_schema=ReasoningSteps,
|
|
86
88
|
use_json_mode=use_json_mode,
|
|
87
89
|
telemetry=telemetry,
|
agno/reasoning/groq.py
CHANGED
|
@@ -20,7 +20,7 @@ def get_groq_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Opt
|
|
|
20
20
|
message.role = "system"
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
reasoning_agent_response: RunOutput = reasoning_agent.run(
|
|
23
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
24
24
|
except Exception as e:
|
|
25
25
|
logger.warning(f"Reasoning error: {e}")
|
|
26
26
|
return None
|
|
@@ -50,7 +50,7 @@ async def aget_groq_reasoning(reasoning_agent: "Agent", messages: List[Message])
|
|
|
50
50
|
message.role = "system"
|
|
51
51
|
|
|
52
52
|
try:
|
|
53
|
-
reasoning_agent_response: RunOutput = await reasoning_agent.arun(
|
|
53
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
54
54
|
except Exception as e:
|
|
55
55
|
logger.warning(f"Reasoning error: {e}")
|
|
56
56
|
return None
|
agno/reasoning/ollama.py
CHANGED
|
@@ -20,7 +20,7 @@ def get_ollama_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> O
|
|
|
20
20
|
from agno.run.agent import RunOutput
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
|
-
reasoning_agent_response: RunOutput = reasoning_agent.run(
|
|
23
|
+
reasoning_agent_response: RunOutput = reasoning_agent.run(input=messages)
|
|
24
24
|
except Exception as e:
|
|
25
25
|
logger.warning(f"Reasoning error: {e}")
|
|
26
26
|
return None
|
|
@@ -46,7 +46,7 @@ async def aget_ollama_reasoning(reasoning_agent: "Agent", messages: List[Message
|
|
|
46
46
|
from agno.run.agent import RunOutput
|
|
47
47
|
|
|
48
48
|
try:
|
|
49
|
-
reasoning_agent_response: RunOutput = await reasoning_agent.arun(
|
|
49
|
+
reasoning_agent_response: RunOutput = await reasoning_agent.arun(input=messages)
|
|
50
50
|
except Exception as e:
|
|
51
51
|
logger.warning(f"Reasoning error: {e}")
|
|
52
52
|
return None
|