agno 2.2.5__py3-none-any.whl → 2.2.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 +500 -423
- agno/api/os.py +1 -1
- agno/culture/manager.py +12 -8
- agno/guardrails/prompt_injection.py +1 -0
- agno/knowledge/chunking/agentic.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +37 -5
- agno/memory/manager.py +9 -4
- agno/models/anthropic/claude.py +1 -2
- agno/models/azure/ai_foundry.py +31 -14
- agno/models/azure/openai_chat.py +12 -4
- agno/models/base.py +106 -65
- agno/models/cerebras/cerebras.py +11 -6
- agno/models/groq/groq.py +7 -4
- agno/models/meta/llama.py +12 -6
- agno/models/meta/llama_openai.py +5 -1
- agno/models/openai/chat.py +26 -17
- agno/models/openai/responses.py +11 -63
- agno/models/requesty/requesty.py +5 -2
- agno/models/utils.py +254 -8
- agno/models/vertexai/claude.py +9 -13
- agno/os/app.py +13 -12
- agno/os/routers/evals/evals.py +8 -8
- agno/os/routers/evals/utils.py +1 -0
- agno/os/schema.py +56 -38
- agno/os/utils.py +27 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +5 -0
- agno/run/base.py +18 -1
- agno/run/team.py +13 -9
- agno/run/workflow.py +39 -0
- agno/session/summary.py +8 -2
- agno/session/workflow.py +4 -3
- agno/team/team.py +302 -369
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -25
- agno/tools/function.py +98 -17
- agno/tools/mcp/mcp.py +8 -1
- agno/tools/notion.py +204 -0
- agno/utils/agent.py +78 -0
- agno/utils/events.py +2 -0
- agno/utils/hooks.py +1 -1
- agno/utils/models/claude.py +25 -8
- agno/utils/print_response/workflow.py +115 -16
- agno/vectordb/__init__.py +2 -1
- agno/vectordb/milvus/milvus.py +5 -0
- agno/vectordb/redis/__init__.py +5 -0
- agno/vectordb/redis/redisdb.py +687 -0
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/step.py +13 -2
- agno/workflow/workflow.py +969 -72
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/top_level.txt +0 -0
agno/workflow/workflow.py
CHANGED
|
@@ -29,8 +29,8 @@ from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledExcep
|
|
|
29
29
|
from agno.media import Audio, File, Image, Video
|
|
30
30
|
from agno.models.message import Message
|
|
31
31
|
from agno.models.metrics import Metrics
|
|
32
|
-
from agno.run
|
|
33
|
-
from agno.run.
|
|
32
|
+
from agno.run import RunContext, RunStatus
|
|
33
|
+
from agno.run.agent import RunContentEvent, RunEvent, RunOutput
|
|
34
34
|
from agno.run.cancel import (
|
|
35
35
|
cancel_run as cancel_run_global,
|
|
36
36
|
)
|
|
@@ -68,6 +68,7 @@ from agno.utils.print_response.workflow import (
|
|
|
68
68
|
print_response,
|
|
69
69
|
print_response_stream,
|
|
70
70
|
)
|
|
71
|
+
from agno.workflow import WorkflowAgent
|
|
71
72
|
from agno.workflow.condition import Condition
|
|
72
73
|
from agno.workflow.loop import Loop
|
|
73
74
|
from agno.workflow.parallel import Parallel
|
|
@@ -132,6 +133,9 @@ class Workflow:
|
|
|
132
133
|
# Database to use for this workflow
|
|
133
134
|
db: Optional[Union[BaseDb, AsyncBaseDb]] = None
|
|
134
135
|
|
|
136
|
+
# Agentic Workflow - WorkflowAgent that decides when to run the workflow
|
|
137
|
+
agent: Optional[WorkflowAgent] = None # type: ignore
|
|
138
|
+
|
|
135
139
|
# Default session_id to use for this workflow (autogenerated if not set)
|
|
136
140
|
session_id: Optional[str] = None
|
|
137
141
|
# Default user_id to use for this workflow
|
|
@@ -187,6 +191,7 @@ class Workflow:
|
|
|
187
191
|
description: Optional[str] = None,
|
|
188
192
|
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
189
193
|
steps: Optional[WorkflowSteps] = None,
|
|
194
|
+
agent: Optional[WorkflowAgent] = None,
|
|
190
195
|
session_id: Optional[str] = None,
|
|
191
196
|
session_state: Optional[Dict[str, Any]] = None,
|
|
192
197
|
overwrite_db_session_state: bool = False,
|
|
@@ -210,6 +215,7 @@ class Workflow:
|
|
|
210
215
|
self.name = name
|
|
211
216
|
self.description = description
|
|
212
217
|
self.steps = steps
|
|
218
|
+
self.agent = agent
|
|
213
219
|
self.session_id = session_id
|
|
214
220
|
self.session_state = session_state
|
|
215
221
|
self.overwrite_db_session_state = overwrite_db_session_state
|
|
@@ -231,6 +237,13 @@ class Workflow:
|
|
|
231
237
|
self.num_history_runs = num_history_runs
|
|
232
238
|
self._workflow_session: Optional[WorkflowSession] = None
|
|
233
239
|
|
|
240
|
+
# Warn if workflow history is enabled without a database
|
|
241
|
+
if self.add_workflow_history_to_steps and self.db is None:
|
|
242
|
+
log_warning(
|
|
243
|
+
"Workflow history is enabled (add_workflow_history_to_steps=True) but no database is configured. "
|
|
244
|
+
"History won't be persisted. Add a database to persist runs across executions. "
|
|
245
|
+
)
|
|
246
|
+
|
|
234
247
|
def set_id(self) -> None:
|
|
235
248
|
if self.id is None:
|
|
236
249
|
if self.name is not None:
|
|
@@ -940,6 +953,20 @@ class Workflow:
|
|
|
940
953
|
|
|
941
954
|
return workflow_data
|
|
942
955
|
|
|
956
|
+
def _broadcast_to_websocket(
|
|
957
|
+
self,
|
|
958
|
+
event: Any,
|
|
959
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
960
|
+
) -> None:
|
|
961
|
+
"""Broadcast events to WebSocket if available (async context only)"""
|
|
962
|
+
if websocket_handler:
|
|
963
|
+
try:
|
|
964
|
+
loop = asyncio.get_running_loop()
|
|
965
|
+
if loop:
|
|
966
|
+
asyncio.create_task(websocket_handler.handle_event(event))
|
|
967
|
+
except RuntimeError:
|
|
968
|
+
pass
|
|
969
|
+
|
|
943
970
|
def _handle_event(
|
|
944
971
|
self,
|
|
945
972
|
event: "WorkflowRunOutputEvent",
|
|
@@ -947,6 +974,11 @@ class Workflow:
|
|
|
947
974
|
websocket_handler: Optional[WebSocketHandler] = None,
|
|
948
975
|
) -> "WorkflowRunOutputEvent":
|
|
949
976
|
"""Handle workflow events for storage - similar to Team._handle_event"""
|
|
977
|
+
from agno.run.agent import RunOutput
|
|
978
|
+
from agno.run.team import TeamRunOutput
|
|
979
|
+
|
|
980
|
+
if isinstance(event, (RunOutput, TeamRunOutput)):
|
|
981
|
+
return event
|
|
950
982
|
if self.store_events:
|
|
951
983
|
# Check if this event type should be skipped
|
|
952
984
|
if self.events_to_skip:
|
|
@@ -967,15 +999,7 @@ class Workflow:
|
|
|
967
999
|
workflow_run_response.events.append(event)
|
|
968
1000
|
|
|
969
1001
|
# Broadcast to WebSocket if available (async context only)
|
|
970
|
-
|
|
971
|
-
import asyncio
|
|
972
|
-
|
|
973
|
-
try:
|
|
974
|
-
loop = asyncio.get_running_loop()
|
|
975
|
-
if loop:
|
|
976
|
-
asyncio.create_task(websocket_handler.handle_event(event))
|
|
977
|
-
except RuntimeError:
|
|
978
|
-
pass
|
|
1002
|
+
self._broadcast_to_websocket(event, websocket_handler)
|
|
979
1003
|
|
|
980
1004
|
return event
|
|
981
1005
|
|
|
@@ -1192,7 +1216,7 @@ class Workflow:
|
|
|
1192
1216
|
session: WorkflowSession,
|
|
1193
1217
|
execution_input: WorkflowExecutionInput,
|
|
1194
1218
|
workflow_run_response: WorkflowRunOutput,
|
|
1195
|
-
|
|
1219
|
+
run_context: RunContext,
|
|
1196
1220
|
**kwargs: Any,
|
|
1197
1221
|
) -> WorkflowRunOutput:
|
|
1198
1222
|
"""Execute a specific pipeline by name synchronously"""
|
|
@@ -1259,7 +1283,7 @@ class Workflow:
|
|
|
1259
1283
|
session_id=session.session_id,
|
|
1260
1284
|
user_id=self.user_id,
|
|
1261
1285
|
workflow_run_response=workflow_run_response,
|
|
1262
|
-
session_state=session_state,
|
|
1286
|
+
session_state=run_context.session_state,
|
|
1263
1287
|
store_executor_outputs=self.store_executor_outputs,
|
|
1264
1288
|
workflow_session=session,
|
|
1265
1289
|
add_workflow_history_to_steps=self.add_workflow_history_to_steps
|
|
@@ -1353,7 +1377,7 @@ class Workflow:
|
|
|
1353
1377
|
session: WorkflowSession,
|
|
1354
1378
|
execution_input: WorkflowExecutionInput,
|
|
1355
1379
|
workflow_run_response: WorkflowRunOutput,
|
|
1356
|
-
|
|
1380
|
+
run_context: RunContext,
|
|
1357
1381
|
stream_events: bool = False,
|
|
1358
1382
|
**kwargs: Any,
|
|
1359
1383
|
) -> Iterator[WorkflowRunOutputEvent]:
|
|
@@ -1444,7 +1468,7 @@ class Workflow:
|
|
|
1444
1468
|
stream_events=stream_events,
|
|
1445
1469
|
stream_executor_events=self.stream_executor_events,
|
|
1446
1470
|
workflow_run_response=workflow_run_response,
|
|
1447
|
-
session_state=session_state,
|
|
1471
|
+
session_state=run_context.session_state,
|
|
1448
1472
|
step_index=i,
|
|
1449
1473
|
store_executor_outputs=self.store_executor_outputs,
|
|
1450
1474
|
workflow_session=session,
|
|
@@ -2243,6 +2267,13 @@ class Workflow:
|
|
|
2243
2267
|
session_id=session_id, user_id=user_id, session_state=session_state
|
|
2244
2268
|
)
|
|
2245
2269
|
|
|
2270
|
+
run_context = RunContext(
|
|
2271
|
+
run_id=run_id,
|
|
2272
|
+
session_id=session_id,
|
|
2273
|
+
user_id=user_id,
|
|
2274
|
+
session_state=session_state,
|
|
2275
|
+
)
|
|
2276
|
+
|
|
2246
2277
|
self._prepare_steps()
|
|
2247
2278
|
|
|
2248
2279
|
# Create workflow run response with PENDING status
|
|
@@ -2285,14 +2316,23 @@ class Workflow:
|
|
|
2285
2316
|
else:
|
|
2286
2317
|
self.save_session(session=workflow_session)
|
|
2287
2318
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2319
|
+
if self.agent is not None:
|
|
2320
|
+
self._aexecute_workflow_agent(
|
|
2321
|
+
user_input=input, # type: ignore
|
|
2322
|
+
execution_input=inputs,
|
|
2323
|
+
run_context=run_context,
|
|
2324
|
+
stream=False,
|
|
2325
|
+
**kwargs,
|
|
2326
|
+
)
|
|
2327
|
+
else:
|
|
2328
|
+
await self._aexecute(
|
|
2329
|
+
session_id=session_id,
|
|
2330
|
+
user_id=user_id,
|
|
2331
|
+
execution_input=inputs,
|
|
2332
|
+
workflow_run_response=workflow_run_response,
|
|
2333
|
+
session_state=session_state,
|
|
2334
|
+
**kwargs,
|
|
2335
|
+
)
|
|
2296
2336
|
|
|
2297
2337
|
log_debug(f"Background execution completed with status: {workflow_run_response.status}")
|
|
2298
2338
|
|
|
@@ -2340,6 +2380,13 @@ class Workflow:
|
|
|
2340
2380
|
session_id=session_id, user_id=user_id, session_state=session_state
|
|
2341
2381
|
)
|
|
2342
2382
|
|
|
2383
|
+
run_context = RunContext(
|
|
2384
|
+
run_id=run_id,
|
|
2385
|
+
session_id=session_id,
|
|
2386
|
+
user_id=user_id,
|
|
2387
|
+
session_state=session_state,
|
|
2388
|
+
)
|
|
2389
|
+
|
|
2343
2390
|
self._prepare_steps()
|
|
2344
2391
|
|
|
2345
2392
|
# Create workflow run response with PENDING status
|
|
@@ -2353,13 +2400,6 @@ class Workflow:
|
|
|
2353
2400
|
status=RunStatus.pending,
|
|
2354
2401
|
)
|
|
2355
2402
|
|
|
2356
|
-
# Store PENDING response immediately
|
|
2357
|
-
workflow_session.upsert_run(run=workflow_run_response)
|
|
2358
|
-
if self._has_async_db():
|
|
2359
|
-
await self.asave_session(session=workflow_session)
|
|
2360
|
-
else:
|
|
2361
|
-
self.save_session(session=workflow_session)
|
|
2362
|
-
|
|
2363
2403
|
# Prepare execution input
|
|
2364
2404
|
inputs = WorkflowExecutionInput(
|
|
2365
2405
|
input=input,
|
|
@@ -2375,27 +2415,45 @@ class Workflow:
|
|
|
2375
2415
|
async def execute_workflow_background_stream():
|
|
2376
2416
|
"""Background execution with streaming and WebSocket broadcasting"""
|
|
2377
2417
|
try:
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2418
|
+
if self.agent is not None:
|
|
2419
|
+
result = self._aexecute_workflow_agent(
|
|
2420
|
+
user_input=input, # type: ignore
|
|
2421
|
+
run_context=run_context,
|
|
2422
|
+
execution_input=inputs,
|
|
2423
|
+
stream=True,
|
|
2424
|
+
websocket_handler=websocket_handler,
|
|
2425
|
+
**kwargs,
|
|
2426
|
+
)
|
|
2427
|
+
# For streaming, result is an async iterator
|
|
2428
|
+
async for event in result: # type: ignore
|
|
2429
|
+
# Events are automatically broadcast by _handle_event in the agent execution
|
|
2430
|
+
# We just consume them here to drive the execution
|
|
2431
|
+
pass
|
|
2432
|
+
log_debug(
|
|
2433
|
+
f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
|
|
2434
|
+
)
|
|
2382
2435
|
else:
|
|
2383
|
-
|
|
2436
|
+
# Update status to RUNNING and save
|
|
2437
|
+
workflow_run_response.status = RunStatus.running
|
|
2438
|
+
if self._has_async_db():
|
|
2439
|
+
await self.asave_session(session=workflow_session)
|
|
2440
|
+
else:
|
|
2441
|
+
self.save_session(session=workflow_session)
|
|
2384
2442
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2443
|
+
# Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
|
|
2444
|
+
async for event in self._aexecute_stream(
|
|
2445
|
+
session_id=session_id,
|
|
2446
|
+
user_id=user_id,
|
|
2447
|
+
execution_input=inputs,
|
|
2448
|
+
workflow_run_response=workflow_run_response,
|
|
2449
|
+
stream_events=stream_events,
|
|
2450
|
+
run_context=run_context,
|
|
2451
|
+
websocket_handler=websocket_handler,
|
|
2452
|
+
**kwargs,
|
|
2453
|
+
):
|
|
2454
|
+
# Events are automatically broadcast by _handle_event
|
|
2455
|
+
# We just consume them here to drive the execution
|
|
2456
|
+
pass
|
|
2399
2457
|
|
|
2400
2458
|
log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
|
|
2401
2459
|
|
|
@@ -2439,6 +2497,801 @@ class Workflow:
|
|
|
2439
2497
|
|
|
2440
2498
|
return None
|
|
2441
2499
|
|
|
2500
|
+
def _initialize_workflow_agent(
|
|
2501
|
+
self,
|
|
2502
|
+
session: WorkflowSession,
|
|
2503
|
+
execution_input: WorkflowExecutionInput,
|
|
2504
|
+
run_context: RunContext,
|
|
2505
|
+
stream: bool = False,
|
|
2506
|
+
) -> None:
|
|
2507
|
+
"""Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
|
|
2508
|
+
from agno.tools.function import Function
|
|
2509
|
+
|
|
2510
|
+
workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
|
|
2511
|
+
workflow=self,
|
|
2512
|
+
session=session,
|
|
2513
|
+
execution_input=execution_input,
|
|
2514
|
+
run_context=run_context,
|
|
2515
|
+
stream=stream,
|
|
2516
|
+
)
|
|
2517
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
2518
|
+
|
|
2519
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
2520
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
2521
|
+
|
|
2522
|
+
log_debug("Workflow agent initialized with run_workflow tool")
|
|
2523
|
+
|
|
2524
|
+
def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
|
|
2525
|
+
"""Build dependencies dict with workflow context to pass to agent.run()"""
|
|
2526
|
+
# Get configuration from the WorkflowAgent instance
|
|
2527
|
+
add_history = True
|
|
2528
|
+
num_runs = 5
|
|
2529
|
+
|
|
2530
|
+
if self.agent and isinstance(self.agent, WorkflowAgent):
|
|
2531
|
+
add_history = self.agent.add_workflow_history
|
|
2532
|
+
num_runs = self.agent.num_history_runs or 5
|
|
2533
|
+
|
|
2534
|
+
if add_history:
|
|
2535
|
+
history_context = (
|
|
2536
|
+
session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
|
|
2537
|
+
)
|
|
2538
|
+
else:
|
|
2539
|
+
history_context = "No workflow history available."
|
|
2540
|
+
|
|
2541
|
+
# Build workflow context with description and history
|
|
2542
|
+
workflow_context = ""
|
|
2543
|
+
if self.description:
|
|
2544
|
+
workflow_context += f"Workflow Description: {self.description}\n\n"
|
|
2545
|
+
|
|
2546
|
+
workflow_context += history_context
|
|
2547
|
+
|
|
2548
|
+
return {
|
|
2549
|
+
"workflow_context": workflow_context,
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
def _execute_workflow_agent(
|
|
2553
|
+
self,
|
|
2554
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2555
|
+
session: WorkflowSession,
|
|
2556
|
+
execution_input: WorkflowExecutionInput,
|
|
2557
|
+
run_context: RunContext,
|
|
2558
|
+
stream: bool = False,
|
|
2559
|
+
**kwargs: Any,
|
|
2560
|
+
) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
|
|
2561
|
+
"""
|
|
2562
|
+
Execute the workflow agent in streaming or non-streaming mode.
|
|
2563
|
+
|
|
2564
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2565
|
+
|
|
2566
|
+
Args:
|
|
2567
|
+
user_input: The user's input
|
|
2568
|
+
session: The workflow session
|
|
2569
|
+
execution_input: The execution input
|
|
2570
|
+
run_context: The run context
|
|
2571
|
+
stream: Whether to stream the response
|
|
2572
|
+
stream_intermediate_steps: Whether to stream intermediate steps
|
|
2573
|
+
|
|
2574
|
+
Returns:
|
|
2575
|
+
WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
|
|
2576
|
+
"""
|
|
2577
|
+
if stream:
|
|
2578
|
+
return self._run_workflow_agent_stream(
|
|
2579
|
+
agent_input=user_input,
|
|
2580
|
+
session=session,
|
|
2581
|
+
execution_input=execution_input,
|
|
2582
|
+
run_context=run_context,
|
|
2583
|
+
stream=stream,
|
|
2584
|
+
**kwargs,
|
|
2585
|
+
)
|
|
2586
|
+
else:
|
|
2587
|
+
return self._run_workflow_agent(
|
|
2588
|
+
agent_input=user_input,
|
|
2589
|
+
session=session,
|
|
2590
|
+
execution_input=execution_input,
|
|
2591
|
+
run_context=run_context,
|
|
2592
|
+
stream=stream,
|
|
2593
|
+
)
|
|
2594
|
+
|
|
2595
|
+
def _run_workflow_agent_stream(
|
|
2596
|
+
self,
|
|
2597
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2598
|
+
session: WorkflowSession,
|
|
2599
|
+
execution_input: WorkflowExecutionInput,
|
|
2600
|
+
run_context: RunContext,
|
|
2601
|
+
stream: bool = False,
|
|
2602
|
+
**kwargs: Any,
|
|
2603
|
+
) -> Iterator[WorkflowRunOutputEvent]:
|
|
2604
|
+
"""
|
|
2605
|
+
Execute the workflow agent in streaming mode.
|
|
2606
|
+
|
|
2607
|
+
The agent's tool (run_workflow) is a generator that yields workflow events directly.
|
|
2608
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
2609
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
2610
|
+
|
|
2611
|
+
Yields:
|
|
2612
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
2613
|
+
"""
|
|
2614
|
+
from typing import get_args
|
|
2615
|
+
|
|
2616
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
2617
|
+
|
|
2618
|
+
# Initialize agent with stream_intermediate_steps=True so tool yields events
|
|
2619
|
+
self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
2620
|
+
|
|
2621
|
+
# Build dependencies with workflow context
|
|
2622
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
2623
|
+
|
|
2624
|
+
# Run agent with streaming - workflow events will bubble up from the tool
|
|
2625
|
+
agent_response: Optional[RunOutput] = None
|
|
2626
|
+
workflow_executed = False
|
|
2627
|
+
|
|
2628
|
+
from agno.run.agent import RunContentEvent
|
|
2629
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
2630
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
2631
|
+
|
|
2632
|
+
log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
|
|
2633
|
+
|
|
2634
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
2635
|
+
run_id = str(uuid4())
|
|
2636
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
2637
|
+
run_id=run_id,
|
|
2638
|
+
input=execution_input.input,
|
|
2639
|
+
session_id=session.session_id,
|
|
2640
|
+
workflow_id=self.id,
|
|
2641
|
+
workflow_name=self.name,
|
|
2642
|
+
created_at=int(datetime.now().timestamp()),
|
|
2643
|
+
)
|
|
2644
|
+
|
|
2645
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
2646
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
2647
|
+
workflow_name=self.name,
|
|
2648
|
+
workflow_id=self.id,
|
|
2649
|
+
session_id=session.session_id,
|
|
2650
|
+
)
|
|
2651
|
+
yield agent_started_event
|
|
2652
|
+
|
|
2653
|
+
# Run the agent in streaming mode and yield all events
|
|
2654
|
+
for event in self.agent.run( # type: ignore[union-attr]
|
|
2655
|
+
input=agent_input,
|
|
2656
|
+
stream=True,
|
|
2657
|
+
stream_intermediate_steps=True,
|
|
2658
|
+
yield_run_response=True,
|
|
2659
|
+
session_id=session.session_id,
|
|
2660
|
+
dependencies=run_context.dependencies, # Pass context dynamically per-run
|
|
2661
|
+
session_state=run_context.session_state, # Pass session state dynamically per-run
|
|
2662
|
+
): # type: ignore
|
|
2663
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
2664
|
+
yield event # type: ignore[misc]
|
|
2665
|
+
|
|
2666
|
+
# Track if workflow was executed by checking for WorkflowCompletedEvent
|
|
2667
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
2668
|
+
workflow_executed = True
|
|
2669
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
2670
|
+
if event.step_name is None:
|
|
2671
|
+
# This is from the workflow agent itself
|
|
2672
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
2673
|
+
|
|
2674
|
+
if workflow_executed:
|
|
2675
|
+
continue # Skip if workflow was already executed
|
|
2676
|
+
|
|
2677
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
2678
|
+
event.workflow_agent = True # type: ignore
|
|
2679
|
+
yield event # type: ignore[misc]
|
|
2680
|
+
|
|
2681
|
+
# Capture the final RunOutput (but don't yield it)
|
|
2682
|
+
if isinstance(event, RunOutput):
|
|
2683
|
+
agent_response = event
|
|
2684
|
+
|
|
2685
|
+
# Handle direct answer case (no workflow execution)
|
|
2686
|
+
if not workflow_executed:
|
|
2687
|
+
# Update the pre-created workflow run response with the direct answer
|
|
2688
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
2689
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
2690
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
2691
|
+
|
|
2692
|
+
workflow_run_response = direct_reply_run_response
|
|
2693
|
+
|
|
2694
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2695
|
+
if agent_response:
|
|
2696
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2697
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2698
|
+
|
|
2699
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2700
|
+
|
|
2701
|
+
# Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
|
|
2702
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2703
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2704
|
+
workflow_name=self.name,
|
|
2705
|
+
workflow_id=self.id,
|
|
2706
|
+
session_id=session.session_id,
|
|
2707
|
+
content=workflow_run_response.content,
|
|
2708
|
+
)
|
|
2709
|
+
yield agent_completed_event
|
|
2710
|
+
|
|
2711
|
+
# Yield a workflow completed event with the agent's direct response
|
|
2712
|
+
completed_event = WorkflowCompletedEvent(
|
|
2713
|
+
run_id=workflow_run_response.run_id or "",
|
|
2714
|
+
content=workflow_run_response.content,
|
|
2715
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
2716
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
2717
|
+
session_id=workflow_run_response.session_id,
|
|
2718
|
+
step_results=[],
|
|
2719
|
+
metadata={"agent_direct_response": True},
|
|
2720
|
+
)
|
|
2721
|
+
yield completed_event
|
|
2722
|
+
|
|
2723
|
+
# Update the run in session
|
|
2724
|
+
session.upsert_run(run=workflow_run_response)
|
|
2725
|
+
# Save session
|
|
2726
|
+
self.save_session(session=session)
|
|
2727
|
+
|
|
2728
|
+
else:
|
|
2729
|
+
# Workflow was executed by the tool
|
|
2730
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2731
|
+
|
|
2732
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2733
|
+
# Get the last run (which is the one just created by the tool)
|
|
2734
|
+
last_run = reloaded_session.runs[-1]
|
|
2735
|
+
|
|
2736
|
+
# Yield WorkflowAgentCompletedEvent
|
|
2737
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2738
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2739
|
+
workflow_name=self.name,
|
|
2740
|
+
workflow_id=self.id,
|
|
2741
|
+
session_id=session.session_id,
|
|
2742
|
+
content=agent_response.content if agent_response else None,
|
|
2743
|
+
)
|
|
2744
|
+
yield agent_completed_event
|
|
2745
|
+
|
|
2746
|
+
# Update the last run with workflow_agent_run
|
|
2747
|
+
last_run.workflow_agent_run = agent_response
|
|
2748
|
+
|
|
2749
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2750
|
+
if agent_response:
|
|
2751
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2752
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2753
|
+
|
|
2754
|
+
# Save the reloaded session (which has the updated run)
|
|
2755
|
+
self.save_session(session=reloaded_session)
|
|
2756
|
+
|
|
2757
|
+
else:
|
|
2758
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
2759
|
+
|
|
2760
|
+
def _run_workflow_agent(
|
|
2761
|
+
self,
|
|
2762
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2763
|
+
session: WorkflowSession,
|
|
2764
|
+
execution_input: WorkflowExecutionInput,
|
|
2765
|
+
run_context: RunContext,
|
|
2766
|
+
stream: bool = False,
|
|
2767
|
+
) -> WorkflowRunOutput:
|
|
2768
|
+
"""
|
|
2769
|
+
Execute the workflow agent in non-streaming mode.
|
|
2770
|
+
|
|
2771
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2772
|
+
|
|
2773
|
+
Returns:
|
|
2774
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
2775
|
+
"""
|
|
2776
|
+
|
|
2777
|
+
# Initialize the agent
|
|
2778
|
+
self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
2779
|
+
|
|
2780
|
+
# Build dependencies with workflow context
|
|
2781
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
2782
|
+
|
|
2783
|
+
# Run the agent
|
|
2784
|
+
agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
|
|
2785
|
+
input=agent_input,
|
|
2786
|
+
session_id=session.session_id,
|
|
2787
|
+
dependencies=run_context.dependencies,
|
|
2788
|
+
session_state=run_context.session_state,
|
|
2789
|
+
stream=stream,
|
|
2790
|
+
) # type: ignore
|
|
2791
|
+
|
|
2792
|
+
# Check if the agent called the workflow tool
|
|
2793
|
+
workflow_executed = False
|
|
2794
|
+
if agent_response.messages:
|
|
2795
|
+
for message in agent_response.messages:
|
|
2796
|
+
if message.role == "assistant" and message.tool_calls:
|
|
2797
|
+
# Check if the tool call is specifically for run_workflow
|
|
2798
|
+
for tool_call in message.tool_calls:
|
|
2799
|
+
# Handle both dict and object formats
|
|
2800
|
+
if isinstance(tool_call, dict):
|
|
2801
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
2802
|
+
else:
|
|
2803
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
2804
|
+
|
|
2805
|
+
if tool_name == "run_workflow":
|
|
2806
|
+
workflow_executed = True
|
|
2807
|
+
break
|
|
2808
|
+
if workflow_executed:
|
|
2809
|
+
break
|
|
2810
|
+
|
|
2811
|
+
log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
|
|
2812
|
+
|
|
2813
|
+
# Handle direct answer case (no workflow execution)
|
|
2814
|
+
if not workflow_executed:
|
|
2815
|
+
# Create a new workflow run output for the direct answer
|
|
2816
|
+
run_id = str(uuid4())
|
|
2817
|
+
workflow_run_response = WorkflowRunOutput(
|
|
2818
|
+
run_id=run_id,
|
|
2819
|
+
input=execution_input.input,
|
|
2820
|
+
session_id=session.session_id,
|
|
2821
|
+
workflow_id=self.id,
|
|
2822
|
+
workflow_name=self.name,
|
|
2823
|
+
created_at=int(datetime.now().timestamp()),
|
|
2824
|
+
content=agent_response.content,
|
|
2825
|
+
status=RunStatus.completed,
|
|
2826
|
+
workflow_agent_run=agent_response,
|
|
2827
|
+
)
|
|
2828
|
+
|
|
2829
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2830
|
+
if agent_response:
|
|
2831
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2832
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2833
|
+
|
|
2834
|
+
# Update the run in session
|
|
2835
|
+
session.upsert_run(run=workflow_run_response)
|
|
2836
|
+
self.save_session(session=session)
|
|
2837
|
+
|
|
2838
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2839
|
+
|
|
2840
|
+
return workflow_run_response
|
|
2841
|
+
else:
|
|
2842
|
+
# Workflow was executed by the tool
|
|
2843
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2844
|
+
|
|
2845
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2846
|
+
# Get the last run (which is the one just created by the tool)
|
|
2847
|
+
last_run = reloaded_session.runs[-1]
|
|
2848
|
+
|
|
2849
|
+
# Update the last run directly with workflow_agent_run
|
|
2850
|
+
last_run.workflow_agent_run = agent_response
|
|
2851
|
+
|
|
2852
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2853
|
+
if agent_response:
|
|
2854
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2855
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2856
|
+
|
|
2857
|
+
# Save the reloaded session (which has the updated run)
|
|
2858
|
+
self.save_session(session=reloaded_session)
|
|
2859
|
+
|
|
2860
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
2861
|
+
return last_run
|
|
2862
|
+
else:
|
|
2863
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
2864
|
+
# Return a placeholder error response
|
|
2865
|
+
return WorkflowRunOutput(
|
|
2866
|
+
run_id=str(uuid4()),
|
|
2867
|
+
input=execution_input.input,
|
|
2868
|
+
session_id=session.session_id,
|
|
2869
|
+
workflow_id=self.id,
|
|
2870
|
+
workflow_name=self.name,
|
|
2871
|
+
created_at=int(datetime.now().timestamp()),
|
|
2872
|
+
content="Error: Workflow execution failed",
|
|
2873
|
+
status=RunStatus.error,
|
|
2874
|
+
)
|
|
2875
|
+
|
|
2876
|
+
def _async_initialize_workflow_agent(
|
|
2877
|
+
self,
|
|
2878
|
+
session: WorkflowSession,
|
|
2879
|
+
execution_input: WorkflowExecutionInput,
|
|
2880
|
+
run_context: RunContext,
|
|
2881
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2882
|
+
stream: bool = False,
|
|
2883
|
+
) -> None:
|
|
2884
|
+
"""Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
|
|
2885
|
+
from agno.tools.function import Function
|
|
2886
|
+
|
|
2887
|
+
workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
|
|
2888
|
+
workflow=self,
|
|
2889
|
+
session=session,
|
|
2890
|
+
execution_input=execution_input,
|
|
2891
|
+
run_context=run_context,
|
|
2892
|
+
stream=stream,
|
|
2893
|
+
websocket_handler=websocket_handler,
|
|
2894
|
+
)
|
|
2895
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
2896
|
+
|
|
2897
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
2898
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
2899
|
+
|
|
2900
|
+
log_debug("Workflow agent initialized with async run_workflow tool")
|
|
2901
|
+
|
|
2902
|
+
async def _aload_session_for_workflow_agent(
|
|
2903
|
+
self,
|
|
2904
|
+
session_id: str,
|
|
2905
|
+
user_id: Optional[str],
|
|
2906
|
+
session_state: Optional[Dict[str, Any]],
|
|
2907
|
+
) -> Tuple[WorkflowSession, Dict[str, Any]]:
|
|
2908
|
+
"""Helper to load or create session for workflow agent execution"""
|
|
2909
|
+
return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
|
|
2910
|
+
|
|
2911
|
+
def _aexecute_workflow_agent(
|
|
2912
|
+
self,
|
|
2913
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2914
|
+
run_context: RunContext,
|
|
2915
|
+
execution_input: WorkflowExecutionInput,
|
|
2916
|
+
stream: bool = False,
|
|
2917
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2918
|
+
**kwargs: Any,
|
|
2919
|
+
):
|
|
2920
|
+
"""
|
|
2921
|
+
Execute the workflow agent asynchronously in streaming or non-streaming mode.
|
|
2922
|
+
|
|
2923
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2924
|
+
|
|
2925
|
+
Args:
|
|
2926
|
+
user_input: The user's input
|
|
2927
|
+
session: The workflow session
|
|
2928
|
+
run_context: The run context
|
|
2929
|
+
execution_input: The execution input
|
|
2930
|
+
stream: Whether to stream the response
|
|
2931
|
+
websocket_handler: The WebSocket handler
|
|
2932
|
+
|
|
2933
|
+
Returns:
|
|
2934
|
+
Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
|
|
2935
|
+
"""
|
|
2936
|
+
|
|
2937
|
+
if stream:
|
|
2938
|
+
|
|
2939
|
+
async def _stream():
|
|
2940
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
2941
|
+
run_context.session_id, run_context.user_id, run_context.session_state
|
|
2942
|
+
)
|
|
2943
|
+
async for event in self._arun_workflow_agent_stream(
|
|
2944
|
+
agent_input=user_input,
|
|
2945
|
+
session=session,
|
|
2946
|
+
execution_input=execution_input,
|
|
2947
|
+
run_context=run_context,
|
|
2948
|
+
stream=stream,
|
|
2949
|
+
websocket_handler=websocket_handler,
|
|
2950
|
+
**kwargs,
|
|
2951
|
+
):
|
|
2952
|
+
yield event
|
|
2953
|
+
|
|
2954
|
+
return _stream()
|
|
2955
|
+
else:
|
|
2956
|
+
|
|
2957
|
+
async def _execute():
|
|
2958
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
2959
|
+
run_context.session_id, run_context.user_id, run_context.session_state
|
|
2960
|
+
)
|
|
2961
|
+
return await self._arun_workflow_agent(
|
|
2962
|
+
agent_input=user_input,
|
|
2963
|
+
session=session,
|
|
2964
|
+
execution_input=execution_input,
|
|
2965
|
+
run_context=run_context,
|
|
2966
|
+
stream=stream,
|
|
2967
|
+
)
|
|
2968
|
+
|
|
2969
|
+
return _execute()
|
|
2970
|
+
|
|
2971
|
+
async def _arun_workflow_agent_stream(
|
|
2972
|
+
self,
|
|
2973
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2974
|
+
session: WorkflowSession,
|
|
2975
|
+
execution_input: WorkflowExecutionInput,
|
|
2976
|
+
run_context: RunContext,
|
|
2977
|
+
stream: bool = False,
|
|
2978
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2979
|
+
**kwargs: Any,
|
|
2980
|
+
) -> AsyncIterator[WorkflowRunOutputEvent]:
|
|
2981
|
+
"""
|
|
2982
|
+
Execute the workflow agent asynchronously in streaming mode.
|
|
2983
|
+
|
|
2984
|
+
The agent's tool (run_workflow) is an async generator that yields workflow events directly.
|
|
2985
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
2986
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
2987
|
+
|
|
2988
|
+
Yields:
|
|
2989
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
2990
|
+
"""
|
|
2991
|
+
from typing import get_args
|
|
2992
|
+
|
|
2993
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
2994
|
+
|
|
2995
|
+
logger.info("Workflow agent enabled - async streaming mode")
|
|
2996
|
+
log_debug(f"User input: {agent_input}")
|
|
2997
|
+
|
|
2998
|
+
self._async_initialize_workflow_agent(
|
|
2999
|
+
session,
|
|
3000
|
+
execution_input,
|
|
3001
|
+
run_context=run_context,
|
|
3002
|
+
stream=stream,
|
|
3003
|
+
websocket_handler=websocket_handler,
|
|
3004
|
+
)
|
|
3005
|
+
|
|
3006
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
3007
|
+
|
|
3008
|
+
agent_response: Optional[RunOutput] = None
|
|
3009
|
+
workflow_executed = False
|
|
3010
|
+
|
|
3011
|
+
from agno.run.agent import RunContentEvent
|
|
3012
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
3013
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
3014
|
+
|
|
3015
|
+
log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
|
|
3016
|
+
|
|
3017
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
3018
|
+
run_id = str(uuid4())
|
|
3019
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
3020
|
+
run_id=run_id,
|
|
3021
|
+
input=execution_input.input,
|
|
3022
|
+
session_id=session.session_id,
|
|
3023
|
+
workflow_id=self.id,
|
|
3024
|
+
workflow_name=self.name,
|
|
3025
|
+
created_at=int(datetime.now().timestamp()),
|
|
3026
|
+
)
|
|
3027
|
+
|
|
3028
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
3029
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
3030
|
+
workflow_name=self.name,
|
|
3031
|
+
workflow_id=self.id,
|
|
3032
|
+
session_id=session.session_id,
|
|
3033
|
+
)
|
|
3034
|
+
self._broadcast_to_websocket(agent_started_event, websocket_handler)
|
|
3035
|
+
yield agent_started_event
|
|
3036
|
+
|
|
3037
|
+
# Run the agent in streaming mode and yield all events
|
|
3038
|
+
async for event in self.agent.arun( # type: ignore[union-attr]
|
|
3039
|
+
input=agent_input,
|
|
3040
|
+
stream=True,
|
|
3041
|
+
stream_intermediate_steps=True,
|
|
3042
|
+
yield_run_response=True,
|
|
3043
|
+
session_id=session.session_id,
|
|
3044
|
+
dependencies=run_context.dependencies, # Pass context dynamically per-run
|
|
3045
|
+
session_state=run_context.session_state, # Pass session state dynamically per-run
|
|
3046
|
+
): # type: ignore
|
|
3047
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
3048
|
+
yield event # type: ignore[misc]
|
|
3049
|
+
|
|
3050
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
3051
|
+
workflow_executed = True
|
|
3052
|
+
log_debug("Workflow execution detected via WorkflowCompletedEvent")
|
|
3053
|
+
|
|
3054
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
3055
|
+
if event.step_name is None:
|
|
3056
|
+
# This is from the workflow agent itself
|
|
3057
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
3058
|
+
|
|
3059
|
+
if workflow_executed:
|
|
3060
|
+
continue # Skip if workflow was already executed
|
|
3061
|
+
|
|
3062
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
3063
|
+
event.workflow_agent = True # type: ignore
|
|
3064
|
+
|
|
3065
|
+
# Broadcast to WebSocket if available (async context only)
|
|
3066
|
+
self._broadcast_to_websocket(event, websocket_handler)
|
|
3067
|
+
|
|
3068
|
+
yield event # type: ignore[misc]
|
|
3069
|
+
|
|
3070
|
+
# Capture the final RunOutput (but don't yield it)
|
|
3071
|
+
if isinstance(event, RunOutput):
|
|
3072
|
+
agent_response = event
|
|
3073
|
+
log_debug(
|
|
3074
|
+
f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
# Handle direct answer case (no workflow execution)
|
|
3078
|
+
if not workflow_executed:
|
|
3079
|
+
# Update the pre-created workflow run response with the direct answer
|
|
3080
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
3081
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
3082
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
3083
|
+
|
|
3084
|
+
workflow_run_response = direct_reply_run_response
|
|
3085
|
+
|
|
3086
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3087
|
+
if agent_response:
|
|
3088
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3089
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
3090
|
+
|
|
3091
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3092
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3093
|
+
workflow_name=self.name,
|
|
3094
|
+
workflow_id=self.id,
|
|
3095
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3096
|
+
session_id=session.session_id,
|
|
3097
|
+
content=workflow_run_response.content,
|
|
3098
|
+
)
|
|
3099
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3100
|
+
yield agent_completed_event
|
|
3101
|
+
|
|
3102
|
+
# Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
|
|
3103
|
+
completed_event = WorkflowCompletedEvent(
|
|
3104
|
+
run_id=workflow_run_response.run_id or "",
|
|
3105
|
+
content=workflow_run_response.content,
|
|
3106
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
3107
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
3108
|
+
session_id=workflow_run_response.session_id,
|
|
3109
|
+
step_results=[],
|
|
3110
|
+
metadata={"agent_direct_response": True},
|
|
3111
|
+
)
|
|
3112
|
+
yield completed_event
|
|
3113
|
+
|
|
3114
|
+
# Update the run in session
|
|
3115
|
+
session.upsert_run(run=workflow_run_response)
|
|
3116
|
+
# Save session
|
|
3117
|
+
if self._has_async_db():
|
|
3118
|
+
await self.asave_session(session=session)
|
|
3119
|
+
else:
|
|
3120
|
+
self.save_session(session=session)
|
|
3121
|
+
|
|
3122
|
+
else:
|
|
3123
|
+
# Workflow was executed by the tool
|
|
3124
|
+
if self._has_async_db():
|
|
3125
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3126
|
+
else:
|
|
3127
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
3128
|
+
|
|
3129
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3130
|
+
# Get the last run (which is the one just created by the tool)
|
|
3131
|
+
last_run = reloaded_session.runs[-1]
|
|
3132
|
+
|
|
3133
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3134
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3135
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3136
|
+
workflow_name=self.name,
|
|
3137
|
+
workflow_id=self.id,
|
|
3138
|
+
session_id=session.session_id,
|
|
3139
|
+
content=agent_response.content if agent_response else None,
|
|
3140
|
+
)
|
|
3141
|
+
|
|
3142
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3143
|
+
|
|
3144
|
+
yield agent_completed_event
|
|
3145
|
+
|
|
3146
|
+
# Update the last run with workflow_agent_run
|
|
3147
|
+
last_run.workflow_agent_run = agent_response
|
|
3148
|
+
|
|
3149
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3150
|
+
if agent_response:
|
|
3151
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3152
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
3153
|
+
|
|
3154
|
+
# Save the reloaded session (which has the updated run)
|
|
3155
|
+
if self._has_async_db():
|
|
3156
|
+
await self.asave_session(session=reloaded_session)
|
|
3157
|
+
else:
|
|
3158
|
+
self.save_session(session=reloaded_session)
|
|
3159
|
+
|
|
3160
|
+
else:
|
|
3161
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3162
|
+
|
|
3163
|
+
async def _arun_workflow_agent(
|
|
3164
|
+
self,
|
|
3165
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
3166
|
+
session: WorkflowSession,
|
|
3167
|
+
execution_input: WorkflowExecutionInput,
|
|
3168
|
+
run_context: RunContext,
|
|
3169
|
+
stream: bool = False,
|
|
3170
|
+
) -> WorkflowRunOutput:
|
|
3171
|
+
"""
|
|
3172
|
+
Execute the workflow agent asynchronously in non-streaming mode.
|
|
3173
|
+
|
|
3174
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
3175
|
+
|
|
3176
|
+
Returns:
|
|
3177
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
3178
|
+
"""
|
|
3179
|
+
# Initialize the agent
|
|
3180
|
+
self._async_initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
3181
|
+
|
|
3182
|
+
# Build dependencies with workflow context
|
|
3183
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
3184
|
+
|
|
3185
|
+
# Run the agent
|
|
3186
|
+
agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
|
|
3187
|
+
input=agent_input,
|
|
3188
|
+
session_id=session.session_id,
|
|
3189
|
+
dependencies=run_context.dependencies,
|
|
3190
|
+
session_state=run_context.session_state,
|
|
3191
|
+
stream=stream,
|
|
3192
|
+
) # type: ignore
|
|
3193
|
+
|
|
3194
|
+
# Check if the agent called the workflow tool
|
|
3195
|
+
workflow_executed = False
|
|
3196
|
+
if agent_response.messages:
|
|
3197
|
+
for message in agent_response.messages:
|
|
3198
|
+
if message.role == "assistant" and message.tool_calls:
|
|
3199
|
+
# Check if the tool call is specifically for run_workflow
|
|
3200
|
+
for tool_call in message.tool_calls:
|
|
3201
|
+
# Handle both dict and object formats
|
|
3202
|
+
if isinstance(tool_call, dict):
|
|
3203
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
3204
|
+
else:
|
|
3205
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
3206
|
+
|
|
3207
|
+
if tool_name == "run_workflow":
|
|
3208
|
+
workflow_executed = True
|
|
3209
|
+
break
|
|
3210
|
+
if workflow_executed:
|
|
3211
|
+
break
|
|
3212
|
+
|
|
3213
|
+
# Handle direct answer case (no workflow execution)
|
|
3214
|
+
if not workflow_executed:
|
|
3215
|
+
# Create a new workflow run output for the direct answer
|
|
3216
|
+
run_id = str(uuid4())
|
|
3217
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3218
|
+
run_id=run_id,
|
|
3219
|
+
input=execution_input.input,
|
|
3220
|
+
session_id=session.session_id,
|
|
3221
|
+
workflow_id=self.id,
|
|
3222
|
+
workflow_name=self.name,
|
|
3223
|
+
created_at=int(datetime.now().timestamp()),
|
|
3224
|
+
content=agent_response.content,
|
|
3225
|
+
status=RunStatus.completed,
|
|
3226
|
+
workflow_agent_run=agent_response,
|
|
3227
|
+
)
|
|
3228
|
+
|
|
3229
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3230
|
+
if agent_response:
|
|
3231
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3232
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
3233
|
+
|
|
3234
|
+
# Update the run in session
|
|
3235
|
+
session.upsert_run(run=workflow_run_response)
|
|
3236
|
+
if self._has_async_db():
|
|
3237
|
+
await self.asave_session(session=session)
|
|
3238
|
+
else:
|
|
3239
|
+
self.save_session(session=session)
|
|
3240
|
+
|
|
3241
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
3242
|
+
|
|
3243
|
+
return workflow_run_response
|
|
3244
|
+
else:
|
|
3245
|
+
# Workflow was executed by the tool
|
|
3246
|
+
logger.info("=" * 80)
|
|
3247
|
+
logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
|
|
3248
|
+
logger.info(" ➜ Workflow was executed, retrieving results...")
|
|
3249
|
+
logger.info("=" * 80)
|
|
3250
|
+
|
|
3251
|
+
log_debug("Reloading session from database to get the latest workflow run...")
|
|
3252
|
+
if self._has_async_db():
|
|
3253
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3254
|
+
else:
|
|
3255
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
3256
|
+
|
|
3257
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3258
|
+
# Get the last run (which is the one just created by the tool)
|
|
3259
|
+
last_run = reloaded_session.runs[-1]
|
|
3260
|
+
log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
|
|
3261
|
+
log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
|
|
3262
|
+
|
|
3263
|
+
# Update the last run with workflow_agent_run
|
|
3264
|
+
last_run.workflow_agent_run = agent_response
|
|
3265
|
+
|
|
3266
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3267
|
+
if agent_response:
|
|
3268
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3269
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
3270
|
+
|
|
3271
|
+
# Save the reloaded session (which has the updated run)
|
|
3272
|
+
if self._has_async_db():
|
|
3273
|
+
await self.asave_session(session=reloaded_session)
|
|
3274
|
+
else:
|
|
3275
|
+
self.save_session(session=reloaded_session)
|
|
3276
|
+
|
|
3277
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
3278
|
+
|
|
3279
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
3280
|
+
return last_run
|
|
3281
|
+
else:
|
|
3282
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3283
|
+
# Return a placeholder error response
|
|
3284
|
+
return WorkflowRunOutput(
|
|
3285
|
+
run_id=str(uuid4()),
|
|
3286
|
+
input=execution_input.input,
|
|
3287
|
+
session_id=session.session_id,
|
|
3288
|
+
workflow_id=self.id,
|
|
3289
|
+
workflow_name=self.name,
|
|
3290
|
+
created_at=int(datetime.now().timestamp()),
|
|
3291
|
+
content="Error: Workflow execution failed",
|
|
3292
|
+
status=RunStatus.error,
|
|
3293
|
+
)
|
|
3294
|
+
|
|
2442
3295
|
def cancel_run(self, run_id: str) -> bool:
|
|
2443
3296
|
"""Cancel a running workflow execution.
|
|
2444
3297
|
|
|
@@ -2529,6 +3382,14 @@ class Workflow:
|
|
|
2529
3382
|
# Update session state from DB
|
|
2530
3383
|
session_state = self._load_session_state(session=workflow_session, session_state=session_state)
|
|
2531
3384
|
|
|
3385
|
+
# Initialize run context
|
|
3386
|
+
run_context = RunContext(
|
|
3387
|
+
run_id=run_id,
|
|
3388
|
+
session_id=session_id,
|
|
3389
|
+
user_id=user_id,
|
|
3390
|
+
session_state=session_state,
|
|
3391
|
+
)
|
|
3392
|
+
|
|
2532
3393
|
log_debug(f"Workflow Run Start: {self.name}", center=True)
|
|
2533
3394
|
|
|
2534
3395
|
# Use simple defaults
|
|
@@ -2547,16 +3408,6 @@ class Workflow:
|
|
|
2547
3408
|
# Prepare steps
|
|
2548
3409
|
self._prepare_steps()
|
|
2549
3410
|
|
|
2550
|
-
# Create workflow run response that will be updated by reference
|
|
2551
|
-
workflow_run_response = WorkflowRunOutput(
|
|
2552
|
-
run_id=run_id,
|
|
2553
|
-
input=input,
|
|
2554
|
-
session_id=session_id,
|
|
2555
|
-
workflow_id=self.id,
|
|
2556
|
-
workflow_name=self.name,
|
|
2557
|
-
created_at=int(datetime.now().timestamp()),
|
|
2558
|
-
)
|
|
2559
|
-
|
|
2560
3411
|
inputs = WorkflowExecutionInput(
|
|
2561
3412
|
input=input,
|
|
2562
3413
|
additional_data=additional_data,
|
|
@@ -2571,13 +3422,34 @@ class Workflow:
|
|
|
2571
3422
|
|
|
2572
3423
|
self.update_agents_and_teams_session_info()
|
|
2573
3424
|
|
|
3425
|
+
# Execute workflow agent if configured
|
|
3426
|
+
if self.agent is not None:
|
|
3427
|
+
return self._execute_workflow_agent(
|
|
3428
|
+
user_input=input, # type: ignore
|
|
3429
|
+
session=workflow_session,
|
|
3430
|
+
execution_input=inputs,
|
|
3431
|
+
run_context=run_context,
|
|
3432
|
+
stream=stream,
|
|
3433
|
+
**kwargs,
|
|
3434
|
+
)
|
|
3435
|
+
|
|
3436
|
+
# Create workflow run response for regular workflow execution
|
|
3437
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3438
|
+
run_id=run_id,
|
|
3439
|
+
input=input,
|
|
3440
|
+
session_id=session_id,
|
|
3441
|
+
workflow_id=self.id,
|
|
3442
|
+
workflow_name=self.name,
|
|
3443
|
+
created_at=int(datetime.now().timestamp()),
|
|
3444
|
+
)
|
|
3445
|
+
|
|
2574
3446
|
if stream:
|
|
2575
3447
|
return self._execute_stream(
|
|
2576
3448
|
session=workflow_session,
|
|
2577
3449
|
execution_input=inputs, # type: ignore[arg-type]
|
|
2578
3450
|
workflow_run_response=workflow_run_response,
|
|
2579
3451
|
stream_events=stream_events,
|
|
2580
|
-
|
|
3452
|
+
run_context=run_context,
|
|
2581
3453
|
**kwargs,
|
|
2582
3454
|
)
|
|
2583
3455
|
else:
|
|
@@ -2585,7 +3457,7 @@ class Workflow:
|
|
|
2585
3457
|
session=workflow_session,
|
|
2586
3458
|
execution_input=inputs, # type: ignore[arg-type]
|
|
2587
3459
|
workflow_run_response=workflow_run_response,
|
|
2588
|
-
|
|
3460
|
+
run_context=run_context,
|
|
2589
3461
|
**kwargs,
|
|
2590
3462
|
)
|
|
2591
3463
|
|
|
@@ -2700,6 +3572,14 @@ class Workflow:
|
|
|
2700
3572
|
self.initialize_workflow()
|
|
2701
3573
|
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
2702
3574
|
|
|
3575
|
+
# Initialize run context
|
|
3576
|
+
run_context = RunContext(
|
|
3577
|
+
run_id=run_id,
|
|
3578
|
+
session_id=session_id,
|
|
3579
|
+
user_id=user_id,
|
|
3580
|
+
session_state=session_state,
|
|
3581
|
+
)
|
|
3582
|
+
|
|
2703
3583
|
log_debug(f"Async Workflow Run Start: {self.name}", center=True)
|
|
2704
3584
|
|
|
2705
3585
|
# Use simple defaults
|
|
@@ -2717,16 +3597,6 @@ class Workflow:
|
|
|
2717
3597
|
# Prepare steps
|
|
2718
3598
|
self._prepare_steps()
|
|
2719
3599
|
|
|
2720
|
-
# Create workflow run response that will be updated by reference
|
|
2721
|
-
workflow_run_response = WorkflowRunOutput(
|
|
2722
|
-
run_id=run_id,
|
|
2723
|
-
input=input,
|
|
2724
|
-
session_id=session_id,
|
|
2725
|
-
workflow_id=self.id,
|
|
2726
|
-
workflow_name=self.name,
|
|
2727
|
-
created_at=int(datetime.now().timestamp()),
|
|
2728
|
-
)
|
|
2729
|
-
|
|
2730
3600
|
inputs = WorkflowExecutionInput(
|
|
2731
3601
|
input=input,
|
|
2732
3602
|
additional_data=additional_data,
|
|
@@ -2741,6 +3611,25 @@ class Workflow:
|
|
|
2741
3611
|
|
|
2742
3612
|
self.update_agents_and_teams_session_info()
|
|
2743
3613
|
|
|
3614
|
+
if self.agent is not None:
|
|
3615
|
+
return self._aexecute_workflow_agent( # type: ignore
|
|
3616
|
+
user_input=input, # type: ignore
|
|
3617
|
+
execution_input=inputs,
|
|
3618
|
+
run_context=run_context,
|
|
3619
|
+
stream=stream,
|
|
3620
|
+
**kwargs,
|
|
3621
|
+
)
|
|
3622
|
+
|
|
3623
|
+
# Create workflow run response for regular workflow execution
|
|
3624
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3625
|
+
run_id=run_id,
|
|
3626
|
+
input=input,
|
|
3627
|
+
session_id=session_id,
|
|
3628
|
+
workflow_id=self.id,
|
|
3629
|
+
workflow_name=self.name,
|
|
3630
|
+
created_at=int(datetime.now().timestamp()),
|
|
3631
|
+
)
|
|
3632
|
+
|
|
2744
3633
|
if stream:
|
|
2745
3634
|
return self._aexecute_stream( # type: ignore
|
|
2746
3635
|
execution_input=inputs,
|
|
@@ -2782,6 +3671,12 @@ class Workflow:
|
|
|
2782
3671
|
step_name = step.name or f"step_{i + 1}"
|
|
2783
3672
|
log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
|
|
2784
3673
|
prepared_steps.append(Step(name=step_name, description=step.description, team=step))
|
|
3674
|
+
elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
|
|
3675
|
+
log_warning(
|
|
3676
|
+
f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
|
|
3677
|
+
"but no database is configured in the Workflow. "
|
|
3678
|
+
"History won't be persisted. Add a database to persist runs across executions."
|
|
3679
|
+
)
|
|
2785
3680
|
elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
|
|
2786
3681
|
step_type = type(step).__name__
|
|
2787
3682
|
step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")
|
|
@@ -2822,6 +3717,7 @@ class Workflow:
|
|
|
2822
3717
|
audio: Audio input
|
|
2823
3718
|
images: Image input
|
|
2824
3719
|
videos: Video input
|
|
3720
|
+
files: File input
|
|
2825
3721
|
stream: Whether to stream the response content
|
|
2826
3722
|
stream_events: Whether to stream intermediate steps
|
|
2827
3723
|
markdown: Whether to render content as markdown
|
|
@@ -2915,6 +3811,7 @@ class Workflow:
|
|
|
2915
3811
|
audio: Audio input
|
|
2916
3812
|
images: Image input
|
|
2917
3813
|
videos: Video input
|
|
3814
|
+
files: Files input
|
|
2918
3815
|
stream: Whether to stream the response content
|
|
2919
3816
|
stream_events: Whether to stream intermediate steps
|
|
2920
3817
|
markdown: Whether to render content as markdown
|
|
@@ -3021,7 +3918,7 @@ class Workflow:
|
|
|
3021
3918
|
step_dict["team"] = step.team if hasattr(step, "team") else None # type: ignore
|
|
3022
3919
|
|
|
3023
3920
|
# Handle nested steps for Router/Loop
|
|
3024
|
-
if isinstance(step,
|
|
3921
|
+
if isinstance(step, Router):
|
|
3025
3922
|
step_dict["steps"] = (
|
|
3026
3923
|
[serialize_step(step) for step in step.choices] if hasattr(step, "choices") else None
|
|
3027
3924
|
)
|