agno 2.2.5__py3-none-any.whl → 2.2.6__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 +82 -19
- agno/culture/manager.py +3 -4
- agno/knowledge/chunking/agentic.py +6 -2
- 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 +44 -11
- 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 +20 -12
- agno/models/openai/responses.py +10 -5
- agno/models/utils.py +254 -8
- agno/models/vertexai/claude.py +9 -13
- agno/os/routers/evals/evals.py +8 -8
- agno/os/routers/evals/utils.py +1 -0
- agno/os/schema.py +48 -33
- agno/os/utils.py +27 -0
- agno/run/agent.py +5 -0
- agno/run/team.py +2 -0
- agno/run/workflow.py +39 -0
- agno/session/summary.py +8 -2
- agno/session/workflow.py +4 -3
- agno/team/team.py +50 -14
- agno/tools/file.py +153 -25
- agno/tools/function.py +5 -1
- agno/tools/notion.py +201 -0
- agno/utils/events.py +2 -0
- agno/utils/print_response/workflow.py +115 -16
- agno/vectordb/milvus/milvus.py +5 -0
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +298 -0
- agno/workflow/workflow.py +929 -64
- {agno-2.2.5.dist-info → agno-2.2.6.dist-info}/METADATA +4 -1
- {agno-2.2.5.dist-info → agno-2.2.6.dist-info}/RECORD +40 -38
- {agno-2.2.5.dist-info → agno-2.2.6.dist-info}/WHEEL +0 -0
- {agno-2.2.5.dist-info → agno-2.2.6.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.5.dist-info → agno-2.2.6.dist-info}/top_level.txt +0 -0
agno/workflow/workflow.py
CHANGED
|
@@ -29,7 +29,7 @@ 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.agent import RunContentEvent, RunEvent
|
|
32
|
+
from agno.run.agent import RunContentEvent, RunEvent, RunOutput
|
|
33
33
|
from agno.run.base import RunStatus
|
|
34
34
|
from agno.run.cancel import (
|
|
35
35
|
cancel_run as cancel_run_global,
|
|
@@ -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
|
|
|
@@ -2285,14 +2309,25 @@ class Workflow:
|
|
|
2285
2309
|
else:
|
|
2286
2310
|
self.save_session(session=workflow_session)
|
|
2287
2311
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2312
|
+
if self.agent is not None:
|
|
2313
|
+
self._aexecute_workflow_agent(
|
|
2314
|
+
user_input=input, # type: ignore
|
|
2315
|
+
session_id=session_id,
|
|
2316
|
+
user_id=user_id,
|
|
2317
|
+
execution_input=inputs,
|
|
2318
|
+
session_state=session_state,
|
|
2319
|
+
stream=False,
|
|
2320
|
+
**kwargs,
|
|
2321
|
+
)
|
|
2322
|
+
else:
|
|
2323
|
+
await self._aexecute(
|
|
2324
|
+
session_id=session_id,
|
|
2325
|
+
user_id=user_id,
|
|
2326
|
+
execution_input=inputs,
|
|
2327
|
+
workflow_run_response=workflow_run_response,
|
|
2328
|
+
session_state=session_state,
|
|
2329
|
+
**kwargs,
|
|
2330
|
+
)
|
|
2296
2331
|
|
|
2297
2332
|
log_debug(f"Background execution completed with status: {workflow_run_response.status}")
|
|
2298
2333
|
|
|
@@ -2353,13 +2388,6 @@ class Workflow:
|
|
|
2353
2388
|
status=RunStatus.pending,
|
|
2354
2389
|
)
|
|
2355
2390
|
|
|
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
2391
|
# Prepare execution input
|
|
2364
2392
|
inputs = WorkflowExecutionInput(
|
|
2365
2393
|
input=input,
|
|
@@ -2375,27 +2403,47 @@ class Workflow:
|
|
|
2375
2403
|
async def execute_workflow_background_stream():
|
|
2376
2404
|
"""Background execution with streaming and WebSocket broadcasting"""
|
|
2377
2405
|
try:
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2406
|
+
if self.agent is not None:
|
|
2407
|
+
result = self._aexecute_workflow_agent(
|
|
2408
|
+
user_input=input, # type: ignore
|
|
2409
|
+
session_id=session_id,
|
|
2410
|
+
user_id=user_id,
|
|
2411
|
+
execution_input=inputs,
|
|
2412
|
+
session_state=session_state,
|
|
2413
|
+
stream=True,
|
|
2414
|
+
websocket_handler=websocket_handler,
|
|
2415
|
+
**kwargs,
|
|
2416
|
+
)
|
|
2417
|
+
# For streaming, result is an async iterator
|
|
2418
|
+
async for event in result: # type: ignore
|
|
2419
|
+
# Events are automatically broadcast by _handle_event in the agent execution
|
|
2420
|
+
# We just consume them here to drive the execution
|
|
2421
|
+
pass
|
|
2422
|
+
log_debug(
|
|
2423
|
+
f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
|
|
2424
|
+
)
|
|
2382
2425
|
else:
|
|
2383
|
-
|
|
2426
|
+
# Update status to RUNNING and save
|
|
2427
|
+
workflow_run_response.status = RunStatus.running
|
|
2428
|
+
if self._has_async_db():
|
|
2429
|
+
await self.asave_session(session=workflow_session)
|
|
2430
|
+
else:
|
|
2431
|
+
self.save_session(session=workflow_session)
|
|
2384
2432
|
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2433
|
+
# Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
|
|
2434
|
+
async for event in self._aexecute_stream(
|
|
2435
|
+
session_id=session_id,
|
|
2436
|
+
user_id=user_id,
|
|
2437
|
+
execution_input=inputs,
|
|
2438
|
+
workflow_run_response=workflow_run_response,
|
|
2439
|
+
stream_events=stream_events,
|
|
2440
|
+
session_state=session_state,
|
|
2441
|
+
websocket_handler=websocket_handler,
|
|
2442
|
+
**kwargs,
|
|
2443
|
+
):
|
|
2444
|
+
# Events are automatically broadcast by _handle_event
|
|
2445
|
+
# We just consume them here to drive the execution
|
|
2446
|
+
pass
|
|
2399
2447
|
|
|
2400
2448
|
log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
|
|
2401
2449
|
|
|
@@ -2439,6 +2487,795 @@ class Workflow:
|
|
|
2439
2487
|
|
|
2440
2488
|
return None
|
|
2441
2489
|
|
|
2490
|
+
def _initialize_workflow_agent(
|
|
2491
|
+
self,
|
|
2492
|
+
session: WorkflowSession,
|
|
2493
|
+
execution_input: WorkflowExecutionInput,
|
|
2494
|
+
session_state: Optional[Dict[str, Any]],
|
|
2495
|
+
stream: bool = False,
|
|
2496
|
+
) -> None:
|
|
2497
|
+
"""Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
|
|
2498
|
+
from agno.tools.function import Function
|
|
2499
|
+
|
|
2500
|
+
workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
|
|
2501
|
+
workflow=self,
|
|
2502
|
+
session=session,
|
|
2503
|
+
execution_input=execution_input,
|
|
2504
|
+
session_state=session_state,
|
|
2505
|
+
stream=stream,
|
|
2506
|
+
)
|
|
2507
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
2508
|
+
|
|
2509
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
2510
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
2511
|
+
|
|
2512
|
+
log_debug("Workflow agent initialized with run_workflow tool")
|
|
2513
|
+
|
|
2514
|
+
def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
|
|
2515
|
+
"""Build dependencies dict with workflow context to pass to agent.run()"""
|
|
2516
|
+
# Get configuration from the WorkflowAgent instance
|
|
2517
|
+
add_history = True
|
|
2518
|
+
num_runs = 5
|
|
2519
|
+
|
|
2520
|
+
if self.agent and isinstance(self.agent, WorkflowAgent):
|
|
2521
|
+
add_history = self.agent.add_workflow_history
|
|
2522
|
+
num_runs = self.agent.num_history_runs or 5
|
|
2523
|
+
|
|
2524
|
+
if add_history:
|
|
2525
|
+
history_context = (
|
|
2526
|
+
session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
|
|
2527
|
+
)
|
|
2528
|
+
else:
|
|
2529
|
+
history_context = "No workflow history available."
|
|
2530
|
+
|
|
2531
|
+
# Build workflow context with description and history
|
|
2532
|
+
workflow_context = ""
|
|
2533
|
+
if self.description:
|
|
2534
|
+
workflow_context += f"Workflow Description: {self.description}\n\n"
|
|
2535
|
+
|
|
2536
|
+
workflow_context += history_context
|
|
2537
|
+
|
|
2538
|
+
return {
|
|
2539
|
+
"workflow_context": workflow_context,
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
def _execute_workflow_agent(
|
|
2543
|
+
self,
|
|
2544
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2545
|
+
session: WorkflowSession,
|
|
2546
|
+
execution_input: WorkflowExecutionInput,
|
|
2547
|
+
session_state: Optional[Dict[str, Any]],
|
|
2548
|
+
stream: bool = False,
|
|
2549
|
+
**kwargs: Any,
|
|
2550
|
+
) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
|
|
2551
|
+
"""
|
|
2552
|
+
Execute the workflow agent in streaming or non-streaming mode.
|
|
2553
|
+
|
|
2554
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2555
|
+
|
|
2556
|
+
Args:
|
|
2557
|
+
user_input: The user's input
|
|
2558
|
+
session: The workflow session
|
|
2559
|
+
execution_input: The execution input
|
|
2560
|
+
session_state: The session state
|
|
2561
|
+
stream: Whether to stream the response
|
|
2562
|
+
stream_intermediate_steps: Whether to stream intermediate steps
|
|
2563
|
+
|
|
2564
|
+
Returns:
|
|
2565
|
+
WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
|
|
2566
|
+
"""
|
|
2567
|
+
if stream:
|
|
2568
|
+
return self._run_workflow_agent_stream(
|
|
2569
|
+
agent_input=user_input,
|
|
2570
|
+
session=session,
|
|
2571
|
+
execution_input=execution_input,
|
|
2572
|
+
session_state=session_state,
|
|
2573
|
+
stream=stream,
|
|
2574
|
+
**kwargs,
|
|
2575
|
+
)
|
|
2576
|
+
else:
|
|
2577
|
+
return self._run_workflow_agent(
|
|
2578
|
+
agent_input=user_input,
|
|
2579
|
+
session=session,
|
|
2580
|
+
execution_input=execution_input,
|
|
2581
|
+
session_state=session_state,
|
|
2582
|
+
stream=stream,
|
|
2583
|
+
)
|
|
2584
|
+
|
|
2585
|
+
def _run_workflow_agent_stream(
|
|
2586
|
+
self,
|
|
2587
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2588
|
+
session: WorkflowSession,
|
|
2589
|
+
execution_input: WorkflowExecutionInput,
|
|
2590
|
+
session_state: Optional[Dict[str, Any]],
|
|
2591
|
+
stream: bool = False,
|
|
2592
|
+
**kwargs: Any,
|
|
2593
|
+
) -> Iterator[WorkflowRunOutputEvent]:
|
|
2594
|
+
"""
|
|
2595
|
+
Execute the workflow agent in streaming mode.
|
|
2596
|
+
|
|
2597
|
+
The agent's tool (run_workflow) is a generator that yields workflow events directly.
|
|
2598
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
2599
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
2600
|
+
|
|
2601
|
+
Yields:
|
|
2602
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
2603
|
+
"""
|
|
2604
|
+
from typing import get_args
|
|
2605
|
+
|
|
2606
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
2607
|
+
|
|
2608
|
+
# Initialize agent with stream_intermediate_steps=True so tool yields events
|
|
2609
|
+
self._initialize_workflow_agent(session, execution_input, session_state, stream=stream)
|
|
2610
|
+
|
|
2611
|
+
# Build dependencies with workflow context
|
|
2612
|
+
dependencies = self._get_workflow_agent_dependencies(session)
|
|
2613
|
+
|
|
2614
|
+
# Run agent with streaming - workflow events will bubble up from the tool
|
|
2615
|
+
agent_response: Optional[RunOutput] = None
|
|
2616
|
+
workflow_executed = False
|
|
2617
|
+
|
|
2618
|
+
from agno.run.agent import RunContentEvent
|
|
2619
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
2620
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
2621
|
+
|
|
2622
|
+
log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
|
|
2623
|
+
|
|
2624
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
2625
|
+
run_id = str(uuid4())
|
|
2626
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
2627
|
+
run_id=run_id,
|
|
2628
|
+
input=execution_input.input,
|
|
2629
|
+
session_id=session.session_id,
|
|
2630
|
+
workflow_id=self.id,
|
|
2631
|
+
workflow_name=self.name,
|
|
2632
|
+
created_at=int(datetime.now().timestamp()),
|
|
2633
|
+
)
|
|
2634
|
+
|
|
2635
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
2636
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
2637
|
+
workflow_name=self.name,
|
|
2638
|
+
workflow_id=self.id,
|
|
2639
|
+
session_id=session.session_id,
|
|
2640
|
+
)
|
|
2641
|
+
yield agent_started_event
|
|
2642
|
+
|
|
2643
|
+
# Run the agent in streaming mode and yield all events
|
|
2644
|
+
for event in self.agent.run( # type: ignore[union-attr]
|
|
2645
|
+
input=agent_input,
|
|
2646
|
+
stream=True,
|
|
2647
|
+
stream_intermediate_steps=True,
|
|
2648
|
+
yield_run_response=True,
|
|
2649
|
+
session_id=session.session_id,
|
|
2650
|
+
dependencies=dependencies, # Pass context dynamically per-run
|
|
2651
|
+
): # type: ignore
|
|
2652
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
2653
|
+
yield event # type: ignore[misc]
|
|
2654
|
+
|
|
2655
|
+
# Track if workflow was executed by checking for WorkflowCompletedEvent
|
|
2656
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
2657
|
+
workflow_executed = True
|
|
2658
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
2659
|
+
if event.step_name is None:
|
|
2660
|
+
# This is from the workflow agent itself
|
|
2661
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
2662
|
+
|
|
2663
|
+
if workflow_executed:
|
|
2664
|
+
continue # Skip if workflow was already executed
|
|
2665
|
+
|
|
2666
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
2667
|
+
event.workflow_agent = True # type: ignore
|
|
2668
|
+
yield event # type: ignore[misc]
|
|
2669
|
+
|
|
2670
|
+
# Capture the final RunOutput (but don't yield it)
|
|
2671
|
+
if isinstance(event, RunOutput):
|
|
2672
|
+
agent_response = event
|
|
2673
|
+
|
|
2674
|
+
# Handle direct answer case (no workflow execution)
|
|
2675
|
+
if not workflow_executed:
|
|
2676
|
+
# Update the pre-created workflow run response with the direct answer
|
|
2677
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
2678
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
2679
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
2680
|
+
|
|
2681
|
+
workflow_run_response = direct_reply_run_response
|
|
2682
|
+
|
|
2683
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2684
|
+
if agent_response:
|
|
2685
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2686
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2687
|
+
|
|
2688
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2689
|
+
|
|
2690
|
+
# Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
|
|
2691
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2692
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2693
|
+
workflow_name=self.name,
|
|
2694
|
+
workflow_id=self.id,
|
|
2695
|
+
session_id=session.session_id,
|
|
2696
|
+
content=workflow_run_response.content,
|
|
2697
|
+
)
|
|
2698
|
+
yield agent_completed_event
|
|
2699
|
+
|
|
2700
|
+
# Yield a workflow completed event with the agent's direct response
|
|
2701
|
+
completed_event = WorkflowCompletedEvent(
|
|
2702
|
+
run_id=workflow_run_response.run_id or "",
|
|
2703
|
+
content=workflow_run_response.content,
|
|
2704
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
2705
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
2706
|
+
session_id=workflow_run_response.session_id,
|
|
2707
|
+
step_results=[],
|
|
2708
|
+
metadata={"agent_direct_response": True},
|
|
2709
|
+
)
|
|
2710
|
+
yield completed_event
|
|
2711
|
+
|
|
2712
|
+
# Update the run in session
|
|
2713
|
+
session.upsert_run(run=workflow_run_response)
|
|
2714
|
+
# Save session
|
|
2715
|
+
self.save_session(session=session)
|
|
2716
|
+
|
|
2717
|
+
else:
|
|
2718
|
+
# Workflow was executed by the tool
|
|
2719
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2720
|
+
|
|
2721
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2722
|
+
# Get the last run (which is the one just created by the tool)
|
|
2723
|
+
last_run = reloaded_session.runs[-1]
|
|
2724
|
+
|
|
2725
|
+
# Yield WorkflowAgentCompletedEvent
|
|
2726
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2727
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2728
|
+
workflow_name=self.name,
|
|
2729
|
+
workflow_id=self.id,
|
|
2730
|
+
session_id=session.session_id,
|
|
2731
|
+
content=agent_response.content if agent_response else None,
|
|
2732
|
+
)
|
|
2733
|
+
yield agent_completed_event
|
|
2734
|
+
|
|
2735
|
+
# Update the last run with workflow_agent_run
|
|
2736
|
+
last_run.workflow_agent_run = agent_response
|
|
2737
|
+
|
|
2738
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2739
|
+
if agent_response:
|
|
2740
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2741
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2742
|
+
|
|
2743
|
+
# Save the reloaded session (which has the updated run)
|
|
2744
|
+
self.save_session(session=reloaded_session)
|
|
2745
|
+
|
|
2746
|
+
else:
|
|
2747
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
2748
|
+
|
|
2749
|
+
def _run_workflow_agent(
|
|
2750
|
+
self,
|
|
2751
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2752
|
+
session: WorkflowSession,
|
|
2753
|
+
execution_input: WorkflowExecutionInput,
|
|
2754
|
+
session_state: Optional[Dict[str, Any]],
|
|
2755
|
+
stream: bool = False,
|
|
2756
|
+
) -> WorkflowRunOutput:
|
|
2757
|
+
"""
|
|
2758
|
+
Execute the workflow agent in non-streaming mode.
|
|
2759
|
+
|
|
2760
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2761
|
+
|
|
2762
|
+
Returns:
|
|
2763
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
2764
|
+
"""
|
|
2765
|
+
|
|
2766
|
+
# Initialize the agent
|
|
2767
|
+
self._initialize_workflow_agent(session, execution_input, session_state, stream=stream)
|
|
2768
|
+
|
|
2769
|
+
# Build dependencies with workflow context
|
|
2770
|
+
dependencies = self._get_workflow_agent_dependencies(session)
|
|
2771
|
+
|
|
2772
|
+
# Run the agent
|
|
2773
|
+
agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
|
|
2774
|
+
input=agent_input,
|
|
2775
|
+
session_id=session.session_id,
|
|
2776
|
+
dependencies=dependencies,
|
|
2777
|
+
stream=stream,
|
|
2778
|
+
) # type: ignore
|
|
2779
|
+
|
|
2780
|
+
# Check if the agent called the workflow tool
|
|
2781
|
+
workflow_executed = False
|
|
2782
|
+
if agent_response.messages:
|
|
2783
|
+
for message in agent_response.messages:
|
|
2784
|
+
if message.role == "assistant" and message.tool_calls:
|
|
2785
|
+
# Check if the tool call is specifically for run_workflow
|
|
2786
|
+
for tool_call in message.tool_calls:
|
|
2787
|
+
# Handle both dict and object formats
|
|
2788
|
+
if isinstance(tool_call, dict):
|
|
2789
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
2790
|
+
else:
|
|
2791
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
2792
|
+
|
|
2793
|
+
if tool_name == "run_workflow":
|
|
2794
|
+
workflow_executed = True
|
|
2795
|
+
break
|
|
2796
|
+
if workflow_executed:
|
|
2797
|
+
break
|
|
2798
|
+
|
|
2799
|
+
log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
|
|
2800
|
+
|
|
2801
|
+
# Handle direct answer case (no workflow execution)
|
|
2802
|
+
if not workflow_executed:
|
|
2803
|
+
# Create a new workflow run output for the direct answer
|
|
2804
|
+
run_id = str(uuid4())
|
|
2805
|
+
workflow_run_response = WorkflowRunOutput(
|
|
2806
|
+
run_id=run_id,
|
|
2807
|
+
input=execution_input.input,
|
|
2808
|
+
session_id=session.session_id,
|
|
2809
|
+
workflow_id=self.id,
|
|
2810
|
+
workflow_name=self.name,
|
|
2811
|
+
created_at=int(datetime.now().timestamp()),
|
|
2812
|
+
content=agent_response.content,
|
|
2813
|
+
status=RunStatus.completed,
|
|
2814
|
+
workflow_agent_run=agent_response,
|
|
2815
|
+
)
|
|
2816
|
+
|
|
2817
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2818
|
+
if agent_response:
|
|
2819
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2820
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2821
|
+
|
|
2822
|
+
# Update the run in session
|
|
2823
|
+
session.upsert_run(run=workflow_run_response)
|
|
2824
|
+
self.save_session(session=session)
|
|
2825
|
+
|
|
2826
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2827
|
+
|
|
2828
|
+
return workflow_run_response
|
|
2829
|
+
else:
|
|
2830
|
+
# Workflow was executed by the tool
|
|
2831
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2832
|
+
|
|
2833
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2834
|
+
# Get the last run (which is the one just created by the tool)
|
|
2835
|
+
last_run = reloaded_session.runs[-1]
|
|
2836
|
+
|
|
2837
|
+
# Update the last run directly with workflow_agent_run
|
|
2838
|
+
last_run.workflow_agent_run = agent_response
|
|
2839
|
+
|
|
2840
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2841
|
+
if agent_response:
|
|
2842
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2843
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2844
|
+
|
|
2845
|
+
# Save the reloaded session (which has the updated run)
|
|
2846
|
+
self.save_session(session=reloaded_session)
|
|
2847
|
+
|
|
2848
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
2849
|
+
return last_run
|
|
2850
|
+
else:
|
|
2851
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
2852
|
+
# Return a placeholder error response
|
|
2853
|
+
return WorkflowRunOutput(
|
|
2854
|
+
run_id=str(uuid4()),
|
|
2855
|
+
input=execution_input.input,
|
|
2856
|
+
session_id=session.session_id,
|
|
2857
|
+
workflow_id=self.id,
|
|
2858
|
+
workflow_name=self.name,
|
|
2859
|
+
created_at=int(datetime.now().timestamp()),
|
|
2860
|
+
content="Error: Workflow execution failed",
|
|
2861
|
+
status=RunStatus.error,
|
|
2862
|
+
)
|
|
2863
|
+
|
|
2864
|
+
def _async_initialize_workflow_agent(
|
|
2865
|
+
self,
|
|
2866
|
+
session: WorkflowSession,
|
|
2867
|
+
execution_input: WorkflowExecutionInput,
|
|
2868
|
+
session_state: Optional[Dict[str, Any]],
|
|
2869
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2870
|
+
stream: bool = False,
|
|
2871
|
+
) -> None:
|
|
2872
|
+
"""Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
|
|
2873
|
+
from agno.tools.function import Function
|
|
2874
|
+
|
|
2875
|
+
workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
|
|
2876
|
+
workflow=self,
|
|
2877
|
+
session=session,
|
|
2878
|
+
execution_input=execution_input,
|
|
2879
|
+
session_state=session_state,
|
|
2880
|
+
stream=stream,
|
|
2881
|
+
websocket_handler=websocket_handler,
|
|
2882
|
+
)
|
|
2883
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
2884
|
+
|
|
2885
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
2886
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
2887
|
+
|
|
2888
|
+
log_debug("Workflow agent initialized with async run_workflow tool")
|
|
2889
|
+
|
|
2890
|
+
async def _aload_session_for_workflow_agent(
|
|
2891
|
+
self,
|
|
2892
|
+
session_id: str,
|
|
2893
|
+
user_id: Optional[str],
|
|
2894
|
+
session_state: Optional[Dict[str, Any]],
|
|
2895
|
+
) -> Tuple[WorkflowSession, Dict[str, Any]]:
|
|
2896
|
+
"""Helper to load or create session for workflow agent execution"""
|
|
2897
|
+
return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
|
|
2898
|
+
|
|
2899
|
+
def _aexecute_workflow_agent(
|
|
2900
|
+
self,
|
|
2901
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2902
|
+
session_id: str,
|
|
2903
|
+
user_id: Optional[str],
|
|
2904
|
+
execution_input: WorkflowExecutionInput,
|
|
2905
|
+
session_state: Optional[Dict[str, Any]],
|
|
2906
|
+
stream: bool = False,
|
|
2907
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2908
|
+
**kwargs: Any,
|
|
2909
|
+
):
|
|
2910
|
+
"""
|
|
2911
|
+
Execute the workflow agent asynchronously in streaming or non-streaming mode.
|
|
2912
|
+
|
|
2913
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2914
|
+
|
|
2915
|
+
Args:
|
|
2916
|
+
user_input: The user's input
|
|
2917
|
+
session_id: The workflow session ID
|
|
2918
|
+
user_id: The user ID
|
|
2919
|
+
execution_input: The execution input
|
|
2920
|
+
session_state: The session state
|
|
2921
|
+
stream: Whether to stream the response
|
|
2922
|
+
websocket_handler: The WebSocket handler
|
|
2923
|
+
|
|
2924
|
+
Returns:
|
|
2925
|
+
Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
|
|
2926
|
+
"""
|
|
2927
|
+
if stream:
|
|
2928
|
+
|
|
2929
|
+
async def _stream():
|
|
2930
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
2931
|
+
session_id, user_id, session_state
|
|
2932
|
+
)
|
|
2933
|
+
async for event in self._arun_workflow_agent_stream(
|
|
2934
|
+
agent_input=user_input,
|
|
2935
|
+
session=session,
|
|
2936
|
+
execution_input=execution_input,
|
|
2937
|
+
session_state=session_state_loaded,
|
|
2938
|
+
stream=stream,
|
|
2939
|
+
websocket_handler=websocket_handler,
|
|
2940
|
+
**kwargs,
|
|
2941
|
+
):
|
|
2942
|
+
yield event
|
|
2943
|
+
|
|
2944
|
+
return _stream()
|
|
2945
|
+
else:
|
|
2946
|
+
|
|
2947
|
+
async def _execute():
|
|
2948
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
2949
|
+
session_id, user_id, session_state
|
|
2950
|
+
)
|
|
2951
|
+
return await self._arun_workflow_agent(
|
|
2952
|
+
agent_input=user_input,
|
|
2953
|
+
session=session,
|
|
2954
|
+
execution_input=execution_input,
|
|
2955
|
+
session_state=session_state_loaded,
|
|
2956
|
+
stream=stream,
|
|
2957
|
+
)
|
|
2958
|
+
|
|
2959
|
+
return _execute()
|
|
2960
|
+
|
|
2961
|
+
async def _arun_workflow_agent_stream(
|
|
2962
|
+
self,
|
|
2963
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2964
|
+
session: WorkflowSession,
|
|
2965
|
+
execution_input: WorkflowExecutionInput,
|
|
2966
|
+
session_state: Optional[Dict[str, Any]],
|
|
2967
|
+
stream: bool = False,
|
|
2968
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2969
|
+
**kwargs: Any,
|
|
2970
|
+
) -> AsyncIterator[WorkflowRunOutputEvent]:
|
|
2971
|
+
"""
|
|
2972
|
+
Execute the workflow agent asynchronously in streaming mode.
|
|
2973
|
+
|
|
2974
|
+
The agent's tool (run_workflow) is an async generator that yields workflow events directly.
|
|
2975
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
2976
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
2977
|
+
|
|
2978
|
+
Yields:
|
|
2979
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
2980
|
+
"""
|
|
2981
|
+
from typing import get_args
|
|
2982
|
+
|
|
2983
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
2984
|
+
|
|
2985
|
+
logger.info("Workflow agent enabled - async streaming mode")
|
|
2986
|
+
log_debug(f"User input: {agent_input}")
|
|
2987
|
+
|
|
2988
|
+
self._async_initialize_workflow_agent(
|
|
2989
|
+
session, execution_input, session_state, stream=stream, websocket_handler=websocket_handler
|
|
2990
|
+
)
|
|
2991
|
+
|
|
2992
|
+
dependencies = self._get_workflow_agent_dependencies(session)
|
|
2993
|
+
|
|
2994
|
+
agent_response: Optional[RunOutput] = None
|
|
2995
|
+
workflow_executed = False
|
|
2996
|
+
|
|
2997
|
+
from agno.run.agent import RunContentEvent
|
|
2998
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
2999
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
3000
|
+
|
|
3001
|
+
log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
|
|
3002
|
+
|
|
3003
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
3004
|
+
run_id = str(uuid4())
|
|
3005
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
3006
|
+
run_id=run_id,
|
|
3007
|
+
input=execution_input.input,
|
|
3008
|
+
session_id=session.session_id,
|
|
3009
|
+
workflow_id=self.id,
|
|
3010
|
+
workflow_name=self.name,
|
|
3011
|
+
created_at=int(datetime.now().timestamp()),
|
|
3012
|
+
)
|
|
3013
|
+
|
|
3014
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
3015
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
3016
|
+
workflow_name=self.name,
|
|
3017
|
+
workflow_id=self.id,
|
|
3018
|
+
session_id=session.session_id,
|
|
3019
|
+
)
|
|
3020
|
+
self._broadcast_to_websocket(agent_started_event, websocket_handler)
|
|
3021
|
+
yield agent_started_event
|
|
3022
|
+
|
|
3023
|
+
# Run the agent in streaming mode and yield all events
|
|
3024
|
+
async for event in self.agent.arun( # type: ignore[union-attr]
|
|
3025
|
+
input=agent_input,
|
|
3026
|
+
stream=True,
|
|
3027
|
+
stream_intermediate_steps=True,
|
|
3028
|
+
yield_run_response=True,
|
|
3029
|
+
session_id=session.session_id,
|
|
3030
|
+
dependencies=dependencies, # Pass context dynamically per-run
|
|
3031
|
+
): # type: ignore
|
|
3032
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
3033
|
+
yield event # type: ignore[misc]
|
|
3034
|
+
|
|
3035
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
3036
|
+
workflow_executed = True
|
|
3037
|
+
log_debug("Workflow execution detected via WorkflowCompletedEvent")
|
|
3038
|
+
|
|
3039
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
3040
|
+
if event.step_name is None:
|
|
3041
|
+
# This is from the workflow agent itself
|
|
3042
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
3043
|
+
|
|
3044
|
+
if workflow_executed:
|
|
3045
|
+
continue # Skip if workflow was already executed
|
|
3046
|
+
|
|
3047
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
3048
|
+
event.workflow_agent = True # type: ignore
|
|
3049
|
+
|
|
3050
|
+
# Broadcast to WebSocket if available (async context only)
|
|
3051
|
+
self._broadcast_to_websocket(event, websocket_handler)
|
|
3052
|
+
|
|
3053
|
+
yield event # type: ignore[misc]
|
|
3054
|
+
|
|
3055
|
+
# Capture the final RunOutput (but don't yield it)
|
|
3056
|
+
if isinstance(event, RunOutput):
|
|
3057
|
+
agent_response = event
|
|
3058
|
+
log_debug(
|
|
3059
|
+
f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
|
|
3060
|
+
)
|
|
3061
|
+
|
|
3062
|
+
# Handle direct answer case (no workflow execution)
|
|
3063
|
+
if not workflow_executed:
|
|
3064
|
+
# Update the pre-created workflow run response with the direct answer
|
|
3065
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
3066
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
3067
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
3068
|
+
|
|
3069
|
+
workflow_run_response = direct_reply_run_response
|
|
3070
|
+
|
|
3071
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3072
|
+
if agent_response:
|
|
3073
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3074
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
3075
|
+
|
|
3076
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3077
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3078
|
+
workflow_name=self.name,
|
|
3079
|
+
workflow_id=self.id,
|
|
3080
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3081
|
+
session_id=session.session_id,
|
|
3082
|
+
content=workflow_run_response.content,
|
|
3083
|
+
)
|
|
3084
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3085
|
+
yield agent_completed_event
|
|
3086
|
+
|
|
3087
|
+
# Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
|
|
3088
|
+
completed_event = WorkflowCompletedEvent(
|
|
3089
|
+
run_id=workflow_run_response.run_id or "",
|
|
3090
|
+
content=workflow_run_response.content,
|
|
3091
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
3092
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
3093
|
+
session_id=workflow_run_response.session_id,
|
|
3094
|
+
step_results=[],
|
|
3095
|
+
metadata={"agent_direct_response": True},
|
|
3096
|
+
)
|
|
3097
|
+
yield completed_event
|
|
3098
|
+
|
|
3099
|
+
# Update the run in session
|
|
3100
|
+
session.upsert_run(run=workflow_run_response)
|
|
3101
|
+
# Save session
|
|
3102
|
+
if self._has_async_db():
|
|
3103
|
+
await self.asave_session(session=session)
|
|
3104
|
+
else:
|
|
3105
|
+
self.save_session(session=session)
|
|
3106
|
+
|
|
3107
|
+
else:
|
|
3108
|
+
# Workflow was executed by the tool
|
|
3109
|
+
if self._has_async_db():
|
|
3110
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3111
|
+
else:
|
|
3112
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
3113
|
+
|
|
3114
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3115
|
+
# Get the last run (which is the one just created by the tool)
|
|
3116
|
+
last_run = reloaded_session.runs[-1]
|
|
3117
|
+
|
|
3118
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3119
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3120
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3121
|
+
workflow_name=self.name,
|
|
3122
|
+
workflow_id=self.id,
|
|
3123
|
+
session_id=session.session_id,
|
|
3124
|
+
content=agent_response.content if agent_response else None,
|
|
3125
|
+
)
|
|
3126
|
+
|
|
3127
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3128
|
+
|
|
3129
|
+
yield agent_completed_event
|
|
3130
|
+
|
|
3131
|
+
# Update the last run with workflow_agent_run
|
|
3132
|
+
last_run.workflow_agent_run = agent_response
|
|
3133
|
+
|
|
3134
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3135
|
+
if agent_response:
|
|
3136
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3137
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
3138
|
+
|
|
3139
|
+
# Save the reloaded session (which has the updated run)
|
|
3140
|
+
if self._has_async_db():
|
|
3141
|
+
await self.asave_session(session=reloaded_session)
|
|
3142
|
+
else:
|
|
3143
|
+
self.save_session(session=reloaded_session)
|
|
3144
|
+
|
|
3145
|
+
else:
|
|
3146
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3147
|
+
|
|
3148
|
+
async def _arun_workflow_agent(
|
|
3149
|
+
self,
|
|
3150
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
3151
|
+
session: WorkflowSession,
|
|
3152
|
+
execution_input: WorkflowExecutionInput,
|
|
3153
|
+
session_state: Optional[Dict[str, Any]],
|
|
3154
|
+
stream: bool = False,
|
|
3155
|
+
) -> WorkflowRunOutput:
|
|
3156
|
+
"""
|
|
3157
|
+
Execute the workflow agent asynchronously in non-streaming mode.
|
|
3158
|
+
|
|
3159
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
3160
|
+
|
|
3161
|
+
Returns:
|
|
3162
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
3163
|
+
"""
|
|
3164
|
+
# Initialize the agent
|
|
3165
|
+
self._async_initialize_workflow_agent(session, execution_input, session_state, stream=stream)
|
|
3166
|
+
|
|
3167
|
+
# Build dependencies with workflow context
|
|
3168
|
+
dependencies = self._get_workflow_agent_dependencies(session)
|
|
3169
|
+
|
|
3170
|
+
# Run the agent
|
|
3171
|
+
agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
|
|
3172
|
+
input=agent_input,
|
|
3173
|
+
session_id=session.session_id,
|
|
3174
|
+
dependencies=dependencies,
|
|
3175
|
+
stream=stream,
|
|
3176
|
+
) # type: ignore
|
|
3177
|
+
|
|
3178
|
+
# Check if the agent called the workflow tool
|
|
3179
|
+
workflow_executed = False
|
|
3180
|
+
if agent_response.messages:
|
|
3181
|
+
for message in agent_response.messages:
|
|
3182
|
+
if message.role == "assistant" and message.tool_calls:
|
|
3183
|
+
# Check if the tool call is specifically for run_workflow
|
|
3184
|
+
for tool_call in message.tool_calls:
|
|
3185
|
+
# Handle both dict and object formats
|
|
3186
|
+
if isinstance(tool_call, dict):
|
|
3187
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
3188
|
+
else:
|
|
3189
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
3190
|
+
|
|
3191
|
+
if tool_name == "run_workflow":
|
|
3192
|
+
workflow_executed = True
|
|
3193
|
+
break
|
|
3194
|
+
if workflow_executed:
|
|
3195
|
+
break
|
|
3196
|
+
|
|
3197
|
+
# Handle direct answer case (no workflow execution)
|
|
3198
|
+
if not workflow_executed:
|
|
3199
|
+
# Create a new workflow run output for the direct answer
|
|
3200
|
+
run_id = str(uuid4())
|
|
3201
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3202
|
+
run_id=run_id,
|
|
3203
|
+
input=execution_input.input,
|
|
3204
|
+
session_id=session.session_id,
|
|
3205
|
+
workflow_id=self.id,
|
|
3206
|
+
workflow_name=self.name,
|
|
3207
|
+
created_at=int(datetime.now().timestamp()),
|
|
3208
|
+
content=agent_response.content,
|
|
3209
|
+
status=RunStatus.completed,
|
|
3210
|
+
workflow_agent_run=agent_response,
|
|
3211
|
+
)
|
|
3212
|
+
|
|
3213
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3214
|
+
if agent_response:
|
|
3215
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3216
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
3217
|
+
|
|
3218
|
+
# Update the run in session
|
|
3219
|
+
session.upsert_run(run=workflow_run_response)
|
|
3220
|
+
if self._has_async_db():
|
|
3221
|
+
await self.asave_session(session=session)
|
|
3222
|
+
else:
|
|
3223
|
+
self.save_session(session=session)
|
|
3224
|
+
|
|
3225
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
3226
|
+
|
|
3227
|
+
return workflow_run_response
|
|
3228
|
+
else:
|
|
3229
|
+
# Workflow was executed by the tool
|
|
3230
|
+
logger.info("=" * 80)
|
|
3231
|
+
logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
|
|
3232
|
+
logger.info(" ➜ Workflow was executed, retrieving results...")
|
|
3233
|
+
logger.info("=" * 80)
|
|
3234
|
+
|
|
3235
|
+
log_debug("Reloading session from database to get the latest workflow run...")
|
|
3236
|
+
if self._has_async_db():
|
|
3237
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3238
|
+
else:
|
|
3239
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
3240
|
+
|
|
3241
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3242
|
+
# Get the last run (which is the one just created by the tool)
|
|
3243
|
+
last_run = reloaded_session.runs[-1]
|
|
3244
|
+
log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
|
|
3245
|
+
log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
|
|
3246
|
+
|
|
3247
|
+
# Update the last run with workflow_agent_run
|
|
3248
|
+
last_run.workflow_agent_run = agent_response
|
|
3249
|
+
|
|
3250
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3251
|
+
if agent_response:
|
|
3252
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3253
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
3254
|
+
|
|
3255
|
+
# Save the reloaded session (which has the updated run)
|
|
3256
|
+
if self._has_async_db():
|
|
3257
|
+
await self.asave_session(session=reloaded_session)
|
|
3258
|
+
else:
|
|
3259
|
+
self.save_session(session=reloaded_session)
|
|
3260
|
+
|
|
3261
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
3262
|
+
|
|
3263
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
3264
|
+
return last_run
|
|
3265
|
+
else:
|
|
3266
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3267
|
+
# Return a placeholder error response
|
|
3268
|
+
return WorkflowRunOutput(
|
|
3269
|
+
run_id=str(uuid4()),
|
|
3270
|
+
input=execution_input.input,
|
|
3271
|
+
session_id=session.session_id,
|
|
3272
|
+
workflow_id=self.id,
|
|
3273
|
+
workflow_name=self.name,
|
|
3274
|
+
created_at=int(datetime.now().timestamp()),
|
|
3275
|
+
content="Error: Workflow execution failed",
|
|
3276
|
+
status=RunStatus.error,
|
|
3277
|
+
)
|
|
3278
|
+
|
|
2442
3279
|
def cancel_run(self, run_id: str) -> bool:
|
|
2443
3280
|
"""Cancel a running workflow execution.
|
|
2444
3281
|
|
|
@@ -2547,16 +3384,6 @@ class Workflow:
|
|
|
2547
3384
|
# Prepare steps
|
|
2548
3385
|
self._prepare_steps()
|
|
2549
3386
|
|
|
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
3387
|
inputs = WorkflowExecutionInput(
|
|
2561
3388
|
input=input,
|
|
2562
3389
|
additional_data=additional_data,
|
|
@@ -2571,6 +3398,27 @@ class Workflow:
|
|
|
2571
3398
|
|
|
2572
3399
|
self.update_agents_and_teams_session_info()
|
|
2573
3400
|
|
|
3401
|
+
# Execute workflow agent if configured
|
|
3402
|
+
if self.agent is not None:
|
|
3403
|
+
return self._execute_workflow_agent(
|
|
3404
|
+
user_input=input, # type: ignore
|
|
3405
|
+
session=workflow_session,
|
|
3406
|
+
execution_input=inputs,
|
|
3407
|
+
session_state=session_state,
|
|
3408
|
+
stream=stream,
|
|
3409
|
+
**kwargs,
|
|
3410
|
+
)
|
|
3411
|
+
|
|
3412
|
+
# Create workflow run response for regular workflow execution
|
|
3413
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3414
|
+
run_id=run_id,
|
|
3415
|
+
input=input,
|
|
3416
|
+
session_id=session_id,
|
|
3417
|
+
workflow_id=self.id,
|
|
3418
|
+
workflow_name=self.name,
|
|
3419
|
+
created_at=int(datetime.now().timestamp()),
|
|
3420
|
+
)
|
|
3421
|
+
|
|
2574
3422
|
if stream:
|
|
2575
3423
|
return self._execute_stream(
|
|
2576
3424
|
session=workflow_session,
|
|
@@ -2717,16 +3565,6 @@ class Workflow:
|
|
|
2717
3565
|
# Prepare steps
|
|
2718
3566
|
self._prepare_steps()
|
|
2719
3567
|
|
|
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
3568
|
inputs = WorkflowExecutionInput(
|
|
2731
3569
|
input=input,
|
|
2732
3570
|
additional_data=additional_data,
|
|
@@ -2741,6 +3579,27 @@ class Workflow:
|
|
|
2741
3579
|
|
|
2742
3580
|
self.update_agents_and_teams_session_info()
|
|
2743
3581
|
|
|
3582
|
+
if self.agent is not None:
|
|
3583
|
+
return self._aexecute_workflow_agent( # type: ignore
|
|
3584
|
+
user_input=input, # type: ignore
|
|
3585
|
+
session_id=session_id,
|
|
3586
|
+
user_id=user_id,
|
|
3587
|
+
execution_input=inputs,
|
|
3588
|
+
session_state=session_state,
|
|
3589
|
+
stream=stream,
|
|
3590
|
+
**kwargs,
|
|
3591
|
+
)
|
|
3592
|
+
|
|
3593
|
+
# Create workflow run response for regular workflow execution
|
|
3594
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3595
|
+
run_id=run_id,
|
|
3596
|
+
input=input,
|
|
3597
|
+
session_id=session_id,
|
|
3598
|
+
workflow_id=self.id,
|
|
3599
|
+
workflow_name=self.name,
|
|
3600
|
+
created_at=int(datetime.now().timestamp()),
|
|
3601
|
+
)
|
|
3602
|
+
|
|
2744
3603
|
if stream:
|
|
2745
3604
|
return self._aexecute_stream( # type: ignore
|
|
2746
3605
|
execution_input=inputs,
|
|
@@ -2782,6 +3641,12 @@ class Workflow:
|
|
|
2782
3641
|
step_name = step.name or f"step_{i + 1}"
|
|
2783
3642
|
log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
|
|
2784
3643
|
prepared_steps.append(Step(name=step_name, description=step.description, team=step))
|
|
3644
|
+
elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
|
|
3645
|
+
log_warning(
|
|
3646
|
+
f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
|
|
3647
|
+
"but no database is configured in the Workflow. "
|
|
3648
|
+
"History won't be persisted. Add a database to persist runs across executions."
|
|
3649
|
+
)
|
|
2785
3650
|
elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
|
|
2786
3651
|
step_type = type(step).__name__
|
|
2787
3652
|
step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")
|