agno 2.3.6__py3-none-any.whl → 2.3.8__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 +540 -369
- agno/db/mongo/async_mongo.py +0 -24
- agno/db/mongo/mongo.py +0 -16
- agno/db/mysql/mysql.py +0 -19
- agno/db/postgres/async_postgres.py +23 -28
- agno/db/postgres/postgres.py +0 -23
- agno/db/redis/redis.py +0 -4
- agno/db/singlestore/singlestore.py +0 -11
- agno/db/sqlite/async_sqlite.py +0 -24
- agno/db/sqlite/sqlite.py +0 -20
- agno/db/utils.py +2 -0
- agno/models/base.py +168 -15
- agno/models/openai/responses.py +3 -2
- agno/models/response.py +1 -1
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/middleware/jwt.py +8 -6
- agno/os/routers/evals/utils.py +13 -3
- agno/run/agent.py +17 -0
- agno/run/requirement.py +98 -0
- agno/run/team.py +10 -0
- agno/team/team.py +179 -96
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/utils/events.py +5 -1
- agno/workflow/parallel.py +8 -2
- agno/workflow/step.py +3 -3
- {agno-2.3.6.dist-info → agno-2.3.8.dist-info}/METADATA +5 -2
- {agno-2.3.6.dist-info → agno-2.3.8.dist-info}/RECORD +33 -32
- agno/tools/memori.py +0 -339
- {agno-2.3.6.dist-info → agno-2.3.8.dist-info}/WHEEL +0 -0
- {agno-2.3.6.dist-info → agno-2.3.8.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.6.dist-info → agno-2.3.8.dist-info}/top_level.txt +0 -0
agno/models/base.py
CHANGED
|
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
6
|
from hashlib import md5
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from time import time
|
|
8
|
+
from time import sleep, time
|
|
9
9
|
from types import AsyncGeneratorType, GeneratorType
|
|
10
10
|
from typing import (
|
|
11
11
|
Any,
|
|
@@ -24,12 +24,13 @@ from uuid import uuid4
|
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel
|
|
26
26
|
|
|
27
|
-
from agno.exceptions import AgentRunException
|
|
27
|
+
from agno.exceptions import AgentRunException, ModelProviderError
|
|
28
28
|
from agno.media import Audio, File, Image, Video
|
|
29
29
|
from agno.models.message import Citations, Message
|
|
30
30
|
from agno.models.metrics import Metrics
|
|
31
31
|
from agno.models.response import ModelResponse, ModelResponseEvent, ToolExecution
|
|
32
32
|
from agno.run.agent import CustomEvent, RunContentEvent, RunOutput, RunOutputEvent
|
|
33
|
+
from agno.run.requirement import RunRequirement
|
|
33
34
|
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
34
35
|
from agno.run.team import TeamRunOutput, TeamRunOutputEvent
|
|
35
36
|
from agno.run.workflow import WorkflowRunOutputEvent
|
|
@@ -145,10 +146,133 @@ class Model(ABC):
|
|
|
145
146
|
cache_ttl: Optional[int] = None
|
|
146
147
|
cache_dir: Optional[str] = None
|
|
147
148
|
|
|
149
|
+
# Retry configuration for model provider errors
|
|
150
|
+
# Number of retries to attempt when a ModelProviderError occurs
|
|
151
|
+
retries: int = 0
|
|
152
|
+
# Delay between retries (in seconds)
|
|
153
|
+
delay_between_retries: int = 1
|
|
154
|
+
# Exponential backoff: if True, the delay between retries is doubled each time
|
|
155
|
+
exponential_backoff: bool = False
|
|
156
|
+
|
|
148
157
|
def __post_init__(self):
|
|
149
158
|
if self.provider is None and self.name is not None:
|
|
150
159
|
self.provider = f"{self.name} ({self.id})"
|
|
151
160
|
|
|
161
|
+
def _get_retry_delay(self, attempt: int) -> float:
|
|
162
|
+
"""Calculate the delay before the next retry attempt."""
|
|
163
|
+
if self.exponential_backoff:
|
|
164
|
+
return self.delay_between_retries * (2**attempt)
|
|
165
|
+
return self.delay_between_retries
|
|
166
|
+
|
|
167
|
+
def _invoke_with_retry(self, **kwargs) -> ModelResponse:
|
|
168
|
+
"""
|
|
169
|
+
Invoke the model with retry logic for ModelProviderError.
|
|
170
|
+
|
|
171
|
+
This method wraps the invoke() call and retries on ModelProviderError
|
|
172
|
+
with optional exponential backoff.
|
|
173
|
+
"""
|
|
174
|
+
last_exception: Optional[ModelProviderError] = None
|
|
175
|
+
|
|
176
|
+
for attempt in range(self.retries + 1):
|
|
177
|
+
try:
|
|
178
|
+
return self.invoke(**kwargs)
|
|
179
|
+
except ModelProviderError as e:
|
|
180
|
+
last_exception = e
|
|
181
|
+
if attempt < self.retries:
|
|
182
|
+
delay = self._get_retry_delay(attempt)
|
|
183
|
+
log_warning(
|
|
184
|
+
f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
|
|
185
|
+
)
|
|
186
|
+
sleep(delay)
|
|
187
|
+
else:
|
|
188
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
189
|
+
|
|
190
|
+
# If we've exhausted all retries, raise the last exception
|
|
191
|
+
raise last_exception # type: ignore
|
|
192
|
+
|
|
193
|
+
async def _ainvoke_with_retry(self, **kwargs) -> ModelResponse:
|
|
194
|
+
"""
|
|
195
|
+
Asynchronously invoke the model with retry logic for ModelProviderError.
|
|
196
|
+
|
|
197
|
+
This method wraps the ainvoke() call and retries on ModelProviderError
|
|
198
|
+
with optional exponential backoff.
|
|
199
|
+
"""
|
|
200
|
+
last_exception: Optional[ModelProviderError] = None
|
|
201
|
+
|
|
202
|
+
for attempt in range(self.retries + 1):
|
|
203
|
+
try:
|
|
204
|
+
return await self.ainvoke(**kwargs)
|
|
205
|
+
except ModelProviderError as e:
|
|
206
|
+
last_exception = e
|
|
207
|
+
if attempt < self.retries:
|
|
208
|
+
delay = self._get_retry_delay(attempt)
|
|
209
|
+
log_warning(
|
|
210
|
+
f"Model provider error (attempt {attempt + 1}/{self.retries + 1}): {e}. Retrying in {delay}s..."
|
|
211
|
+
)
|
|
212
|
+
await asyncio.sleep(delay)
|
|
213
|
+
else:
|
|
214
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
215
|
+
|
|
216
|
+
# If we've exhausted all retries, raise the last exception
|
|
217
|
+
raise last_exception # type: ignore
|
|
218
|
+
|
|
219
|
+
def _invoke_stream_with_retry(self, **kwargs) -> Iterator[ModelResponse]:
|
|
220
|
+
"""
|
|
221
|
+
Invoke the model stream with retry logic for ModelProviderError.
|
|
222
|
+
|
|
223
|
+
This method wraps the invoke_stream() call and retries on ModelProviderError
|
|
224
|
+
with optional exponential backoff. Note that retries restart the entire stream.
|
|
225
|
+
"""
|
|
226
|
+
last_exception: Optional[ModelProviderError] = None
|
|
227
|
+
|
|
228
|
+
for attempt in range(self.retries + 1):
|
|
229
|
+
try:
|
|
230
|
+
yield from self.invoke_stream(**kwargs)
|
|
231
|
+
return # Success, exit the retry loop
|
|
232
|
+
except ModelProviderError as e:
|
|
233
|
+
last_exception = e
|
|
234
|
+
if attempt < self.retries:
|
|
235
|
+
delay = self._get_retry_delay(attempt)
|
|
236
|
+
log_warning(
|
|
237
|
+
f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
|
|
238
|
+
f"Retrying in {delay}s..."
|
|
239
|
+
)
|
|
240
|
+
sleep(delay)
|
|
241
|
+
else:
|
|
242
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
243
|
+
|
|
244
|
+
# If we've exhausted all retries, raise the last exception
|
|
245
|
+
raise last_exception # type: ignore
|
|
246
|
+
|
|
247
|
+
async def _ainvoke_stream_with_retry(self, **kwargs) -> AsyncIterator[ModelResponse]:
|
|
248
|
+
"""
|
|
249
|
+
Asynchronously invoke the model stream with retry logic for ModelProviderError.
|
|
250
|
+
|
|
251
|
+
This method wraps the ainvoke_stream() call and retries on ModelProviderError
|
|
252
|
+
with optional exponential backoff. Note that retries restart the entire stream.
|
|
253
|
+
"""
|
|
254
|
+
last_exception: Optional[ModelProviderError] = None
|
|
255
|
+
|
|
256
|
+
for attempt in range(self.retries + 1):
|
|
257
|
+
try:
|
|
258
|
+
async for response in self.ainvoke_stream(**kwargs):
|
|
259
|
+
yield response
|
|
260
|
+
return # Success, exit the retry loop
|
|
261
|
+
except ModelProviderError as e:
|
|
262
|
+
last_exception = e
|
|
263
|
+
if attempt < self.retries:
|
|
264
|
+
delay = self._get_retry_delay(attempt)
|
|
265
|
+
log_warning(
|
|
266
|
+
f"Model provider error during stream (attempt {attempt + 1}/{self.retries + 1}): {e}. "
|
|
267
|
+
f"Retrying in {delay}s..."
|
|
268
|
+
)
|
|
269
|
+
await asyncio.sleep(delay)
|
|
270
|
+
else:
|
|
271
|
+
log_error(f"Model provider error after {self.retries + 1} attempts: {e}")
|
|
272
|
+
|
|
273
|
+
# If we've exhausted all retries, raise the last exception
|
|
274
|
+
raise last_exception # type: ignore
|
|
275
|
+
|
|
152
276
|
def to_dict(self) -> Dict[str, Any]:
|
|
153
277
|
fields = {"name", "id", "provider"}
|
|
154
278
|
_dict = {field: getattr(self, field) for field in fields if getattr(self, field) is not None}
|
|
@@ -423,10 +547,23 @@ class Model(ABC):
|
|
|
423
547
|
]
|
|
424
548
|
and function_call_response.tool_executions is not None
|
|
425
549
|
):
|
|
550
|
+
# Record the tool execution in the model response
|
|
426
551
|
if model_response.tool_executions is None:
|
|
427
552
|
model_response.tool_executions = []
|
|
428
553
|
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
429
554
|
|
|
555
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
556
|
+
if (
|
|
557
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
558
|
+
and run_response is not None
|
|
559
|
+
):
|
|
560
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
561
|
+
if run_response.requirements is None:
|
|
562
|
+
run_response.requirements = []
|
|
563
|
+
run_response.requirements.append(
|
|
564
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
565
|
+
)
|
|
566
|
+
|
|
430
567
|
elif function_call_response.event not in [
|
|
431
568
|
ModelResponseEvent.tool_call_started.value,
|
|
432
569
|
ModelResponseEvent.tool_call_completed.value,
|
|
@@ -615,6 +752,19 @@ class Model(ABC):
|
|
|
615
752
|
if model_response.tool_executions is None:
|
|
616
753
|
model_response.tool_executions = []
|
|
617
754
|
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
755
|
+
|
|
756
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
757
|
+
if (
|
|
758
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
759
|
+
and run_response is not None
|
|
760
|
+
):
|
|
761
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
762
|
+
if run_response.requirements is None:
|
|
763
|
+
run_response.requirements = []
|
|
764
|
+
run_response.requirements.append(
|
|
765
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
766
|
+
)
|
|
767
|
+
|
|
618
768
|
elif function_call_response.event not in [
|
|
619
769
|
ModelResponseEvent.tool_call_started.value,
|
|
620
770
|
ModelResponseEvent.tool_call_completed.value,
|
|
@@ -707,8 +857,8 @@ class Model(ABC):
|
|
|
707
857
|
Returns:
|
|
708
858
|
Tuple[Message, bool]: (assistant_message, should_continue)
|
|
709
859
|
"""
|
|
710
|
-
# Generate response
|
|
711
|
-
provider_response = self.
|
|
860
|
+
# Generate response with retry logic for ModelProviderError
|
|
861
|
+
provider_response = self._invoke_with_retry(
|
|
712
862
|
assistant_message=assistant_message,
|
|
713
863
|
messages=messages,
|
|
714
864
|
response_format=response_format,
|
|
@@ -764,8 +914,8 @@ class Model(ABC):
|
|
|
764
914
|
Returns:
|
|
765
915
|
Tuple[Message, bool]: (assistant_message, should_continue)
|
|
766
916
|
"""
|
|
767
|
-
# Generate response
|
|
768
|
-
provider_response = await self.
|
|
917
|
+
# Generate response with retry logic for ModelProviderError
|
|
918
|
+
provider_response = await self._ainvoke_with_retry(
|
|
769
919
|
messages=messages,
|
|
770
920
|
response_format=response_format,
|
|
771
921
|
tools=tools,
|
|
@@ -886,10 +1036,10 @@ class Model(ABC):
|
|
|
886
1036
|
compress_tool_results: bool = False,
|
|
887
1037
|
) -> Iterator[ModelResponse]:
|
|
888
1038
|
"""
|
|
889
|
-
Process a streaming response from the model.
|
|
1039
|
+
Process a streaming response from the model with retry logic for ModelProviderError.
|
|
890
1040
|
"""
|
|
891
1041
|
|
|
892
|
-
for response_delta in self.
|
|
1042
|
+
for response_delta in self._invoke_stream_with_retry(
|
|
893
1043
|
messages=messages,
|
|
894
1044
|
assistant_message=assistant_message,
|
|
895
1045
|
response_format=response_format,
|
|
@@ -1105,9 +1255,9 @@ class Model(ABC):
|
|
|
1105
1255
|
compress_tool_results: bool = False,
|
|
1106
1256
|
) -> AsyncIterator[ModelResponse]:
|
|
1107
1257
|
"""
|
|
1108
|
-
Process a streaming response from the model.
|
|
1258
|
+
Process a streaming response from the model with retry logic for ModelProviderError.
|
|
1109
1259
|
"""
|
|
1110
|
-
async for response_delta in self.
|
|
1260
|
+
async for response_delta in self._ainvoke_stream_with_retry(
|
|
1111
1261
|
messages=messages,
|
|
1112
1262
|
assistant_message=assistant_message,
|
|
1113
1263
|
response_format=response_format,
|
|
@@ -1115,7 +1265,7 @@ class Model(ABC):
|
|
|
1115
1265
|
tool_choice=tool_choice or self._tool_choice,
|
|
1116
1266
|
run_response=run_response,
|
|
1117
1267
|
compress_tool_results=compress_tool_results,
|
|
1118
|
-
):
|
|
1268
|
+
):
|
|
1119
1269
|
for model_response_delta in self._populate_stream_data(
|
|
1120
1270
|
stream_data=stream_data,
|
|
1121
1271
|
model_response_delta=response_delta,
|
|
@@ -1706,7 +1856,7 @@ class Model(ABC):
|
|
|
1706
1856
|
|
|
1707
1857
|
paused_tool_executions = []
|
|
1708
1858
|
|
|
1709
|
-
# The function
|
|
1859
|
+
# The function requires user confirmation (HITL)
|
|
1710
1860
|
if fc.function.requires_confirmation:
|
|
1711
1861
|
paused_tool_executions.append(
|
|
1712
1862
|
ToolExecution(
|
|
@@ -1716,7 +1866,8 @@ class Model(ABC):
|
|
|
1716
1866
|
requires_confirmation=True,
|
|
1717
1867
|
)
|
|
1718
1868
|
)
|
|
1719
|
-
|
|
1869
|
+
|
|
1870
|
+
# The function requires user input (HITL)
|
|
1720
1871
|
if fc.function.requires_user_input:
|
|
1721
1872
|
user_input_schema = fc.function.user_input_schema
|
|
1722
1873
|
if fc.arguments and user_input_schema:
|
|
@@ -1734,7 +1885,8 @@ class Model(ABC):
|
|
|
1734
1885
|
user_input_schema=user_input_schema,
|
|
1735
1886
|
)
|
|
1736
1887
|
)
|
|
1737
|
-
|
|
1888
|
+
|
|
1889
|
+
# If the function is from the user control flow (HITL) tools, we handle it here
|
|
1738
1890
|
if fc.function.name == "get_user_input" and fc.arguments and fc.arguments.get("user_input_fields"):
|
|
1739
1891
|
user_input_schema = []
|
|
1740
1892
|
for input_field in fc.arguments.get("user_input_fields", []):
|
|
@@ -1760,7 +1912,8 @@ class Model(ABC):
|
|
|
1760
1912
|
user_input_schema=user_input_schema,
|
|
1761
1913
|
)
|
|
1762
1914
|
)
|
|
1763
|
-
|
|
1915
|
+
|
|
1916
|
+
# The function requires external execution (HITL)
|
|
1764
1917
|
if fc.function.external_execution:
|
|
1765
1918
|
paused_tool_executions.append(
|
|
1766
1919
|
ToolExecution(
|
agno/models/openai/responses.py
CHANGED
|
@@ -307,6 +307,8 @@ class OpenAIResponses(Model):
|
|
|
307
307
|
|
|
308
308
|
def _upload_file(self, file: File) -> Optional[str]:
|
|
309
309
|
"""Upload a file to the OpenAI vector database."""
|
|
310
|
+
from pathlib import Path
|
|
311
|
+
from urllib.parse import urlparse
|
|
310
312
|
|
|
311
313
|
if file.url is not None:
|
|
312
314
|
file_content_tuple = file.file_url_content
|
|
@@ -314,13 +316,12 @@ class OpenAIResponses(Model):
|
|
|
314
316
|
file_content = file_content_tuple[0]
|
|
315
317
|
else:
|
|
316
318
|
return None
|
|
317
|
-
file_name = file.url.
|
|
319
|
+
file_name = Path(urlparse(file.url).path).name or "file"
|
|
318
320
|
file_tuple = (file_name, file_content)
|
|
319
321
|
result = self.get_client().files.create(file=file_tuple, purpose="assistants")
|
|
320
322
|
return result.id
|
|
321
323
|
elif file.filepath is not None:
|
|
322
324
|
import mimetypes
|
|
323
|
-
from pathlib import Path
|
|
324
325
|
|
|
325
326
|
file_path = file.filepath if isinstance(file.filepath, Path) else Path(file.filepath)
|
|
326
327
|
if file_path.exists() and file_path.is_file():
|
agno/models/response.py
CHANGED
|
@@ -37,7 +37,7 @@ class ToolExecution:
|
|
|
37
37
|
|
|
38
38
|
created_at: int = field(default_factory=lambda: int(time()))
|
|
39
39
|
|
|
40
|
-
# User control flow
|
|
40
|
+
# User control flow (HITL) fields
|
|
41
41
|
requires_confirmation: Optional[bool] = None
|
|
42
42
|
confirmed: Optional[bool] = None
|
|
43
43
|
confirmation_note: Optional[str] = None
|
agno/os/interfaces/a2a/utils.py
CHANGED
|
@@ -110,7 +110,7 @@ async def map_a2a_request_to_run_input(request_body: dict, stream: bool = True)
|
|
|
110
110
|
|
|
111
111
|
Returns:
|
|
112
112
|
RunInput: The Agno RunInput
|
|
113
|
-
stream:
|
|
113
|
+
stream: Whether we are in stream mode
|
|
114
114
|
"""
|
|
115
115
|
|
|
116
116
|
# 1. Validate the request
|
agno/os/middleware/jwt.py
CHANGED
|
@@ -188,18 +188,20 @@ class JWTMiddleware(BaseHTTPMiddleware):
|
|
|
188
188
|
|
|
189
189
|
# Extract dependency claims
|
|
190
190
|
dependencies = {}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
191
|
+
if self.dependencies_claims:
|
|
192
|
+
for claim in self.dependencies_claims:
|
|
193
|
+
if claim in payload:
|
|
194
|
+
dependencies[claim] = payload[claim]
|
|
194
195
|
|
|
195
196
|
if dependencies:
|
|
196
197
|
request.state.dependencies = dependencies
|
|
197
198
|
|
|
198
199
|
# Extract session state claims
|
|
199
200
|
session_state = {}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
if self.session_state_claims:
|
|
202
|
+
for claim in self.session_state_claims:
|
|
203
|
+
if claim in payload:
|
|
204
|
+
session_state[claim] = payload[claim]
|
|
203
205
|
|
|
204
206
|
if session_state:
|
|
205
207
|
request.state.session_state = session_state
|
agno/os/routers/evals/utils.py
CHANGED
|
@@ -36,7 +36,10 @@ async def run_accuracy_eval(
|
|
|
36
36
|
model=default_model,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
if isinstance(db, AsyncBaseDb):
|
|
40
|
+
result = await accuracy_eval.arun(print_results=False, print_summary=False)
|
|
41
|
+
else:
|
|
42
|
+
result = accuracy_eval.run(print_results=False, print_summary=False)
|
|
40
43
|
if not result:
|
|
41
44
|
raise HTTPException(status_code=500, detail="Failed to run accuracy evaluation")
|
|
42
45
|
|
|
@@ -86,7 +89,11 @@ async def run_performance_eval(
|
|
|
86
89
|
model_id=model_id,
|
|
87
90
|
model_provider=model_provider,
|
|
88
91
|
)
|
|
89
|
-
|
|
92
|
+
|
|
93
|
+
if isinstance(db, AsyncBaseDb):
|
|
94
|
+
result = await performance_eval.arun(print_results=False, print_summary=False)
|
|
95
|
+
else:
|
|
96
|
+
result = performance_eval.run(print_results=False, print_summary=False)
|
|
90
97
|
if not result:
|
|
91
98
|
raise HTTPException(status_code=500, detail="Failed to run performance evaluation")
|
|
92
99
|
|
|
@@ -141,7 +148,10 @@ async def run_reliability_eval(
|
|
|
141
148
|
model_id = team.model.id if team and team.model else None
|
|
142
149
|
model_provider = team.model.provider if team and team.model else None
|
|
143
150
|
|
|
144
|
-
|
|
151
|
+
if isinstance(db, AsyncBaseDb):
|
|
152
|
+
result = await reliability_eval.arun(print_results=False)
|
|
153
|
+
else:
|
|
154
|
+
result = reliability_eval.run(print_results=False)
|
|
145
155
|
if not result:
|
|
146
156
|
raise HTTPException(status_code=500, detail="Failed to run reliability evaluation")
|
|
147
157
|
|
agno/run/agent.py
CHANGED
|
@@ -11,6 +11,7 @@ from agno.models.metrics import Metrics
|
|
|
11
11
|
from agno.models.response import ToolExecution
|
|
12
12
|
from agno.reasoning.step import ReasoningStep
|
|
13
13
|
from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
|
|
14
|
+
from agno.run.requirement import RunRequirement
|
|
14
15
|
from agno.utils.log import logger
|
|
15
16
|
from agno.utils.media import (
|
|
16
17
|
reconstruct_audio_list,
|
|
@@ -273,11 +274,18 @@ class RunCompletedEvent(BaseAgentRunEvent):
|
|
|
273
274
|
class RunPausedEvent(BaseAgentRunEvent):
|
|
274
275
|
event: str = RunEvent.run_paused.value
|
|
275
276
|
tools: Optional[List[ToolExecution]] = None
|
|
277
|
+
requirements: Optional[List[RunRequirement]] = None
|
|
276
278
|
|
|
277
279
|
@property
|
|
278
280
|
def is_paused(self):
|
|
279
281
|
return True
|
|
280
282
|
|
|
283
|
+
@property
|
|
284
|
+
def active_requirements(self) -> List[RunRequirement]:
|
|
285
|
+
if not self.requirements:
|
|
286
|
+
return []
|
|
287
|
+
return [requirement for requirement in self.requirements if not requirement.is_resolved()]
|
|
288
|
+
|
|
281
289
|
|
|
282
290
|
@dataclass
|
|
283
291
|
class RunContinuedEvent(BaseAgentRunEvent):
|
|
@@ -539,11 +547,20 @@ class RunOutput:
|
|
|
539
547
|
|
|
540
548
|
status: RunStatus = RunStatus.running
|
|
541
549
|
|
|
550
|
+
# User control flow (HITL) requirements to continue a run when paused, in order of arrival
|
|
551
|
+
requirements: Optional[list[RunRequirement]] = None
|
|
552
|
+
|
|
542
553
|
# === FOREIGN KEY RELATIONSHIPS ===
|
|
543
554
|
# These fields establish relationships to parent workflow/step structures
|
|
544
555
|
# and should be treated as foreign keys for data integrity
|
|
545
556
|
workflow_step_id: Optional[str] = None # FK: Points to StepOutput.step_id
|
|
546
557
|
|
|
558
|
+
@property
|
|
559
|
+
def active_requirements(self) -> list[RunRequirement]:
|
|
560
|
+
if not self.requirements:
|
|
561
|
+
return []
|
|
562
|
+
return [requirement for requirement in self.requirements if not requirement.is_resolved()]
|
|
563
|
+
|
|
547
564
|
@property
|
|
548
565
|
def is_paused(self):
|
|
549
566
|
return self.status == RunStatus.paused
|
agno/run/requirement.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from agno.models.response import ToolExecution, UserInputField
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RunRequirement:
|
|
14
|
+
"""Requirement to complete a paused run (used in HITL flows)"""
|
|
15
|
+
|
|
16
|
+
tool_execution: Optional[ToolExecution] = None
|
|
17
|
+
created_at: datetime = datetime.now(timezone.utc)
|
|
18
|
+
|
|
19
|
+
# User confirmation
|
|
20
|
+
confirmation: Optional[bool] = None
|
|
21
|
+
confirmation_note: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
# User input
|
|
24
|
+
user_input_schema: Optional[List[UserInputField]] = None
|
|
25
|
+
|
|
26
|
+
# External execution
|
|
27
|
+
external_execution_result: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
def __init__(self, tool_execution: ToolExecution):
|
|
30
|
+
self.id = str(uuid4())
|
|
31
|
+
self.tool_execution = tool_execution
|
|
32
|
+
self.user_input_schema = tool_execution.user_input_schema
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def needs_confirmation(self) -> bool:
|
|
36
|
+
if self.confirmation is not None:
|
|
37
|
+
return False
|
|
38
|
+
if not self.tool_execution:
|
|
39
|
+
return False
|
|
40
|
+
if self.tool_execution.confirmed is True:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
return self.tool_execution.requires_confirmation or False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def needs_user_input(self) -> bool:
|
|
47
|
+
if not self.tool_execution:
|
|
48
|
+
return False
|
|
49
|
+
if self.tool_execution.answered is True:
|
|
50
|
+
return False
|
|
51
|
+
if self.user_input_schema and not all(field.value is not None for field in self.user_input_schema):
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
return self.tool_execution.requires_user_input or False
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def needs_external_execution(self) -> bool:
|
|
58
|
+
if not self.tool_execution:
|
|
59
|
+
return False
|
|
60
|
+
if self.external_execution_result is not None:
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return self.tool_execution.external_execution_required or False
|
|
64
|
+
|
|
65
|
+
def confirm(self):
|
|
66
|
+
if not self.needs_confirmation:
|
|
67
|
+
raise ValueError("This requirement does not require confirmation")
|
|
68
|
+
self.confirmation = True
|
|
69
|
+
if self.tool_execution:
|
|
70
|
+
self.tool_execution.confirmed = True
|
|
71
|
+
|
|
72
|
+
def reject(self):
|
|
73
|
+
if not self.needs_confirmation:
|
|
74
|
+
raise ValueError("This requirement does not require confirmation")
|
|
75
|
+
self.confirmation = False
|
|
76
|
+
if self.tool_execution:
|
|
77
|
+
self.tool_execution.confirmed = False
|
|
78
|
+
|
|
79
|
+
def set_external_execution_result(self, result: str):
|
|
80
|
+
if not self.needs_external_execution:
|
|
81
|
+
raise ValueError("This requirement does not require external execution")
|
|
82
|
+
self.external_execution_result = result
|
|
83
|
+
if self.tool_execution:
|
|
84
|
+
self.tool_execution.result = result
|
|
85
|
+
|
|
86
|
+
def update_tool(self):
|
|
87
|
+
if not self.tool_execution:
|
|
88
|
+
return
|
|
89
|
+
if self.confirmation is True:
|
|
90
|
+
self.tool_execution.confirmed = True
|
|
91
|
+
elif self.confirmation is False:
|
|
92
|
+
self.tool_execution.confirmed = False
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError("This requirement does not require confirmation or user input")
|
|
95
|
+
|
|
96
|
+
def is_resolved(self) -> bool:
|
|
97
|
+
"""Return True if the requirement has been resolved"""
|
|
98
|
+
return not self.needs_confirmation and not self.needs_user_input and not self.needs_external_execution
|
agno/run/team.py
CHANGED
|
@@ -12,6 +12,7 @@ from agno.models.response import ToolExecution
|
|
|
12
12
|
from agno.reasoning.step import ReasoningStep
|
|
13
13
|
from agno.run.agent import RunEvent, RunOutput, RunOutputEvent, run_output_event_from_dict
|
|
14
14
|
from agno.run.base import BaseRunOutputEvent, MessageReferences, RunStatus
|
|
15
|
+
from agno.run.requirement import RunRequirement
|
|
15
16
|
from agno.utils.log import log_error
|
|
16
17
|
from agno.utils.media import (
|
|
17
18
|
reconstruct_audio_list,
|
|
@@ -515,11 +516,20 @@ class TeamRunOutput:
|
|
|
515
516
|
|
|
516
517
|
status: RunStatus = RunStatus.running
|
|
517
518
|
|
|
519
|
+
# User control flow (HITL) requirements to continue a run when paused, in order of arrival
|
|
520
|
+
requirements: Optional[list[RunRequirement]] = None
|
|
521
|
+
|
|
518
522
|
# === FOREIGN KEY RELATIONSHIPS ===
|
|
519
523
|
# These fields establish relationships to parent workflow/step structures
|
|
520
524
|
# and should be treated as foreign keys for data integrity
|
|
521
525
|
workflow_step_id: Optional[str] = None # FK: Points to StepOutput.step_id
|
|
522
526
|
|
|
527
|
+
@property
|
|
528
|
+
def active_requirements(self) -> list[RunRequirement]:
|
|
529
|
+
if not self.requirements:
|
|
530
|
+
return []
|
|
531
|
+
return [requirement for requirement in self.requirements if not requirement.is_resolved()]
|
|
532
|
+
|
|
523
533
|
@property
|
|
524
534
|
def is_paused(self):
|
|
525
535
|
return self.status == RunStatus.paused
|