agno 2.3.5__py3-none-any.whl → 2.3.7__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 +200 -30
- agno/db/postgres/async_postgres.py +37 -11
- agno/db/postgres/postgres.py +9 -3
- agno/db/sqlite/async_sqlite.py +1 -1
- agno/db/sqlite/sqlite.py +3 -4
- agno/db/utils.py +2 -0
- agno/eval/accuracy.py +8 -4
- agno/integrations/discord/client.py +1 -1
- agno/models/base.py +34 -4
- agno/models/cerebras/cerebras.py +11 -12
- agno/models/response.py +1 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/routers/evals/utils.py +13 -3
- agno/os/schema.py +2 -1
- agno/run/agent.py +17 -0
- agno/run/requirement.py +98 -0
- agno/run/team.py +10 -0
- agno/session/team.py +0 -1
- agno/table.py +1 -1
- agno/team/team.py +98 -14
- agno/tools/google_drive.py +4 -3
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/spotify.py +922 -0
- agno/tools/toolkit.py +25 -0
- agno/utils/agent.py +2 -2
- agno/utils/events.py +5 -1
- agno/utils/mcp.py +1 -1
- agno/workflow/workflow.py +5 -2
- {agno-2.3.5.dist-info → agno-2.3.7.dist-info}/METADATA +40 -32
- {agno-2.3.5.dist-info → agno-2.3.7.dist-info}/RECORD +34 -31
- {agno-2.3.5.dist-info → agno-2.3.7.dist-info}/WHEEL +0 -0
- {agno-2.3.5.dist-info → agno-2.3.7.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.5.dist-info → agno-2.3.7.dist-info}/top_level.txt +0 -0
|
@@ -32,6 +32,7 @@ from agno.utils.log import log_debug, log_error, log_info, log_warning
|
|
|
32
32
|
try:
|
|
33
33
|
from sqlalchemy import Index, String, Table, UniqueConstraint, func, update
|
|
34
34
|
from sqlalchemy.dialects import postgresql
|
|
35
|
+
from sqlalchemy.exc import ProgrammingError
|
|
35
36
|
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
|
36
37
|
from sqlalchemy.schema import Column, MetaData
|
|
37
38
|
from sqlalchemy.sql.expression import select, text
|
|
@@ -254,47 +255,63 @@ class AsyncPostgresDb(AsyncBaseDb):
|
|
|
254
255
|
if table_type == "sessions":
|
|
255
256
|
if not hasattr(self, "session_table"):
|
|
256
257
|
self.session_table = await self._get_or_create_table(
|
|
257
|
-
table_name=self.session_table_name,
|
|
258
|
+
table_name=self.session_table_name,
|
|
259
|
+
table_type="sessions",
|
|
260
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
258
261
|
)
|
|
259
262
|
return self.session_table
|
|
260
263
|
|
|
261
264
|
if table_type == "memories":
|
|
262
265
|
if not hasattr(self, "memory_table"):
|
|
263
266
|
self.memory_table = await self._get_or_create_table(
|
|
264
|
-
table_name=self.memory_table_name,
|
|
267
|
+
table_name=self.memory_table_name,
|
|
268
|
+
table_type="memories",
|
|
269
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
265
270
|
)
|
|
266
271
|
return self.memory_table
|
|
267
272
|
|
|
268
273
|
if table_type == "metrics":
|
|
269
274
|
if not hasattr(self, "metrics_table"):
|
|
270
275
|
self.metrics_table = await self._get_or_create_table(
|
|
271
|
-
table_name=self.metrics_table_name,
|
|
276
|
+
table_name=self.metrics_table_name,
|
|
277
|
+
table_type="metrics",
|
|
278
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
272
279
|
)
|
|
273
280
|
return self.metrics_table
|
|
274
281
|
|
|
275
282
|
if table_type == "evals":
|
|
276
283
|
if not hasattr(self, "eval_table"):
|
|
277
|
-
self.eval_table = await self._get_or_create_table(
|
|
284
|
+
self.eval_table = await self._get_or_create_table(
|
|
285
|
+
table_name=self.eval_table_name,
|
|
286
|
+
table_type="evals",
|
|
287
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
288
|
+
)
|
|
278
289
|
return self.eval_table
|
|
279
290
|
|
|
280
291
|
if table_type == "knowledge":
|
|
281
292
|
if not hasattr(self, "knowledge_table"):
|
|
282
293
|
self.knowledge_table = await self._get_or_create_table(
|
|
283
|
-
table_name=self.knowledge_table_name,
|
|
294
|
+
table_name=self.knowledge_table_name,
|
|
295
|
+
table_type="knowledge",
|
|
296
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
284
297
|
)
|
|
285
298
|
return self.knowledge_table
|
|
286
299
|
|
|
287
300
|
if table_type == "culture":
|
|
288
301
|
if not hasattr(self, "culture_table"):
|
|
289
302
|
self.culture_table = await self._get_or_create_table(
|
|
290
|
-
table_name=self.culture_table_name,
|
|
303
|
+
table_name=self.culture_table_name,
|
|
304
|
+
table_type="culture",
|
|
305
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
291
306
|
)
|
|
292
307
|
return self.culture_table
|
|
293
308
|
|
|
294
309
|
if table_type == "versions":
|
|
295
310
|
if not hasattr(self, "versions_table"):
|
|
296
311
|
self.versions_table = await self._get_or_create_table(
|
|
297
|
-
table_name=self.versions_table_name,
|
|
312
|
+
table_name=self.versions_table_name,
|
|
313
|
+
table_type="versions",
|
|
314
|
+
create_table_if_not_found=create_table_if_not_found,
|
|
298
315
|
)
|
|
299
316
|
return self.versions_table
|
|
300
317
|
|
|
@@ -897,10 +914,19 @@ class AsyncPostgresDb(AsyncBaseDb):
|
|
|
897
914
|
table = await self._get_table(table_type="memories")
|
|
898
915
|
|
|
899
916
|
async with self.async_session_factory() as sess, sess.begin():
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
917
|
+
try:
|
|
918
|
+
stmt = select(func.jsonb_array_elements_text(table.c.topics))
|
|
919
|
+
if user_id is not None:
|
|
920
|
+
stmt = stmt.where(table.c.user_id == user_id)
|
|
921
|
+
result = await sess.execute(stmt)
|
|
922
|
+
except ProgrammingError:
|
|
923
|
+
# Retrying with json_array_elements_text. This works in older versions,
|
|
924
|
+
# where the topics column was of type JSON instead of JSONB
|
|
925
|
+
stmt = select(func.json_array_elements_text(table.c.topics))
|
|
926
|
+
if user_id is not None:
|
|
927
|
+
stmt = stmt.where(table.c.user_id == user_id)
|
|
928
|
+
result = await sess.execute(stmt)
|
|
929
|
+
|
|
904
930
|
records = result.fetchall()
|
|
905
931
|
|
|
906
932
|
return list(set([record[0] for record in records]))
|
agno/db/postgres/postgres.py
CHANGED
|
@@ -33,6 +33,7 @@ try:
|
|
|
33
33
|
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint, func, select, update
|
|
34
34
|
from sqlalchemy.dialects import postgresql
|
|
35
35
|
from sqlalchemy.engine import Engine, create_engine
|
|
36
|
+
from sqlalchemy.exc import ProgrammingError
|
|
36
37
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
37
38
|
from sqlalchemy.schema import Column, MetaData, Table
|
|
38
39
|
from sqlalchemy.sql.expression import text
|
|
@@ -1082,9 +1083,14 @@ class PostgresDb(BaseDb):
|
|
|
1082
1083
|
return []
|
|
1083
1084
|
|
|
1084
1085
|
with self.Session() as sess, sess.begin():
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1086
|
+
try:
|
|
1087
|
+
stmt = select(func.jsonb_array_elements_text(table.c.topics))
|
|
1088
|
+
result = sess.execute(stmt).fetchall()
|
|
1089
|
+
except ProgrammingError:
|
|
1090
|
+
# Retrying with json_array_elements_text. This works in older versions,
|
|
1091
|
+
# where the topics column was of type JSON instead of JSONB
|
|
1092
|
+
stmt = select(func.json_array_elements_text(table.c.topics))
|
|
1093
|
+
result = sess.execute(stmt).fetchall()
|
|
1088
1094
|
|
|
1089
1095
|
return list(set([record[0] for record in result]))
|
|
1090
1096
|
|
agno/db/sqlite/async_sqlite.py
CHANGED
|
@@ -1114,7 +1114,7 @@ class AsyncSqliteDb(AsyncBaseDb):
|
|
|
1114
1114
|
|
|
1115
1115
|
async with self.async_session_factory() as sess, sess.begin():
|
|
1116
1116
|
# Select topics from all results
|
|
1117
|
-
stmt = select(
|
|
1117
|
+
stmt = select(table.c.topics)
|
|
1118
1118
|
result = (await sess.execute(stmt)).fetchall()
|
|
1119
1119
|
|
|
1120
1120
|
return list(set([record[0] for record in result]))
|
agno/db/sqlite/sqlite.py
CHANGED
|
@@ -245,9 +245,9 @@ class SqliteDb(BaseDb):
|
|
|
245
245
|
return table
|
|
246
246
|
|
|
247
247
|
except Exception as e:
|
|
248
|
-
from traceback import
|
|
248
|
+
from traceback import print_exc
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
print_exc()
|
|
251
251
|
log_error(f"Could not create table '{table_name}': {e}")
|
|
252
252
|
raise e
|
|
253
253
|
|
|
@@ -1109,9 +1109,8 @@ class SqliteDb(BaseDb):
|
|
|
1109
1109
|
|
|
1110
1110
|
with self.Session() as sess, sess.begin():
|
|
1111
1111
|
# Select topics from all results
|
|
1112
|
-
stmt = select(
|
|
1112
|
+
stmt = select(table.c.topics)
|
|
1113
1113
|
result = sess.execute(stmt).fetchall()
|
|
1114
|
-
|
|
1115
1114
|
return list(set([record[0] for record in result]))
|
|
1116
1115
|
|
|
1117
1116
|
except Exception as e:
|
agno/db/utils.py
CHANGED
agno/eval/accuracy.py
CHANGED
|
@@ -359,10 +359,12 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
359
359
|
status = Status(f"Running evaluation {i + 1}...", spinner="dots", speed=1.0, refresh_per_second=10)
|
|
360
360
|
live_log.update(status)
|
|
361
361
|
|
|
362
|
+
agent_session_id = f"eval_{self.eval_id}_{i + 1}"
|
|
363
|
+
|
|
362
364
|
if self.agent is not None:
|
|
363
|
-
output = self.agent.run(input=eval_input).content
|
|
365
|
+
output = self.agent.run(input=eval_input, session_id=agent_session_id).content
|
|
364
366
|
elif self.team is not None:
|
|
365
|
-
output = self.team.run(input=eval_input).content
|
|
367
|
+
output = self.team.run(input=eval_input, session_id=agent_session_id).content
|
|
366
368
|
|
|
367
369
|
if not output:
|
|
368
370
|
logger.error(f"Failed to generate a valid answer on iteration {i + 1}: {output}")
|
|
@@ -500,11 +502,13 @@ Remember: You must only compare the agent_output to the expected_output. The exp
|
|
|
500
502
|
status = Status(f"Running evaluation {i + 1}...", spinner="dots", speed=1.0, refresh_per_second=10)
|
|
501
503
|
live_log.update(status)
|
|
502
504
|
|
|
505
|
+
agent_session_id = f"eval_{self.eval_id}_{i + 1}"
|
|
506
|
+
|
|
503
507
|
if self.agent is not None:
|
|
504
|
-
response = await self.agent.arun(input=eval_input)
|
|
508
|
+
response = await self.agent.arun(input=eval_input, session_id=agent_session_id)
|
|
505
509
|
output = response.content
|
|
506
510
|
elif self.team is not None:
|
|
507
|
-
response = await self.team.arun(input=eval_input) # type: ignore
|
|
511
|
+
response = await self.team.arun(input=eval_input, session_id=agent_session_id) # type: ignore
|
|
508
512
|
output = response.content
|
|
509
513
|
|
|
510
514
|
if not output:
|
|
@@ -14,7 +14,7 @@ try:
|
|
|
14
14
|
import discord
|
|
15
15
|
|
|
16
16
|
except (ImportError, ModuleNotFoundError):
|
|
17
|
-
|
|
17
|
+
raise ImportError("`discord.py` not installed. Please install using `pip install discord.py`")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class RequiresConfirmationView(discord.ui.View):
|
agno/models/base.py
CHANGED
|
@@ -30,6 +30,7 @@ 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
|
|
@@ -423,10 +424,23 @@ class Model(ABC):
|
|
|
423
424
|
]
|
|
424
425
|
and function_call_response.tool_executions is not None
|
|
425
426
|
):
|
|
427
|
+
# Record the tool execution in the model response
|
|
426
428
|
if model_response.tool_executions is None:
|
|
427
429
|
model_response.tool_executions = []
|
|
428
430
|
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
429
431
|
|
|
432
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
433
|
+
if (
|
|
434
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
435
|
+
and run_response is not None
|
|
436
|
+
):
|
|
437
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
438
|
+
if run_response.requirements is None:
|
|
439
|
+
run_response.requirements = []
|
|
440
|
+
run_response.requirements.append(
|
|
441
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
442
|
+
)
|
|
443
|
+
|
|
430
444
|
elif function_call_response.event not in [
|
|
431
445
|
ModelResponseEvent.tool_call_started.value,
|
|
432
446
|
ModelResponseEvent.tool_call_completed.value,
|
|
@@ -615,6 +629,19 @@ class Model(ABC):
|
|
|
615
629
|
if model_response.tool_executions is None:
|
|
616
630
|
model_response.tool_executions = []
|
|
617
631
|
model_response.tool_executions.extend(function_call_response.tool_executions)
|
|
632
|
+
|
|
633
|
+
# If the tool is currently paused (HITL flow), add the requirement to the run response
|
|
634
|
+
if (
|
|
635
|
+
function_call_response.event == ModelResponseEvent.tool_call_paused.value
|
|
636
|
+
and run_response is not None
|
|
637
|
+
):
|
|
638
|
+
current_tool_execution = function_call_response.tool_executions[-1]
|
|
639
|
+
if run_response.requirements is None:
|
|
640
|
+
run_response.requirements = []
|
|
641
|
+
run_response.requirements.append(
|
|
642
|
+
RunRequirement(tool_execution=current_tool_execution)
|
|
643
|
+
)
|
|
644
|
+
|
|
618
645
|
elif function_call_response.event not in [
|
|
619
646
|
ModelResponseEvent.tool_call_started.value,
|
|
620
647
|
ModelResponseEvent.tool_call_completed.value,
|
|
@@ -1706,7 +1733,7 @@ class Model(ABC):
|
|
|
1706
1733
|
|
|
1707
1734
|
paused_tool_executions = []
|
|
1708
1735
|
|
|
1709
|
-
# The function
|
|
1736
|
+
# The function requires user confirmation (HITL)
|
|
1710
1737
|
if fc.function.requires_confirmation:
|
|
1711
1738
|
paused_tool_executions.append(
|
|
1712
1739
|
ToolExecution(
|
|
@@ -1716,7 +1743,8 @@ class Model(ABC):
|
|
|
1716
1743
|
requires_confirmation=True,
|
|
1717
1744
|
)
|
|
1718
1745
|
)
|
|
1719
|
-
|
|
1746
|
+
|
|
1747
|
+
# The function requires user input (HITL)
|
|
1720
1748
|
if fc.function.requires_user_input:
|
|
1721
1749
|
user_input_schema = fc.function.user_input_schema
|
|
1722
1750
|
if fc.arguments and user_input_schema:
|
|
@@ -1734,7 +1762,8 @@ class Model(ABC):
|
|
|
1734
1762
|
user_input_schema=user_input_schema,
|
|
1735
1763
|
)
|
|
1736
1764
|
)
|
|
1737
|
-
|
|
1765
|
+
|
|
1766
|
+
# If the function is from the user control flow (HITL) tools, we handle it here
|
|
1738
1767
|
if fc.function.name == "get_user_input" and fc.arguments and fc.arguments.get("user_input_fields"):
|
|
1739
1768
|
user_input_schema = []
|
|
1740
1769
|
for input_field in fc.arguments.get("user_input_fields", []):
|
|
@@ -1760,7 +1789,8 @@ class Model(ABC):
|
|
|
1760
1789
|
user_input_schema=user_input_schema,
|
|
1761
1790
|
)
|
|
1762
1791
|
)
|
|
1763
|
-
|
|
1792
|
+
|
|
1793
|
+
# The function requires external execution (HITL)
|
|
1764
1794
|
if fc.function.external_execution:
|
|
1765
1795
|
paused_tool_executions.append(
|
|
1766
1796
|
ToolExecution(
|
agno/models/cerebras/cerebras.py
CHANGED
|
@@ -512,14 +512,16 @@ class Cerebras(Model):
|
|
|
512
512
|
|
|
513
513
|
# Extend the list if needed
|
|
514
514
|
while len(tool_calls) <= index:
|
|
515
|
-
tool_calls.append(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
"
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
515
|
+
tool_calls.append(
|
|
516
|
+
{
|
|
517
|
+
"id": None,
|
|
518
|
+
"type": None,
|
|
519
|
+
"function": {
|
|
520
|
+
"name": "",
|
|
521
|
+
"arguments": "",
|
|
522
|
+
},
|
|
523
|
+
}
|
|
524
|
+
)
|
|
523
525
|
|
|
524
526
|
tool_call_entry = tool_calls[index]
|
|
525
527
|
|
|
@@ -540,10 +542,7 @@ class Cerebras(Model):
|
|
|
540
542
|
tool_call_entry["function"]["arguments"] += func_delta["arguments"]
|
|
541
543
|
|
|
542
544
|
# Filter out any incomplete tool calls (missing id or function name)
|
|
543
|
-
complete_tool_calls = [
|
|
544
|
-
tc for tc in tool_calls
|
|
545
|
-
if tc.get("id") and tc.get("function", {}).get("name")
|
|
546
|
-
]
|
|
545
|
+
complete_tool_calls = [tc for tc in tool_calls if tc.get("id") and tc.get("function", {}).get("name")]
|
|
547
546
|
|
|
548
547
|
return complete_tool_calls
|
|
549
548
|
|
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
|
|
@@ -3,6 +3,8 @@ import hmac
|
|
|
3
3
|
import os
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
|
+
from agno.utils.log import log_warning
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def is_development_mode() -> bool:
|
|
8
10
|
"""Check if the application is running in development mode."""
|
|
@@ -36,7 +38,7 @@ def validate_webhook_signature(payload: bytes, signature_header: Optional[str])
|
|
|
36
38
|
"""
|
|
37
39
|
# In development mode, we can bypass signature validation
|
|
38
40
|
if is_development_mode():
|
|
39
|
-
|
|
41
|
+
log_warning("Bypassing signature validation in development mode")
|
|
40
42
|
return True
|
|
41
43
|
|
|
42
44
|
if not signature_header or not signature_header.startswith("sha256="):
|
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/os/schema.py
CHANGED
|
@@ -742,10 +742,11 @@ class SessionSchema(BaseModel):
|
|
|
742
742
|
@classmethod
|
|
743
743
|
def from_dict(cls, session: Dict[str, Any]) -> "SessionSchema":
|
|
744
744
|
session_name = get_session_name(session)
|
|
745
|
+
session_data = session.get("session_data", {}) or {}
|
|
745
746
|
return cls(
|
|
746
747
|
session_id=session.get("session_id", ""),
|
|
747
748
|
session_name=session_name,
|
|
748
|
-
session_state=
|
|
749
|
+
session_state=session_data.get("session_state", None),
|
|
749
750
|
created_at=datetime.fromtimestamp(session.get("created_at", 0), tz=timezone.utc)
|
|
750
751
|
if session.get("created_at")
|
|
751
752
|
else None,
|
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
|
agno/session/team.py
CHANGED
|
@@ -46,7 +46,6 @@ class TeamSession:
|
|
|
46
46
|
session_dict = asdict(self)
|
|
47
47
|
|
|
48
48
|
session_dict["runs"] = [run.to_dict() for run in self.runs] if self.runs else None
|
|
49
|
-
print(session_dict["runs"])
|
|
50
49
|
session_dict["summary"] = self.summary.to_dict() if self.summary else None
|
|
51
50
|
|
|
52
51
|
return session_dict
|
agno/table.py
CHANGED