agno 2.1.2__py3-none-any.whl → 2.3.13__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 +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/workflow/workflow.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import warnings
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
from os import getenv
|
|
@@ -24,13 +25,13 @@ from fastapi import WebSocket
|
|
|
24
25
|
from pydantic import BaseModel
|
|
25
26
|
|
|
26
27
|
from agno.agent.agent import Agent
|
|
27
|
-
from agno.db.base import BaseDb, SessionType
|
|
28
|
+
from agno.db.base import AsyncBaseDb, BaseDb, SessionType
|
|
28
29
|
from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledException
|
|
29
30
|
from agno.media import Audio, File, Image, Video
|
|
30
31
|
from agno.models.message import Message
|
|
31
32
|
from agno.models.metrics import Metrics
|
|
32
|
-
from agno.run
|
|
33
|
-
from agno.run.
|
|
33
|
+
from agno.run import RunContext, RunStatus
|
|
34
|
+
from agno.run.agent import RunContentEvent, RunEvent, RunOutput
|
|
34
35
|
from agno.run.cancel import (
|
|
35
36
|
cancel_run as cancel_run_global,
|
|
36
37
|
)
|
|
@@ -39,6 +40,7 @@ from agno.run.cancel import (
|
|
|
39
40
|
raise_if_cancelled,
|
|
40
41
|
register_run,
|
|
41
42
|
)
|
|
43
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
42
44
|
from agno.run.team import TeamRunEvent
|
|
43
45
|
from agno.run.workflow import (
|
|
44
46
|
StepOutputEvent,
|
|
@@ -49,7 +51,7 @@ from agno.run.workflow import (
|
|
|
49
51
|
WorkflowRunOutputEvent,
|
|
50
52
|
WorkflowStartedEvent,
|
|
51
53
|
)
|
|
52
|
-
from agno.session.workflow import WorkflowSession
|
|
54
|
+
from agno.session.workflow import WorkflowChatInteraction, WorkflowSession
|
|
53
55
|
from agno.team.team import Team
|
|
54
56
|
from agno.utils.common import is_typed_dict, validate_typed_dict
|
|
55
57
|
from agno.utils.log import (
|
|
@@ -67,6 +69,7 @@ from agno.utils.print_response.workflow import (
|
|
|
67
69
|
print_response,
|
|
68
70
|
print_response_stream,
|
|
69
71
|
)
|
|
72
|
+
from agno.workflow import WorkflowAgent
|
|
70
73
|
from agno.workflow.condition import Condition
|
|
71
74
|
from agno.workflow.loop import Loop
|
|
72
75
|
from agno.workflow.parallel import Parallel
|
|
@@ -129,7 +132,10 @@ class Workflow:
|
|
|
129
132
|
steps: Optional[WorkflowSteps] = None
|
|
130
133
|
|
|
131
134
|
# Database to use for this workflow
|
|
132
|
-
db: Optional[BaseDb] = None
|
|
135
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None
|
|
136
|
+
|
|
137
|
+
# Agentic Workflow - WorkflowAgent that decides when to run the workflow
|
|
138
|
+
agent: Optional[WorkflowAgent] = None # type: ignore
|
|
133
139
|
|
|
134
140
|
# Default session_id to use for this workflow (autogenerated if not set)
|
|
135
141
|
session_id: Optional[str] = None
|
|
@@ -147,7 +153,9 @@ class Workflow:
|
|
|
147
153
|
# Stream the response from the Workflow
|
|
148
154
|
stream: Optional[bool] = None
|
|
149
155
|
# Stream the intermediate steps from the Workflow
|
|
150
|
-
|
|
156
|
+
stream_events: bool = False
|
|
157
|
+
# Stream events from executors (agents/teams/functions) within steps
|
|
158
|
+
stream_executor_events: bool = True
|
|
151
159
|
|
|
152
160
|
# Persist the events on the run response
|
|
153
161
|
store_events: bool = False
|
|
@@ -170,20 +178,35 @@ class Workflow:
|
|
|
170
178
|
# This helps us improve the Agent and provide better support
|
|
171
179
|
telemetry: bool = True
|
|
172
180
|
|
|
181
|
+
# Add this flag to control if the workflow should add history to the steps
|
|
182
|
+
add_workflow_history_to_steps: bool = False
|
|
183
|
+
# Number of historical runs to include in the messages
|
|
184
|
+
num_history_runs: int = 3
|
|
185
|
+
|
|
186
|
+
# Deprecated. Use stream_events instead.
|
|
187
|
+
stream_intermediate_steps: bool = False
|
|
188
|
+
|
|
189
|
+
# If True, run hooks as FastAPI background tasks (non-blocking). Set by AgentOS.
|
|
190
|
+
_run_hooks_in_background: bool = False
|
|
191
|
+
|
|
173
192
|
def __init__(
|
|
174
193
|
self,
|
|
175
194
|
id: Optional[str] = None,
|
|
176
195
|
name: Optional[str] = None,
|
|
177
196
|
description: Optional[str] = None,
|
|
178
|
-
db: Optional[BaseDb] = None,
|
|
197
|
+
db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
|
|
179
198
|
steps: Optional[WorkflowSteps] = None,
|
|
199
|
+
agent: Optional[WorkflowAgent] = None,
|
|
180
200
|
session_id: Optional[str] = None,
|
|
181
201
|
session_state: Optional[Dict[str, Any]] = None,
|
|
182
202
|
overwrite_db_session_state: bool = False,
|
|
183
203
|
user_id: Optional[str] = None,
|
|
204
|
+
debug_level: Literal[1, 2] = 1,
|
|
184
205
|
debug_mode: Optional[bool] = False,
|
|
185
206
|
stream: Optional[bool] = None,
|
|
207
|
+
stream_events: bool = False,
|
|
186
208
|
stream_intermediate_steps: bool = False,
|
|
209
|
+
stream_executor_events: bool = True,
|
|
187
210
|
store_events: bool = False,
|
|
188
211
|
events_to_skip: Optional[List[Union[WorkflowRunEvent, RunEvent, TeamRunEvent]]] = None,
|
|
189
212
|
store_executor_outputs: bool = True,
|
|
@@ -191,29 +214,49 @@ class Workflow:
|
|
|
191
214
|
metadata: Optional[Dict[str, Any]] = None,
|
|
192
215
|
cache_session: bool = False,
|
|
193
216
|
telemetry: bool = True,
|
|
217
|
+
add_workflow_history_to_steps: bool = False,
|
|
218
|
+
num_history_runs: int = 3,
|
|
194
219
|
):
|
|
195
220
|
self.id = id
|
|
196
221
|
self.name = name
|
|
197
222
|
self.description = description
|
|
198
223
|
self.steps = steps
|
|
224
|
+
self.agent = agent
|
|
199
225
|
self.session_id = session_id
|
|
200
226
|
self.session_state = session_state
|
|
201
227
|
self.overwrite_db_session_state = overwrite_db_session_state
|
|
202
228
|
self.user_id = user_id
|
|
203
229
|
self.debug_mode = debug_mode
|
|
230
|
+
self.debug_level = debug_level
|
|
204
231
|
self.store_events = store_events
|
|
205
232
|
self.events_to_skip = events_to_skip or []
|
|
206
233
|
self.stream = stream
|
|
207
|
-
self.
|
|
234
|
+
self.stream_executor_events = stream_executor_events
|
|
208
235
|
self.store_executor_outputs = store_executor_outputs
|
|
209
236
|
self.input_schema = input_schema
|
|
210
237
|
self.metadata = metadata
|
|
211
238
|
self.cache_session = cache_session
|
|
212
239
|
self.db = db
|
|
213
240
|
self.telemetry = telemetry
|
|
214
|
-
|
|
241
|
+
self.add_workflow_history_to_steps = add_workflow_history_to_steps
|
|
242
|
+
self.num_history_runs = num_history_runs
|
|
215
243
|
self._workflow_session: Optional[WorkflowSession] = None
|
|
216
244
|
|
|
245
|
+
if stream_intermediate_steps:
|
|
246
|
+
warnings.warn(
|
|
247
|
+
"The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
|
|
248
|
+
DeprecationWarning,
|
|
249
|
+
stacklevel=2,
|
|
250
|
+
)
|
|
251
|
+
self.stream_events = stream_events or stream_intermediate_steps
|
|
252
|
+
|
|
253
|
+
# Warn if workflow history is enabled without a database
|
|
254
|
+
if self.add_workflow_history_to_steps and self.db is None:
|
|
255
|
+
log_warning(
|
|
256
|
+
"Workflow history is enabled (add_workflow_history_to_steps=True) but no database is configured. "
|
|
257
|
+
"History won't be persisted. Add a database to persist runs across executions. "
|
|
258
|
+
)
|
|
259
|
+
|
|
217
260
|
def set_id(self) -> None:
|
|
218
261
|
if self.id is None:
|
|
219
262
|
if self.name is not None:
|
|
@@ -221,6 +264,9 @@ class Workflow:
|
|
|
221
264
|
else:
|
|
222
265
|
self.id = str(uuid4())
|
|
223
266
|
|
|
267
|
+
def _has_async_db(self) -> bool:
|
|
268
|
+
return self.db is not None and isinstance(self.db, AsyncBaseDb)
|
|
269
|
+
|
|
224
270
|
def _validate_input(
|
|
225
271
|
self, input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]]
|
|
226
272
|
) -> Optional[Union[str, List, Dict, Message, BaseModel]]:
|
|
@@ -329,10 +375,8 @@ class Workflow:
|
|
|
329
375
|
self,
|
|
330
376
|
session_id: Optional[str] = None,
|
|
331
377
|
user_id: Optional[str] = None,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
) -> Tuple[str, Optional[str], Dict[str, Any]]:
|
|
335
|
-
"""Initialize the session for the agent."""
|
|
378
|
+
) -> Tuple[str, Optional[str]]:
|
|
379
|
+
"""Initialize the session for the workflow."""
|
|
336
380
|
|
|
337
381
|
if session_id is None:
|
|
338
382
|
if self.session_id:
|
|
@@ -345,27 +389,25 @@ class Workflow:
|
|
|
345
389
|
log_debug(f"Session ID: {session_id}", center=True)
|
|
346
390
|
|
|
347
391
|
# Use the default user_id when necessary
|
|
348
|
-
if user_id is None:
|
|
392
|
+
if user_id is None or user_id == "":
|
|
349
393
|
user_id = self.user_id
|
|
350
394
|
|
|
351
|
-
|
|
352
|
-
if session_state is None:
|
|
353
|
-
session_state = self.session_state or {}
|
|
354
|
-
else:
|
|
355
|
-
# If run session_state is provided, merge agent defaults under it
|
|
356
|
-
# This ensures run state takes precedence over agent defaults
|
|
357
|
-
if self.session_state:
|
|
358
|
-
from agno.utils.merge_dict import merge_dictionaries
|
|
359
|
-
|
|
360
|
-
base_state = self.session_state.copy()
|
|
361
|
-
merge_dictionaries(base_state, session_state)
|
|
362
|
-
session_state.clear()
|
|
363
|
-
session_state.update(base_state)
|
|
395
|
+
return session_id, user_id
|
|
364
396
|
|
|
365
|
-
|
|
397
|
+
def _initialize_session_state(
|
|
398
|
+
self,
|
|
399
|
+
session_state: Dict[str, Any],
|
|
400
|
+
user_id: Optional[str] = None,
|
|
401
|
+
session_id: Optional[str] = None,
|
|
402
|
+
run_id: Optional[str] = None,
|
|
403
|
+
) -> Dict[str, Any]:
|
|
404
|
+
"""Initialize the session state for the workflow."""
|
|
405
|
+
if user_id:
|
|
366
406
|
session_state["current_user_id"] = user_id
|
|
367
407
|
if session_id is not None:
|
|
368
408
|
session_state["current_session_id"] = session_id
|
|
409
|
+
if run_id is not None:
|
|
410
|
+
session_state["current_run_id"] = run_id
|
|
369
411
|
|
|
370
412
|
session_state.update(
|
|
371
413
|
{
|
|
@@ -377,7 +419,7 @@ class Workflow:
|
|
|
377
419
|
if self.name:
|
|
378
420
|
session_state["workflow_name"] = self.name
|
|
379
421
|
|
|
380
|
-
return
|
|
422
|
+
return session_state
|
|
381
423
|
|
|
382
424
|
def _generate_workflow_session_name(self) -> str:
|
|
383
425
|
"""Generate a name for the workflow session"""
|
|
@@ -393,6 +435,33 @@ class Workflow:
|
|
|
393
435
|
new_session_name = f"{truncated_desc} - {datetime_str}"
|
|
394
436
|
return new_session_name
|
|
395
437
|
|
|
438
|
+
async def aset_session_name(
|
|
439
|
+
self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
|
|
440
|
+
) -> WorkflowSession:
|
|
441
|
+
"""Set the session name and save to storage, using an async database"""
|
|
442
|
+
session_id = session_id or self.session_id
|
|
443
|
+
|
|
444
|
+
if session_id is None:
|
|
445
|
+
raise Exception("Session ID is not set")
|
|
446
|
+
|
|
447
|
+
# -*- Read from storage
|
|
448
|
+
session = await self.aget_session(session_id=session_id) # type: ignore
|
|
449
|
+
|
|
450
|
+
if autogenerate:
|
|
451
|
+
# -*- Generate name for session
|
|
452
|
+
session_name = self._generate_workflow_session_name()
|
|
453
|
+
log_debug(f"Generated Workflow Session Name: {session_name}")
|
|
454
|
+
elif session_name is None:
|
|
455
|
+
raise Exception("Session name is not set")
|
|
456
|
+
|
|
457
|
+
# -*- Rename session
|
|
458
|
+
session.session_data["session_name"] = session_name # type: ignore
|
|
459
|
+
|
|
460
|
+
# -*- Save to storage
|
|
461
|
+
await self.asave_session(session=session) # type: ignore
|
|
462
|
+
|
|
463
|
+
return session # type: ignore
|
|
464
|
+
|
|
396
465
|
def set_session_name(
|
|
397
466
|
self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
|
|
398
467
|
) -> WorkflowSession:
|
|
@@ -420,6 +489,16 @@ class Workflow:
|
|
|
420
489
|
|
|
421
490
|
return session # type: ignore
|
|
422
491
|
|
|
492
|
+
async def aget_session_name(self, session_id: Optional[str] = None) -> str:
|
|
493
|
+
"""Get the session name for the given session ID and user ID."""
|
|
494
|
+
session_id = session_id or self.session_id
|
|
495
|
+
if session_id is None:
|
|
496
|
+
raise Exception("Session ID is not set")
|
|
497
|
+
session = await self.aget_session(session_id=session_id) # type: ignore
|
|
498
|
+
if session is None:
|
|
499
|
+
raise Exception("Session not found")
|
|
500
|
+
return session.session_data.get("session_name", "") if session.session_data else ""
|
|
501
|
+
|
|
423
502
|
def get_session_name(self, session_id: Optional[str] = None) -> str:
|
|
424
503
|
"""Get the session name for the given session ID and user ID."""
|
|
425
504
|
session_id = session_id or self.session_id
|
|
@@ -430,6 +509,16 @@ class Workflow:
|
|
|
430
509
|
raise Exception("Session not found")
|
|
431
510
|
return session.session_data.get("session_name", "") if session.session_data else ""
|
|
432
511
|
|
|
512
|
+
async def aget_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
513
|
+
"""Get the session state for the given session ID and user ID."""
|
|
514
|
+
session_id = session_id or self.session_id
|
|
515
|
+
if session_id is None:
|
|
516
|
+
raise Exception("Session ID is not set")
|
|
517
|
+
session = await self.aget_session(session_id=session_id) # type: ignore
|
|
518
|
+
if session is None:
|
|
519
|
+
raise Exception("Session not found")
|
|
520
|
+
return session.session_data.get("session_state", {}) if session.session_data else {}
|
|
521
|
+
|
|
433
522
|
def get_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
434
523
|
"""Get the session state for the given session ID and user ID."""
|
|
435
524
|
session_id = session_id or self.session_id
|
|
@@ -440,6 +529,69 @@ class Workflow:
|
|
|
440
529
|
raise Exception("Session not found")
|
|
441
530
|
return session.session_data.get("session_state", {}) if session.session_data else {}
|
|
442
531
|
|
|
532
|
+
def update_session_state(
|
|
533
|
+
self, session_state_updates: Dict[str, Any], session_id: Optional[str] = None
|
|
534
|
+
) -> Dict[str, Any]:
|
|
535
|
+
"""
|
|
536
|
+
Update the session state for the given session ID.
|
|
537
|
+
Args:
|
|
538
|
+
session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
|
|
539
|
+
session_id: The session ID to update. If not provided, the current cached session ID is used.
|
|
540
|
+
Returns:
|
|
541
|
+
dict: The updated session state.
|
|
542
|
+
"""
|
|
543
|
+
session_id = session_id or self.session_id
|
|
544
|
+
if session_id is None:
|
|
545
|
+
raise Exception("Session ID is not set")
|
|
546
|
+
session = self.get_session(session_id=session_id) # type: ignore
|
|
547
|
+
if session is None:
|
|
548
|
+
raise Exception("Session not found")
|
|
549
|
+
|
|
550
|
+
if session.session_data is not None and "session_state" not in session.session_data:
|
|
551
|
+
session.session_data["session_state"] = {}
|
|
552
|
+
|
|
553
|
+
for key, value in session_state_updates.items():
|
|
554
|
+
session.session_data["session_state"][key] = value # type: ignore
|
|
555
|
+
|
|
556
|
+
self.save_session(session=session)
|
|
557
|
+
|
|
558
|
+
return session.session_data["session_state"] # type: ignore
|
|
559
|
+
|
|
560
|
+
async def aupdate_session_state(
|
|
561
|
+
self, session_state_updates: Dict[str, Any], session_id: Optional[str] = None
|
|
562
|
+
) -> Dict[str, Any]:
|
|
563
|
+
"""
|
|
564
|
+
Update the session state for the given session ID (async).
|
|
565
|
+
Args:
|
|
566
|
+
session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
|
|
567
|
+
session_id: The session ID to update. If not provided, the current cached session ID is used.
|
|
568
|
+
Returns:
|
|
569
|
+
dict: The updated session state.
|
|
570
|
+
"""
|
|
571
|
+
session_id = session_id or self.session_id
|
|
572
|
+
if session_id is None:
|
|
573
|
+
raise Exception("Session ID is not set")
|
|
574
|
+
session = await self.aget_session(session_id=session_id) # type: ignore
|
|
575
|
+
if session is None:
|
|
576
|
+
raise Exception("Session not found")
|
|
577
|
+
|
|
578
|
+
if session.session_data is not None and "session_state" not in session.session_data:
|
|
579
|
+
session.session_data["session_state"] = {} # type: ignore
|
|
580
|
+
|
|
581
|
+
for key, value in session_state_updates.items():
|
|
582
|
+
session.session_data["session_state"][key] = value # type: ignore
|
|
583
|
+
|
|
584
|
+
await self.asave_session(session=session)
|
|
585
|
+
|
|
586
|
+
return session.session_data["session_state"] # type: ignore
|
|
587
|
+
|
|
588
|
+
async def adelete_session(self, session_id: str):
|
|
589
|
+
"""Delete the current session and save to storage"""
|
|
590
|
+
if self.db is None:
|
|
591
|
+
return
|
|
592
|
+
# -*- Delete session
|
|
593
|
+
await self.db.delete_session(session_id=session_id) # type: ignore
|
|
594
|
+
|
|
443
595
|
def delete_session(self, session_id: str):
|
|
444
596
|
"""Delete the current session and save to storage"""
|
|
445
597
|
if self.db is None:
|
|
@@ -447,6 +599,25 @@ class Workflow:
|
|
|
447
599
|
# -*- Delete session
|
|
448
600
|
self.db.delete_session(session_id=session_id)
|
|
449
601
|
|
|
602
|
+
async def aget_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
603
|
+
"""Get a RunOutput from the database."""
|
|
604
|
+
if self._workflow_session is not None:
|
|
605
|
+
run_response = self._workflow_session.get_run(run_id=run_id)
|
|
606
|
+
if run_response is not None:
|
|
607
|
+
return run_response
|
|
608
|
+
else:
|
|
609
|
+
log_warning(f"RunOutput {run_id} not found in AgentSession {self._workflow_session.session_id}")
|
|
610
|
+
return None
|
|
611
|
+
else:
|
|
612
|
+
workflow_session = await self.aget_session(session_id=session_id) # type: ignore
|
|
613
|
+
if workflow_session is not None:
|
|
614
|
+
run_response = workflow_session.get_run(run_id=run_id)
|
|
615
|
+
if run_response is not None:
|
|
616
|
+
return run_response
|
|
617
|
+
else:
|
|
618
|
+
log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
|
|
619
|
+
return None
|
|
620
|
+
|
|
450
621
|
def get_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
451
622
|
"""Get a RunOutput from the database."""
|
|
452
623
|
if self._workflow_session is not None:
|
|
@@ -466,6 +637,26 @@ class Workflow:
|
|
|
466
637
|
log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
|
|
467
638
|
return None
|
|
468
639
|
|
|
640
|
+
async def aget_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
641
|
+
"""Get the last run response from the database."""
|
|
642
|
+
if (
|
|
643
|
+
self._workflow_session is not None
|
|
644
|
+
and self._workflow_session.runs is not None
|
|
645
|
+
and len(self._workflow_session.runs) > 0
|
|
646
|
+
):
|
|
647
|
+
run_response = self._workflow_session.runs[-1]
|
|
648
|
+
if run_response is not None:
|
|
649
|
+
return run_response
|
|
650
|
+
else:
|
|
651
|
+
workflow_session = await self.aget_session(session_id=session_id) # type: ignore
|
|
652
|
+
if workflow_session is not None and workflow_session.runs is not None and len(workflow_session.runs) > 0:
|
|
653
|
+
run_response = workflow_session.runs[-1]
|
|
654
|
+
if run_response is not None:
|
|
655
|
+
return run_response
|
|
656
|
+
else:
|
|
657
|
+
log_warning(f"No run responses found in WorkflowSession {session_id}")
|
|
658
|
+
return None
|
|
659
|
+
|
|
469
660
|
def get_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
470
661
|
"""Get the last run response from the database."""
|
|
471
662
|
if (
|
|
@@ -504,6 +695,48 @@ class Workflow:
|
|
|
504
695
|
|
|
505
696
|
workflow_session = cast(WorkflowSession, self._read_session(session_id=session_id))
|
|
506
697
|
|
|
698
|
+
if workflow_session is None:
|
|
699
|
+
# Creating new session if none found
|
|
700
|
+
log_debug(f"Creating new WorkflowSession: {session_id}")
|
|
701
|
+
session_data = {}
|
|
702
|
+
if self.session_state is not None:
|
|
703
|
+
from copy import deepcopy
|
|
704
|
+
|
|
705
|
+
session_data["session_state"] = deepcopy(self.session_state)
|
|
706
|
+
workflow_session = WorkflowSession(
|
|
707
|
+
session_id=session_id,
|
|
708
|
+
workflow_id=self.id,
|
|
709
|
+
user_id=user_id,
|
|
710
|
+
workflow_data=self._get_workflow_data(),
|
|
711
|
+
session_data=session_data,
|
|
712
|
+
metadata=self.metadata,
|
|
713
|
+
created_at=int(time()),
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
# Cache the session if relevant
|
|
717
|
+
if workflow_session is not None and self.cache_session:
|
|
718
|
+
self._workflow_session = workflow_session
|
|
719
|
+
|
|
720
|
+
return workflow_session
|
|
721
|
+
|
|
722
|
+
async def aread_or_create_session(
|
|
723
|
+
self,
|
|
724
|
+
session_id: str,
|
|
725
|
+
user_id: Optional[str] = None,
|
|
726
|
+
) -> WorkflowSession:
|
|
727
|
+
from time import time
|
|
728
|
+
|
|
729
|
+
# Returning cached session if we have one
|
|
730
|
+
if self._workflow_session is not None and self._workflow_session.session_id == session_id:
|
|
731
|
+
return self._workflow_session
|
|
732
|
+
|
|
733
|
+
# Try to load from database
|
|
734
|
+
workflow_session = None
|
|
735
|
+
if self.db is not None:
|
|
736
|
+
log_debug(f"Reading WorkflowSession: {session_id}")
|
|
737
|
+
|
|
738
|
+
workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id))
|
|
739
|
+
|
|
507
740
|
if workflow_session is None:
|
|
508
741
|
# Creating new session if none found
|
|
509
742
|
log_debug(f"Creating new WorkflowSession: {session_id}")
|
|
@@ -523,6 +756,30 @@ class Workflow:
|
|
|
523
756
|
|
|
524
757
|
return workflow_session
|
|
525
758
|
|
|
759
|
+
async def aget_session(
|
|
760
|
+
self,
|
|
761
|
+
session_id: Optional[str] = None,
|
|
762
|
+
) -> Optional[WorkflowSession]:
|
|
763
|
+
"""Load an WorkflowSession from database.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
session_id: The session_id to load from storage.
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
WorkflowSession: The WorkflowSession loaded from the database or created if it does not exist.
|
|
770
|
+
"""
|
|
771
|
+
session_id_to_load = session_id or self.session_id
|
|
772
|
+
if session_id_to_load is None:
|
|
773
|
+
raise Exception("No session_id provided")
|
|
774
|
+
|
|
775
|
+
# Try to load from database
|
|
776
|
+
if self.db is not None:
|
|
777
|
+
workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id_to_load))
|
|
778
|
+
return workflow_session
|
|
779
|
+
|
|
780
|
+
log_warning(f"WorkflowSession {session_id_to_load} not found in db")
|
|
781
|
+
return None
|
|
782
|
+
|
|
526
783
|
def get_session(
|
|
527
784
|
self,
|
|
528
785
|
session_id: Optional[str] = None,
|
|
@@ -548,6 +805,25 @@ class Workflow:
|
|
|
548
805
|
log_warning(f"WorkflowSession {session_id_to_load} not found in db")
|
|
549
806
|
return None
|
|
550
807
|
|
|
808
|
+
async def asave_session(self, session: WorkflowSession) -> None:
|
|
809
|
+
"""Save the WorkflowSession to storage, using an async database.
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
Optional[WorkflowSession]: The saved WorkflowSession or None if not saved.
|
|
813
|
+
"""
|
|
814
|
+
if self.db is not None and session.session_data is not None:
|
|
815
|
+
if session.session_data.get("session_state") is not None:
|
|
816
|
+
session.session_data["session_state"].pop("current_session_id", None)
|
|
817
|
+
session.session_data["session_state"].pop("current_user_id", None)
|
|
818
|
+
session.session_data["session_state"].pop("current_run_id", None)
|
|
819
|
+
session.session_data["session_state"].pop("workflow_id", None)
|
|
820
|
+
session.session_data["session_state"].pop("run_id", None)
|
|
821
|
+
session.session_data["session_state"].pop("session_id", None)
|
|
822
|
+
session.session_data["session_state"].pop("workflow_name", None)
|
|
823
|
+
|
|
824
|
+
await self._aupsert_session(session=session) # type: ignore
|
|
825
|
+
log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
|
|
826
|
+
|
|
551
827
|
def save_session(self, session: WorkflowSession) -> None:
|
|
552
828
|
"""Save the WorkflowSession to storage
|
|
553
829
|
|
|
@@ -567,7 +843,66 @@ class Workflow:
|
|
|
567
843
|
self._upsert_session(session=session)
|
|
568
844
|
log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
|
|
569
845
|
|
|
846
|
+
def get_chat_history(
|
|
847
|
+
self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
|
|
848
|
+
) -> List[WorkflowChatInteraction]:
|
|
849
|
+
"""Return a list of dictionaries containing the input and output for each run in the session.
|
|
850
|
+
|
|
851
|
+
Args:
|
|
852
|
+
session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
|
|
853
|
+
last_n_runs: Number of recent runs to include. If None, all runs will be considered.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
A list of WorkflowChatInteraction objects.
|
|
857
|
+
"""
|
|
858
|
+
session_id = session_id or self.session_id
|
|
859
|
+
if session_id is None:
|
|
860
|
+
log_warning("Session ID is not set, cannot get messages for session")
|
|
861
|
+
return []
|
|
862
|
+
|
|
863
|
+
session = self.get_session(
|
|
864
|
+
session_id=session_id,
|
|
865
|
+
)
|
|
866
|
+
if session is None:
|
|
867
|
+
raise Exception("Session not found")
|
|
868
|
+
|
|
869
|
+
return session.get_chat_history(last_n_runs=last_n_runs)
|
|
870
|
+
|
|
871
|
+
async def aget_chat_history(
|
|
872
|
+
self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
|
|
873
|
+
) -> List[WorkflowChatInteraction]:
|
|
874
|
+
"""Return a list of dictionaries containing the input and output for each run in the session.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
|
|
878
|
+
last_n_runs: Number of recent runs to include. If None, all runs will be considered.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
A list of dictionaries containing the input and output for each run.
|
|
882
|
+
"""
|
|
883
|
+
session_id = session_id or self.session_id
|
|
884
|
+
if session_id is None:
|
|
885
|
+
log_warning("Session ID is not set, cannot get messages for session")
|
|
886
|
+
return []
|
|
887
|
+
|
|
888
|
+
session = await self.aget_session(session_id=session_id)
|
|
889
|
+
if session is None:
|
|
890
|
+
raise Exception("Session not found")
|
|
891
|
+
|
|
892
|
+
return session.get_chat_history(last_n_runs=last_n_runs)
|
|
893
|
+
|
|
570
894
|
# -*- Session Database Functions
|
|
895
|
+
async def _aread_session(self, session_id: str) -> Optional[WorkflowSession]:
|
|
896
|
+
"""Get a Session from the database."""
|
|
897
|
+
try:
|
|
898
|
+
if not self.db:
|
|
899
|
+
raise ValueError("Db not initialized")
|
|
900
|
+
session = await self.db.get_session(session_id=session_id, session_type=SessionType.WORKFLOW) # type: ignore
|
|
901
|
+
return session if isinstance(session, (WorkflowSession, type(None))) else None
|
|
902
|
+
except Exception as e:
|
|
903
|
+
log_warning(f"Error getting session from db: {e}")
|
|
904
|
+
return None
|
|
905
|
+
|
|
571
906
|
def _read_session(self, session_id: str) -> Optional[WorkflowSession]:
|
|
572
907
|
"""Get a Session from the database."""
|
|
573
908
|
try:
|
|
@@ -579,9 +914,19 @@ class Workflow:
|
|
|
579
914
|
log_warning(f"Error getting session from db: {e}")
|
|
580
915
|
return None
|
|
581
916
|
|
|
582
|
-
def
|
|
917
|
+
async def _aupsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
|
|
583
918
|
"""Upsert a Session into the database."""
|
|
919
|
+
try:
|
|
920
|
+
if not self.db:
|
|
921
|
+
raise ValueError("Db not initialized")
|
|
922
|
+
result = await self.db.upsert_session(session=session) # type: ignore
|
|
923
|
+
return result if isinstance(result, (WorkflowSession, type(None))) else None
|
|
924
|
+
except Exception as e:
|
|
925
|
+
log_warning(f"Error upserting session into db: {e}")
|
|
926
|
+
return None
|
|
584
927
|
|
|
928
|
+
def _upsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
|
|
929
|
+
"""Upsert a Session into the database."""
|
|
585
930
|
try:
|
|
586
931
|
if not self.db:
|
|
587
932
|
raise ValueError("Db not initialized")
|
|
@@ -649,7 +994,7 @@ class Workflow:
|
|
|
649
994
|
else:
|
|
650
995
|
step_type = STEP_TYPE_MAPPING[type(step)]
|
|
651
996
|
step_dict = {
|
|
652
|
-
"name": step.name if hasattr(step, "name") else step.__name__,
|
|
997
|
+
"name": step.name if hasattr(step, "name") else step.__name__, # type: ignore
|
|
653
998
|
"description": step.description if hasattr(step, "description") else "User-defined callable step",
|
|
654
999
|
"type": step_type.value,
|
|
655
1000
|
}
|
|
@@ -668,16 +1013,36 @@ class Workflow:
|
|
|
668
1013
|
|
|
669
1014
|
return workflow_data
|
|
670
1015
|
|
|
671
|
-
def
|
|
1016
|
+
def _broadcast_to_websocket(
|
|
672
1017
|
self,
|
|
673
|
-
event:
|
|
674
|
-
workflow_run_response: WorkflowRunOutput,
|
|
1018
|
+
event: Any,
|
|
675
1019
|
websocket_handler: Optional[WebSocketHandler] = None,
|
|
676
|
-
) ->
|
|
677
|
-
"""
|
|
678
|
-
if
|
|
679
|
-
|
|
680
|
-
|
|
1020
|
+
) -> None:
|
|
1021
|
+
"""Broadcast events to WebSocket if available (async context only)"""
|
|
1022
|
+
if websocket_handler:
|
|
1023
|
+
try:
|
|
1024
|
+
loop = asyncio.get_running_loop()
|
|
1025
|
+
if loop:
|
|
1026
|
+
asyncio.create_task(websocket_handler.handle_event(event))
|
|
1027
|
+
except RuntimeError:
|
|
1028
|
+
pass
|
|
1029
|
+
|
|
1030
|
+
def _handle_event(
|
|
1031
|
+
self,
|
|
1032
|
+
event: "WorkflowRunOutputEvent",
|
|
1033
|
+
workflow_run_response: WorkflowRunOutput,
|
|
1034
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
1035
|
+
) -> "WorkflowRunOutputEvent":
|
|
1036
|
+
"""Handle workflow events for storage - similar to Team._handle_event"""
|
|
1037
|
+
from agno.run.agent import RunOutput
|
|
1038
|
+
from agno.run.base import BaseRunOutputEvent
|
|
1039
|
+
from agno.run.team import TeamRunOutput
|
|
1040
|
+
|
|
1041
|
+
if isinstance(event, (RunOutput, TeamRunOutput)):
|
|
1042
|
+
return event
|
|
1043
|
+
if self.store_events:
|
|
1044
|
+
# Check if this event type should be skipped
|
|
1045
|
+
if self.events_to_skip:
|
|
681
1046
|
event_type = event.event
|
|
682
1047
|
for skip_event in self.events_to_skip:
|
|
683
1048
|
if isinstance(skip_event, str):
|
|
@@ -689,21 +1054,41 @@ class Workflow:
|
|
|
689
1054
|
return event
|
|
690
1055
|
|
|
691
1056
|
# Store the event
|
|
692
|
-
if
|
|
693
|
-
workflow_run_response.events
|
|
694
|
-
|
|
695
|
-
|
|
1057
|
+
if isinstance(event, BaseRunOutputEvent):
|
|
1058
|
+
if workflow_run_response.events is None:
|
|
1059
|
+
workflow_run_response.events = []
|
|
1060
|
+
workflow_run_response.events.append(event)
|
|
696
1061
|
|
|
697
1062
|
# Broadcast to WebSocket if available (async context only)
|
|
698
|
-
|
|
699
|
-
import asyncio
|
|
1063
|
+
self._broadcast_to_websocket(event, websocket_handler)
|
|
700
1064
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1065
|
+
return event
|
|
1066
|
+
|
|
1067
|
+
def _enrich_event_with_workflow_context(
|
|
1068
|
+
self,
|
|
1069
|
+
event: Any,
|
|
1070
|
+
workflow_run_response: WorkflowRunOutput,
|
|
1071
|
+
step_index: Optional[Union[int, tuple]] = None,
|
|
1072
|
+
step: Optional[Any] = None,
|
|
1073
|
+
) -> Any:
|
|
1074
|
+
"""Enrich any event with workflow context information for frontend tracking"""
|
|
1075
|
+
|
|
1076
|
+
step_id = getattr(step, "step_id", None) if step else None
|
|
1077
|
+
step_name = getattr(step, "name", None) if step else None
|
|
1078
|
+
|
|
1079
|
+
if hasattr(event, "workflow_id"):
|
|
1080
|
+
event.workflow_id = workflow_run_response.workflow_id
|
|
1081
|
+
if hasattr(event, "workflow_run_id"):
|
|
1082
|
+
event.workflow_run_id = workflow_run_response.run_id
|
|
1083
|
+
if hasattr(event, "step_id") and step_id:
|
|
1084
|
+
event.step_id = step_id
|
|
1085
|
+
if hasattr(event, "step_name") and step_name is not None:
|
|
1086
|
+
if event.step_name is None:
|
|
1087
|
+
event.step_name = step_name
|
|
1088
|
+
# Only set step_index if it's not already set (preserve parallel.py's tuples)
|
|
1089
|
+
if hasattr(event, "step_index") and step_index is not None:
|
|
1090
|
+
if event.step_index is None:
|
|
1091
|
+
event.step_index = step_index
|
|
707
1092
|
|
|
708
1093
|
return event
|
|
709
1094
|
|
|
@@ -725,9 +1110,12 @@ class Workflow:
|
|
|
725
1110
|
"""Set debug mode and configure logging"""
|
|
726
1111
|
if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
|
|
727
1112
|
use_workflow_logger()
|
|
1113
|
+
debug_level: Literal[1, 2] = (
|
|
1114
|
+
cast(Literal[1, 2], int(env)) if (env := getenv("AGNO_DEBUG_LEVEL")) in ("1", "2") else self.debug_level
|
|
1115
|
+
)
|
|
728
1116
|
|
|
729
1117
|
self.debug_mode = True
|
|
730
|
-
set_log_level_to_debug(source_type="workflow")
|
|
1118
|
+
set_log_level_to_debug(source_type="workflow", level=debug_level)
|
|
731
1119
|
|
|
732
1120
|
# Propagate to steps - only if steps is iterable (not callable)
|
|
733
1121
|
if self.steps and not callable(self.steps):
|
|
@@ -811,7 +1199,11 @@ class Workflow:
|
|
|
811
1199
|
else:
|
|
812
1200
|
return len(self.steps)
|
|
813
1201
|
|
|
814
|
-
def _aggregate_workflow_metrics(
|
|
1202
|
+
def _aggregate_workflow_metrics(
|
|
1203
|
+
self,
|
|
1204
|
+
step_results: List[Union[StepOutput, List[StepOutput]]],
|
|
1205
|
+
current_workflow_metrics: Optional[WorkflowMetrics] = None,
|
|
1206
|
+
) -> WorkflowMetrics:
|
|
815
1207
|
"""Aggregate metrics from all step responses into structured workflow metrics"""
|
|
816
1208
|
steps_dict = {}
|
|
817
1209
|
|
|
@@ -839,8 +1231,13 @@ class Workflow:
|
|
|
839
1231
|
for step_result in step_results:
|
|
840
1232
|
process_step_output(cast(StepOutput, step_result))
|
|
841
1233
|
|
|
1234
|
+
duration = None
|
|
1235
|
+
if current_workflow_metrics and current_workflow_metrics.duration is not None:
|
|
1236
|
+
duration = current_workflow_metrics.duration
|
|
1237
|
+
|
|
842
1238
|
return WorkflowMetrics(
|
|
843
1239
|
steps=steps_dict,
|
|
1240
|
+
duration=duration,
|
|
844
1241
|
)
|
|
845
1242
|
|
|
846
1243
|
def _call_custom_function(self, func: Callable, execution_input: WorkflowExecutionInput, **kwargs: Any) -> Any:
|
|
@@ -875,24 +1272,31 @@ class Workflow:
|
|
|
875
1272
|
return func(**call_kwargs)
|
|
876
1273
|
except TypeError as e:
|
|
877
1274
|
# If signature inspection fails, fall back to original method
|
|
878
|
-
logger.error(
|
|
879
|
-
f"Function signature inspection failed: {e}. Falling back to original calling convention."
|
|
880
|
-
)
|
|
1275
|
+
logger.error(f"Function signature inspection failed: {e}. Falling back to original calling convention.")
|
|
881
1276
|
return func(**kwargs)
|
|
882
1277
|
|
|
1278
|
+
def _accumulate_partial_step_data(
|
|
1279
|
+
self, event: Union[RunContentEvent, TeamRunContentEvent], partial_step_content: str
|
|
1280
|
+
) -> str:
|
|
1281
|
+
"""Accumulate partial step data from streaming events"""
|
|
1282
|
+
if isinstance(event, (RunContentEvent, TeamRunContentEvent)) and event.content:
|
|
1283
|
+
if isinstance(event.content, str):
|
|
1284
|
+
partial_step_content += event.content
|
|
1285
|
+
return partial_step_content
|
|
1286
|
+
|
|
883
1287
|
def _execute(
|
|
884
1288
|
self,
|
|
885
1289
|
session: WorkflowSession,
|
|
886
1290
|
execution_input: WorkflowExecutionInput,
|
|
887
1291
|
workflow_run_response: WorkflowRunOutput,
|
|
888
|
-
|
|
1292
|
+
run_context: RunContext,
|
|
1293
|
+
background_tasks: Optional[Any] = None,
|
|
889
1294
|
**kwargs: Any,
|
|
890
1295
|
) -> WorkflowRunOutput:
|
|
891
1296
|
"""Execute a specific pipeline by name synchronously"""
|
|
892
1297
|
from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
|
|
893
1298
|
|
|
894
1299
|
workflow_run_response.status = RunStatus.running
|
|
895
|
-
register_run(workflow_run_response.run_id) # type: ignore
|
|
896
1300
|
|
|
897
1301
|
if callable(self.steps):
|
|
898
1302
|
if iscoroutinefunction(self.steps) or isasyncgenfunction(self.steps):
|
|
@@ -951,8 +1355,14 @@ class Workflow:
|
|
|
951
1355
|
session_id=session.session_id,
|
|
952
1356
|
user_id=self.user_id,
|
|
953
1357
|
workflow_run_response=workflow_run_response,
|
|
954
|
-
|
|
1358
|
+
run_context=run_context,
|
|
955
1359
|
store_executor_outputs=self.store_executor_outputs,
|
|
1360
|
+
workflow_session=session,
|
|
1361
|
+
add_workflow_history_to_steps=self.add_workflow_history_to_steps
|
|
1362
|
+
if self.add_workflow_history_to_steps
|
|
1363
|
+
else None,
|
|
1364
|
+
num_history_runs=self.num_history_runs,
|
|
1365
|
+
background_tasks=background_tasks,
|
|
956
1366
|
)
|
|
957
1367
|
|
|
958
1368
|
# Check for cancellation after step execution
|
|
@@ -978,7 +1388,14 @@ class Workflow:
|
|
|
978
1388
|
|
|
979
1389
|
# Update the workflow_run_response with completion data
|
|
980
1390
|
if collected_step_outputs:
|
|
981
|
-
|
|
1391
|
+
# Stop the timer for the Run duration
|
|
1392
|
+
if workflow_run_response.metrics:
|
|
1393
|
+
workflow_run_response.metrics.stop_timer()
|
|
1394
|
+
|
|
1395
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
1396
|
+
collected_step_outputs,
|
|
1397
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
1398
|
+
)
|
|
982
1399
|
last_output = cast(StepOutput, collected_step_outputs[-1])
|
|
983
1400
|
|
|
984
1401
|
# Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
|
|
@@ -1023,6 +1440,10 @@ class Workflow:
|
|
|
1023
1440
|
raise e
|
|
1024
1441
|
|
|
1025
1442
|
finally:
|
|
1443
|
+
# Stop timer on error
|
|
1444
|
+
if workflow_run_response.metrics:
|
|
1445
|
+
workflow_run_response.metrics.stop_timer()
|
|
1446
|
+
|
|
1026
1447
|
self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
|
|
1027
1448
|
session.upsert_run(run=workflow_run_response)
|
|
1028
1449
|
self.save_session(session=session)
|
|
@@ -1040,8 +1461,9 @@ class Workflow:
|
|
|
1040
1461
|
session: WorkflowSession,
|
|
1041
1462
|
execution_input: WorkflowExecutionInput,
|
|
1042
1463
|
workflow_run_response: WorkflowRunOutput,
|
|
1043
|
-
|
|
1044
|
-
|
|
1464
|
+
run_context: RunContext,
|
|
1465
|
+
stream_events: bool = False,
|
|
1466
|
+
background_tasks: Optional[Any] = None,
|
|
1045
1467
|
**kwargs: Any,
|
|
1046
1468
|
) -> Iterator[WorkflowRunOutputEvent]:
|
|
1047
1469
|
"""Execute a specific pipeline by name with event streaming"""
|
|
@@ -1049,10 +1471,6 @@ class Workflow:
|
|
|
1049
1471
|
|
|
1050
1472
|
workflow_run_response.status = RunStatus.running
|
|
1051
1473
|
|
|
1052
|
-
# Register run for cancellation tracking
|
|
1053
|
-
if workflow_run_response.run_id:
|
|
1054
|
-
register_run(workflow_run_response.run_id)
|
|
1055
|
-
|
|
1056
1474
|
workflow_started_event = WorkflowStartedEvent(
|
|
1057
1475
|
run_id=workflow_run_response.run_id or "",
|
|
1058
1476
|
workflow_name=workflow_run_response.workflow_name,
|
|
@@ -1097,11 +1515,22 @@ class Workflow:
|
|
|
1097
1515
|
|
|
1098
1516
|
early_termination = False
|
|
1099
1517
|
|
|
1518
|
+
# Track partial step data in case of cancellation
|
|
1519
|
+
current_step_name = ""
|
|
1520
|
+
current_step = None
|
|
1521
|
+
partial_step_content = ""
|
|
1522
|
+
|
|
1100
1523
|
for i, step in enumerate(self.steps): # type: ignore[arg-type]
|
|
1101
1524
|
raise_if_cancelled(workflow_run_response.run_id) # type: ignore
|
|
1102
1525
|
step_name = getattr(step, "name", f"step_{i + 1}")
|
|
1103
1526
|
log_debug(f"Streaming step {i + 1}/{self._get_step_count()}: {step_name}")
|
|
1104
1527
|
|
|
1528
|
+
# Track current step for cancellation handler
|
|
1529
|
+
current_step_name = step_name
|
|
1530
|
+
current_step = step
|
|
1531
|
+
# Reset partial data for this step
|
|
1532
|
+
partial_step_content = ""
|
|
1533
|
+
|
|
1105
1534
|
# Create enhanced StepInput
|
|
1106
1535
|
step_input = self._create_step_input(
|
|
1107
1536
|
execution_input=execution_input,
|
|
@@ -1117,13 +1546,24 @@ class Workflow:
|
|
|
1117
1546
|
step_input,
|
|
1118
1547
|
session_id=session.session_id,
|
|
1119
1548
|
user_id=self.user_id,
|
|
1120
|
-
|
|
1549
|
+
stream_events=stream_events,
|
|
1550
|
+
stream_executor_events=self.stream_executor_events,
|
|
1121
1551
|
workflow_run_response=workflow_run_response,
|
|
1122
|
-
|
|
1552
|
+
run_context=run_context,
|
|
1123
1553
|
step_index=i,
|
|
1124
1554
|
store_executor_outputs=self.store_executor_outputs,
|
|
1555
|
+
workflow_session=session,
|
|
1556
|
+
add_workflow_history_to_steps=self.add_workflow_history_to_steps
|
|
1557
|
+
if self.add_workflow_history_to_steps
|
|
1558
|
+
else None,
|
|
1559
|
+
num_history_runs=self.num_history_runs,
|
|
1560
|
+
background_tasks=background_tasks,
|
|
1125
1561
|
):
|
|
1126
1562
|
raise_if_cancelled(workflow_run_response.run_id) # type: ignore
|
|
1563
|
+
|
|
1564
|
+
# Accumulate partial data from streaming events
|
|
1565
|
+
partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
|
|
1566
|
+
|
|
1127
1567
|
# Handle events
|
|
1128
1568
|
if isinstance(event, StepOutput):
|
|
1129
1569
|
step_output = event
|
|
@@ -1172,11 +1612,19 @@ class Workflow:
|
|
|
1172
1612
|
yield step_output_event
|
|
1173
1613
|
|
|
1174
1614
|
elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
|
|
1175
|
-
|
|
1615
|
+
# Enrich event with workflow context before yielding
|
|
1616
|
+
enriched_event = self._enrich_event_with_workflow_context(
|
|
1617
|
+
event, workflow_run_response, step_index=i, step=step
|
|
1618
|
+
)
|
|
1619
|
+
yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
|
|
1176
1620
|
|
|
1177
1621
|
else:
|
|
1178
|
-
#
|
|
1179
|
-
|
|
1622
|
+
# Enrich other events with workflow context before yielding
|
|
1623
|
+
enriched_event = self._enrich_event_with_workflow_context(
|
|
1624
|
+
event, workflow_run_response, step_index=i, step=step
|
|
1625
|
+
)
|
|
1626
|
+
if self.stream_executor_events:
|
|
1627
|
+
yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
|
|
1180
1628
|
|
|
1181
1629
|
# Break out of main step loop if early termination was requested
|
|
1182
1630
|
if "early_termination" in locals() and early_termination:
|
|
@@ -1184,7 +1632,14 @@ class Workflow:
|
|
|
1184
1632
|
|
|
1185
1633
|
# Update the workflow_run_response with completion data
|
|
1186
1634
|
if collected_step_outputs:
|
|
1187
|
-
|
|
1635
|
+
# Stop the timer for the Run duration
|
|
1636
|
+
if workflow_run_response.metrics:
|
|
1637
|
+
workflow_run_response.metrics.stop_timer()
|
|
1638
|
+
|
|
1639
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
1640
|
+
collected_step_outputs,
|
|
1641
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
1642
|
+
)
|
|
1188
1643
|
last_output = cast(StepOutput, collected_step_outputs[-1])
|
|
1189
1644
|
|
|
1190
1645
|
# Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
|
|
@@ -1230,6 +1685,36 @@ class Workflow:
|
|
|
1230
1685
|
logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
|
|
1231
1686
|
workflow_run_response.status = RunStatus.cancelled
|
|
1232
1687
|
workflow_run_response.content = str(e)
|
|
1688
|
+
|
|
1689
|
+
# Capture partial progress from the step that was cancelled mid-stream
|
|
1690
|
+
if partial_step_content:
|
|
1691
|
+
logger.info(
|
|
1692
|
+
f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
|
|
1693
|
+
)
|
|
1694
|
+
partial_step_output = StepOutput(
|
|
1695
|
+
step_name=current_step_name,
|
|
1696
|
+
step_id=getattr(current_step, "step_id", None) if current_step else None,
|
|
1697
|
+
step_type=StepType.STEP,
|
|
1698
|
+
executor_type=getattr(current_step, "executor_type", None) if current_step else None,
|
|
1699
|
+
executor_name=getattr(current_step, "executor_name", None) if current_step else None,
|
|
1700
|
+
content=partial_step_content,
|
|
1701
|
+
success=False,
|
|
1702
|
+
error="Cancelled during execution",
|
|
1703
|
+
)
|
|
1704
|
+
collected_step_outputs.append(partial_step_output)
|
|
1705
|
+
|
|
1706
|
+
# Preserve all progress (completed steps + partial step) before cancellation
|
|
1707
|
+
if collected_step_outputs:
|
|
1708
|
+
workflow_run_response.step_results = collected_step_outputs
|
|
1709
|
+
# Stop the timer for the Run duration
|
|
1710
|
+
if workflow_run_response.metrics:
|
|
1711
|
+
workflow_run_response.metrics.stop_timer()
|
|
1712
|
+
|
|
1713
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
1714
|
+
collected_step_outputs,
|
|
1715
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1233
1718
|
cancelled_event = WorkflowCancelledEvent(
|
|
1234
1719
|
run_id=workflow_run_response.run_id or "",
|
|
1235
1720
|
workflow_id=self.id,
|
|
@@ -1270,6 +1755,10 @@ class Workflow:
|
|
|
1270
1755
|
)
|
|
1271
1756
|
yield self._handle_event(workflow_completed_event, workflow_run_response)
|
|
1272
1757
|
|
|
1758
|
+
# Stop timer on error
|
|
1759
|
+
if workflow_run_response.metrics:
|
|
1760
|
+
workflow_run_response.metrics.stop_timer()
|
|
1761
|
+
|
|
1273
1762
|
# Store the completed workflow response
|
|
1274
1763
|
self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
|
|
1275
1764
|
session.upsert_run(run=workflow_run_response)
|
|
@@ -1332,21 +1821,46 @@ class Workflow:
|
|
|
1332
1821
|
# For regular async functions, use the same signature inspection logic in fallback
|
|
1333
1822
|
return await func(**call_kwargs) # type: ignore
|
|
1334
1823
|
|
|
1824
|
+
async def _aload_or_create_session(
|
|
1825
|
+
self, session_id: str, user_id: Optional[str], session_state: Optional[Dict[str, Any]]
|
|
1826
|
+
) -> Tuple[WorkflowSession, Dict[str, Any]]:
|
|
1827
|
+
"""Load or create session from database, update metadata, and prepare session state.
|
|
1828
|
+
|
|
1829
|
+
Returns:
|
|
1830
|
+
Tuple of (workflow_session, prepared_session_state)
|
|
1831
|
+
"""
|
|
1832
|
+
# Read existing session from database
|
|
1833
|
+
if self._has_async_db():
|
|
1834
|
+
workflow_session = await self.aread_or_create_session(session_id=session_id, user_id=user_id)
|
|
1835
|
+
else:
|
|
1836
|
+
workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
|
|
1837
|
+
self._update_metadata(session=workflow_session)
|
|
1838
|
+
|
|
1839
|
+
# Update session state from DB
|
|
1840
|
+
_session_state = session_state if session_state is not None else {}
|
|
1841
|
+
_session_state = self._load_session_state(session=workflow_session, session_state=_session_state)
|
|
1842
|
+
|
|
1843
|
+
return workflow_session, _session_state
|
|
1844
|
+
|
|
1335
1845
|
async def _aexecute(
|
|
1336
1846
|
self,
|
|
1337
|
-
|
|
1847
|
+
session_id: str,
|
|
1848
|
+
user_id: Optional[str],
|
|
1338
1849
|
execution_input: WorkflowExecutionInput,
|
|
1339
1850
|
workflow_run_response: WorkflowRunOutput,
|
|
1340
|
-
|
|
1851
|
+
run_context: RunContext,
|
|
1852
|
+
background_tasks: Optional[Any] = None,
|
|
1341
1853
|
**kwargs: Any,
|
|
1342
1854
|
) -> WorkflowRunOutput:
|
|
1343
1855
|
"""Execute a specific pipeline by name asynchronously"""
|
|
1344
1856
|
from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
|
|
1345
1857
|
|
|
1346
|
-
|
|
1858
|
+
# Read existing session from database
|
|
1859
|
+
workflow_session, run_context.session_state = await self._aload_or_create_session(
|
|
1860
|
+
session_id=session_id, user_id=user_id, session_state=run_context.session_state
|
|
1861
|
+
)
|
|
1347
1862
|
|
|
1348
|
-
|
|
1349
|
-
register_run(workflow_run_response.run_id) # type: ignore
|
|
1863
|
+
workflow_run_response.status = RunStatus.running
|
|
1350
1864
|
|
|
1351
1865
|
if callable(self.steps):
|
|
1352
1866
|
# Execute the workflow with the custom executor
|
|
@@ -1410,11 +1924,17 @@ class Workflow:
|
|
|
1410
1924
|
|
|
1411
1925
|
step_output = await step.aexecute( # type: ignore[union-attr]
|
|
1412
1926
|
step_input,
|
|
1413
|
-
session_id=
|
|
1927
|
+
session_id=session_id,
|
|
1414
1928
|
user_id=self.user_id,
|
|
1415
1929
|
workflow_run_response=workflow_run_response,
|
|
1416
|
-
|
|
1930
|
+
run_context=run_context,
|
|
1417
1931
|
store_executor_outputs=self.store_executor_outputs,
|
|
1932
|
+
workflow_session=workflow_session,
|
|
1933
|
+
add_workflow_history_to_steps=self.add_workflow_history_to_steps
|
|
1934
|
+
if self.add_workflow_history_to_steps
|
|
1935
|
+
else None,
|
|
1936
|
+
num_history_runs=self.num_history_runs,
|
|
1937
|
+
background_tasks=background_tasks,
|
|
1418
1938
|
)
|
|
1419
1939
|
|
|
1420
1940
|
# Check for cancellation after step execution
|
|
@@ -1440,7 +1960,14 @@ class Workflow:
|
|
|
1440
1960
|
|
|
1441
1961
|
# Update the workflow_run_response with completion data
|
|
1442
1962
|
if collected_step_outputs:
|
|
1443
|
-
|
|
1963
|
+
# Stop the timer for the Run duration
|
|
1964
|
+
if workflow_run_response.metrics:
|
|
1965
|
+
workflow_run_response.metrics.stop_timer()
|
|
1966
|
+
|
|
1967
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
1968
|
+
collected_step_outputs,
|
|
1969
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
1970
|
+
)
|
|
1444
1971
|
last_output = cast(StepOutput, collected_step_outputs[-1])
|
|
1445
1972
|
|
|
1446
1973
|
# Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
|
|
@@ -1480,31 +2007,45 @@ class Workflow:
|
|
|
1480
2007
|
workflow_run_response.content = f"Workflow execution failed: {e}"
|
|
1481
2008
|
raise e
|
|
1482
2009
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
2010
|
+
# Stop timer on error
|
|
2011
|
+
if workflow_run_response.metrics:
|
|
2012
|
+
workflow_run_response.metrics.stop_timer()
|
|
2013
|
+
|
|
2014
|
+
self._update_session_metrics(session=workflow_session, workflow_run_response=workflow_run_response)
|
|
2015
|
+
workflow_session.upsert_run(run=workflow_run_response)
|
|
2016
|
+
if self._has_async_db():
|
|
2017
|
+
await self.asave_session(session=workflow_session)
|
|
2018
|
+
else:
|
|
2019
|
+
self.save_session(session=workflow_session)
|
|
1486
2020
|
# Always clean up the run tracking
|
|
1487
2021
|
cleanup_run(workflow_run_response.run_id) # type: ignore
|
|
1488
2022
|
|
|
1489
2023
|
# Log Workflow Telemetry
|
|
1490
2024
|
if self.telemetry:
|
|
1491
|
-
await self._alog_workflow_telemetry(session_id=
|
|
2025
|
+
await self._alog_workflow_telemetry(session_id=session_id, run_id=workflow_run_response.run_id)
|
|
1492
2026
|
|
|
1493
2027
|
return workflow_run_response
|
|
1494
2028
|
|
|
1495
2029
|
async def _aexecute_stream(
|
|
1496
2030
|
self,
|
|
1497
|
-
|
|
2031
|
+
session_id: str,
|
|
2032
|
+
user_id: Optional[str],
|
|
1498
2033
|
execution_input: WorkflowExecutionInput,
|
|
1499
2034
|
workflow_run_response: WorkflowRunOutput,
|
|
1500
|
-
|
|
1501
|
-
|
|
2035
|
+
run_context: RunContext,
|
|
2036
|
+
stream_events: bool = False,
|
|
1502
2037
|
websocket_handler: Optional[WebSocketHandler] = None,
|
|
2038
|
+
background_tasks: Optional[Any] = None,
|
|
1503
2039
|
**kwargs: Any,
|
|
1504
2040
|
) -> AsyncIterator[WorkflowRunOutputEvent]:
|
|
1505
2041
|
"""Execute a specific pipeline by name with event streaming"""
|
|
1506
2042
|
from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
|
|
1507
2043
|
|
|
2044
|
+
# Read existing session from database
|
|
2045
|
+
workflow_session, run_context.session_state = await self._aload_or_create_session(
|
|
2046
|
+
session_id=session_id, user_id=user_id, session_state=run_context.session_state
|
|
2047
|
+
)
|
|
2048
|
+
|
|
1508
2049
|
workflow_run_response.status = RunStatus.running
|
|
1509
2050
|
|
|
1510
2051
|
workflow_started_event = WorkflowStartedEvent(
|
|
@@ -1559,12 +2100,22 @@ class Workflow:
|
|
|
1559
2100
|
|
|
1560
2101
|
early_termination = False
|
|
1561
2102
|
|
|
2103
|
+
# Track partial step data in case of cancellation
|
|
2104
|
+
current_step_name = ""
|
|
2105
|
+
current_step = None
|
|
2106
|
+
partial_step_content = ""
|
|
2107
|
+
|
|
1562
2108
|
for i, step in enumerate(self.steps): # type: ignore[arg-type]
|
|
1563
2109
|
if workflow_run_response.run_id:
|
|
1564
2110
|
raise_if_cancelled(workflow_run_response.run_id)
|
|
1565
2111
|
step_name = getattr(step, "name", f"step_{i + 1}")
|
|
1566
2112
|
log_debug(f"Async streaming step {i + 1}/{self._get_step_count()}: {step_name}")
|
|
1567
2113
|
|
|
2114
|
+
current_step_name = step_name
|
|
2115
|
+
current_step = step
|
|
2116
|
+
# Reset partial data for this step
|
|
2117
|
+
partial_step_content = ""
|
|
2118
|
+
|
|
1568
2119
|
# Create enhanced StepInput
|
|
1569
2120
|
step_input = self._create_step_input(
|
|
1570
2121
|
execution_input=execution_input,
|
|
@@ -1578,16 +2129,27 @@ class Workflow:
|
|
|
1578
2129
|
# Execute step with streaming and yield all events
|
|
1579
2130
|
async for event in step.aexecute_stream( # type: ignore[union-attr]
|
|
1580
2131
|
step_input,
|
|
1581
|
-
session_id=
|
|
2132
|
+
session_id=session_id,
|
|
1582
2133
|
user_id=self.user_id,
|
|
1583
|
-
|
|
2134
|
+
stream_events=stream_events,
|
|
2135
|
+
stream_executor_events=self.stream_executor_events,
|
|
1584
2136
|
workflow_run_response=workflow_run_response,
|
|
1585
|
-
|
|
2137
|
+
run_context=run_context,
|
|
1586
2138
|
step_index=i,
|
|
1587
2139
|
store_executor_outputs=self.store_executor_outputs,
|
|
2140
|
+
workflow_session=workflow_session,
|
|
2141
|
+
add_workflow_history_to_steps=self.add_workflow_history_to_steps
|
|
2142
|
+
if self.add_workflow_history_to_steps
|
|
2143
|
+
else None,
|
|
2144
|
+
num_history_runs=self.num_history_runs,
|
|
2145
|
+
background_tasks=background_tasks,
|
|
1588
2146
|
):
|
|
1589
2147
|
if workflow_run_response.run_id:
|
|
1590
2148
|
raise_if_cancelled(workflow_run_response.run_id)
|
|
2149
|
+
|
|
2150
|
+
# Accumulate partial data from streaming events
|
|
2151
|
+
partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
|
|
2152
|
+
|
|
1591
2153
|
if isinstance(event, StepOutput):
|
|
1592
2154
|
step_output = event
|
|
1593
2155
|
collected_step_outputs.append(step_output)
|
|
@@ -1634,11 +2196,23 @@ class Workflow:
|
|
|
1634
2196
|
yield step_output_event
|
|
1635
2197
|
|
|
1636
2198
|
elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
|
|
1637
|
-
|
|
2199
|
+
# Enrich event with workflow context before yielding
|
|
2200
|
+
enriched_event = self._enrich_event_with_workflow_context(
|
|
2201
|
+
event, workflow_run_response, step_index=i, step=step
|
|
2202
|
+
)
|
|
2203
|
+
yield self._handle_event(
|
|
2204
|
+
enriched_event, workflow_run_response, websocket_handler=websocket_handler
|
|
2205
|
+
) # type: ignore
|
|
1638
2206
|
|
|
1639
2207
|
else:
|
|
1640
|
-
#
|
|
1641
|
-
|
|
2208
|
+
# Enrich other events with workflow context before yielding
|
|
2209
|
+
enriched_event = self._enrich_event_with_workflow_context(
|
|
2210
|
+
event, workflow_run_response, step_index=i, step=step
|
|
2211
|
+
)
|
|
2212
|
+
if self.stream_executor_events:
|
|
2213
|
+
yield self._handle_event(
|
|
2214
|
+
enriched_event, workflow_run_response, websocket_handler=websocket_handler
|
|
2215
|
+
) # type: ignore
|
|
1642
2216
|
|
|
1643
2217
|
# Break out of main step loop if early termination was requested
|
|
1644
2218
|
if "early_termination" in locals() and early_termination:
|
|
@@ -1646,7 +2220,14 @@ class Workflow:
|
|
|
1646
2220
|
|
|
1647
2221
|
# Update the workflow_run_response with completion data
|
|
1648
2222
|
if collected_step_outputs:
|
|
1649
|
-
|
|
2223
|
+
# Stop the timer for the Run duration
|
|
2224
|
+
if workflow_run_response.metrics:
|
|
2225
|
+
workflow_run_response.metrics.stop_timer()
|
|
2226
|
+
|
|
2227
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
2228
|
+
collected_step_outputs,
|
|
2229
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
2230
|
+
)
|
|
1650
2231
|
last_output = cast(StepOutput, collected_step_outputs[-1])
|
|
1651
2232
|
|
|
1652
2233
|
# Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
|
|
@@ -1678,7 +2259,7 @@ class Workflow:
|
|
|
1678
2259
|
run_id=workflow_run_response.run_id or "",
|
|
1679
2260
|
workflow_id=self.id,
|
|
1680
2261
|
workflow_name=self.name,
|
|
1681
|
-
session_id=
|
|
2262
|
+
session_id=session_id,
|
|
1682
2263
|
error=str(e),
|
|
1683
2264
|
)
|
|
1684
2265
|
|
|
@@ -1692,11 +2273,41 @@ class Workflow:
|
|
|
1692
2273
|
logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
|
|
1693
2274
|
workflow_run_response.status = RunStatus.cancelled
|
|
1694
2275
|
workflow_run_response.content = str(e)
|
|
2276
|
+
|
|
2277
|
+
# Capture partial progress from the step that was cancelled mid-stream
|
|
2278
|
+
if partial_step_content:
|
|
2279
|
+
logger.info(
|
|
2280
|
+
f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
|
|
2281
|
+
)
|
|
2282
|
+
partial_step_output = StepOutput(
|
|
2283
|
+
step_name=current_step_name,
|
|
2284
|
+
step_id=getattr(current_step, "step_id", None) if current_step else None,
|
|
2285
|
+
step_type=StepType.STEP,
|
|
2286
|
+
executor_type=getattr(current_step, "executor_type", None) if current_step else None,
|
|
2287
|
+
executor_name=getattr(current_step, "executor_name", None) if current_step else None,
|
|
2288
|
+
content=partial_step_content,
|
|
2289
|
+
success=False,
|
|
2290
|
+
error="Cancelled during execution",
|
|
2291
|
+
)
|
|
2292
|
+
collected_step_outputs.append(partial_step_output)
|
|
2293
|
+
|
|
2294
|
+
# Preserve all progress (completed steps + partial step) before cancellation
|
|
2295
|
+
if collected_step_outputs:
|
|
2296
|
+
workflow_run_response.step_results = collected_step_outputs
|
|
2297
|
+
# Stop the timer for the Run duration
|
|
2298
|
+
if workflow_run_response.metrics:
|
|
2299
|
+
workflow_run_response.metrics.stop_timer()
|
|
2300
|
+
|
|
2301
|
+
workflow_run_response.metrics = self._aggregate_workflow_metrics(
|
|
2302
|
+
collected_step_outputs,
|
|
2303
|
+
workflow_run_response.metrics, # type: ignore[arg-type]
|
|
2304
|
+
)
|
|
2305
|
+
|
|
1695
2306
|
cancelled_event = WorkflowCancelledEvent(
|
|
1696
2307
|
run_id=workflow_run_response.run_id or "",
|
|
1697
2308
|
workflow_id=self.id,
|
|
1698
2309
|
workflow_name=self.name,
|
|
1699
|
-
session_id=
|
|
2310
|
+
session_id=session_id,
|
|
1700
2311
|
reason=str(e),
|
|
1701
2312
|
)
|
|
1702
2313
|
yield self._handle_event(
|
|
@@ -1713,7 +2324,7 @@ class Workflow:
|
|
|
1713
2324
|
run_id=workflow_run_response.run_id or "",
|
|
1714
2325
|
workflow_id=self.id,
|
|
1715
2326
|
workflow_name=self.name,
|
|
1716
|
-
session_id=
|
|
2327
|
+
session_id=session_id,
|
|
1717
2328
|
error=str(e),
|
|
1718
2329
|
)
|
|
1719
2330
|
|
|
@@ -1736,14 +2347,21 @@ class Workflow:
|
|
|
1736
2347
|
)
|
|
1737
2348
|
yield self._handle_event(workflow_completed_event, workflow_run_response, websocket_handler=websocket_handler)
|
|
1738
2349
|
|
|
2350
|
+
# Stop timer on error
|
|
2351
|
+
if workflow_run_response.metrics:
|
|
2352
|
+
workflow_run_response.metrics.stop_timer()
|
|
2353
|
+
|
|
1739
2354
|
# Store the completed workflow response
|
|
1740
|
-
self._update_session_metrics(session=
|
|
1741
|
-
|
|
1742
|
-
self.
|
|
2355
|
+
self._update_session_metrics(session=workflow_session, workflow_run_response=workflow_run_response)
|
|
2356
|
+
workflow_session.upsert_run(run=workflow_run_response)
|
|
2357
|
+
if self._has_async_db():
|
|
2358
|
+
await self.asave_session(session=workflow_session)
|
|
2359
|
+
else:
|
|
2360
|
+
self.save_session(session=workflow_session)
|
|
1743
2361
|
|
|
1744
2362
|
# Log Workflow Telemetry
|
|
1745
2363
|
if self.telemetry:
|
|
1746
|
-
await self._alog_workflow_telemetry(session_id=
|
|
2364
|
+
await self._alog_workflow_telemetry(session_id=session_id, run_id=workflow_run_response.run_id)
|
|
1747
2365
|
|
|
1748
2366
|
# Always clean up the run tracking
|
|
1749
2367
|
cleanup_run(workflow_run_response.run_id) # type: ignore
|
|
@@ -1767,16 +2385,19 @@ class Workflow:
|
|
|
1767
2385
|
|
|
1768
2386
|
self.initialize_workflow()
|
|
1769
2387
|
|
|
1770
|
-
session_id, user_id
|
|
1771
|
-
session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
|
|
1772
|
-
)
|
|
2388
|
+
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
1773
2389
|
|
|
1774
2390
|
# Read existing session from database
|
|
1775
|
-
workflow_session = self.
|
|
1776
|
-
|
|
2391
|
+
workflow_session, session_state = await self._aload_or_create_session(
|
|
2392
|
+
session_id=session_id, user_id=user_id, session_state=session_state
|
|
2393
|
+
)
|
|
1777
2394
|
|
|
1778
|
-
|
|
1779
|
-
|
|
2395
|
+
run_context = RunContext(
|
|
2396
|
+
run_id=run_id,
|
|
2397
|
+
session_id=session_id,
|
|
2398
|
+
user_id=user_id,
|
|
2399
|
+
session_state=session_state,
|
|
2400
|
+
)
|
|
1780
2401
|
|
|
1781
2402
|
self._prepare_steps()
|
|
1782
2403
|
|
|
@@ -1791,9 +2412,16 @@ class Workflow:
|
|
|
1791
2412
|
status=RunStatus.pending,
|
|
1792
2413
|
)
|
|
1793
2414
|
|
|
2415
|
+
# Start the run metrics timer
|
|
2416
|
+
workflow_run_response.metrics = WorkflowMetrics(steps={})
|
|
2417
|
+
workflow_run_response.metrics.start_timer()
|
|
2418
|
+
|
|
1794
2419
|
# Store PENDING response immediately
|
|
1795
2420
|
workflow_session.upsert_run(run=workflow_run_response)
|
|
1796
|
-
self.
|
|
2421
|
+
if self._has_async_db():
|
|
2422
|
+
await self.asave_session(session=workflow_session)
|
|
2423
|
+
else:
|
|
2424
|
+
self.save_session(session=workflow_session)
|
|
1797
2425
|
|
|
1798
2426
|
# Prepare execution input
|
|
1799
2427
|
inputs = WorkflowExecutionInput(
|
|
@@ -1812,15 +2440,29 @@ class Workflow:
|
|
|
1812
2440
|
try:
|
|
1813
2441
|
# Update status to RUNNING and save
|
|
1814
2442
|
workflow_run_response.status = RunStatus.running
|
|
1815
|
-
self.
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
session=workflow_session
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2443
|
+
if self._has_async_db():
|
|
2444
|
+
await self.asave_session(session=workflow_session)
|
|
2445
|
+
else:
|
|
2446
|
+
self.save_session(session=workflow_session)
|
|
2447
|
+
|
|
2448
|
+
if self.agent is not None:
|
|
2449
|
+
self._aexecute_workflow_agent(
|
|
2450
|
+
user_input=input, # type: ignore
|
|
2451
|
+
execution_input=inputs,
|
|
2452
|
+
run_context=run_context,
|
|
2453
|
+
stream=False,
|
|
2454
|
+
**kwargs,
|
|
2455
|
+
)
|
|
2456
|
+
else:
|
|
2457
|
+
await self._aexecute(
|
|
2458
|
+
session_id=session_id,
|
|
2459
|
+
user_id=user_id,
|
|
2460
|
+
execution_input=inputs,
|
|
2461
|
+
workflow_run_response=workflow_run_response,
|
|
2462
|
+
run_context=run_context,
|
|
2463
|
+
session_state=session_state,
|
|
2464
|
+
**kwargs,
|
|
2465
|
+
)
|
|
1824
2466
|
|
|
1825
2467
|
log_debug(f"Background execution completed with status: {workflow_run_response.status}")
|
|
1826
2468
|
|
|
@@ -1828,7 +2470,10 @@ class Workflow:
|
|
|
1828
2470
|
logger.error(f"Background workflow execution failed: {e}")
|
|
1829
2471
|
workflow_run_response.status = RunStatus.error
|
|
1830
2472
|
workflow_run_response.content = f"Background execution failed: {str(e)}"
|
|
1831
|
-
self.
|
|
2473
|
+
if self._has_async_db():
|
|
2474
|
+
await self.asave_session(session=workflow_session)
|
|
2475
|
+
else:
|
|
2476
|
+
self.save_session(session=workflow_session)
|
|
1832
2477
|
|
|
1833
2478
|
# Create and start asyncio task
|
|
1834
2479
|
loop = asyncio.get_running_loop()
|
|
@@ -1848,7 +2493,7 @@ class Workflow:
|
|
|
1848
2493
|
images: Optional[List[Image]] = None,
|
|
1849
2494
|
videos: Optional[List[Video]] = None,
|
|
1850
2495
|
files: Optional[List[File]] = None,
|
|
1851
|
-
|
|
2496
|
+
stream_events: bool = False,
|
|
1852
2497
|
websocket_handler: Optional[WebSocketHandler] = None,
|
|
1853
2498
|
**kwargs: Any,
|
|
1854
2499
|
) -> WorkflowRunOutput:
|
|
@@ -1858,93 +2503,936 @@ class Workflow:
|
|
|
1858
2503
|
|
|
1859
2504
|
self.initialize_workflow()
|
|
1860
2505
|
|
|
1861
|
-
session_id, user_id
|
|
1862
|
-
session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
|
|
1863
|
-
)
|
|
2506
|
+
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
1864
2507
|
|
|
1865
2508
|
# Read existing session from database
|
|
1866
|
-
workflow_session = self.
|
|
1867
|
-
|
|
2509
|
+
workflow_session, session_state = await self._aload_or_create_session(
|
|
2510
|
+
session_id=session_id, user_id=user_id, session_state=session_state
|
|
2511
|
+
)
|
|
1868
2512
|
|
|
1869
|
-
|
|
1870
|
-
|
|
2513
|
+
run_context = RunContext(
|
|
2514
|
+
run_id=run_id,
|
|
2515
|
+
session_id=session_id,
|
|
2516
|
+
user_id=user_id,
|
|
2517
|
+
session_state=session_state,
|
|
2518
|
+
)
|
|
2519
|
+
|
|
2520
|
+
self._prepare_steps()
|
|
2521
|
+
|
|
2522
|
+
# Create workflow run response with PENDING status
|
|
2523
|
+
workflow_run_response = WorkflowRunOutput(
|
|
2524
|
+
run_id=run_id,
|
|
2525
|
+
input=input,
|
|
2526
|
+
session_id=session_id,
|
|
2527
|
+
workflow_id=self.id,
|
|
2528
|
+
workflow_name=self.name,
|
|
2529
|
+
created_at=int(datetime.now().timestamp()),
|
|
2530
|
+
status=RunStatus.pending,
|
|
2531
|
+
)
|
|
2532
|
+
|
|
2533
|
+
# Start the run metrics timer
|
|
2534
|
+
workflow_run_response.metrics = WorkflowMetrics(steps={})
|
|
2535
|
+
workflow_run_response.metrics.start_timer()
|
|
2536
|
+
|
|
2537
|
+
# Prepare execution input
|
|
2538
|
+
inputs = WorkflowExecutionInput(
|
|
2539
|
+
input=input,
|
|
2540
|
+
additional_data=additional_data,
|
|
2541
|
+
audio=audio, # type: ignore
|
|
2542
|
+
images=images, # type: ignore
|
|
2543
|
+
videos=videos, # type: ignore
|
|
2544
|
+
files=files, # type: ignore
|
|
2545
|
+
)
|
|
2546
|
+
|
|
2547
|
+
self.update_agents_and_teams_session_info()
|
|
2548
|
+
|
|
2549
|
+
async def execute_workflow_background_stream():
|
|
2550
|
+
"""Background execution with streaming and WebSocket broadcasting"""
|
|
2551
|
+
try:
|
|
2552
|
+
if self.agent is not None:
|
|
2553
|
+
result = self._aexecute_workflow_agent(
|
|
2554
|
+
user_input=input, # type: ignore
|
|
2555
|
+
run_context=run_context,
|
|
2556
|
+
execution_input=inputs,
|
|
2557
|
+
stream=True,
|
|
2558
|
+
websocket_handler=websocket_handler,
|
|
2559
|
+
**kwargs,
|
|
2560
|
+
)
|
|
2561
|
+
# For streaming, result is an async iterator
|
|
2562
|
+
async for event in result: # type: ignore
|
|
2563
|
+
# Events are automatically broadcast by _handle_event in the agent execution
|
|
2564
|
+
# We just consume them here to drive the execution
|
|
2565
|
+
pass
|
|
2566
|
+
log_debug(
|
|
2567
|
+
f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
|
|
2568
|
+
)
|
|
2569
|
+
else:
|
|
2570
|
+
# Update status to RUNNING and save
|
|
2571
|
+
workflow_run_response.status = RunStatus.running
|
|
2572
|
+
|
|
2573
|
+
workflow_session.upsert_run(run=workflow_run_response)
|
|
2574
|
+
if self._has_async_db():
|
|
2575
|
+
await self.asave_session(session=workflow_session)
|
|
2576
|
+
else:
|
|
2577
|
+
self.save_session(session=workflow_session)
|
|
2578
|
+
|
|
2579
|
+
# Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
|
|
2580
|
+
async for event in self._aexecute_stream(
|
|
2581
|
+
session_id=session_id,
|
|
2582
|
+
user_id=user_id,
|
|
2583
|
+
execution_input=inputs,
|
|
2584
|
+
workflow_run_response=workflow_run_response,
|
|
2585
|
+
stream_events=stream_events,
|
|
2586
|
+
run_context=run_context,
|
|
2587
|
+
websocket_handler=websocket_handler,
|
|
2588
|
+
**kwargs,
|
|
2589
|
+
):
|
|
2590
|
+
# Events are automatically broadcast by _handle_event
|
|
2591
|
+
# We just consume them here to drive the execution
|
|
2592
|
+
pass
|
|
2593
|
+
|
|
2594
|
+
log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
|
|
2595
|
+
|
|
2596
|
+
except Exception as e:
|
|
2597
|
+
logger.error(f"Background streaming workflow execution failed: {e}")
|
|
2598
|
+
workflow_run_response.status = RunStatus.error
|
|
2599
|
+
workflow_run_response.content = f"Background streaming execution failed: {str(e)}"
|
|
2600
|
+
if self._has_async_db():
|
|
2601
|
+
await self.asave_session(session=workflow_session)
|
|
2602
|
+
else:
|
|
2603
|
+
self.save_session(session=workflow_session)
|
|
2604
|
+
|
|
2605
|
+
# Create and start asyncio task for background streaming execution
|
|
2606
|
+
loop = asyncio.get_running_loop()
|
|
2607
|
+
loop.create_task(execute_workflow_background_stream())
|
|
2608
|
+
|
|
2609
|
+
# Return SAME object that will be updated by background execution
|
|
2610
|
+
return workflow_run_response
|
|
2611
|
+
|
|
2612
|
+
async def aget_run(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
2613
|
+
"""Get the status and details of a background workflow run - SIMPLIFIED"""
|
|
2614
|
+
# Use provided session_id or fall back to self.session_id
|
|
2615
|
+
_session_id = session_id if session_id is not None else self.session_id
|
|
2616
|
+
|
|
2617
|
+
if self.db is not None and _session_id is not None:
|
|
2618
|
+
session = await self.db.aget_session(session_id=_session_id, session_type=SessionType.WORKFLOW) # type: ignore
|
|
2619
|
+
if session and isinstance(session, WorkflowSession) and session.runs:
|
|
2620
|
+
# Find the run by ID
|
|
2621
|
+
for run in session.runs:
|
|
2622
|
+
if run.run_id == run_id:
|
|
2623
|
+
return run
|
|
2624
|
+
|
|
2625
|
+
return None
|
|
2626
|
+
|
|
2627
|
+
def get_run(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
|
|
2628
|
+
"""Get the status and details of a background workflow run - SIMPLIFIED"""
|
|
2629
|
+
# Use provided session_id or fall back to self.session_id
|
|
2630
|
+
_session_id = session_id if session_id is not None else self.session_id
|
|
2631
|
+
|
|
2632
|
+
if self.db is not None and _session_id is not None:
|
|
2633
|
+
session = self.db.get_session(session_id=_session_id, session_type=SessionType.WORKFLOW)
|
|
2634
|
+
if session and isinstance(session, WorkflowSession) and session.runs:
|
|
2635
|
+
# Find the run by ID
|
|
2636
|
+
for run in session.runs:
|
|
2637
|
+
if run.run_id == run_id:
|
|
2638
|
+
return run
|
|
2639
|
+
|
|
2640
|
+
return None
|
|
2641
|
+
|
|
2642
|
+
def _initialize_workflow_agent(
|
|
2643
|
+
self,
|
|
2644
|
+
session: WorkflowSession,
|
|
2645
|
+
execution_input: WorkflowExecutionInput,
|
|
2646
|
+
run_context: RunContext,
|
|
2647
|
+
stream: bool = False,
|
|
2648
|
+
) -> None:
|
|
2649
|
+
"""Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
|
|
2650
|
+
from agno.tools.function import Function
|
|
2651
|
+
|
|
2652
|
+
workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
|
|
2653
|
+
workflow=self,
|
|
2654
|
+
session=session,
|
|
2655
|
+
execution_input=execution_input,
|
|
2656
|
+
run_context=run_context,
|
|
2657
|
+
stream=stream,
|
|
2658
|
+
)
|
|
2659
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
2660
|
+
|
|
2661
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
2662
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
2663
|
+
|
|
2664
|
+
log_debug("Workflow agent initialized with run_workflow tool")
|
|
2665
|
+
|
|
2666
|
+
def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
|
|
2667
|
+
"""Build dependencies dict with workflow context to pass to agent.run()"""
|
|
2668
|
+
# Get configuration from the WorkflowAgent instance
|
|
2669
|
+
add_history = True
|
|
2670
|
+
num_runs = 5
|
|
2671
|
+
|
|
2672
|
+
if self.agent and isinstance(self.agent, WorkflowAgent):
|
|
2673
|
+
add_history = self.agent.add_workflow_history
|
|
2674
|
+
num_runs = self.agent.num_history_runs or 5
|
|
2675
|
+
|
|
2676
|
+
if add_history:
|
|
2677
|
+
history_context = (
|
|
2678
|
+
session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
|
|
2679
|
+
)
|
|
2680
|
+
else:
|
|
2681
|
+
history_context = "No workflow history available."
|
|
2682
|
+
|
|
2683
|
+
# Build workflow context with description and history
|
|
2684
|
+
workflow_context = ""
|
|
2685
|
+
if self.description:
|
|
2686
|
+
workflow_context += f"Workflow Description: {self.description}\n\n"
|
|
2687
|
+
|
|
2688
|
+
workflow_context += history_context
|
|
2689
|
+
|
|
2690
|
+
return {
|
|
2691
|
+
"workflow_context": workflow_context,
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
def _execute_workflow_agent(
|
|
2695
|
+
self,
|
|
2696
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2697
|
+
session: WorkflowSession,
|
|
2698
|
+
execution_input: WorkflowExecutionInput,
|
|
2699
|
+
run_context: RunContext,
|
|
2700
|
+
stream: bool = False,
|
|
2701
|
+
**kwargs: Any,
|
|
2702
|
+
) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
|
|
2703
|
+
"""
|
|
2704
|
+
Execute the workflow agent in streaming or non-streaming mode.
|
|
2705
|
+
|
|
2706
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2707
|
+
|
|
2708
|
+
Args:
|
|
2709
|
+
user_input: The user's input
|
|
2710
|
+
session: The workflow session
|
|
2711
|
+
execution_input: The execution input
|
|
2712
|
+
run_context: The run context
|
|
2713
|
+
stream: Whether to stream the response
|
|
2714
|
+
stream_intermediate_steps: Whether to stream intermediate steps
|
|
2715
|
+
|
|
2716
|
+
Returns:
|
|
2717
|
+
WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
|
|
2718
|
+
"""
|
|
2719
|
+
if stream:
|
|
2720
|
+
return self._run_workflow_agent_stream(
|
|
2721
|
+
agent_input=user_input,
|
|
2722
|
+
session=session,
|
|
2723
|
+
execution_input=execution_input,
|
|
2724
|
+
run_context=run_context,
|
|
2725
|
+
stream=stream,
|
|
2726
|
+
**kwargs,
|
|
2727
|
+
)
|
|
2728
|
+
else:
|
|
2729
|
+
return self._run_workflow_agent(
|
|
2730
|
+
agent_input=user_input,
|
|
2731
|
+
session=session,
|
|
2732
|
+
execution_input=execution_input,
|
|
2733
|
+
run_context=run_context,
|
|
2734
|
+
stream=stream,
|
|
2735
|
+
)
|
|
2736
|
+
|
|
2737
|
+
def _run_workflow_agent_stream(
|
|
2738
|
+
self,
|
|
2739
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2740
|
+
session: WorkflowSession,
|
|
2741
|
+
execution_input: WorkflowExecutionInput,
|
|
2742
|
+
run_context: RunContext,
|
|
2743
|
+
stream: bool = False,
|
|
2744
|
+
**kwargs: Any,
|
|
2745
|
+
) -> Iterator[WorkflowRunOutputEvent]:
|
|
2746
|
+
"""
|
|
2747
|
+
Execute the workflow agent in streaming mode.
|
|
2748
|
+
|
|
2749
|
+
The agent's tool (run_workflow) is a generator that yields workflow events directly.
|
|
2750
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
2751
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
2752
|
+
|
|
2753
|
+
Yields:
|
|
2754
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
2755
|
+
"""
|
|
2756
|
+
from typing import get_args
|
|
2757
|
+
|
|
2758
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
2759
|
+
|
|
2760
|
+
# Initialize agent with stream_intermediate_steps=True so tool yields events
|
|
2761
|
+
self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
2762
|
+
|
|
2763
|
+
# Build dependencies with workflow context
|
|
2764
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
2765
|
+
|
|
2766
|
+
# Run agent with streaming - workflow events will bubble up from the tool
|
|
2767
|
+
agent_response: Optional[RunOutput] = None
|
|
2768
|
+
workflow_executed = False
|
|
2769
|
+
|
|
2770
|
+
from agno.run.agent import RunContentEvent
|
|
2771
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
2772
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
2773
|
+
|
|
2774
|
+
log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
|
|
2775
|
+
|
|
2776
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
2777
|
+
run_id = str(uuid4())
|
|
2778
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
2779
|
+
run_id=run_id,
|
|
2780
|
+
input=execution_input.input,
|
|
2781
|
+
session_id=session.session_id,
|
|
2782
|
+
workflow_id=self.id,
|
|
2783
|
+
workflow_name=self.name,
|
|
2784
|
+
created_at=int(datetime.now().timestamp()),
|
|
2785
|
+
)
|
|
2786
|
+
|
|
2787
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
2788
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
2789
|
+
workflow_name=self.name,
|
|
2790
|
+
workflow_id=self.id,
|
|
2791
|
+
session_id=session.session_id,
|
|
2792
|
+
)
|
|
2793
|
+
yield agent_started_event
|
|
2794
|
+
|
|
2795
|
+
# Run the agent in streaming mode and yield all events
|
|
2796
|
+
for event in self.agent.run( # type: ignore[union-attr]
|
|
2797
|
+
input=agent_input,
|
|
2798
|
+
stream=True,
|
|
2799
|
+
stream_intermediate_steps=True,
|
|
2800
|
+
yield_run_response=True,
|
|
2801
|
+
session_id=session.session_id,
|
|
2802
|
+
dependencies=run_context.dependencies, # Pass context dynamically per-run
|
|
2803
|
+
session_state=run_context.session_state, # Pass session state dynamically per-run
|
|
2804
|
+
): # type: ignore
|
|
2805
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
2806
|
+
yield event # type: ignore[misc]
|
|
2807
|
+
|
|
2808
|
+
# Track if workflow was executed by checking for WorkflowCompletedEvent
|
|
2809
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
2810
|
+
workflow_executed = True
|
|
2811
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
2812
|
+
if event.step_name is None:
|
|
2813
|
+
# This is from the workflow agent itself
|
|
2814
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
2815
|
+
|
|
2816
|
+
if workflow_executed:
|
|
2817
|
+
continue # Skip if workflow was already executed
|
|
2818
|
+
|
|
2819
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
2820
|
+
event.workflow_agent = True # type: ignore
|
|
2821
|
+
yield event # type: ignore[misc]
|
|
2822
|
+
|
|
2823
|
+
# Capture the final RunOutput (but don't yield it)
|
|
2824
|
+
if isinstance(event, RunOutput):
|
|
2825
|
+
agent_response = event
|
|
2826
|
+
|
|
2827
|
+
# Handle direct answer case (no workflow execution)
|
|
2828
|
+
if not workflow_executed:
|
|
2829
|
+
# Update the pre-created workflow run response with the direct answer
|
|
2830
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
2831
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
2832
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
2833
|
+
|
|
2834
|
+
workflow_run_response = direct_reply_run_response
|
|
2835
|
+
|
|
2836
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2837
|
+
if agent_response:
|
|
2838
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2839
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2840
|
+
|
|
2841
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2842
|
+
|
|
2843
|
+
# Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
|
|
2844
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2845
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2846
|
+
workflow_name=self.name,
|
|
2847
|
+
workflow_id=self.id,
|
|
2848
|
+
session_id=session.session_id,
|
|
2849
|
+
content=workflow_run_response.content,
|
|
2850
|
+
)
|
|
2851
|
+
yield agent_completed_event
|
|
2852
|
+
|
|
2853
|
+
# Yield a workflow completed event with the agent's direct response
|
|
2854
|
+
completed_event = WorkflowCompletedEvent(
|
|
2855
|
+
run_id=workflow_run_response.run_id or "",
|
|
2856
|
+
content=workflow_run_response.content,
|
|
2857
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
2858
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
2859
|
+
session_id=workflow_run_response.session_id,
|
|
2860
|
+
step_results=[],
|
|
2861
|
+
metadata={"agent_direct_response": True},
|
|
2862
|
+
)
|
|
2863
|
+
yield completed_event
|
|
2864
|
+
|
|
2865
|
+
# Update the run in session
|
|
2866
|
+
session.upsert_run(run=workflow_run_response)
|
|
2867
|
+
# Save session
|
|
2868
|
+
self.save_session(session=session)
|
|
2869
|
+
|
|
2870
|
+
else:
|
|
2871
|
+
# Workflow was executed by the tool
|
|
2872
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2873
|
+
|
|
2874
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2875
|
+
# Get the last run (which is the one just created by the tool)
|
|
2876
|
+
last_run = reloaded_session.runs[-1]
|
|
2877
|
+
|
|
2878
|
+
# Yield WorkflowAgentCompletedEvent
|
|
2879
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
2880
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
2881
|
+
workflow_name=self.name,
|
|
2882
|
+
workflow_id=self.id,
|
|
2883
|
+
session_id=session.session_id,
|
|
2884
|
+
content=agent_response.content if agent_response else None,
|
|
2885
|
+
)
|
|
2886
|
+
yield agent_completed_event
|
|
2887
|
+
|
|
2888
|
+
# Update the last run with workflow_agent_run
|
|
2889
|
+
last_run.workflow_agent_run = agent_response
|
|
2890
|
+
|
|
2891
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2892
|
+
if agent_response:
|
|
2893
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2894
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2895
|
+
|
|
2896
|
+
# Save the reloaded session (which has the updated run)
|
|
2897
|
+
self.save_session(session=reloaded_session)
|
|
2898
|
+
|
|
2899
|
+
else:
|
|
2900
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
2901
|
+
|
|
2902
|
+
def _run_workflow_agent(
|
|
2903
|
+
self,
|
|
2904
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
2905
|
+
session: WorkflowSession,
|
|
2906
|
+
execution_input: WorkflowExecutionInput,
|
|
2907
|
+
run_context: RunContext,
|
|
2908
|
+
stream: bool = False,
|
|
2909
|
+
) -> WorkflowRunOutput:
|
|
2910
|
+
"""
|
|
2911
|
+
Execute the workflow agent in non-streaming mode.
|
|
2912
|
+
|
|
2913
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
2914
|
+
|
|
2915
|
+
Returns:
|
|
2916
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
2917
|
+
"""
|
|
2918
|
+
|
|
2919
|
+
# Initialize the agent
|
|
2920
|
+
self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
2921
|
+
|
|
2922
|
+
# Build dependencies with workflow context
|
|
2923
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
2924
|
+
|
|
2925
|
+
# Run the agent
|
|
2926
|
+
agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
|
|
2927
|
+
input=agent_input,
|
|
2928
|
+
session_id=session.session_id,
|
|
2929
|
+
dependencies=run_context.dependencies,
|
|
2930
|
+
session_state=run_context.session_state,
|
|
2931
|
+
stream=stream,
|
|
2932
|
+
) # type: ignore
|
|
2933
|
+
|
|
2934
|
+
# Check if the agent called the workflow tool
|
|
2935
|
+
workflow_executed = False
|
|
2936
|
+
if agent_response.messages:
|
|
2937
|
+
for message in agent_response.messages:
|
|
2938
|
+
if message.role == "assistant" and message.tool_calls:
|
|
2939
|
+
# Check if the tool call is specifically for run_workflow
|
|
2940
|
+
for tool_call in message.tool_calls:
|
|
2941
|
+
# Handle both dict and object formats
|
|
2942
|
+
if isinstance(tool_call, dict):
|
|
2943
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
2944
|
+
else:
|
|
2945
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
2946
|
+
|
|
2947
|
+
if tool_name == "run_workflow":
|
|
2948
|
+
workflow_executed = True
|
|
2949
|
+
break
|
|
2950
|
+
if workflow_executed:
|
|
2951
|
+
break
|
|
2952
|
+
|
|
2953
|
+
log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
|
|
2954
|
+
|
|
2955
|
+
# Handle direct answer case (no workflow execution)
|
|
2956
|
+
if not workflow_executed:
|
|
2957
|
+
# Create a new workflow run output for the direct answer
|
|
2958
|
+
run_id = str(uuid4())
|
|
2959
|
+
workflow_run_response = WorkflowRunOutput(
|
|
2960
|
+
run_id=run_id,
|
|
2961
|
+
input=execution_input.input,
|
|
2962
|
+
session_id=session.session_id,
|
|
2963
|
+
workflow_id=self.id,
|
|
2964
|
+
workflow_name=self.name,
|
|
2965
|
+
created_at=int(datetime.now().timestamp()),
|
|
2966
|
+
content=agent_response.content,
|
|
2967
|
+
status=RunStatus.completed,
|
|
2968
|
+
workflow_agent_run=agent_response,
|
|
2969
|
+
)
|
|
2970
|
+
|
|
2971
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2972
|
+
if agent_response:
|
|
2973
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
2974
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
2975
|
+
|
|
2976
|
+
# Update the run in session
|
|
2977
|
+
session.upsert_run(run=workflow_run_response)
|
|
2978
|
+
self.save_session(session=session)
|
|
2979
|
+
|
|
2980
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
2981
|
+
|
|
2982
|
+
return workflow_run_response
|
|
2983
|
+
else:
|
|
2984
|
+
# Workflow was executed by the tool
|
|
2985
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
2986
|
+
|
|
2987
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
2988
|
+
# Get the last run (which is the one just created by the tool)
|
|
2989
|
+
last_run = reloaded_session.runs[-1]
|
|
2990
|
+
|
|
2991
|
+
# Update the last run directly with workflow_agent_run
|
|
2992
|
+
last_run.workflow_agent_run = agent_response
|
|
2993
|
+
|
|
2994
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
2995
|
+
if agent_response:
|
|
2996
|
+
agent_response.parent_run_id = last_run.run_id
|
|
2997
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
2998
|
+
|
|
2999
|
+
# Save the reloaded session (which has the updated run)
|
|
3000
|
+
self.save_session(session=reloaded_session)
|
|
3001
|
+
|
|
3002
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
3003
|
+
return last_run
|
|
3004
|
+
else:
|
|
3005
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3006
|
+
# Return a placeholder error response
|
|
3007
|
+
return WorkflowRunOutput(
|
|
3008
|
+
run_id=str(uuid4()),
|
|
3009
|
+
input=execution_input.input,
|
|
3010
|
+
session_id=session.session_id,
|
|
3011
|
+
workflow_id=self.id,
|
|
3012
|
+
workflow_name=self.name,
|
|
3013
|
+
created_at=int(datetime.now().timestamp()),
|
|
3014
|
+
content="Error: Workflow execution failed",
|
|
3015
|
+
status=RunStatus.error,
|
|
3016
|
+
)
|
|
3017
|
+
|
|
3018
|
+
def _async_initialize_workflow_agent(
|
|
3019
|
+
self,
|
|
3020
|
+
session: WorkflowSession,
|
|
3021
|
+
execution_input: WorkflowExecutionInput,
|
|
3022
|
+
run_context: RunContext,
|
|
3023
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
3024
|
+
stream: bool = False,
|
|
3025
|
+
) -> None:
|
|
3026
|
+
"""Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
|
|
3027
|
+
from agno.tools.function import Function
|
|
3028
|
+
|
|
3029
|
+
workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
|
|
3030
|
+
workflow=self,
|
|
3031
|
+
session=session,
|
|
3032
|
+
execution_input=execution_input,
|
|
3033
|
+
run_context=run_context,
|
|
3034
|
+
stream=stream,
|
|
3035
|
+
websocket_handler=websocket_handler,
|
|
3036
|
+
)
|
|
3037
|
+
workflow_tool = Function.from_callable(workflow_tool_func)
|
|
3038
|
+
|
|
3039
|
+
self.agent.tools = [workflow_tool] # type: ignore
|
|
3040
|
+
self.agent._rebuild_tools = True # type: ignore
|
|
3041
|
+
|
|
3042
|
+
log_debug("Workflow agent initialized with async run_workflow tool")
|
|
3043
|
+
|
|
3044
|
+
async def _aload_session_for_workflow_agent(
|
|
3045
|
+
self,
|
|
3046
|
+
session_id: str,
|
|
3047
|
+
user_id: Optional[str],
|
|
3048
|
+
session_state: Optional[Dict[str, Any]],
|
|
3049
|
+
) -> Tuple[WorkflowSession, Dict[str, Any]]:
|
|
3050
|
+
"""Helper to load or create session for workflow agent execution"""
|
|
3051
|
+
return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
|
|
3052
|
+
|
|
3053
|
+
def _aexecute_workflow_agent(
|
|
3054
|
+
self,
|
|
3055
|
+
user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
3056
|
+
run_context: RunContext,
|
|
3057
|
+
execution_input: WorkflowExecutionInput,
|
|
3058
|
+
stream: bool = False,
|
|
3059
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
3060
|
+
**kwargs: Any,
|
|
3061
|
+
):
|
|
3062
|
+
"""
|
|
3063
|
+
Execute the workflow agent asynchronously in streaming or non-streaming mode.
|
|
3064
|
+
|
|
3065
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
3066
|
+
|
|
3067
|
+
Args:
|
|
3068
|
+
user_input: The user's input
|
|
3069
|
+
session: The workflow session
|
|
3070
|
+
run_context: The run context
|
|
3071
|
+
execution_input: The execution input
|
|
3072
|
+
stream: Whether to stream the response
|
|
3073
|
+
websocket_handler: The WebSocket handler
|
|
3074
|
+
|
|
3075
|
+
Returns:
|
|
3076
|
+
Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
|
|
3077
|
+
"""
|
|
3078
|
+
|
|
3079
|
+
if stream:
|
|
3080
|
+
|
|
3081
|
+
async def _stream():
|
|
3082
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
3083
|
+
run_context.session_id, run_context.user_id, run_context.session_state
|
|
3084
|
+
)
|
|
3085
|
+
async for event in self._arun_workflow_agent_stream(
|
|
3086
|
+
agent_input=user_input,
|
|
3087
|
+
session=session,
|
|
3088
|
+
execution_input=execution_input,
|
|
3089
|
+
run_context=run_context,
|
|
3090
|
+
stream=stream,
|
|
3091
|
+
websocket_handler=websocket_handler,
|
|
3092
|
+
**kwargs,
|
|
3093
|
+
):
|
|
3094
|
+
yield event
|
|
3095
|
+
|
|
3096
|
+
return _stream()
|
|
3097
|
+
else:
|
|
3098
|
+
|
|
3099
|
+
async def _execute():
|
|
3100
|
+
session, session_state_loaded = await self._aload_session_for_workflow_agent(
|
|
3101
|
+
run_context.session_id, run_context.user_id, run_context.session_state
|
|
3102
|
+
)
|
|
3103
|
+
return await self._arun_workflow_agent(
|
|
3104
|
+
agent_input=user_input,
|
|
3105
|
+
session=session,
|
|
3106
|
+
execution_input=execution_input,
|
|
3107
|
+
run_context=run_context,
|
|
3108
|
+
stream=stream,
|
|
3109
|
+
)
|
|
3110
|
+
|
|
3111
|
+
return _execute()
|
|
3112
|
+
|
|
3113
|
+
async def _arun_workflow_agent_stream(
|
|
3114
|
+
self,
|
|
3115
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
3116
|
+
session: WorkflowSession,
|
|
3117
|
+
execution_input: WorkflowExecutionInput,
|
|
3118
|
+
run_context: RunContext,
|
|
3119
|
+
stream: bool = False,
|
|
3120
|
+
websocket_handler: Optional[WebSocketHandler] = None,
|
|
3121
|
+
**kwargs: Any,
|
|
3122
|
+
) -> AsyncIterator[WorkflowRunOutputEvent]:
|
|
3123
|
+
"""
|
|
3124
|
+
Execute the workflow agent asynchronously in streaming mode.
|
|
3125
|
+
|
|
3126
|
+
The agent's tool (run_workflow) is an async generator that yields workflow events directly.
|
|
3127
|
+
These events bubble up through the agent's streaming and are yielded here.
|
|
3128
|
+
We filter to only yield WorkflowRunOutputEvent to the CLI.
|
|
3129
|
+
|
|
3130
|
+
Yields:
|
|
3131
|
+
WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
|
|
3132
|
+
"""
|
|
3133
|
+
from typing import get_args
|
|
3134
|
+
|
|
3135
|
+
from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
|
|
3136
|
+
|
|
3137
|
+
logger.info("Workflow agent enabled - async streaming mode")
|
|
3138
|
+
log_debug(f"User input: {agent_input}")
|
|
3139
|
+
|
|
3140
|
+
self._async_initialize_workflow_agent(
|
|
3141
|
+
session,
|
|
3142
|
+
execution_input,
|
|
3143
|
+
run_context=run_context,
|
|
3144
|
+
stream=stream,
|
|
3145
|
+
websocket_handler=websocket_handler,
|
|
3146
|
+
)
|
|
3147
|
+
|
|
3148
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
3149
|
+
|
|
3150
|
+
agent_response: Optional[RunOutput] = None
|
|
3151
|
+
workflow_executed = False
|
|
3152
|
+
|
|
3153
|
+
from agno.run.agent import RunContentEvent
|
|
3154
|
+
from agno.run.team import RunContentEvent as TeamRunContentEvent
|
|
3155
|
+
from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
|
|
3156
|
+
|
|
3157
|
+
log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
|
|
3158
|
+
|
|
3159
|
+
# Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
|
|
3160
|
+
run_id = str(uuid4())
|
|
3161
|
+
direct_reply_run_response = WorkflowRunOutput(
|
|
3162
|
+
run_id=run_id,
|
|
3163
|
+
input=execution_input.input,
|
|
3164
|
+
session_id=session.session_id,
|
|
3165
|
+
workflow_id=self.id,
|
|
3166
|
+
workflow_name=self.name,
|
|
3167
|
+
created_at=int(datetime.now().timestamp()),
|
|
3168
|
+
)
|
|
3169
|
+
|
|
3170
|
+
# Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
|
|
3171
|
+
agent_started_event = WorkflowAgentStartedEvent(
|
|
3172
|
+
workflow_name=self.name,
|
|
3173
|
+
workflow_id=self.id,
|
|
3174
|
+
session_id=session.session_id,
|
|
3175
|
+
)
|
|
3176
|
+
self._broadcast_to_websocket(agent_started_event, websocket_handler)
|
|
3177
|
+
yield agent_started_event
|
|
3178
|
+
|
|
3179
|
+
# Run the agent in streaming mode and yield all events
|
|
3180
|
+
async for event in self.agent.arun( # type: ignore[union-attr]
|
|
3181
|
+
input=agent_input,
|
|
3182
|
+
stream=True,
|
|
3183
|
+
stream_intermediate_steps=True,
|
|
3184
|
+
yield_run_response=True,
|
|
3185
|
+
session_id=session.session_id,
|
|
3186
|
+
dependencies=run_context.dependencies, # Pass context dynamically per-run
|
|
3187
|
+
session_state=run_context.session_state, # Pass session state dynamically per-run
|
|
3188
|
+
): # type: ignore
|
|
3189
|
+
if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
|
|
3190
|
+
yield event # type: ignore[misc]
|
|
3191
|
+
|
|
3192
|
+
if isinstance(event, WorkflowCompletedEvent):
|
|
3193
|
+
workflow_executed = True
|
|
3194
|
+
log_debug("Workflow execution detected via WorkflowCompletedEvent")
|
|
3195
|
+
|
|
3196
|
+
elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
|
|
3197
|
+
if event.step_name is None:
|
|
3198
|
+
# This is from the workflow agent itself
|
|
3199
|
+
# Enrich with metadata to mark it as a workflow agent event
|
|
3200
|
+
|
|
3201
|
+
if workflow_executed:
|
|
3202
|
+
continue # Skip if workflow was already executed
|
|
3203
|
+
|
|
3204
|
+
# workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
|
|
3205
|
+
event.workflow_agent = True # type: ignore
|
|
3206
|
+
|
|
3207
|
+
# Broadcast to WebSocket if available (async context only)
|
|
3208
|
+
self._broadcast_to_websocket(event, websocket_handler)
|
|
3209
|
+
|
|
3210
|
+
yield event # type: ignore[misc]
|
|
3211
|
+
|
|
3212
|
+
# Capture the final RunOutput (but don't yield it)
|
|
3213
|
+
if isinstance(event, RunOutput):
|
|
3214
|
+
agent_response = event
|
|
3215
|
+
log_debug(
|
|
3216
|
+
f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
|
|
3217
|
+
)
|
|
3218
|
+
|
|
3219
|
+
# Handle direct answer case (no workflow execution)
|
|
3220
|
+
if not workflow_executed:
|
|
3221
|
+
# Update the pre-created workflow run response with the direct answer
|
|
3222
|
+
direct_reply_run_response.content = agent_response.content if agent_response else ""
|
|
3223
|
+
direct_reply_run_response.status = RunStatus.completed
|
|
3224
|
+
direct_reply_run_response.workflow_agent_run = agent_response
|
|
3225
|
+
|
|
3226
|
+
workflow_run_response = direct_reply_run_response
|
|
3227
|
+
|
|
3228
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3229
|
+
if agent_response:
|
|
3230
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3231
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
3232
|
+
|
|
3233
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3234
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3235
|
+
workflow_name=self.name,
|
|
3236
|
+
workflow_id=self.id,
|
|
3237
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3238
|
+
session_id=session.session_id,
|
|
3239
|
+
content=workflow_run_response.content,
|
|
3240
|
+
)
|
|
3241
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3242
|
+
yield agent_completed_event
|
|
3243
|
+
|
|
3244
|
+
# Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
|
|
3245
|
+
completed_event = WorkflowCompletedEvent(
|
|
3246
|
+
run_id=workflow_run_response.run_id or "",
|
|
3247
|
+
content=workflow_run_response.content,
|
|
3248
|
+
workflow_name=workflow_run_response.workflow_name,
|
|
3249
|
+
workflow_id=workflow_run_response.workflow_id,
|
|
3250
|
+
session_id=workflow_run_response.session_id,
|
|
3251
|
+
step_results=[],
|
|
3252
|
+
metadata={"agent_direct_response": True},
|
|
3253
|
+
)
|
|
3254
|
+
yield completed_event
|
|
3255
|
+
|
|
3256
|
+
# Update the run in session
|
|
3257
|
+
session.upsert_run(run=workflow_run_response)
|
|
3258
|
+
# Save session
|
|
3259
|
+
if self._has_async_db():
|
|
3260
|
+
await self.asave_session(session=session)
|
|
3261
|
+
else:
|
|
3262
|
+
self.save_session(session=session)
|
|
3263
|
+
|
|
3264
|
+
else:
|
|
3265
|
+
# Workflow was executed by the tool
|
|
3266
|
+
if self._has_async_db():
|
|
3267
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3268
|
+
else:
|
|
3269
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
3270
|
+
|
|
3271
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3272
|
+
# Get the last run (which is the one just created by the tool)
|
|
3273
|
+
last_run = reloaded_session.runs[-1]
|
|
3274
|
+
|
|
3275
|
+
# Yield WorkflowAgentCompletedEvent
|
|
3276
|
+
agent_completed_event = WorkflowAgentCompletedEvent(
|
|
3277
|
+
run_id=agent_response.run_id if agent_response else None,
|
|
3278
|
+
workflow_name=self.name,
|
|
3279
|
+
workflow_id=self.id,
|
|
3280
|
+
session_id=session.session_id,
|
|
3281
|
+
content=agent_response.content if agent_response else None,
|
|
3282
|
+
)
|
|
3283
|
+
|
|
3284
|
+
self._broadcast_to_websocket(agent_completed_event, websocket_handler)
|
|
3285
|
+
|
|
3286
|
+
yield agent_completed_event
|
|
3287
|
+
|
|
3288
|
+
# Update the last run with workflow_agent_run
|
|
3289
|
+
last_run.workflow_agent_run = agent_response
|
|
3290
|
+
|
|
3291
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3292
|
+
if agent_response:
|
|
3293
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3294
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
3295
|
+
|
|
3296
|
+
# Save the reloaded session (which has the updated run)
|
|
3297
|
+
if self._has_async_db():
|
|
3298
|
+
await self.asave_session(session=reloaded_session)
|
|
3299
|
+
else:
|
|
3300
|
+
self.save_session(session=reloaded_session)
|
|
3301
|
+
|
|
3302
|
+
else:
|
|
3303
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3304
|
+
|
|
3305
|
+
async def _arun_workflow_agent(
|
|
3306
|
+
self,
|
|
3307
|
+
agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
|
|
3308
|
+
session: WorkflowSession,
|
|
3309
|
+
execution_input: WorkflowExecutionInput,
|
|
3310
|
+
run_context: RunContext,
|
|
3311
|
+
stream: bool = False,
|
|
3312
|
+
) -> WorkflowRunOutput:
|
|
3313
|
+
"""
|
|
3314
|
+
Execute the workflow agent asynchronously in non-streaming mode.
|
|
3315
|
+
|
|
3316
|
+
The agent decides whether to run the workflow or answer directly from history.
|
|
1871
3317
|
|
|
1872
|
-
|
|
3318
|
+
Returns:
|
|
3319
|
+
WorkflowRunOutput: The workflow run output with agent response
|
|
3320
|
+
"""
|
|
3321
|
+
# Initialize the agent
|
|
3322
|
+
self._async_initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
|
|
3323
|
+
|
|
3324
|
+
# Build dependencies with workflow context
|
|
3325
|
+
run_context.dependencies = self._get_workflow_agent_dependencies(session)
|
|
3326
|
+
|
|
3327
|
+
# Run the agent
|
|
3328
|
+
agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
|
|
3329
|
+
input=agent_input,
|
|
3330
|
+
session_id=session.session_id,
|
|
3331
|
+
dependencies=run_context.dependencies,
|
|
3332
|
+
session_state=run_context.session_state,
|
|
3333
|
+
stream=stream,
|
|
3334
|
+
) # type: ignore
|
|
3335
|
+
|
|
3336
|
+
# Check if the agent called the workflow tool
|
|
3337
|
+
workflow_executed = False
|
|
3338
|
+
if agent_response.messages:
|
|
3339
|
+
for message in agent_response.messages:
|
|
3340
|
+
if message.role == "assistant" and message.tool_calls:
|
|
3341
|
+
# Check if the tool call is specifically for run_workflow
|
|
3342
|
+
for tool_call in message.tool_calls:
|
|
3343
|
+
# Handle both dict and object formats
|
|
3344
|
+
if isinstance(tool_call, dict):
|
|
3345
|
+
tool_name = tool_call.get("function", {}).get("name", "")
|
|
3346
|
+
else:
|
|
3347
|
+
tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
|
|
1873
3348
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
workflow_id=self.id,
|
|
1880
|
-
workflow_name=self.name,
|
|
1881
|
-
created_at=int(datetime.now().timestamp()),
|
|
1882
|
-
status=RunStatus.pending,
|
|
1883
|
-
)
|
|
3349
|
+
if tool_name == "run_workflow":
|
|
3350
|
+
workflow_executed = True
|
|
3351
|
+
break
|
|
3352
|
+
if workflow_executed:
|
|
3353
|
+
break
|
|
1884
3354
|
|
|
1885
|
-
#
|
|
1886
|
-
|
|
1887
|
-
|
|
3355
|
+
# Handle direct answer case (no workflow execution)
|
|
3356
|
+
if not workflow_executed:
|
|
3357
|
+
# Create a new workflow run output for the direct answer
|
|
3358
|
+
run_id = str(uuid4())
|
|
3359
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3360
|
+
run_id=run_id,
|
|
3361
|
+
input=execution_input.input,
|
|
3362
|
+
session_id=session.session_id,
|
|
3363
|
+
workflow_id=self.id,
|
|
3364
|
+
workflow_name=self.name,
|
|
3365
|
+
created_at=int(datetime.now().timestamp()),
|
|
3366
|
+
content=agent_response.content,
|
|
3367
|
+
status=RunStatus.completed,
|
|
3368
|
+
workflow_agent_run=agent_response,
|
|
3369
|
+
)
|
|
1888
3370
|
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
audio=audio, # type: ignore
|
|
1894
|
-
images=images, # type: ignore
|
|
1895
|
-
videos=videos, # type: ignore
|
|
1896
|
-
files=files, # type: ignore
|
|
1897
|
-
)
|
|
3371
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3372
|
+
if agent_response:
|
|
3373
|
+
agent_response.parent_run_id = workflow_run_response.run_id
|
|
3374
|
+
agent_response.workflow_id = workflow_run_response.workflow_id
|
|
1898
3375
|
|
|
1899
|
-
|
|
3376
|
+
# Update the run in session
|
|
3377
|
+
session.upsert_run(run=workflow_run_response)
|
|
3378
|
+
if self._has_async_db():
|
|
3379
|
+
await self.asave_session(session=session)
|
|
3380
|
+
else:
|
|
3381
|
+
self.save_session(session=session)
|
|
1900
3382
|
|
|
1901
|
-
|
|
1902
|
-
"""Background execution with streaming and WebSocket broadcasting"""
|
|
1903
|
-
try:
|
|
1904
|
-
# Update status to RUNNING and save
|
|
1905
|
-
workflow_run_response.status = RunStatus.running
|
|
1906
|
-
self.save_session(session=workflow_session)
|
|
1907
|
-
|
|
1908
|
-
# Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
|
|
1909
|
-
async for event in self._aexecute_stream(
|
|
1910
|
-
execution_input=inputs,
|
|
1911
|
-
session=workflow_session,
|
|
1912
|
-
workflow_run_response=workflow_run_response,
|
|
1913
|
-
stream_intermediate_steps=stream_intermediate_steps,
|
|
1914
|
-
session_state=session_state,
|
|
1915
|
-
websocket_handler=websocket_handler,
|
|
1916
|
-
**kwargs,
|
|
1917
|
-
):
|
|
1918
|
-
# Events are automatically broadcast by _handle_event
|
|
1919
|
-
# We just consume them here to drive the execution
|
|
1920
|
-
pass
|
|
3383
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
1921
3384
|
|
|
1922
|
-
|
|
3385
|
+
return workflow_run_response
|
|
3386
|
+
else:
|
|
3387
|
+
# Workflow was executed by the tool
|
|
3388
|
+
logger.info("=" * 80)
|
|
3389
|
+
logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
|
|
3390
|
+
logger.info(" ➜ Workflow was executed, retrieving results...")
|
|
3391
|
+
logger.info("=" * 80)
|
|
3392
|
+
|
|
3393
|
+
log_debug("Reloading session from database to get the latest workflow run...")
|
|
3394
|
+
if self._has_async_db():
|
|
3395
|
+
reloaded_session = await self.aget_session(session_id=session.session_id)
|
|
3396
|
+
else:
|
|
3397
|
+
reloaded_session = self.get_session(session_id=session.session_id)
|
|
1923
3398
|
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
3399
|
+
if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
|
|
3400
|
+
# Get the last run (which is the one just created by the tool)
|
|
3401
|
+
last_run = reloaded_session.runs[-1]
|
|
3402
|
+
log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
|
|
3403
|
+
log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
|
|
1929
3404
|
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
loop.create_task(execute_workflow_background_stream())
|
|
3405
|
+
# Update the last run with workflow_agent_run
|
|
3406
|
+
last_run.workflow_agent_run = agent_response
|
|
1933
3407
|
|
|
1934
|
-
|
|
1935
|
-
|
|
3408
|
+
# Store the full agent RunOutput and establish parent-child relationship
|
|
3409
|
+
if agent_response:
|
|
3410
|
+
agent_response.parent_run_id = last_run.run_id
|
|
3411
|
+
agent_response.workflow_id = last_run.workflow_id
|
|
1936
3412
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
# Find the run by ID
|
|
1943
|
-
for run in session.runs:
|
|
1944
|
-
if run.run_id == run_id:
|
|
1945
|
-
return run
|
|
3413
|
+
# Save the reloaded session (which has the updated run)
|
|
3414
|
+
if self._has_async_db():
|
|
3415
|
+
await self.asave_session(session=reloaded_session)
|
|
3416
|
+
else:
|
|
3417
|
+
self.save_session(session=reloaded_session)
|
|
1946
3418
|
|
|
1947
|
-
|
|
3419
|
+
log_debug(f"Agent decision: workflow_executed={workflow_executed}")
|
|
3420
|
+
|
|
3421
|
+
# Return the last run directly (WRO2 from inner workflow)
|
|
3422
|
+
return last_run
|
|
3423
|
+
else:
|
|
3424
|
+
log_warning("Could not reload session or no runs found after workflow execution")
|
|
3425
|
+
# Return a placeholder error response
|
|
3426
|
+
return WorkflowRunOutput(
|
|
3427
|
+
run_id=str(uuid4()),
|
|
3428
|
+
input=execution_input.input,
|
|
3429
|
+
session_id=session.session_id,
|
|
3430
|
+
workflow_id=self.id,
|
|
3431
|
+
workflow_name=self.name,
|
|
3432
|
+
created_at=int(datetime.now().timestamp()),
|
|
3433
|
+
content="Error: Workflow execution failed",
|
|
3434
|
+
status=RunStatus.error,
|
|
3435
|
+
)
|
|
1948
3436
|
|
|
1949
3437
|
def cancel_run(self, run_id: str) -> bool:
|
|
1950
3438
|
"""Cancel a running workflow execution.
|
|
@@ -1963,6 +3451,7 @@ class Workflow:
|
|
|
1963
3451
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
|
|
1964
3452
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
1965
3453
|
user_id: Optional[str] = None,
|
|
3454
|
+
run_id: Optional[str] = None,
|
|
1966
3455
|
session_id: Optional[str] = None,
|
|
1967
3456
|
session_state: Optional[Dict[str, Any]] = None,
|
|
1968
3457
|
audio: Optional[List[Audio]] = None,
|
|
@@ -1970,8 +3459,10 @@ class Workflow:
|
|
|
1970
3459
|
videos: Optional[List[Video]] = None,
|
|
1971
3460
|
files: Optional[List[File]] = None,
|
|
1972
3461
|
stream: Literal[False] = False,
|
|
3462
|
+
stream_events: Optional[bool] = None,
|
|
1973
3463
|
stream_intermediate_steps: Optional[bool] = None,
|
|
1974
3464
|
background: Optional[bool] = False,
|
|
3465
|
+
background_tasks: Optional[Any] = None,
|
|
1975
3466
|
) -> WorkflowRunOutput: ...
|
|
1976
3467
|
|
|
1977
3468
|
@overload
|
|
@@ -1980,6 +3471,7 @@ class Workflow:
|
|
|
1980
3471
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
|
|
1981
3472
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
1982
3473
|
user_id: Optional[str] = None,
|
|
3474
|
+
run_id: Optional[str] = None,
|
|
1983
3475
|
session_id: Optional[str] = None,
|
|
1984
3476
|
session_state: Optional[Dict[str, Any]] = None,
|
|
1985
3477
|
audio: Optional[List[Audio]] = None,
|
|
@@ -1987,8 +3479,10 @@ class Workflow:
|
|
|
1987
3479
|
videos: Optional[List[Video]] = None,
|
|
1988
3480
|
files: Optional[List[File]] = None,
|
|
1989
3481
|
stream: Literal[True] = True,
|
|
3482
|
+
stream_events: Optional[bool] = None,
|
|
1990
3483
|
stream_intermediate_steps: Optional[bool] = None,
|
|
1991
3484
|
background: Optional[bool] = False,
|
|
3485
|
+
background_tasks: Optional[Any] = None,
|
|
1992
3486
|
) -> Iterator[WorkflowRunOutputEvent]: ...
|
|
1993
3487
|
|
|
1994
3488
|
def run(
|
|
@@ -1996,6 +3490,7 @@ class Workflow:
|
|
|
1996
3490
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
|
|
1997
3491
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
1998
3492
|
user_id: Optional[str] = None,
|
|
3493
|
+
run_id: Optional[str] = None,
|
|
1999
3494
|
session_id: Optional[str] = None,
|
|
2000
3495
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2001
3496
|
audio: Optional[List[Audio]] = None,
|
|
@@ -2003,11 +3498,19 @@ class Workflow:
|
|
|
2003
3498
|
videos: Optional[List[Video]] = None,
|
|
2004
3499
|
files: Optional[List[File]] = None,
|
|
2005
3500
|
stream: bool = False,
|
|
3501
|
+
stream_events: Optional[bool] = None,
|
|
2006
3502
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2007
3503
|
background: Optional[bool] = False,
|
|
3504
|
+
background_tasks: Optional[Any] = None,
|
|
2008
3505
|
**kwargs: Any,
|
|
2009
3506
|
) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
|
|
2010
3507
|
"""Execute the workflow synchronously with optional streaming"""
|
|
3508
|
+
if self._has_async_db():
|
|
3509
|
+
raise Exception("`run()` is not supported with an async DB. Please use `arun()`.")
|
|
3510
|
+
|
|
3511
|
+
# Set the id for the run and register it immediately for cancellation tracking
|
|
3512
|
+
run_id = run_id or str(uuid4())
|
|
3513
|
+
register_run(run_id)
|
|
2011
3514
|
|
|
2012
3515
|
input = self._validate_input(input)
|
|
2013
3516
|
if background:
|
|
@@ -2015,17 +3518,20 @@ class Workflow:
|
|
|
2015
3518
|
|
|
2016
3519
|
self._set_debug()
|
|
2017
3520
|
|
|
2018
|
-
run_id = str(uuid4())
|
|
2019
|
-
|
|
2020
3521
|
self.initialize_workflow()
|
|
2021
|
-
session_id, user_id
|
|
2022
|
-
session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
|
|
2023
|
-
)
|
|
3522
|
+
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
2024
3523
|
|
|
2025
3524
|
# Read existing session from database
|
|
2026
3525
|
workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
|
|
2027
3526
|
self._update_metadata(session=workflow_session)
|
|
2028
3527
|
|
|
3528
|
+
# Initialize session state
|
|
3529
|
+
session_state = self._initialize_session_state(
|
|
3530
|
+
session_state=session_state if session_state is not None else {},
|
|
3531
|
+
user_id=user_id,
|
|
3532
|
+
session_id=session_id,
|
|
3533
|
+
run_id=run_id,
|
|
3534
|
+
)
|
|
2029
3535
|
# Update session state from DB
|
|
2030
3536
|
session_state = self._load_session_state(session=workflow_session, session_state=session_state)
|
|
2031
3537
|
|
|
@@ -2033,11 +3539,13 @@ class Workflow:
|
|
|
2033
3539
|
|
|
2034
3540
|
# Use simple defaults
|
|
2035
3541
|
stream = stream or self.stream or False
|
|
2036
|
-
|
|
3542
|
+
stream_events = (stream_events or stream_intermediate_steps) or (
|
|
3543
|
+
self.stream_events or self.stream_intermediate_steps
|
|
3544
|
+
)
|
|
2037
3545
|
|
|
2038
|
-
# Can't
|
|
2039
|
-
if
|
|
2040
|
-
|
|
3546
|
+
# Can't stream events if streaming is disabled
|
|
3547
|
+
if stream is False:
|
|
3548
|
+
stream_events = False
|
|
2041
3549
|
|
|
2042
3550
|
log_debug(f"Stream: {stream}")
|
|
2043
3551
|
log_debug(f"Total steps: {self._get_step_count()}")
|
|
@@ -2045,16 +3553,6 @@ class Workflow:
|
|
|
2045
3553
|
# Prepare steps
|
|
2046
3554
|
self._prepare_steps()
|
|
2047
3555
|
|
|
2048
|
-
# Create workflow run response that will be updated by reference
|
|
2049
|
-
workflow_run_response = WorkflowRunOutput(
|
|
2050
|
-
run_id=run_id,
|
|
2051
|
-
input=input,
|
|
2052
|
-
session_id=session_id,
|
|
2053
|
-
workflow_id=self.id,
|
|
2054
|
-
workflow_name=self.name,
|
|
2055
|
-
created_at=int(datetime.now().timestamp()),
|
|
2056
|
-
)
|
|
2057
|
-
|
|
2058
3556
|
inputs = WorkflowExecutionInput(
|
|
2059
3557
|
input=input,
|
|
2060
3558
|
additional_data=additional_data,
|
|
@@ -2069,13 +3567,47 @@ class Workflow:
|
|
|
2069
3567
|
|
|
2070
3568
|
self.update_agents_and_teams_session_info()
|
|
2071
3569
|
|
|
3570
|
+
# Initialize run context
|
|
3571
|
+
run_context = RunContext(
|
|
3572
|
+
run_id=run_id,
|
|
3573
|
+
session_id=session_id,
|
|
3574
|
+
user_id=user_id,
|
|
3575
|
+
session_state=session_state,
|
|
3576
|
+
)
|
|
3577
|
+
|
|
3578
|
+
# Execute workflow agent if configured
|
|
3579
|
+
if self.agent is not None:
|
|
3580
|
+
return self._execute_workflow_agent(
|
|
3581
|
+
user_input=input, # type: ignore
|
|
3582
|
+
session=workflow_session,
|
|
3583
|
+
execution_input=inputs,
|
|
3584
|
+
run_context=run_context,
|
|
3585
|
+
stream=stream,
|
|
3586
|
+
**kwargs,
|
|
3587
|
+
)
|
|
3588
|
+
|
|
3589
|
+
# Create workflow run response for regular workflow execution
|
|
3590
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3591
|
+
run_id=run_id,
|
|
3592
|
+
input=input,
|
|
3593
|
+
session_id=session_id,
|
|
3594
|
+
workflow_id=self.id,
|
|
3595
|
+
workflow_name=self.name,
|
|
3596
|
+
created_at=int(datetime.now().timestamp()),
|
|
3597
|
+
)
|
|
3598
|
+
|
|
3599
|
+
# Start the run metrics timer
|
|
3600
|
+
workflow_run_response.metrics = WorkflowMetrics(steps={})
|
|
3601
|
+
workflow_run_response.metrics.start_timer()
|
|
3602
|
+
|
|
2072
3603
|
if stream:
|
|
2073
3604
|
return self._execute_stream(
|
|
2074
3605
|
session=workflow_session,
|
|
2075
3606
|
execution_input=inputs, # type: ignore[arg-type]
|
|
2076
3607
|
workflow_run_response=workflow_run_response,
|
|
2077
|
-
|
|
2078
|
-
|
|
3608
|
+
stream_events=stream_events,
|
|
3609
|
+
run_context=run_context,
|
|
3610
|
+
background_tasks=background_tasks,
|
|
2079
3611
|
**kwargs,
|
|
2080
3612
|
)
|
|
2081
3613
|
else:
|
|
@@ -2083,7 +3615,8 @@ class Workflow:
|
|
|
2083
3615
|
session=workflow_session,
|
|
2084
3616
|
execution_input=inputs, # type: ignore[arg-type]
|
|
2085
3617
|
workflow_run_response=workflow_run_response,
|
|
2086
|
-
|
|
3618
|
+
run_context=run_context,
|
|
3619
|
+
background_tasks=background_tasks,
|
|
2087
3620
|
**kwargs,
|
|
2088
3621
|
)
|
|
2089
3622
|
|
|
@@ -2093,6 +3626,7 @@ class Workflow:
|
|
|
2093
3626
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
|
|
2094
3627
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
2095
3628
|
user_id: Optional[str] = None,
|
|
3629
|
+
run_id: Optional[str] = None,
|
|
2096
3630
|
session_id: Optional[str] = None,
|
|
2097
3631
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2098
3632
|
audio: Optional[List[Audio]] = None,
|
|
@@ -2100,17 +3634,20 @@ class Workflow:
|
|
|
2100
3634
|
videos: Optional[List[Video]] = None,
|
|
2101
3635
|
files: Optional[List[File]] = None,
|
|
2102
3636
|
stream: Literal[False] = False,
|
|
3637
|
+
stream_events: Optional[bool] = None,
|
|
2103
3638
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2104
3639
|
background: Optional[bool] = False,
|
|
2105
3640
|
websocket: Optional[WebSocket] = None,
|
|
3641
|
+
background_tasks: Optional[Any] = None,
|
|
2106
3642
|
) -> WorkflowRunOutput: ...
|
|
2107
3643
|
|
|
2108
3644
|
@overload
|
|
2109
|
-
|
|
3645
|
+
def arun(
|
|
2110
3646
|
self,
|
|
2111
3647
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
|
|
2112
3648
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
2113
3649
|
user_id: Optional[str] = None,
|
|
3650
|
+
run_id: Optional[str] = None,
|
|
2114
3651
|
session_id: Optional[str] = None,
|
|
2115
3652
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2116
3653
|
audio: Optional[List[Audio]] = None,
|
|
@@ -2118,16 +3655,19 @@ class Workflow:
|
|
|
2118
3655
|
videos: Optional[List[Video]] = None,
|
|
2119
3656
|
files: Optional[List[File]] = None,
|
|
2120
3657
|
stream: Literal[True] = True,
|
|
3658
|
+
stream_events: Optional[bool] = None,
|
|
2121
3659
|
stream_intermediate_steps: Optional[bool] = None,
|
|
2122
3660
|
background: Optional[bool] = False,
|
|
2123
3661
|
websocket: Optional[WebSocket] = None,
|
|
3662
|
+
background_tasks: Optional[Any] = None,
|
|
2124
3663
|
) -> AsyncIterator[WorkflowRunOutputEvent]: ...
|
|
2125
3664
|
|
|
2126
|
-
|
|
3665
|
+
def arun( # type: ignore
|
|
2127
3666
|
self,
|
|
2128
3667
|
input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
|
|
2129
3668
|
additional_data: Optional[Dict[str, Any]] = None,
|
|
2130
3669
|
user_id: Optional[str] = None,
|
|
3670
|
+
run_id: Optional[str] = None,
|
|
2131
3671
|
session_id: Optional[str] = None,
|
|
2132
3672
|
session_state: Optional[Dict[str, Any]] = None,
|
|
2133
3673
|
audio: Optional[List[Audio]] = None,
|
|
@@ -2135,9 +3675,11 @@ class Workflow:
|
|
|
2135
3675
|
videos: Optional[List[Video]] = None,
|
|
2136
3676
|
files: Optional[List[File]] = None,
|
|
2137
3677
|
stream: bool = False,
|
|
3678
|
+
stream_events: Optional[bool] = None,
|
|
2138
3679
|
stream_intermediate_steps: Optional[bool] = False,
|
|
2139
3680
|
background: Optional[bool] = False,
|
|
2140
3681
|
websocket: Optional[WebSocket] = None,
|
|
3682
|
+
background_tasks: Optional[Any] = None,
|
|
2141
3683
|
**kwargs: Any,
|
|
2142
3684
|
) -> Union[WorkflowRunOutput, AsyncIterator[WorkflowRunOutputEvent]]:
|
|
2143
3685
|
"""Execute the workflow synchronously with optional streaming"""
|
|
@@ -2152,8 +3694,17 @@ class Workflow:
|
|
|
2152
3694
|
|
|
2153
3695
|
if background:
|
|
2154
3696
|
if stream and websocket:
|
|
3697
|
+
# Consider both stream_events and stream_intermediate_steps (deprecated)
|
|
3698
|
+
if stream_intermediate_steps is not None:
|
|
3699
|
+
warnings.warn(
|
|
3700
|
+
"The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
|
|
3701
|
+
DeprecationWarning,
|
|
3702
|
+
stacklevel=2,
|
|
3703
|
+
)
|
|
3704
|
+
stream_events = stream_events or stream_intermediate_steps or False
|
|
3705
|
+
|
|
2155
3706
|
# Background + Streaming + WebSocket = Real-time events
|
|
2156
|
-
return
|
|
3707
|
+
return self._arun_background_stream( # type: ignore
|
|
2157
3708
|
input=input,
|
|
2158
3709
|
additional_data=additional_data,
|
|
2159
3710
|
user_id=user_id,
|
|
@@ -2163,7 +3714,7 @@ class Workflow:
|
|
|
2163
3714
|
images=images,
|
|
2164
3715
|
videos=videos,
|
|
2165
3716
|
files=files,
|
|
2166
|
-
|
|
3717
|
+
stream_events=stream_events,
|
|
2167
3718
|
websocket_handler=websocket_handler,
|
|
2168
3719
|
**kwargs,
|
|
2169
3720
|
)
|
|
@@ -2172,7 +3723,7 @@ class Workflow:
|
|
|
2172
3723
|
raise ValueError("Background streaming execution requires a WebSocket for real-time events")
|
|
2173
3724
|
else:
|
|
2174
3725
|
# Background + Non-streaming = Polling (existing)
|
|
2175
|
-
return
|
|
3726
|
+
return self._arun_background( # type: ignore
|
|
2176
3727
|
input=input,
|
|
2177
3728
|
additional_data=additional_data,
|
|
2178
3729
|
user_id=user_id,
|
|
@@ -2187,45 +3738,38 @@ class Workflow:
|
|
|
2187
3738
|
|
|
2188
3739
|
self._set_debug()
|
|
2189
3740
|
|
|
2190
|
-
|
|
3741
|
+
# Set the id for the run and register it immediately for cancellation tracking
|
|
3742
|
+
run_id = run_id or str(uuid4())
|
|
3743
|
+
register_run(run_id)
|
|
2191
3744
|
|
|
2192
3745
|
self.initialize_workflow()
|
|
2193
|
-
session_id, user_id
|
|
2194
|
-
session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
|
|
2195
|
-
)
|
|
2196
|
-
|
|
2197
|
-
# Read existing session from database
|
|
2198
|
-
workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
|
|
2199
|
-
self._update_metadata(session=workflow_session)
|
|
3746
|
+
session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
|
|
2200
3747
|
|
|
2201
|
-
#
|
|
2202
|
-
|
|
3748
|
+
# Initialize run context
|
|
3749
|
+
run_context = RunContext(
|
|
3750
|
+
run_id=run_id,
|
|
3751
|
+
session_id=session_id,
|
|
3752
|
+
user_id=user_id,
|
|
3753
|
+
session_state=session_state,
|
|
3754
|
+
)
|
|
2203
3755
|
|
|
2204
3756
|
log_debug(f"Async Workflow Run Start: {self.name}", center=True)
|
|
2205
3757
|
|
|
2206
3758
|
# Use simple defaults
|
|
2207
3759
|
stream = stream or self.stream or False
|
|
2208
|
-
|
|
3760
|
+
stream_events = (stream_events or stream_intermediate_steps) or (
|
|
3761
|
+
self.stream_events or self.stream_intermediate_steps
|
|
3762
|
+
)
|
|
2209
3763
|
|
|
2210
|
-
# Can't
|
|
2211
|
-
if
|
|
2212
|
-
|
|
3764
|
+
# Can't stream events if streaming is disabled
|
|
3765
|
+
if stream is False:
|
|
3766
|
+
stream_events = False
|
|
2213
3767
|
|
|
2214
3768
|
log_debug(f"Stream: {stream}")
|
|
2215
3769
|
|
|
2216
3770
|
# Prepare steps
|
|
2217
3771
|
self._prepare_steps()
|
|
2218
3772
|
|
|
2219
|
-
# Create workflow run response that will be updated by reference
|
|
2220
|
-
workflow_run_response = WorkflowRunOutput(
|
|
2221
|
-
run_id=run_id,
|
|
2222
|
-
input=input,
|
|
2223
|
-
session_id=session_id,
|
|
2224
|
-
workflow_id=self.id,
|
|
2225
|
-
workflow_name=self.name,
|
|
2226
|
-
created_at=int(datetime.now().timestamp()),
|
|
2227
|
-
)
|
|
2228
|
-
|
|
2229
3773
|
inputs = WorkflowExecutionInput(
|
|
2230
3774
|
input=input,
|
|
2231
3775
|
additional_data=additional_data,
|
|
@@ -2240,25 +3784,54 @@ class Workflow:
|
|
|
2240
3784
|
|
|
2241
3785
|
self.update_agents_and_teams_session_info()
|
|
2242
3786
|
|
|
3787
|
+
if self.agent is not None:
|
|
3788
|
+
return self._aexecute_workflow_agent( # type: ignore
|
|
3789
|
+
user_input=input, # type: ignore
|
|
3790
|
+
execution_input=inputs,
|
|
3791
|
+
run_context=run_context,
|
|
3792
|
+
stream=stream,
|
|
3793
|
+
**kwargs,
|
|
3794
|
+
)
|
|
3795
|
+
|
|
3796
|
+
# Create workflow run response for regular workflow execution
|
|
3797
|
+
workflow_run_response = WorkflowRunOutput(
|
|
3798
|
+
run_id=run_id,
|
|
3799
|
+
input=input,
|
|
3800
|
+
session_id=session_id,
|
|
3801
|
+
workflow_id=self.id,
|
|
3802
|
+
workflow_name=self.name,
|
|
3803
|
+
created_at=int(datetime.now().timestamp()),
|
|
3804
|
+
)
|
|
3805
|
+
|
|
3806
|
+
# Start the run metrics timer
|
|
3807
|
+
workflow_run_response.metrics = WorkflowMetrics(steps={})
|
|
3808
|
+
workflow_run_response.metrics.start_timer()
|
|
3809
|
+
|
|
2243
3810
|
if stream:
|
|
2244
|
-
return self._aexecute_stream(
|
|
3811
|
+
return self._aexecute_stream( # type: ignore
|
|
2245
3812
|
execution_input=inputs,
|
|
2246
3813
|
workflow_run_response=workflow_run_response,
|
|
2247
|
-
|
|
2248
|
-
|
|
3814
|
+
session_id=session_id,
|
|
3815
|
+
user_id=user_id,
|
|
3816
|
+
stream_events=stream_events,
|
|
2249
3817
|
websocket=websocket,
|
|
2250
3818
|
files=files,
|
|
2251
3819
|
session_state=session_state,
|
|
3820
|
+
run_context=run_context,
|
|
3821
|
+
background_tasks=background_tasks,
|
|
2252
3822
|
**kwargs,
|
|
2253
3823
|
)
|
|
2254
3824
|
else:
|
|
2255
|
-
return
|
|
3825
|
+
return self._aexecute( # type: ignore
|
|
2256
3826
|
execution_input=inputs,
|
|
2257
3827
|
workflow_run_response=workflow_run_response,
|
|
2258
|
-
|
|
3828
|
+
session_id=session_id,
|
|
3829
|
+
user_id=user_id,
|
|
2259
3830
|
websocket=websocket,
|
|
2260
3831
|
files=files,
|
|
2261
3832
|
session_state=session_state,
|
|
3833
|
+
run_context=run_context,
|
|
3834
|
+
background_tasks=background_tasks,
|
|
2262
3835
|
**kwargs,
|
|
2263
3836
|
)
|
|
2264
3837
|
|
|
@@ -2270,7 +3843,7 @@ class Workflow:
|
|
|
2270
3843
|
if callable(step) and hasattr(step, "__name__"):
|
|
2271
3844
|
step_name = step.__name__
|
|
2272
3845
|
log_debug(f"Step {i + 1}: Wrapping callable function '{step_name}'")
|
|
2273
|
-
prepared_steps.append(Step(name=step_name, description="User-defined callable step", executor=step))
|
|
3846
|
+
prepared_steps.append(Step(name=step_name, description="User-defined callable step", executor=step)) # type: ignore
|
|
2274
3847
|
elif isinstance(step, Agent):
|
|
2275
3848
|
step_name = step.name or f"step_{i + 1}"
|
|
2276
3849
|
log_debug(f"Step {i + 1}: Agent '{step_name}'")
|
|
@@ -2279,6 +3852,12 @@ class Workflow:
|
|
|
2279
3852
|
step_name = step.name or f"step_{i + 1}"
|
|
2280
3853
|
log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
|
|
2281
3854
|
prepared_steps.append(Step(name=step_name, description=step.description, team=step))
|
|
3855
|
+
elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
|
|
3856
|
+
log_warning(
|
|
3857
|
+
f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
|
|
3858
|
+
"but no database is configured in the Workflow. "
|
|
3859
|
+
"History won't be persisted. Add a database to persist runs across executions."
|
|
3860
|
+
)
|
|
2282
3861
|
elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
|
|
2283
3862
|
step_type = type(step).__name__
|
|
2284
3863
|
step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")
|
|
@@ -2301,7 +3880,6 @@ class Workflow:
|
|
|
2301
3880
|
videos: Optional[List[Video]] = None,
|
|
2302
3881
|
files: Optional[List[File]] = None,
|
|
2303
3882
|
stream: Optional[bool] = None,
|
|
2304
|
-
stream_intermediate_steps: Optional[bool] = None,
|
|
2305
3883
|
markdown: bool = True,
|
|
2306
3884
|
show_time: bool = True,
|
|
2307
3885
|
show_step_details: bool = True,
|
|
@@ -2318,19 +3896,21 @@ class Workflow:
|
|
|
2318
3896
|
audio: Audio input
|
|
2319
3897
|
images: Image input
|
|
2320
3898
|
videos: Video input
|
|
3899
|
+
files: File input
|
|
2321
3900
|
stream: Whether to stream the response content
|
|
2322
|
-
stream_intermediate_steps: Whether to stream intermediate steps
|
|
2323
3901
|
markdown: Whether to render content as markdown
|
|
2324
3902
|
show_time: Whether to show execution time
|
|
2325
3903
|
show_step_details: Whether to show individual step outputs
|
|
2326
3904
|
console: Rich console instance (optional)
|
|
2327
3905
|
"""
|
|
3906
|
+
if self._has_async_db():
|
|
3907
|
+
raise Exception("`print_response()` is not supported with an async DB. Please use `aprint_response()`.")
|
|
2328
3908
|
|
|
2329
3909
|
if stream is None:
|
|
2330
3910
|
stream = self.stream or False
|
|
2331
3911
|
|
|
2332
|
-
if
|
|
2333
|
-
|
|
3912
|
+
if "stream_events" in kwargs:
|
|
3913
|
+
kwargs.pop("stream_events")
|
|
2334
3914
|
|
|
2335
3915
|
if stream:
|
|
2336
3916
|
print_response_stream(
|
|
@@ -2343,7 +3923,7 @@ class Workflow:
|
|
|
2343
3923
|
images=images,
|
|
2344
3924
|
videos=videos,
|
|
2345
3925
|
files=files,
|
|
2346
|
-
|
|
3926
|
+
stream_events=True,
|
|
2347
3927
|
markdown=markdown,
|
|
2348
3928
|
show_time=show_time,
|
|
2349
3929
|
show_step_details=show_step_details,
|
|
@@ -2379,7 +3959,6 @@ class Workflow:
|
|
|
2379
3959
|
videos: Optional[List[Video]] = None,
|
|
2380
3960
|
files: Optional[List[File]] = None,
|
|
2381
3961
|
stream: Optional[bool] = None,
|
|
2382
|
-
stream_intermediate_steps: Optional[bool] = None,
|
|
2383
3962
|
markdown: bool = True,
|
|
2384
3963
|
show_time: bool = True,
|
|
2385
3964
|
show_step_details: bool = True,
|
|
@@ -2396,7 +3975,7 @@ class Workflow:
|
|
|
2396
3975
|
audio: Audio input
|
|
2397
3976
|
images: Image input
|
|
2398
3977
|
videos: Video input
|
|
2399
|
-
|
|
3978
|
+
files: Files input
|
|
2400
3979
|
stream: Whether to stream the response content
|
|
2401
3980
|
markdown: Whether to render content as markdown
|
|
2402
3981
|
show_time: Whether to show execution time
|
|
@@ -2406,8 +3985,8 @@ class Workflow:
|
|
|
2406
3985
|
if stream is None:
|
|
2407
3986
|
stream = self.stream or False
|
|
2408
3987
|
|
|
2409
|
-
if
|
|
2410
|
-
|
|
3988
|
+
if "stream_events" in kwargs:
|
|
3989
|
+
kwargs.pop("stream_events")
|
|
2411
3990
|
|
|
2412
3991
|
if stream:
|
|
2413
3992
|
await aprint_response_stream(
|
|
@@ -2420,7 +3999,7 @@ class Workflow:
|
|
|
2420
3999
|
images=images,
|
|
2421
4000
|
videos=videos,
|
|
2422
4001
|
files=files,
|
|
2423
|
-
|
|
4002
|
+
stream_events=True,
|
|
2424
4003
|
markdown=markdown,
|
|
2425
4004
|
show_time=show_time,
|
|
2426
4005
|
show_step_details=show_step_details,
|
|
@@ -2490,7 +4069,7 @@ class Workflow:
|
|
|
2490
4069
|
step_dict["team"] = step.team if hasattr(step, "team") else None # type: ignore
|
|
2491
4070
|
|
|
2492
4071
|
# Handle nested steps for Router/Loop
|
|
2493
|
-
if isinstance(step,
|
|
4072
|
+
if isinstance(step, Router):
|
|
2494
4073
|
step_dict["steps"] = (
|
|
2495
4074
|
[serialize_step(step) for step in step.choices] if hasattr(step, "choices") else None
|
|
2496
4075
|
)
|
|
@@ -2546,7 +4125,7 @@ class Workflow:
|
|
|
2546
4125
|
|
|
2547
4126
|
# If workflow has metrics, convert and add them to session metrics
|
|
2548
4127
|
if workflow_run_response.metrics:
|
|
2549
|
-
run_session_metrics = self._calculate_session_metrics_from_workflow_metrics(workflow_run_response.metrics)
|
|
4128
|
+
run_session_metrics = self._calculate_session_metrics_from_workflow_metrics(workflow_run_response.metrics) # type: ignore[arg-type]
|
|
2550
4129
|
|
|
2551
4130
|
session_metrics += run_session_metrics
|
|
2552
4131
|
|
|
@@ -2557,6 +4136,18 @@ class Workflow:
|
|
|
2557
4136
|
session.session_data = {}
|
|
2558
4137
|
session.session_data["session_metrics"] = session_metrics.to_dict()
|
|
2559
4138
|
|
|
4139
|
+
async def aget_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
|
|
4140
|
+
"""Get the session metrics for the given session ID and user ID."""
|
|
4141
|
+
session_id = session_id or self.session_id
|
|
4142
|
+
if session_id is None:
|
|
4143
|
+
raise Exception("Session ID is required")
|
|
4144
|
+
|
|
4145
|
+
session = await self.aget_session(session_id=session_id) # type: ignore
|
|
4146
|
+
if session is None:
|
|
4147
|
+
raise Exception("Session not found")
|
|
4148
|
+
|
|
4149
|
+
return self._get_session_metrics(session=session)
|
|
4150
|
+
|
|
2560
4151
|
def get_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
|
|
2561
4152
|
"""Get the session metrics for the given session ID and user ID."""
|
|
2562
4153
|
session_id = session_id or self.session_id
|
|
@@ -2585,10 +4176,60 @@ class Workflow:
|
|
|
2585
4176
|
|
|
2586
4177
|
# If it's a team, update all members
|
|
2587
4178
|
if hasattr(active_executor, "members"):
|
|
2588
|
-
for member in active_executor.members:
|
|
4179
|
+
for member in active_executor.members: # type: ignore
|
|
2589
4180
|
if hasattr(member, "workflow_id"):
|
|
2590
4181
|
member.workflow_id = self.id
|
|
2591
4182
|
|
|
4183
|
+
def propagate_run_hooks_in_background(self, run_in_background: bool = True) -> None:
|
|
4184
|
+
"""
|
|
4185
|
+
Propagate _run_hooks_in_background setting to this workflow and all agents/teams in steps.
|
|
4186
|
+
|
|
4187
|
+
This method sets _run_hooks_in_background on the workflow and all agents/teams
|
|
4188
|
+
within its steps, including nested teams and their members.
|
|
4189
|
+
|
|
4190
|
+
Args:
|
|
4191
|
+
run_in_background: Whether hooks should run in background. Defaults to True.
|
|
4192
|
+
"""
|
|
4193
|
+
self._run_hooks_in_background = run_in_background
|
|
4194
|
+
|
|
4195
|
+
if not self.steps or callable(self.steps):
|
|
4196
|
+
return
|
|
4197
|
+
|
|
4198
|
+
steps_list = self.steps.steps if isinstance(self.steps, Steps) else self.steps
|
|
4199
|
+
|
|
4200
|
+
for step in steps_list:
|
|
4201
|
+
self._propagate_hooks_to_step(step, run_in_background)
|
|
4202
|
+
|
|
4203
|
+
def _propagate_hooks_to_step(self, step: Any, run_in_background: bool) -> None:
|
|
4204
|
+
"""Recursively propagate _run_hooks_in_background to a step and its nested content."""
|
|
4205
|
+
# Handle Step objects with active executor
|
|
4206
|
+
if hasattr(step, "active_executor") and step.active_executor:
|
|
4207
|
+
executor = step.active_executor
|
|
4208
|
+
# If it's a team, use its propagation method
|
|
4209
|
+
if hasattr(executor, "propagate_run_hooks_in_background"):
|
|
4210
|
+
executor.propagate_run_hooks_in_background(run_in_background)
|
|
4211
|
+
elif hasattr(executor, "_run_hooks_in_background"):
|
|
4212
|
+
executor._run_hooks_in_background = run_in_background
|
|
4213
|
+
|
|
4214
|
+
# Handle agent/team directly on step
|
|
4215
|
+
if hasattr(step, "agent") and step.agent:
|
|
4216
|
+
if hasattr(step.agent, "_run_hooks_in_background"):
|
|
4217
|
+
step.agent._run_hooks_in_background = run_in_background
|
|
4218
|
+
if hasattr(step, "team") and step.team:
|
|
4219
|
+
# Use team's method to propagate to all nested members
|
|
4220
|
+
if hasattr(step.team, "propagate_run_hooks_in_background"):
|
|
4221
|
+
step.team.propagate_run_hooks_in_background(run_in_background)
|
|
4222
|
+
elif hasattr(step.team, "_run_hooks_in_background"):
|
|
4223
|
+
step.team._run_hooks_in_background = run_in_background
|
|
4224
|
+
|
|
4225
|
+
# Handle nested primitives - check 'steps' and 'choices' attributes
|
|
4226
|
+
for attr_name in ["steps", "choices"]:
|
|
4227
|
+
if hasattr(step, attr_name):
|
|
4228
|
+
attr_value = getattr(step, attr_name)
|
|
4229
|
+
if attr_value and isinstance(attr_value, list):
|
|
4230
|
+
for nested_step in attr_value:
|
|
4231
|
+
self._propagate_hooks_to_step(nested_step, run_in_background)
|
|
4232
|
+
|
|
2592
4233
|
###########################################################################
|
|
2593
4234
|
# Telemetry functions
|
|
2594
4235
|
###########################################################################
|
|
@@ -2632,3 +4273,139 @@ class Workflow:
|
|
|
2632
4273
|
)
|
|
2633
4274
|
except Exception as e:
|
|
2634
4275
|
log_debug(f"Could not create Workflow run telemetry event: {e}")
|
|
4276
|
+
|
|
4277
|
+
def cli_app(
|
|
4278
|
+
self,
|
|
4279
|
+
input: Optional[str] = None,
|
|
4280
|
+
session_id: Optional[str] = None,
|
|
4281
|
+
user_id: Optional[str] = None,
|
|
4282
|
+
user: str = "User",
|
|
4283
|
+
emoji: str = ":technologist:",
|
|
4284
|
+
stream: Optional[bool] = None,
|
|
4285
|
+
markdown: bool = True,
|
|
4286
|
+
show_time: bool = True,
|
|
4287
|
+
show_step_details: bool = True,
|
|
4288
|
+
exit_on: Optional[List[str]] = None,
|
|
4289
|
+
**kwargs: Any,
|
|
4290
|
+
) -> None:
|
|
4291
|
+
"""
|
|
4292
|
+
Run an interactive command-line interface to interact with the workflow.
|
|
4293
|
+
|
|
4294
|
+
This method creates a CLI interface that allows users to interact with the workflow
|
|
4295
|
+
either by providing a single input or through continuous interactive prompts.
|
|
4296
|
+
|
|
4297
|
+
Arguments:
|
|
4298
|
+
input: Optional initial input to process before starting interactive mode.
|
|
4299
|
+
session_id: Optional session identifier for maintaining conversation context.
|
|
4300
|
+
user_id: Optional user identifier for tracking user-specific data.
|
|
4301
|
+
user: Display name for the user in the CLI prompt. Defaults to "User".
|
|
4302
|
+
emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
|
|
4303
|
+
stream: Whether to stream the workflow response. If None, uses workflow default.
|
|
4304
|
+
markdown: Whether to render output as markdown. Defaults to True.
|
|
4305
|
+
show_time: Whether to display timestamps in the output. Defaults to True.
|
|
4306
|
+
show_step_details: Whether to show detailed step information. Defaults to True.
|
|
4307
|
+
exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
|
|
4308
|
+
**kwargs: Additional keyword arguments passed to the workflow's print_response method.
|
|
4309
|
+
|
|
4310
|
+
Returns:
|
|
4311
|
+
None: This method runs interactively and does not return a value.
|
|
4312
|
+
"""
|
|
4313
|
+
|
|
4314
|
+
from rich.prompt import Prompt
|
|
4315
|
+
|
|
4316
|
+
if input:
|
|
4317
|
+
self.print_response(
|
|
4318
|
+
input=input,
|
|
4319
|
+
stream=stream,
|
|
4320
|
+
markdown=markdown,
|
|
4321
|
+
show_time=show_time,
|
|
4322
|
+
show_step_details=show_step_details,
|
|
4323
|
+
user_id=user_id,
|
|
4324
|
+
session_id=session_id,
|
|
4325
|
+
**kwargs,
|
|
4326
|
+
)
|
|
4327
|
+
|
|
4328
|
+
_exit_on = exit_on or ["exit", "quit", "bye", "stop"]
|
|
4329
|
+
while True:
|
|
4330
|
+
message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
|
|
4331
|
+
if message in _exit_on:
|
|
4332
|
+
break
|
|
4333
|
+
|
|
4334
|
+
self.print_response(
|
|
4335
|
+
input=message,
|
|
4336
|
+
stream=stream,
|
|
4337
|
+
markdown=markdown,
|
|
4338
|
+
show_time=show_time,
|
|
4339
|
+
show_step_details=show_step_details,
|
|
4340
|
+
user_id=user_id,
|
|
4341
|
+
session_id=session_id,
|
|
4342
|
+
**kwargs,
|
|
4343
|
+
)
|
|
4344
|
+
|
|
4345
|
+
async def acli_app(
|
|
4346
|
+
self,
|
|
4347
|
+
input: Optional[str] = None,
|
|
4348
|
+
session_id: Optional[str] = None,
|
|
4349
|
+
user_id: Optional[str] = None,
|
|
4350
|
+
user: str = "User",
|
|
4351
|
+
emoji: str = ":technologist:",
|
|
4352
|
+
stream: Optional[bool] = None,
|
|
4353
|
+
markdown: bool = True,
|
|
4354
|
+
show_time: bool = True,
|
|
4355
|
+
show_step_details: bool = True,
|
|
4356
|
+
exit_on: Optional[List[str]] = None,
|
|
4357
|
+
**kwargs: Any,
|
|
4358
|
+
) -> None:
|
|
4359
|
+
"""
|
|
4360
|
+
Run an interactive command-line interface to interact with the workflow.
|
|
4361
|
+
|
|
4362
|
+
This method creates a CLI interface that allows users to interact with the workflow
|
|
4363
|
+
either by providing a single input or through continuous interactive prompts.
|
|
4364
|
+
|
|
4365
|
+
Arguments:
|
|
4366
|
+
input: Optional initial input to process before starting interactive mode.
|
|
4367
|
+
session_id: Optional session identifier for maintaining conversation context.
|
|
4368
|
+
user_id: Optional user identifier for tracking user-specific data.
|
|
4369
|
+
user: Display name for the user in the CLI prompt. Defaults to "User".
|
|
4370
|
+
emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
|
|
4371
|
+
stream: Whether to stream the workflow response. If None, uses workflow default.
|
|
4372
|
+
markdown: Whether to render output as markdown. Defaults to True.
|
|
4373
|
+
show_time: Whether to display timestamps in the output. Defaults to True.
|
|
4374
|
+
show_step_details: Whether to show detailed step information. Defaults to True.
|
|
4375
|
+
exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
|
|
4376
|
+
**kwargs: Additional keyword arguments passed to the workflow's print_response method.
|
|
4377
|
+
|
|
4378
|
+
Returns:
|
|
4379
|
+
None: This method runs interactively and does not return a value.
|
|
4380
|
+
"""
|
|
4381
|
+
|
|
4382
|
+
from rich.prompt import Prompt
|
|
4383
|
+
|
|
4384
|
+
if input:
|
|
4385
|
+
await self.aprint_response(
|
|
4386
|
+
input=input,
|
|
4387
|
+
stream=stream,
|
|
4388
|
+
markdown=markdown,
|
|
4389
|
+
show_time=show_time,
|
|
4390
|
+
show_step_details=show_step_details,
|
|
4391
|
+
user_id=user_id,
|
|
4392
|
+
session_id=session_id,
|
|
4393
|
+
**kwargs,
|
|
4394
|
+
)
|
|
4395
|
+
|
|
4396
|
+
_exit_on = exit_on or ["exit", "quit", "bye", "stop"]
|
|
4397
|
+
while True:
|
|
4398
|
+
message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
|
|
4399
|
+
if message in _exit_on:
|
|
4400
|
+
break
|
|
4401
|
+
|
|
4402
|
+
await self.aprint_response(
|
|
4403
|
+
input=message,
|
|
4404
|
+
stream=stream,
|
|
4405
|
+
markdown=markdown,
|
|
4406
|
+
show_time=show_time,
|
|
4407
|
+
show_step_details=show_step_details,
|
|
4408
|
+
user_id=user_id,
|
|
4409
|
+
session_id=session_id,
|
|
4410
|
+
**kwargs,
|
|
4411
|
+
)
|