mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 07:20:34 +08:00
merge: integrate reviewed official Graphon 0.8 compatibility
This commit is contained in:
@@ -315,6 +315,10 @@ jobs:
|
||||
|
||||
JUNIT = "builder-confirmed-tool-units.xml"
|
||||
FILES = [
|
||||
"api/tests/unit_tests/core/workflow/test_graphon_v08_scope_regressions.py",
|
||||
"api/tests/unit_tests/core/workflow/test_graphon_v08_contract.py",
|
||||
"api/tests/unit_tests/core/workflow/context/test_execution_context.py",
|
||||
"api/tests/unit_tests/core/workflow/context/test_flask_app_context.py",
|
||||
"api/tests/unit_tests/services/dify_builder/test_reconcile.py",
|
||||
"api/tests/unit_tests/core/app/apps/test_builder_execution_admission.py",
|
||||
"api/tests/unit_tests/services/dify_builder/test_real_call_consent_service.py",
|
||||
@@ -403,6 +407,93 @@ jobs:
|
||||
print(f"COLLECTED {module}: {count} unit cases", flush=True)
|
||||
if count == 0:
|
||||
problems.append(f"missing unit module {module}")
|
||||
graphon_v08_expected = {
|
||||
"tests.unit_tests.core.workflow.test_graphon_v08_scope_regressions": {
|
||||
"test_actual_dify_v07_envelope_resumes_exact_graph_through_entry": 4,
|
||||
"test_actual_single_worker_keeps_engine_adapter_for_second_node": 2,
|
||||
"test_background_summary_keeps_real_image_conversion_without_ambient_runtime": 1,
|
||||
"test_capture_restores_worker_context_and_adapter": 8,
|
||||
"test_captured_otel_context_is_reversible_on_exception": 2,
|
||||
"test_dify_application_context_binds_rendering_for_request_and_background": 2,
|
||||
"test_direct_host_signing_boundary_needs_no_ambient_runtime": 4,
|
||||
"test_json_prevalidation_compares_original_binding_before_dto_adaptation": 2,
|
||||
"test_lazy_file_response_keeps_owner_until_iteration_and_cleanup": 3,
|
||||
"test_lazy_response_retains_ownership_grants_between_steps_without_leaking": 1,
|
||||
"test_public_preview_checks_real_signature_without_ambient_adapter": 2,
|
||||
"test_request_rendering_and_teardown_restore_none_even_on_failure": 1,
|
||||
"test_valid_public_preview_retains_normal_delivery_without_ambient_adapter": 2
|
||||
},
|
||||
"tests.unit_tests.core.workflow.test_graphon_v08_contract": {
|
||||
"test_explicit_file_scope_is_reentrant_and_restores_previous_adapter": 1,
|
||||
"test_historical_v1_filter_binds_original_edge_position_to_public_id": 1,
|
||||
"test_historical_v1_response_filter_bytes_are_accepted_without_rewriting": 1,
|
||||
"test_historical_v2_loop_snapshot_resumes_with_dify_scoped_factory": 2,
|
||||
"test_host_context_does_not_replace_engine_file_adapter_on_any_segment": 1,
|
||||
"test_legacy_v1_child_tasks_fail_closed_without_fabricating_frames": 1,
|
||||
"test_nested_ancestry_uses_actual_direct_container_and_retains_occurrence_id": 1,
|
||||
"test_ordinary_workflow_uses_official_engine_without_external_io": 1,
|
||||
"test_pause_barrier_flushes_before_persisting_and_exposing_pause": 1,
|
||||
"test_pause_barrier_storage_failure_never_exposes_resumable_pause": 1,
|
||||
"test_pause_event_defers_snapshot_until_outer_filter_flush": 1,
|
||||
"test_real_engine_pause_snapshot_follows_quiescence_and_outer_filter_flush": 1,
|
||||
"test_scoped_factory_validation_never_initializes_collaborators": 1,
|
||||
"test_single_node_execution_binds_an_explicit_file_adapter": 1,
|
||||
"test_stop_queue_preserves_frame_and_invocation_tasks": 1
|
||||
},
|
||||
"tests.unit_tests.core.workflow.context.test_execution_context": {
|
||||
"test_builder_fluent_interface": 1,
|
||||
"test_builder_with_all_params": 1,
|
||||
"test_builder_with_partial_params": 1,
|
||||
"test_capture_current_context_captures_contextvars": 1,
|
||||
"test_capture_current_context_returns_context": 1,
|
||||
"test_context_manager_protocol": 1,
|
||||
"test_enter_with_app_context": 1,
|
||||
"test_enter_with_context_vars": 1,
|
||||
"test_enter_without_app_context": 1,
|
||||
"test_execution_context_implements_protocol": 1,
|
||||
"test_initialization_with_all_params": 1,
|
||||
"test_initialization_with_minimal_params": 1,
|
||||
"test_missing_provider_raises_keyerror": 1,
|
||||
"test_null_app_context_enter_yield": 1,
|
||||
"test_null_app_context_get_config": 1,
|
||||
"test_null_app_context_get_config_default": 1,
|
||||
"test_null_app_context_get_extension": 1,
|
||||
"test_null_app_context_get_extension_not_found": 1,
|
||||
"test_protocol_compatibility": 1,
|
||||
"test_protocol_with_flask_execution_context": 1,
|
||||
"test_tenant_provider_read_ok": 1,
|
||||
"test_thread_safe_context_manager": 1,
|
||||
"test_user_property": 1
|
||||
},
|
||||
"tests.unit_tests.core.workflow.context.test_flask_app_context": {
|
||||
"test_app_context_property": 1,
|
||||
"test_capture_flask_context_captures_app": 1,
|
||||
"test_capture_flask_context_captures_contextvars": 1,
|
||||
"test_capture_flask_context_captures_user_from_g": 1,
|
||||
"test_capture_flask_context_with_explicit_user": 1,
|
||||
"test_context_manager_protocol": 1,
|
||||
"test_enter_enters_flask_app_context": 1,
|
||||
"test_enter_method_as_context_manager": 1,
|
||||
"test_enter_restores_context_vars": 1,
|
||||
"test_enter_restores_user_in_g": 1,
|
||||
"test_flask_app_context_enter": 1,
|
||||
"test_flask_app_context_get_config": 1,
|
||||
"test_flask_app_context_get_config_default": 1,
|
||||
"test_flask_app_context_get_extension": 1,
|
||||
"test_flask_app_context_get_extension_not_found": 1,
|
||||
"test_flask_app_context_initialization": 1,
|
||||
"test_initialization": 1
|
||||
}
|
||||
}
|
||||
for module, expected in graphon_v08_expected.items():
|
||||
actual = {}
|
||||
for classname, name in identities:
|
||||
normalized = classname.removeprefix("api.")
|
||||
if normalized == module or normalized.startswith(module + "."):
|
||||
function = name.split("[", 1)[0]
|
||||
actual[function] = actual.get(function, 0) + 1
|
||||
if actual != expected:
|
||||
problems.append(f"Graphon 0.8 case inventory mismatch {module}: {actual}")
|
||||
json_admission_module = "tests.unit_tests.services.dify_builder.test_json_tool_execution_admission"
|
||||
expected_json_admission = {
|
||||
f"test_owned_json_parse_prepare_persists_restricted_request[{variant}]"
|
||||
|
||||
@@ -958,14 +958,14 @@ class WorkflowConfig(BaseSettings):
|
||||
default=400_000,
|
||||
)
|
||||
|
||||
# GraphEngine Worker Pool Configuration
|
||||
# Engine Worker Pool Configuration
|
||||
GRAPH_ENGINE_MIN_WORKERS: PositiveInt = Field(
|
||||
description="Minimum number of workers per GraphEngine instance",
|
||||
description="Minimum number of workers per Engine instance",
|
||||
default=3,
|
||||
)
|
||||
|
||||
GRAPH_ENGINE_MAX_WORKERS: PositiveInt = Field(
|
||||
description="Maximum number of workers per GraphEngine instance",
|
||||
description="Maximum number of workers per Engine instance",
|
||||
default=10,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,28 +96,34 @@ class ExecutionContext:
|
||||
@contextmanager
|
||||
def enter(self) -> Generator[None]:
|
||||
"""Enter this execution context."""
|
||||
if self._context_vars:
|
||||
for var, val in self._context_vars.items():
|
||||
var.set(val)
|
||||
|
||||
if self._app_context is not None:
|
||||
with self._app_context.enter():
|
||||
tokens = []
|
||||
try:
|
||||
if self._context_vars:
|
||||
for var, val in self._context_vars.items():
|
||||
tokens.append((var, var.set(val)))
|
||||
if self._app_context is not None:
|
||||
with self._app_context.enter():
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
finally:
|
||||
for var, token in reversed(tokens):
|
||||
var.reset(token)
|
||||
|
||||
def __enter__(self) -> "ExecutionContext":
|
||||
"""Enter the execution context."""
|
||||
cm = self.enter()
|
||||
self._local.cm = cm
|
||||
cm.__enter__()
|
||||
if not hasattr(self._local, "stack"):
|
||||
self._local.stack = []
|
||||
self._local.stack.append(cm)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
"""Exit the execution context."""
|
||||
cm = getattr(self._local, "cm", None)
|
||||
if cm is not None:
|
||||
cm.__exit__(*args)
|
||||
stack = getattr(self._local, "stack", [])
|
||||
if stack:
|
||||
stack.pop().__exit__(*args)
|
||||
|
||||
|
||||
class NullAppContext(AppContext):
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import Any, final, override
|
||||
|
||||
from flask import Flask, current_app, g
|
||||
|
||||
from context.execution_context import AppContext, IExecutionContext, register_context_capturer
|
||||
from context.execution_context import AppContext, ExecutionContext, IExecutionContext, register_context_capturer
|
||||
|
||||
|
||||
@final
|
||||
@@ -137,37 +137,23 @@ class FlaskExecutionContext:
|
||||
|
||||
def __enter__(self) -> "FlaskExecutionContext":
|
||||
"""Enter the Flask execution context."""
|
||||
# Restore non-Flask context variables to avoid leaking Flask tokens across threads
|
||||
for var, val in self._context_vars.items():
|
||||
var.set(val)
|
||||
|
||||
# Enter Flask app context
|
||||
cm = self._app_context.enter()
|
||||
self._local.cm = cm
|
||||
cm = self.enter()
|
||||
cm.__enter__()
|
||||
|
||||
# Restore user in new app context
|
||||
if self._user is not None:
|
||||
g._login_user = self._user
|
||||
|
||||
if not hasattr(self._local, "stack"):
|
||||
self._local.stack = []
|
||||
self._local.stack.append(cm)
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
"""Exit the Flask execution context."""
|
||||
cm = getattr(self._local, "cm", None)
|
||||
if cm is not None:
|
||||
cm.__exit__(*args)
|
||||
"""Exit the Flask execution context and restore the caller's variables."""
|
||||
stack = getattr(self._local, "stack", [])
|
||||
if stack:
|
||||
stack.pop().__exit__(*args)
|
||||
|
||||
@contextmanager
|
||||
def enter(self) -> Generator[None]:
|
||||
"""Enter Flask execution context as context manager."""
|
||||
# Restore non-Flask context variables to avoid leaking Flask tokens across threads
|
||||
for var, val in self._context_vars.items():
|
||||
var.set(val)
|
||||
|
||||
# Enter Flask app context
|
||||
with self._flask_app.app_context():
|
||||
# Restore user in new app context
|
||||
"""Activate captured state reversibly for one worker segment."""
|
||||
with ExecutionContext(app_context=self._app_context, context_vars=self._context_vars).enter():
|
||||
if self._user is not None:
|
||||
g._login_user = self._user
|
||||
yield
|
||||
|
||||
@@ -83,7 +83,6 @@ from fields.workflow_run_fields import (
|
||||
from graphon.enums import NodeType
|
||||
from graphon.file import File
|
||||
from graphon.file import helpers as file_helpers
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from graphon.variables import SecretVariable, SegmentType, VariableBase
|
||||
from graphon.variables.exc import VariableError
|
||||
@@ -102,6 +101,7 @@ from services.app_generate_service import AppGenerateService
|
||||
from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.errors.workflow_service import EmptyWorkflowConfirmationRequiredError
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
from services.workflow_ref_service import WorkflowRefService
|
||||
from services.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError, WorkflowService
|
||||
from services.workflow_variable_reference_validator import (
|
||||
@@ -1234,7 +1234,7 @@ class WorkflowTaskStopApi(Resource):
|
||||
AppQueueManager.set_stop_flag_no_user_check(task_id)
|
||||
|
||||
# New graph engine command channel mechanism
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=redis_client)
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ from fields.workflow_run_fields import (
|
||||
workflow_run_pagination_response_source,
|
||||
workflow_run_response_source,
|
||||
)
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from libs import helper
|
||||
from libs.external_api import error_responses
|
||||
from libs.helper import TimestampField
|
||||
@@ -69,6 +68,7 @@ from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError,
|
||||
from services.errors.workflow_service import DraftWorkflowDeletionError, WorkflowInUseError
|
||||
from services.snippet_generate_service import SnippetGenerateService
|
||||
from services.snippet_service import SnippetService
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -867,6 +867,6 @@ class SnippetWorkflowTaskStopApi(Resource):
|
||||
AppQueueManager.set_stop_flag_no_user_check(task_id)
|
||||
|
||||
# New graph engine command channel mechanism
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=redis_client)
|
||||
|
||||
return {"result": "success"}
|
||||
|
||||
@@ -68,7 +68,6 @@ from core.errors.error import (
|
||||
QuotaExceededError,
|
||||
)
|
||||
from extensions.ext_redis import redis_client
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs import helper
|
||||
from models.model import App, AppMode
|
||||
@@ -82,6 +81,7 @@ from services.errors.app import (
|
||||
TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError,
|
||||
)
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -375,5 +375,5 @@ class AppRunTaskStopApi(Resource):
|
||||
if owner != f"end-user-{ctx.end_user.id}".encode():
|
||||
raise NotFound("Task not found")
|
||||
AppQueueManager.set_stop_flag_no_user_check(task_id)
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=redis_client)
|
||||
return TaskStopResponse(result="success")
|
||||
|
||||
@@ -55,7 +55,6 @@ from fields.base import ResponseModel
|
||||
from fields.end_user_fields import SimpleEndUser
|
||||
from fields.member_fields import SimpleAccountResponse
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs import helper
|
||||
from libs.external_api import error_responses
|
||||
@@ -73,6 +72,7 @@ from services.errors.app import (
|
||||
TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError,
|
||||
)
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -593,7 +593,7 @@ class WorkflowTaskStopApi(Resource):
|
||||
AppQueueManager.set_stop_flag_no_user_check(task_id)
|
||||
|
||||
# New graph engine command channel mechanism
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=redis_client)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump()
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ from core.errors.error import (
|
||||
QuotaExceededError,
|
||||
)
|
||||
from extensions.ext_redis import redis_client
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
from libs import helper
|
||||
from libs.external_api import error_responses
|
||||
@@ -36,6 +35,7 @@ from services.errors.app import (
|
||||
TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError,
|
||||
)
|
||||
from services.errors.llm import InvokeRateLimitError
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -144,6 +144,6 @@ class WorkflowTaskStopApi(WebApiResource):
|
||||
AppQueueManager.set_stop_flag_no_user_check(task_id)
|
||||
|
||||
# New graph engine command channel mechanism
|
||||
GraphEngineManager(redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=redis_client)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -48,10 +48,10 @@ from core.repositories import DifyCoreRepositoryFactory
|
||||
from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository
|
||||
from extensions.ext_database import db
|
||||
from factories import file_factory
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.model_runtime.errors.invoke import InvokeAuthorizationError
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader
|
||||
from libs.flask_utils import preserve_flask_contexts
|
||||
from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom
|
||||
@@ -281,7 +281,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
application_generate_entity: AdvancedChatAppGenerateEntity,
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
):
|
||||
@@ -312,7 +312,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
message=message,
|
||||
stream=application_generate_entity.stream,
|
||||
pause_state_config=pause_state_config,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
response_stream_filter=response_stream_filter,
|
||||
session=session,
|
||||
)
|
||||
@@ -520,8 +520,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
stream: bool = True,
|
||||
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any]]:
|
||||
"""
|
||||
@@ -574,7 +574,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
message_id=message.id,
|
||||
)
|
||||
|
||||
graph_layers: list[GraphEngineLayer] = list(graph_engine_layers)
|
||||
graph_layers: list[Layer] = list(graph_engine_layers)
|
||||
resolved_response_stream_filter = response_stream_filter or ResponseStreamFilter()
|
||||
if pause_state_config is not None:
|
||||
graph_layers.append(
|
||||
@@ -602,7 +602,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
"workflow_execution_repository": workflow_execution_repository,
|
||||
"workflow_node_execution_repository": workflow_node_execution_repository,
|
||||
"graph_engine_layers": tuple(graph_layers),
|
||||
"graph_runtime_state": graph_runtime_state,
|
||||
"runtime_state": runtime_state,
|
||||
"response_stream_filter": resolved_response_stream_filter,
|
||||
},
|
||||
)
|
||||
@@ -656,8 +656,8 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
variable_loader: VariableLoader,
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
):
|
||||
"""
|
||||
@@ -685,7 +685,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
)
|
||||
if workflow is None:
|
||||
raise ValueError("Workflow not found")
|
||||
if graph_runtime_state is not None:
|
||||
if runtime_state is not None:
|
||||
self._restore_workflow_run_graph(
|
||||
session=session,
|
||||
workflow=workflow,
|
||||
@@ -723,7 +723,7 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
|
||||
workflow_execution_repository=workflow_execution_repository,
|
||||
workflow_node_execution_repository=workflow_node_execution_repository,
|
||||
graph_engine_layers=graph_engine_layers,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
response_stream_filter=response_stream_filter,
|
||||
)
|
||||
|
||||
|
||||
@@ -46,11 +46,11 @@ from core.workflow.workflow_entry import WorkflowEntry
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from extensions.workflow_warm_shutdown import WORKFLOW_WARM_SHUTDOWN_ABORT_REASON, celery_warm_shutdown_started
|
||||
from graphon.engine.command import RedisChannel
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.enums import WorkflowType
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.graph_engine.command_channels import RedisChannel
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from graphon.variable_loader import VariableLoader
|
||||
from graphon.variables.variables import Variable
|
||||
from models import Workflow
|
||||
@@ -80,8 +80,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
app: App,
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
@@ -99,7 +99,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
self._app = app
|
||||
self._workflow_execution_repository = workflow_execution_repository
|
||||
self._workflow_node_execution_repository = workflow_node_execution_repository
|
||||
self._resume_graph_runtime_state = graph_runtime_state
|
||||
self._resume_graph_runtime_state = runtime_state
|
||||
self._response_stream_filter = response_stream_filter
|
||||
|
||||
@trace_span(WorkflowAppRunnerHandler)
|
||||
@@ -132,11 +132,11 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
resume_state = self._resume_graph_runtime_state
|
||||
|
||||
if resume_state is not None:
|
||||
graph_runtime_state = resume_state
|
||||
variable_pool = graph_runtime_state.variable_pool
|
||||
runtime_state = resume_state
|
||||
variable_pool = runtime_state.variable_pool
|
||||
graph = self._init_graph(
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
@@ -147,7 +147,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
)
|
||||
elif self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
# Handle single iteration or single loop run
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
graph, variable_pool, runtime_state = self._prepare_single_node_execution(
|
||||
workflow=self._workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
@@ -213,10 +213,12 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=new_inputs)
|
||||
|
||||
# init graph
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.time())
|
||||
runtime_state = RuntimeState(
|
||||
workflow_id=self._workflow.id, variable_pool=variable_pool, start_at=time.time()
|
||||
)
|
||||
graph = self._init_graph(
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
@@ -235,7 +237,7 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
shutdown_state_getter=celery_warm_shutdown_started,
|
||||
abort_reason=WORKFLOW_WARM_SHUTDOWN_ABORT_REASON,
|
||||
)
|
||||
attach_stop_aware_ready_queue(graph_runtime_state, task_id=task_id)
|
||||
attach_stop_aware_ready_queue(runtime_state, task_id=task_id)
|
||||
command_channel = CombinedCommandChannel(
|
||||
(
|
||||
RedisChannel(redis_client, channel_key),
|
||||
@@ -255,12 +257,12 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
command_channel=command_channel,
|
||||
response_stream_filter=self._response_stream_filter,
|
||||
)
|
||||
|
||||
self._queue_manager.graph_runtime_state = graph_runtime_state
|
||||
self._queue_manager.runtime_state = runtime_state
|
||||
|
||||
persistence_layer = WorkflowPersistenceLayer(
|
||||
application_generate_entity=self.application_generate_entity,
|
||||
@@ -275,8 +277,8 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
trace_manager=self.application_generate_entity.trace_manager,
|
||||
)
|
||||
|
||||
workflow_entry.graph_engine.layer(persistence_layer)
|
||||
workflow_entry.graph_engine.layer(
|
||||
workflow_entry.add_layer(persistence_layer)
|
||||
workflow_entry.add_layer(
|
||||
build_workflow_agent_workspace_retirement_layer(
|
||||
dify_run_context=DifyRunContext(
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
@@ -292,9 +294,9 @@ class AdvancedChatAppRunner(WorkflowBasedAppRunner):
|
||||
conversation_variable_layer = ConversationVariablePersistenceLayer(
|
||||
ConversationVariableUpdater(session_factory.get_session_maker())
|
||||
)
|
||||
workflow_entry.graph_engine.layer(conversation_variable_layer)
|
||||
workflow_entry.add_layer(conversation_variable_layer)
|
||||
for layer in self._graph_engine_layers:
|
||||
workflow_entry.graph_engine.layer(layer)
|
||||
workflow_entry.add_layer(layer)
|
||||
|
||||
generator = workflow_entry.run()
|
||||
for event in generator:
|
||||
|
||||
@@ -82,7 +82,7 @@ from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from graphon.nodes import BuiltinNodeTypes
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account, Conversation, EndUser, Message, MessageFile
|
||||
from models.enums import CreatorUserRole, MessageFileBelongsTo, MessageStatus
|
||||
@@ -209,7 +209,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
self._recorded_files: list[Mapping[str, Any]] = []
|
||||
self._workflow_run_id: str = ""
|
||||
self._draft_var_saver_factory = draft_var_saver_factory
|
||||
self._graph_runtime_state: GraphRuntimeState | None = None
|
||||
self._graph_runtime_state: RuntimeState | None = None
|
||||
self._message_saved_on_pause = False
|
||||
self._seed_graph_runtime_state_from_queue_manager()
|
||||
|
||||
@@ -667,11 +667,11 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow_id,
|
||||
status=WorkflowExecutionStatus.SUCCEEDED,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
)
|
||||
|
||||
yield from self._handle_advanced_chat_message_end_event(
|
||||
QueueAdvancedChatMessageEndEvent(), graph_runtime_state=validated_state
|
||||
QueueAdvancedChatMessageEndEvent(), runtime_state=validated_state
|
||||
)
|
||||
yield workflow_finish_resp
|
||||
|
||||
@@ -690,12 +690,12 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow_id,
|
||||
status=WorkflowExecutionStatus.PARTIAL_SUCCEEDED,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
exceptions_count=event.exceptions_count,
|
||||
)
|
||||
|
||||
yield from self._handle_advanced_chat_message_end_event(
|
||||
QueueAdvancedChatMessageEndEvent(), graph_runtime_state=validated_state
|
||||
QueueAdvancedChatMessageEndEvent(), runtime_state=validated_state
|
||||
)
|
||||
yield workflow_finish_resp
|
||||
|
||||
@@ -709,20 +709,20 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
responses = self._workflow_response_converter.workflow_pause_to_stream_response(
|
||||
event=event,
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
)
|
||||
for reason in event.reasons:
|
||||
if isinstance(reason, HumanInputRequired):
|
||||
self._persist_human_input_extra_content(form_id=reason.form_id, node_id=reason.node_id)
|
||||
yield from responses
|
||||
resolved_state: GraphRuntimeState | None = None
|
||||
resolved_state: RuntimeState | None = None
|
||||
try:
|
||||
resolved_state = self._ensure_graph_runtime_initialized()
|
||||
except ValueError:
|
||||
resolved_state = None
|
||||
|
||||
with self._database_session() as session:
|
||||
self._save_message(session=session, graph_runtime_state=resolved_state)
|
||||
self._save_message(session=session, runtime_state=resolved_state)
|
||||
message = self._get_message(session=session)
|
||||
if message is not None:
|
||||
message.status = MessageStatus.PAUSED
|
||||
@@ -745,7 +745,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow_id,
|
||||
status=WorkflowExecutionStatus.FAILED,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
error=event.error,
|
||||
exceptions_count=event.exceptions_count,
|
||||
)
|
||||
@@ -761,7 +761,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
self,
|
||||
event: QueueStopEvent,
|
||||
*,
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
runtime_state: RuntimeState | None = None,
|
||||
trace_manager: TraceQueueManager | None = None,
|
||||
**kwargs,
|
||||
) -> Generator[StreamResponse]:
|
||||
@@ -769,20 +769,20 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
_ = trace_manager
|
||||
resolved_state = None
|
||||
if self._workflow_run_id:
|
||||
resolved_state = self._resolve_graph_runtime_state(graph_runtime_state)
|
||||
resolved_state = self._resolve_graph_runtime_state(runtime_state)
|
||||
|
||||
if self._workflow_run_id and resolved_state:
|
||||
workflow_finish_resp = self._workflow_response_converter.workflow_finish_to_stream_response(
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow_id,
|
||||
status=WorkflowExecutionStatus.STOPPED,
|
||||
graph_runtime_state=resolved_state,
|
||||
runtime_state=resolved_state,
|
||||
error=event.get_stop_reason(),
|
||||
)
|
||||
|
||||
with self._database_session() as session:
|
||||
# Save message
|
||||
self._save_message(session=session, graph_runtime_state=resolved_state)
|
||||
self._save_message(session=session, runtime_state=resolved_state)
|
||||
self._emit_message_trace()
|
||||
|
||||
yield workflow_finish_resp
|
||||
@@ -802,11 +802,11 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
self,
|
||||
event: QueueAdvancedChatMessageEndEvent,
|
||||
*,
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
runtime_state: RuntimeState | None = None,
|
||||
**kwargs,
|
||||
) -> Generator[StreamResponse]:
|
||||
"""Handle advanced chat message end events."""
|
||||
resolved_state = self._ensure_graph_runtime_initialized(graph_runtime_state)
|
||||
resolved_state = self._ensure_graph_runtime_initialized(runtime_state)
|
||||
|
||||
output_moderation_answer = self._base_task_pipeline.handle_output_moderation_when_task_finished(
|
||||
self._task_state.answer
|
||||
@@ -821,7 +821,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
# Save message unless it has already been persisted on pause.
|
||||
if not self._message_saved_on_pause:
|
||||
with self._database_session() as session:
|
||||
self._save_message(session=session, graph_runtime_state=resolved_state)
|
||||
self._save_message(session=session, runtime_state=resolved_state)
|
||||
self._emit_message_trace()
|
||||
|
||||
yield self._message_end_to_stream_response()
|
||||
@@ -1024,7 +1024,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
break
|
||||
|
||||
case QueueStopEvent():
|
||||
yield from self._handle_stop_event(event, graph_runtime_state=None, trace_manager=trace_manager)
|
||||
yield from self._handle_stop_event(event, runtime_state=None, trace_manager=trace_manager)
|
||||
break
|
||||
|
||||
# Handle all other events through elegant dispatch
|
||||
@@ -1042,7 +1042,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
if self._conversation_name_generate_thread:
|
||||
logger.debug("Conversation name generation running as daemon thread")
|
||||
|
||||
def _save_message(self, *, session: Session, graph_runtime_state: GraphRuntimeState | None = None):
|
||||
def _save_message(self, *, session: Session, runtime_state: RuntimeState | None = None):
|
||||
message = self._get_message(session=session)
|
||||
if message is None:
|
||||
return
|
||||
@@ -1057,8 +1057,8 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
message.provider_response_latency = time.perf_counter() - self._base_task_pipeline.start_at
|
||||
|
||||
# Set usage first before dumping metadata
|
||||
if graph_runtime_state and graph_runtime_state.llm_usage:
|
||||
usage = graph_runtime_state.llm_usage
|
||||
if runtime_state and runtime_state.llm_usage:
|
||||
usage = runtime_state.llm_usage
|
||||
message.message_tokens = usage.prompt_tokens
|
||||
message.message_unit_price = usage.prompt_unit_price
|
||||
message.message_price_unit = usage.prompt_price_unit
|
||||
@@ -1115,7 +1115,7 @@ class AdvancedChatAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
|
||||
def _seed_graph_runtime_state_from_queue_manager(self) -> None:
|
||||
"""Bootstrap the cached runtime state from the queue manager when present."""
|
||||
candidate = self._base_task_pipeline.queue_manager.graph_runtime_state
|
||||
candidate = self._base_task_pipeline.queue_manager.runtime_state
|
||||
if candidate is not None:
|
||||
self._graph_runtime_state = candidate
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from contextlib import AbstractContextManager, nullcontext
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Any, Union, final
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -13,7 +13,12 @@ from core.app.apps.draft_variable_saver import (
|
||||
NoopDraftVariableSaver,
|
||||
)
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom
|
||||
from core.app.file_access import DatabaseFileAccessController, FileAccessScope, bind_file_access_scope
|
||||
from core.app.file_access import (
|
||||
DatabaseFileAccessController,
|
||||
FileAccessScope,
|
||||
bind_file_access_scope,
|
||||
get_current_file_access_scope,
|
||||
)
|
||||
from extensions.ext_database import db
|
||||
from factories import file_factory
|
||||
from graphon.enums import NodeType
|
||||
@@ -97,34 +102,58 @@ class BaseAppGenerator:
|
||||
response_stream: Generator[ResponseT],
|
||||
worker_thread: threading.Thread,
|
||||
) -> Generator[ResponseT]:
|
||||
"""Keep the producer owned by the response stream until both finish."""
|
||||
try:
|
||||
yield from response_stream
|
||||
finally:
|
||||
BaseAppGenerator._join_worker_thread(worker_thread)
|
||||
"""Capture rendering ownership now; restore it for every lazy step and close."""
|
||||
from graphon.file.runtime import peek_workflow_file_runtime, use_workflow_file_runtime
|
||||
|
||||
file_runtime = peek_workflow_file_runtime()
|
||||
file_scope = get_current_file_access_scope()
|
||||
|
||||
def consume() -> Generator[ResponseT]:
|
||||
nonlocal file_scope
|
||||
try:
|
||||
while True:
|
||||
with use_workflow_file_runtime(file_runtime), bind_file_access_scope(file_scope):
|
||||
try:
|
||||
item = next(response_stream)
|
||||
file_scope = get_current_file_access_scope()
|
||||
except StopIteration:
|
||||
return
|
||||
yield item
|
||||
finally:
|
||||
try:
|
||||
with use_workflow_file_runtime(file_runtime), bind_file_access_scope(file_scope):
|
||||
response_stream.close()
|
||||
finally:
|
||||
BaseAppGenerator._join_worker_thread(worker_thread)
|
||||
|
||||
return consume()
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def _bind_file_access_scope(
|
||||
*,
|
||||
tenant_id: str,
|
||||
user: Account | EndUser,
|
||||
invoke_from: InvokeFrom,
|
||||
) -> AbstractContextManager[None]:
|
||||
"""Bind request-scoped file ownership markers for downstream file lookups."""
|
||||
) -> Generator[None]:
|
||||
"""Bind the file adapter and request ownership for downstream file lookups."""
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
user_id = getattr(user, "id", None)
|
||||
if not isinstance(user_id, str) or not user_id:
|
||||
return nullcontext()
|
||||
|
||||
user_from = UserFrom.ACCOUNT if isinstance(user, Account) else UserFrom.END_USER
|
||||
return bind_file_access_scope(
|
||||
FileAccessScope(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
)
|
||||
with bind_dify_workflow_file_runtime():
|
||||
user_id = getattr(user, "id", None)
|
||||
if not isinstance(user_id, str) or not user_id:
|
||||
yield
|
||||
return
|
||||
user_from = UserFrom.ACCOUNT if isinstance(user, Account) else UserFrom.END_USER
|
||||
with bind_file_access_scope(
|
||||
FileAccessScope(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
)
|
||||
):
|
||||
yield
|
||||
|
||||
def _prepare_user_inputs(
|
||||
self,
|
||||
|
||||
@@ -26,7 +26,7 @@ from core.app.entities.queue_entities import (
|
||||
WorkflowQueueMessage,
|
||||
)
|
||||
from extensions.ext_redis import RedisClientWrapper, redis_client
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,7 +64,7 @@ class AppQueueManager(ABC):
|
||||
q: queue.Queue[WorkflowQueueMessage | MessageQueueMessage | None] = queue.Queue()
|
||||
|
||||
self._q = q
|
||||
self._graph_runtime_state: GraphRuntimeState | None = None
|
||||
self._graph_runtime_state: RuntimeState | None = None
|
||||
self._stopped_cache: TTLCache[tuple, bool] = TTLCache(maxsize=1, ttl=1)
|
||||
self._cache_lock = threading.Lock()
|
||||
self._listener_segment_completed = threading.Event()
|
||||
@@ -152,14 +152,14 @@ class AppQueueManager(ABC):
|
||||
self.publish(QueueErrorEvent(error=e), pub_from)
|
||||
|
||||
@property
|
||||
def graph_runtime_state(self) -> GraphRuntimeState | None:
|
||||
def runtime_state(self) -> RuntimeState | None:
|
||||
"""Retrieve the attached graph runtime state, if available."""
|
||||
return self._graph_runtime_state
|
||||
|
||||
@graph_runtime_state.setter
|
||||
def graph_runtime_state(self, graph_runtime_state: GraphRuntimeState | None) -> None:
|
||||
@runtime_state.setter
|
||||
def runtime_state(self, runtime_state: RuntimeState | None) -> None:
|
||||
"""Attach the live graph runtime state reference for downstream consumers."""
|
||||
self._graph_runtime_state = graph_runtime_state
|
||||
self._graph_runtime_state = runtime_state
|
||||
|
||||
def publish(self, event: AppQueueEvent, pub_from: PublishFrom) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Shared helpers for managing GraphRuntimeState across task pipelines."""
|
||||
"""Shared helpers for managing RuntimeState across task pipelines."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class GraphRuntimeStateSupport:
|
||||
"""
|
||||
Mixin that centralises common GraphRuntimeState access patterns used by task pipelines.
|
||||
Mixin that centralises common RuntimeState access patterns used by task pipelines.
|
||||
|
||||
Subclasses are expected to provide:
|
||||
* `_base_task_pipeline` – exposing the queue manager with an optional cached runtime state.
|
||||
@@ -21,32 +21,32 @@ class GraphRuntimeStateSupport:
|
||||
"""
|
||||
|
||||
_base_task_pipeline: BasedGenerateTaskPipeline
|
||||
_graph_runtime_state: GraphRuntimeState | None = None
|
||||
_graph_runtime_state: RuntimeState | None = None
|
||||
|
||||
def _ensure_graph_runtime_initialized(
|
||||
self,
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
) -> GraphRuntimeState:
|
||||
runtime_state: RuntimeState | None = None,
|
||||
) -> RuntimeState:
|
||||
"""Validate and return the active graph runtime state."""
|
||||
return self._resolve_graph_runtime_state(graph_runtime_state)
|
||||
return self._resolve_graph_runtime_state(runtime_state)
|
||||
|
||||
def _extract_workflow_run_id(self, graph_runtime_state: GraphRuntimeState) -> str:
|
||||
workflow_run_id = get_system_text(graph_runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
|
||||
def _extract_workflow_run_id(self, runtime_state: RuntimeState) -> str:
|
||||
workflow_run_id = get_system_text(runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
|
||||
if not workflow_run_id:
|
||||
raise ValueError("workflow_execution_id missing from runtime state")
|
||||
return workflow_run_id
|
||||
|
||||
def _resolve_graph_runtime_state(
|
||||
self,
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
) -> GraphRuntimeState:
|
||||
runtime_state: RuntimeState | None = None,
|
||||
) -> RuntimeState:
|
||||
"""Return the cached runtime state or bootstrap it from the queue manager."""
|
||||
if graph_runtime_state is not None:
|
||||
self._graph_runtime_state = graph_runtime_state
|
||||
return graph_runtime_state
|
||||
if runtime_state is not None:
|
||||
self._graph_runtime_state = runtime_state
|
||||
return runtime_state
|
||||
|
||||
if self._graph_runtime_state is None:
|
||||
candidate = self._base_task_pipeline.queue_manager.graph_runtime_state
|
||||
candidate = self._base_task_pipeline.queue_manager.runtime_state
|
||||
if candidate is not None:
|
||||
self._graph_runtime_state = candidate
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ from graphon.enums import (
|
||||
WorkflowNodeExecutionStatus,
|
||||
)
|
||||
from graphon.file import FILE_MODEL_IDENTITY, File
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment
|
||||
from graphon.variables.variables import Variable
|
||||
from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter
|
||||
@@ -264,7 +264,7 @@ class WorkflowResponseConverter:
|
||||
task_id: str,
|
||||
workflow_id: str,
|
||||
status: WorkflowExecutionStatus,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
error: str | None = None,
|
||||
exceptions_count: int = 0,
|
||||
) -> WorkflowFinishStreamResponse:
|
||||
@@ -278,7 +278,7 @@ class WorkflowResponseConverter:
|
||||
finished_at = naive_utc_now()
|
||||
elapsed_time = (finished_at - started_at).total_seconds()
|
||||
|
||||
outputs_mapping = graph_runtime_state.outputs or {}
|
||||
outputs_mapping = runtime_state.outputs or {}
|
||||
encoded_outputs = WorkflowRuntimeTypeConverter().to_json_encodable(outputs_mapping)
|
||||
|
||||
created_by: CreatedByDict | dict[str, object] = {}
|
||||
@@ -306,8 +306,8 @@ class WorkflowResponseConverter:
|
||||
outputs=encoded_outputs,
|
||||
error=error,
|
||||
elapsed_time=elapsed_time,
|
||||
total_tokens=graph_runtime_state.total_tokens,
|
||||
total_steps=graph_runtime_state.node_run_steps,
|
||||
total_tokens=runtime_state.total_tokens,
|
||||
total_steps=runtime_state.node_run_steps,
|
||||
created_by=created_by,
|
||||
created_at=int(started_at.timestamp()),
|
||||
finished_at=int(finished_at.timestamp()),
|
||||
@@ -321,7 +321,7 @@ class WorkflowResponseConverter:
|
||||
*,
|
||||
event: QueueWorkflowPausedEvent,
|
||||
task_id: str,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
) -> list[StreamResponse]:
|
||||
run_id = self._ensure_workflow_run_id()
|
||||
started_at = self._workflow_started_at
|
||||
@@ -334,7 +334,7 @@ class WorkflowResponseConverter:
|
||||
encoded_outputs = self._encode_outputs(event.outputs) or {}
|
||||
if self._application_generate_entity.invoke_from == InvokeFrom.SERVICE_API:
|
||||
encoded_outputs = {}
|
||||
variable_pool = graph_runtime_state.variable_pool
|
||||
variable_pool = runtime_state.variable_pool
|
||||
resolved_reasons = resolve_human_input_pause_reason_inputs(
|
||||
event.reasons,
|
||||
variable_pool=variable_pool,
|
||||
@@ -416,8 +416,8 @@ class WorkflowResponseConverter:
|
||||
status=WorkflowExecutionStatus.PAUSED,
|
||||
created_at=int(started_at.timestamp()),
|
||||
elapsed_time=elapsed_time,
|
||||
total_tokens=graph_runtime_state.total_tokens,
|
||||
total_steps=graph_runtime_state.node_run_steps,
|
||||
total_tokens=runtime_state.total_tokens,
|
||||
total_steps=runtime_state.node_run_steps,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,8 +8,7 @@ from enum import Enum, auto
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.ext_redis import RedisClientWrapper, redis_client
|
||||
from graphon.graph_engine.command_channels import RedisChannel
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from graphon.engine.command import AbortCommand, RedisChannel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,7 +21,7 @@ class AppExecutionState(Enum):
|
||||
|
||||
|
||||
def app_task_command_channel_key(task_id: str) -> str:
|
||||
"""Redis key of the GraphEngine command channel for one app task."""
|
||||
"""Redis key of the Engine command channel for one app task."""
|
||||
return f"workflow:{task_id}:commands"
|
||||
|
||||
|
||||
@@ -73,12 +72,12 @@ def clear_app_task_cancellation_signals(task_id: str) -> None:
|
||||
redis_client.delete(channel_key)
|
||||
if discarded:
|
||||
logger.info(
|
||||
"Discarded %s stale GraphEngine command(s) for app task %s",
|
||||
"Discarded %s stale Engine command(s) for app task %s",
|
||||
len(discarded),
|
||||
task_id,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to clear pending GraphEngine commands for app task %s", task_id)
|
||||
logger.exception("Failed to clear pending Engine commands for app task %s", task_id)
|
||||
|
||||
|
||||
class AppExecutionCoordinator:
|
||||
@@ -213,7 +212,7 @@ class AppExecutionCoordinator:
|
||||
)
|
||||
|
||||
try:
|
||||
GraphEngineManager(redis_client).send_stop_command(self._task_id, reason=reason)
|
||||
RedisChannel(redis_client, f"workflow:{self._task_id}:commands").send_command(AbortCommand(reason=reason))
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to send stop command for app execution task=%s attempt=%s",
|
||||
|
||||
@@ -22,10 +22,10 @@ from core.workflow.node_factory import DifyGraphInitContext, DifyNodeFactory, ge
|
||||
from core.workflow.system_variables import build_bootstrap_variables, build_system_variables
|
||||
from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
from graphon.engine_events import EngineEvent, GraphRunFailedEvent
|
||||
from graphon.enums import WorkflowType
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from graphon.variable_loader import VariableLoader
|
||||
from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput
|
||||
from models.dataset import Dataset, Pipeline
|
||||
@@ -145,7 +145,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
# if only single iteration run is requested
|
||||
if self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
# Handle single iteration or single loop run
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
graph, variable_pool, runtime_state = self._prepare_single_node_execution(
|
||||
workflow=workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
@@ -199,11 +199,13 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
workflow.graph_dict
|
||||
)
|
||||
add_node_inputs_to_pool(variable_pool, node_id=root_node_id, inputs=inputs)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
runtime_state = RuntimeState(
|
||||
workflow_id=workflow.id, variable_pool=variable_pool, start_at=time.perf_counter()
|
||||
)
|
||||
|
||||
# init graph
|
||||
graph = self._init_rag_pipeline_graph(
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
start_node_id=root_node_id,
|
||||
workflow=workflow,
|
||||
user_from=user_from,
|
||||
@@ -221,11 +223,11 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
user_from=user_from,
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
variable_pool=variable_pool,
|
||||
)
|
||||
|
||||
self._queue_manager.graph_runtime_state = graph_runtime_state
|
||||
self._queue_manager.runtime_state = runtime_state
|
||||
|
||||
persistence_layer = WorkflowPersistenceLayer(
|
||||
application_generate_entity=self.application_generate_entity,
|
||||
@@ -240,7 +242,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
trace_manager=self.application_generate_entity.trace_manager,
|
||||
)
|
||||
|
||||
workflow_entry.graph_engine.layer(persistence_layer)
|
||||
workflow_entry.add_layer(persistence_layer)
|
||||
|
||||
generator = workflow_entry.run()
|
||||
|
||||
@@ -270,7 +272,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
def _init_rag_pipeline_graph(
|
||||
self,
|
||||
workflow: Workflow,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
start_node_id: str | None = None,
|
||||
user_from: UserFrom = UserFrom.ACCOUNT,
|
||||
invoke_from: InvokeFrom = InvokeFrom.SERVICE_API,
|
||||
@@ -327,7 +329,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
|
||||
node_factory = DifyNodeFactory.from_graph_init_context(
|
||||
graph_init_context=graph_init_context,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
if start_node_id is None:
|
||||
start_node_id = get_default_root_node_id(graph_config)
|
||||
@@ -340,7 +342,7 @@ class PipelineRunner(WorkflowBasedAppRunner):
|
||||
|
||||
def _update_document_status(
|
||||
self,
|
||||
event: GraphEngineEvent,
|
||||
event: EngineEvent,
|
||||
*,
|
||||
workspace_id: str,
|
||||
dataset_id: str,
|
||||
|
||||
@@ -53,11 +53,11 @@ from core.trigger.constants import is_trigger_node_type
|
||||
from core.workflow.node_factory import get_default_root_node_id
|
||||
from extensions.ext_database import db
|
||||
from factories import file_factory
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.enums import NodeType
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.model_runtime.errors.invoke import InvokeAuthorizationError
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader
|
||||
from libs.flask_utils import preserve_flask_contexts
|
||||
from models.account import Account
|
||||
@@ -160,7 +160,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_run_id: str | uuid.UUID | None = None,
|
||||
triggered_from: WorkflowRunTriggeredFrom | None = None,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
builder_execution: BuilderExecutionContext
|
||||
| ConfirmedToolExecutionContext
|
||||
@@ -186,7 +186,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_run_id: str | uuid.UUID | None = None,
|
||||
triggered_from: WorkflowRunTriggeredFrom | None = None,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
builder_execution: BuilderExecutionContext
|
||||
| ConfirmedToolExecutionContext
|
||||
@@ -212,7 +212,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_run_id: str | uuid.UUID | None = None,
|
||||
triggered_from: WorkflowRunTriggeredFrom | None = None,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
builder_execution: BuilderExecutionContext
|
||||
| ConfirmedToolExecutionContext
|
||||
@@ -237,7 +237,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_run_id: str | uuid.UUID | None = None,
|
||||
triggered_from: WorkflowRunTriggeredFrom | None = None,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
builder_execution: BuilderExecutionContext
|
||||
| ConfirmedToolExecutionContext
|
||||
@@ -434,10 +434,10 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow: Workflow,
|
||||
user: Account | EndUser,
|
||||
application_generate_entity: WorkflowAppGenerateEntity,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
@@ -475,7 +475,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
streaming=application_generate_entity.stream,
|
||||
variable_loader=variable_loader,
|
||||
graph_engine_layers=graph_engine_layers,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
pause_state_config=pause_state_config,
|
||||
response_stream_filter=response_stream_filter,
|
||||
)
|
||||
@@ -493,8 +493,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
streaming: bool = True,
|
||||
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
pause_state_config: PauseStateLayerConfig | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
builder_execution_admit: BuilderExecutionAdmission | None = None,
|
||||
@@ -517,7 +517,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
user=user,
|
||||
invoke_from=invoke_from,
|
||||
):
|
||||
graph_layers: list[GraphEngineLayer] = list(graph_engine_layers)
|
||||
graph_layers: list[Layer] = list(graph_engine_layers)
|
||||
|
||||
# init queue manager
|
||||
queue_manager = WorkflowAppQueueManager(
|
||||
@@ -556,7 +556,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
"workflow_execution_repository": workflow_execution_repository,
|
||||
"workflow_node_execution_repository": workflow_node_execution_repository,
|
||||
"graph_engine_layers": tuple(graph_layers),
|
||||
"graph_runtime_state": graph_runtime_state,
|
||||
"runtime_state": runtime_state,
|
||||
"response_stream_filter": resolved_response_stream_filter,
|
||||
"builder_execution_admit": builder_execution_admit,
|
||||
"native_runtime": native_runtime,
|
||||
@@ -795,8 +795,8 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
root_node_id: str | None = None,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
builder_execution_admit: BuilderExecutionAdmission | None = None,
|
||||
native_runtime: NativeRuntimeBridge | None = None,
|
||||
@@ -883,7 +883,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
app,
|
||||
application_generate_entity,
|
||||
root_node_id or get_default_root_node_id(workflow.graph_dict),
|
||||
graph_runtime_state is not None,
|
||||
runtime_state is not None,
|
||||
)
|
||||
if execution_recorder is None or not execution_recorder.healthy:
|
||||
raise BuilderExecutionPolicyError("missing_execution_recorder")
|
||||
@@ -894,7 +894,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
else:
|
||||
workflow = self._ensure_snippet_start_node_in_worker(session=session, workflow=workflow)
|
||||
|
||||
if graph_runtime_state is not None and native_context is None:
|
||||
if runtime_state is not None and native_context is None:
|
||||
self._restore_workflow_run_graph(
|
||||
session=session,
|
||||
workflow=workflow,
|
||||
@@ -928,7 +928,7 @@ class WorkflowAppGenerator(BaseAppGenerator):
|
||||
workflow_node_execution_repository=workflow_node_execution_repository,
|
||||
root_node_id=root_node_id,
|
||||
graph_engine_layers=graph_engine_layers,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
response_stream_filter=response_stream_filter,
|
||||
execution_recorder=execution_recorder,
|
||||
)
|
||||
|
||||
@@ -38,11 +38,11 @@ from core.workflow.workflow_entry import WorkflowEntry
|
||||
from extensions.ext_redis import redis_client
|
||||
from extensions.otel import WorkflowAppRunnerHandler, trace_span
|
||||
from extensions.workflow_warm_shutdown import WORKFLOW_WARM_SHUTDOWN_ABORT_REASON, celery_warm_shutdown_started
|
||||
from graphon.engine.command import RedisChannel
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.enums import WorkflowType
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.graph_engine.command_channels import RedisChannel
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from graphon.variable_loader import VariableLoader
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.workflow import Workflow
|
||||
@@ -68,8 +68,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
root_node_id: str | None = None,
|
||||
workflow_execution_repository: WorkflowExecutionRepository,
|
||||
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_runtime_state: GraphRuntimeState | None = None,
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
runtime_state: RuntimeState | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
execution_recorder: BuilderExecutionRecorder | None = None,
|
||||
native_runtime: NativeRuntimeBridge | None = None,
|
||||
@@ -88,7 +88,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
self._root_node_id = root_node_id
|
||||
self._workflow_execution_repository = workflow_execution_repository
|
||||
self._workflow_node_execution_repository = workflow_node_execution_repository
|
||||
self._resume_graph_runtime_state = graph_runtime_state
|
||||
self._resume_graph_runtime_state = runtime_state
|
||||
self._response_stream_filter = response_stream_filter
|
||||
|
||||
@trace_span(WorkflowAppRunnerHandler)
|
||||
@@ -185,8 +185,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
resume_state = self._resume_graph_runtime_state
|
||||
|
||||
if resume_state is not None:
|
||||
graph_runtime_state = resume_state
|
||||
variable_pool = graph_runtime_state.variable_pool
|
||||
runtime_state = resume_state
|
||||
variable_pool = runtime_state.variable_pool
|
||||
graph = self._init_graph(
|
||||
builder_execution=entity.builder_execution,
|
||||
native_test_required=entity.native_test_required,
|
||||
@@ -195,7 +195,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
native_call_depth=entity.call_depth if entity.native_test_ref is not None else 0,
|
||||
execution_recorder=self._execution_recorder,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
@@ -206,7 +206,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
trace_session_id=self.application_generate_entity.extras.get("trace_session_id"),
|
||||
)
|
||||
elif self.application_generate_entity.single_iteration_run or self.application_generate_entity.single_loop_run:
|
||||
graph, variable_pool, graph_runtime_state = self._prepare_single_node_execution(
|
||||
graph, variable_pool, runtime_state = self._prepare_single_node_execution(
|
||||
workflow=self._workflow,
|
||||
single_iteration_run=self.application_generate_entity.single_iteration_run,
|
||||
single_loop_run=self.application_generate_entity.single_loop_run,
|
||||
@@ -249,7 +249,9 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
),
|
||||
)
|
||||
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
runtime_state = RuntimeState(
|
||||
workflow_id=self._workflow.id, variable_pool=variable_pool, start_at=time.perf_counter()
|
||||
)
|
||||
graph = self._init_graph(
|
||||
builder_execution=entity.builder_execution,
|
||||
native_test_required=entity.native_test_required,
|
||||
@@ -258,7 +260,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
native_call_depth=entity.call_depth if entity.native_test_ref is not None else 0,
|
||||
execution_recorder=self._execution_recorder,
|
||||
graph_config=self._workflow.graph_dict,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_id=self._workflow.id,
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
user_id=self.application_generate_entity.user_id,
|
||||
@@ -277,7 +279,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
shutdown_state_getter=celery_warm_shutdown_started,
|
||||
abort_reason=WORKFLOW_WARM_SHUTDOWN_ABORT_REASON,
|
||||
)
|
||||
attach_stop_aware_ready_queue(graph_runtime_state, task_id=task_id)
|
||||
attach_stop_aware_ready_queue(runtime_state, task_id=task_id)
|
||||
command_channel = CombinedCommandChannel(
|
||||
(
|
||||
RedisChannel(redis_client, channel_key),
|
||||
@@ -286,7 +288,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
)
|
||||
)
|
||||
|
||||
self._queue_manager.graph_runtime_state = graph_runtime_state
|
||||
self._queue_manager.runtime_state = runtime_state
|
||||
|
||||
workflow_entry = WorkflowEntry(
|
||||
tenant_id=self._workflow.tenant_id,
|
||||
@@ -299,7 +301,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
invoke_from=invoke_from,
|
||||
call_depth=self.application_generate_entity.call_depth,
|
||||
variable_pool=variable_pool,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
command_channel=command_channel,
|
||||
response_stream_filter=self._response_stream_filter,
|
||||
)
|
||||
@@ -317,8 +319,8 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
trace_manager=self.application_generate_entity.trace_manager,
|
||||
)
|
||||
|
||||
workflow_entry.graph_engine.layer(persistence_layer)
|
||||
workflow_entry.graph_engine.layer(
|
||||
workflow_entry.add_layer(persistence_layer)
|
||||
workflow_entry.add_layer(
|
||||
build_workflow_agent_workspace_retirement_layer(
|
||||
dify_run_context=DifyRunContext(
|
||||
native_test_required=entity.native_test_required,
|
||||
@@ -334,7 +336,7 @@ class WorkflowAppRunner(WorkflowBasedAppRunner):
|
||||
)
|
||||
)
|
||||
for layer in self._graph_engine_layers:
|
||||
workflow_entry.graph_engine.layer(layer)
|
||||
workflow_entry.add_layer(layer)
|
||||
|
||||
generator = workflow_entry.run()
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ from collections.abc import Callable, Sequence
|
||||
from typing import final, override
|
||||
|
||||
from core.app.apps.execution_coordinator import is_app_task_stop_flag_set
|
||||
from graphon.graph_engine.command_channels import CommandChannel
|
||||
from graphon.graph_engine.entities.commands import AbortCommand, GraphEngineCommand
|
||||
from graphon.engine.command import AbortCommand, Command, CommandChannel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ShutdownStateGetter = Callable[[], bool]
|
||||
@@ -23,23 +22,23 @@ class CombinedCommandChannel:
|
||||
raise ValueError("command_channels must not be empty")
|
||||
self._command_channels = tuple(command_channels)
|
||||
|
||||
def fetch_commands(self) -> list[GraphEngineCommand]:
|
||||
commands: list[GraphEngineCommand] = []
|
||||
def fetch_commands(self) -> list[Command]:
|
||||
commands: list[Command] = []
|
||||
for channel in self._command_channels:
|
||||
try:
|
||||
commands.extend(channel.fetch_commands())
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch GraphEngine commands from %s", channel.__class__.__name__)
|
||||
logger.exception("Failed to fetch Engine commands from %s", channel.__class__.__name__)
|
||||
return commands
|
||||
|
||||
def send_command(self, command: GraphEngineCommand) -> None:
|
||||
def send_command(self, command: Command) -> None:
|
||||
"""Send commands through the first channel, which is the runner's primary command sink."""
|
||||
self._command_channels[0].send_command(command)
|
||||
|
||||
|
||||
@final
|
||||
class CelerySignalCommandChannel(CommandChannel):
|
||||
"""Translate process-local Celery shutdown state into one GraphEngine abort command."""
|
||||
"""Translate process-local Celery shutdown state into one Engine abort command."""
|
||||
|
||||
_shutdown_state_getter: ShutdownStateGetter
|
||||
_abort_reason: str
|
||||
@@ -56,7 +55,7 @@ class CelerySignalCommandChannel(CommandChannel):
|
||||
self._abort_emitted = False
|
||||
|
||||
@override
|
||||
def fetch_commands(self) -> list[GraphEngineCommand]:
|
||||
def fetch_commands(self) -> list[Command]:
|
||||
if self._abort_emitted or not self._shutdown_state_getter():
|
||||
return []
|
||||
|
||||
@@ -64,13 +63,13 @@ class CelerySignalCommandChannel(CommandChannel):
|
||||
return [AbortCommand(reason=self._abort_reason)]
|
||||
|
||||
@override
|
||||
def send_command(self, command: GraphEngineCommand) -> None:
|
||||
def send_command(self, command: Command) -> None:
|
||||
_ = command
|
||||
|
||||
|
||||
@final
|
||||
class StopFlagCommandChannel(CommandChannel):
|
||||
"""Translate the legacy Redis stop flag into one GraphEngine abort command."""
|
||||
"""Translate the legacy Redis stop flag into one Engine abort command."""
|
||||
|
||||
_task_id: str
|
||||
_abort_reason: str
|
||||
@@ -87,7 +86,7 @@ class StopFlagCommandChannel(CommandChannel):
|
||||
self._abort_emitted = False
|
||||
|
||||
@override
|
||||
def fetch_commands(self) -> list[GraphEngineCommand]:
|
||||
def fetch_commands(self) -> list[Command]:
|
||||
if self._abort_emitted or not is_app_task_stop_flag_set(self._task_id):
|
||||
return []
|
||||
|
||||
@@ -95,5 +94,5 @@ class StopFlagCommandChannel(CommandChannel):
|
||||
return [AbortCommand(reason=self._abort_reason)]
|
||||
|
||||
@override
|
||||
def send_command(self, command: GraphEngineCommand) -> None:
|
||||
def send_command(self, command: Command) -> None:
|
||||
_ = command
|
||||
|
||||
@@ -64,7 +64,7 @@ from core.workflow.system_variables import build_system_variables
|
||||
from extensions.ext_database import db
|
||||
from graphon.entities import WorkflowStartReason
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from models import Account
|
||||
from models.enums import CreatorUserRole
|
||||
from models.model import EndUser
|
||||
@@ -120,7 +120,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
user=user,
|
||||
system_variables=self._workflow_system_variables,
|
||||
)
|
||||
self._graph_runtime_state: GraphRuntimeState | None = self._base_task_pipeline.queue_manager.graph_runtime_state
|
||||
self._graph_runtime_state: RuntimeState | None = self._base_task_pipeline.queue_manager.runtime_state
|
||||
|
||||
def process(
|
||||
self,
|
||||
@@ -481,7 +481,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow.id,
|
||||
status=WorkflowExecutionStatus.SUCCEEDED,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
)
|
||||
|
||||
yield workflow_finish_resp
|
||||
@@ -501,7 +501,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow.id,
|
||||
status=WorkflowExecutionStatus.PARTIAL_SUCCEEDED,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
exceptions_count=event.exceptions_count,
|
||||
)
|
||||
yield workflow_finish_resp
|
||||
@@ -517,7 +517,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
responses = self._workflow_response_converter.workflow_pause_to_stream_response(
|
||||
event=event,
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
)
|
||||
yield from responses
|
||||
|
||||
@@ -545,7 +545,7 @@ class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport):
|
||||
task_id=self._application_generate_entity.task_id,
|
||||
workflow_id=self._workflow.id,
|
||||
status=status,
|
||||
graph_runtime_state=validated_state,
|
||||
runtime_state=validated_state,
|
||||
error=error,
|
||||
exceptions_count=exceptions_count,
|
||||
)
|
||||
|
||||
@@ -5,15 +5,16 @@ from __future__ import annotations
|
||||
from typing import final
|
||||
|
||||
from core.app.apps.execution_coordinator import is_app_task_stop_flag_set
|
||||
from graphon.graph_engine.ready_queue import ReadyQueue, ReadyTask
|
||||
from graphon.runtime.graph_runtime_state import GraphExecutionProtocol, GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.runtime.execution import GraphExecution
|
||||
from graphon.runtime.ready_queue import ReadyQueue, ReadyTask
|
||||
|
||||
|
||||
@final
|
||||
class StopAwareReadyQueue:
|
||||
"""Reject newly ready nodes once the run has been stopped.
|
||||
|
||||
GraphEngine drain still enqueues successors after abort. Drop those puts so
|
||||
Engine drain still enqueues successors after abort. Drop those puts so
|
||||
later nodes do not start; the in-flight node can finish.
|
||||
"""
|
||||
|
||||
@@ -22,7 +23,7 @@ class StopAwareReadyQueue:
|
||||
inner: ReadyQueue,
|
||||
*,
|
||||
task_id: str,
|
||||
graph_execution: GraphExecutionProtocol,
|
||||
graph_execution: GraphExecution,
|
||||
) -> None:
|
||||
self._inner = inner
|
||||
self._task_id = task_id
|
||||
@@ -45,8 +46,8 @@ class StopAwareReadyQueue:
|
||||
def qsize(self) -> int:
|
||||
return self._inner.qsize()
|
||||
|
||||
def drain(self) -> list[ReadyTask]:
|
||||
return self._inner.drain()
|
||||
def take_all(self) -> list[ReadyTask]:
|
||||
return self._inner.take_all()
|
||||
|
||||
def dumps(self) -> str:
|
||||
return self._inner.dumps()
|
||||
@@ -55,13 +56,13 @@ class StopAwareReadyQueue:
|
||||
self._inner.loads(data)
|
||||
|
||||
|
||||
def attach_stop_aware_ready_queue(graph_runtime_state: GraphRuntimeState, *, task_id: str) -> None:
|
||||
def attach_stop_aware_ready_queue(runtime_state: RuntimeState, *, task_id: str) -> None:
|
||||
"""Install stop-aware enqueue policy on an existing runtime state."""
|
||||
current = graph_runtime_state.ready_queue
|
||||
current = runtime_state.ready_queue
|
||||
if isinstance(current, StopAwareReadyQueue):
|
||||
return
|
||||
graph_runtime_state._ready_queue = StopAwareReadyQueue(
|
||||
runtime_state._ready_queue = StopAwareReadyQueue(
|
||||
current,
|
||||
task_id=task_id,
|
||||
graph_execution=graph_runtime_state.graph_execution,
|
||||
graph_execution=runtime_state.graph_execution,
|
||||
)
|
||||
|
||||
@@ -63,14 +63,9 @@ from core.workflow.system_variables import (
|
||||
from core.workflow.variable_pool_initializer import add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run
|
||||
from graphon.entities.base_node_data import BaseNodeData
|
||||
from graphon.entities.graph_config import NodeConfigDict
|
||||
from graphon.entities.pause_reason import HitlRequired
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import (
|
||||
GraphEngineEvent,
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import (
|
||||
EngineEvent,
|
||||
GraphRunAbortedEvent,
|
||||
GraphRunFailedEvent,
|
||||
GraphRunPartialSucceededEvent,
|
||||
@@ -96,7 +91,12 @@ from graphon.graph_events import (
|
||||
NodeRunStreamChunkEvent,
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.entities.base_node_data import BaseNodeData
|
||||
from graphon.entities.graph_config import NodeConfigDict
|
||||
from graphon.entities.pause_reason import HitlRequired
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.graph import Graph
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool
|
||||
from models.workflow import Workflow
|
||||
from services.dify_builder.native_testing.contracts import NativeTestContextRef
|
||||
@@ -174,7 +174,7 @@ class WorkflowBasedAppRunner:
|
||||
queue_manager: AppQueueManager,
|
||||
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
|
||||
app_id: str,
|
||||
graph_engine_layers: Sequence[GraphEngineLayer] = (),
|
||||
graph_engine_layers: Sequence[Layer] = (),
|
||||
):
|
||||
self._queue_manager = queue_manager
|
||||
self._variable_loader = variable_loader
|
||||
@@ -190,7 +190,7 @@ class WorkflowBasedAppRunner:
|
||||
def _init_graph(
|
||||
self,
|
||||
graph_config: Mapping[str, Any],
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
user_from: UserFrom,
|
||||
invoke_from: InvokeFrom,
|
||||
workflow_id: str = "",
|
||||
@@ -239,13 +239,13 @@ class WorkflowBasedAppRunner:
|
||||
call_depth=native_call_depth,
|
||||
)
|
||||
|
||||
# Use the provided graph_runtime_state for consistent state management
|
||||
# Use the provided runtime_state for consistent state management
|
||||
|
||||
node_factory = DifyNodeFactory.from_graph_init_context(
|
||||
execution_recorder=execution_recorder,
|
||||
native_runtime=native_runtime,
|
||||
graph_init_context=graph_init_context,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
if root_node_id is None:
|
||||
@@ -268,7 +268,7 @@ class WorkflowBasedAppRunner:
|
||||
user_id: str,
|
||||
app_type: CreditUsageAppType | None = None,
|
||||
trace_session_id: str | None = None,
|
||||
) -> tuple[Graph, VariablePool, GraphRuntimeState]:
|
||||
) -> tuple[Graph, VariablePool, RuntimeState]:
|
||||
"""
|
||||
Prepare graph, variable pool, and runtime state for single node execution
|
||||
(either single iteration or single loop).
|
||||
@@ -279,7 +279,7 @@ class WorkflowBasedAppRunner:
|
||||
single_loop_run: SingleLoopRunEntity if running single loop, None otherwise
|
||||
|
||||
Returns:
|
||||
A tuple containing (graph, variable_pool, graph_runtime_state)
|
||||
A tuple containing (graph, variable_pool, runtime_state)
|
||||
|
||||
Raises:
|
||||
ValueError: If neither single_iteration_run nor single_loop_run is specified
|
||||
@@ -293,7 +293,7 @@ class WorkflowBasedAppRunner:
|
||||
environment_variables=workflow.environment_variables,
|
||||
),
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.time())
|
||||
runtime_state = RuntimeState(workflow_id=workflow.id, variable_pool=variable_pool, start_at=time.time())
|
||||
|
||||
# Determine which type of single node execution and get graph/variable_pool
|
||||
if single_iteration_run:
|
||||
@@ -301,7 +301,7 @@ class WorkflowBasedAppRunner:
|
||||
workflow=workflow,
|
||||
node_id=single_iteration_run.node_id,
|
||||
user_inputs=dict(single_iteration_run.inputs),
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
node_type_filter_key="iteration_id",
|
||||
node_type_label="iteration",
|
||||
user_id=user_id,
|
||||
@@ -313,7 +313,7 @@ class WorkflowBasedAppRunner:
|
||||
workflow=workflow,
|
||||
node_id=single_loop_run.node_id,
|
||||
user_inputs=dict(single_loop_run.inputs),
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
node_type_filter_key="loop_id",
|
||||
node_type_label="loop",
|
||||
user_id=user_id,
|
||||
@@ -323,16 +323,16 @@ class WorkflowBasedAppRunner:
|
||||
else:
|
||||
raise ValueError("Neither single_iteration_run nor single_loop_run is specified")
|
||||
|
||||
# Return the graph, variable_pool, and the same graph_runtime_state used during graph creation
|
||||
# This ensures all nodes in the graph reference the same GraphRuntimeState instance
|
||||
return graph, variable_pool, graph_runtime_state
|
||||
# Return the graph, variable_pool, and the same runtime_state used during graph creation
|
||||
# This ensures all nodes in the graph reference the same RuntimeState instance
|
||||
return graph, variable_pool, runtime_state
|
||||
|
||||
def _get_graph_and_variable_pool_for_single_node_run(
|
||||
self,
|
||||
workflow: Workflow,
|
||||
node_id: str,
|
||||
user_inputs: dict[str, Any],
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
node_type_filter_key: str, # 'iteration_id' or 'loop_id'
|
||||
node_type_label: str = "node", # 'iteration' or 'loop' for error messages
|
||||
*,
|
||||
@@ -349,7 +349,7 @@ class WorkflowBasedAppRunner:
|
||||
workflow: The workflow instance
|
||||
node_id: The node ID to execute
|
||||
user_inputs: User inputs for the node
|
||||
graph_runtime_state: The graph runtime state
|
||||
runtime_state: The graph runtime state
|
||||
node_type_filter_key: The key to filter nodes ('iteration_id' or 'loop_id')
|
||||
node_type_label: Label for error messages ('iteration' or 'loop')
|
||||
|
||||
@@ -410,7 +410,7 @@ class WorkflowBasedAppRunner:
|
||||
|
||||
node_factory = DifyNodeFactory.from_graph_init_context(
|
||||
graph_init_context=graph_init_context,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
target_node_config = None
|
||||
@@ -427,8 +427,8 @@ class WorkflowBasedAppRunner:
|
||||
node_version = str(target_node_config.data.version)
|
||||
node_cls = resolve_workflow_node_class(node_type=node_type, node_version=node_version)
|
||||
|
||||
# Use the variable pool from graph_runtime_state instead of creating a new one
|
||||
variable_pool = graph_runtime_state.variable_pool
|
||||
# Use the variable pool from runtime_state instead of creating a new one
|
||||
variable_pool = runtime_state.variable_pool
|
||||
|
||||
preload_node_creation_variables(
|
||||
variable_loader=self._variable_loader,
|
||||
@@ -492,7 +492,7 @@ class WorkflowBasedAppRunner:
|
||||
logger.warning("Invalid agent strategy payload for node %s", event.node_id, exc_info=True)
|
||||
return None
|
||||
|
||||
def _handle_event(self, workflow_entry: WorkflowEntry, event: GraphEngineEvent):
|
||||
def _handle_event(self, workflow_entry: WorkflowEntry, event: EngineEvent):
|
||||
"""
|
||||
Handle event
|
||||
:param workflow_entry: workflow entry
|
||||
@@ -519,7 +519,7 @@ class WorkflowBasedAppRunner:
|
||||
)
|
||||
)
|
||||
case GraphRunPausedEvent():
|
||||
runtime_state = workflow_entry.graph_engine.graph_runtime_state
|
||||
runtime_state = workflow_entry.graph_engine.runtime_state
|
||||
paused_nodes = list(
|
||||
dict.fromkeys(reason.node_id for reason in event.reasons if isinstance(reason, HitlRequired))
|
||||
)
|
||||
@@ -575,8 +575,8 @@ class WorkflowBasedAppRunner:
|
||||
node_title=event.node_title,
|
||||
node_type=event.node_type,
|
||||
start_at=event.start_at,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
inputs=inputs,
|
||||
process_data=process_data,
|
||||
outputs=outputs,
|
||||
@@ -595,8 +595,8 @@ class WorkflowBasedAppRunner:
|
||||
node_title=event.node_title,
|
||||
node_type=event.node_type,
|
||||
start_at=event.start_at,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
agent_strategy=self._build_agent_strategy_info(event),
|
||||
provider_type=event.provider_type,
|
||||
provider_id=event.provider_id,
|
||||
@@ -612,15 +612,17 @@ class WorkflowBasedAppRunner:
|
||||
outputs=node_run_result.outputs,
|
||||
)
|
||||
execution_metadata = node_run_result.metadata
|
||||
if event.node_type == BuiltinNodeTypes.ANSWER and (event.in_iteration_id or event.in_loop_id):
|
||||
if event.node_type == BuiltinNodeTypes.ANSWER and (
|
||||
workflow_entry.event_ancestry(event).iteration_id or workflow_entry.event_ancestry(event).loop_id
|
||||
):
|
||||
answer = outputs.get("answer")
|
||||
if isinstance(answer, str) and answer:
|
||||
self._publish_event(
|
||||
QueueTextChunkEvent(
|
||||
text=answer,
|
||||
from_variable_selector=[event.node_id, "answer"],
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
self._publish_event(
|
||||
@@ -634,8 +636,8 @@ class WorkflowBasedAppRunner:
|
||||
process_data=process_data,
|
||||
outputs=outputs,
|
||||
execution_metadata=execution_metadata,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunFailedEvent():
|
||||
@@ -656,8 +658,8 @@ class WorkflowBasedAppRunner:
|
||||
outputs=outputs,
|
||||
error=event.node_run_result.error or "Unknown error",
|
||||
execution_metadata=event.node_run_result.metadata,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunExceptionEvent():
|
||||
@@ -678,8 +680,8 @@ class WorkflowBasedAppRunner:
|
||||
outputs=outputs,
|
||||
error=event.node_run_result.error or "Unknown error",
|
||||
execution_metadata=event.node_run_result.metadata,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunStreamChunkEvent():
|
||||
@@ -687,8 +689,8 @@ class WorkflowBasedAppRunner:
|
||||
QueueTextChunkEvent(
|
||||
text=event.chunk,
|
||||
from_variable_selector=list(event.selector),
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunReasoningChunkEvent():
|
||||
@@ -697,8 +699,8 @@ class WorkflowBasedAppRunner:
|
||||
reasoning=event.chunk,
|
||||
from_node_id=event.node_id,
|
||||
is_final=event.is_final,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunRetrieverResourceEvent():
|
||||
@@ -707,8 +709,8 @@ class WorkflowBasedAppRunner:
|
||||
retriever_resources=[
|
||||
RetrievalSourceMetadata.model_validate(resource) for resource in event.retriever_resources
|
||||
],
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
in_iteration_id=workflow_entry.event_ancestry(event).iteration_id,
|
||||
in_loop_id=workflow_entry.event_ancestry(event).loop_id,
|
||||
)
|
||||
)
|
||||
case NodeRunAgentLogEvent():
|
||||
@@ -733,7 +735,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
start_at=event.start_at,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
inputs=event.inputs,
|
||||
metadata=event.metadata,
|
||||
)
|
||||
@@ -746,7 +748,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
index=event.index,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
output=event.pre_iteration_output,
|
||||
)
|
||||
)
|
||||
@@ -758,7 +760,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
start_at=event.start_at,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
inputs=event.inputs,
|
||||
outputs=event.outputs,
|
||||
metadata=event.metadata,
|
||||
@@ -774,7 +776,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
start_at=event.start_at,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
inputs=event.inputs,
|
||||
metadata=event.metadata,
|
||||
)
|
||||
@@ -787,7 +789,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
index=event.index,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
output=event.pre_loop_output,
|
||||
)
|
||||
)
|
||||
@@ -799,7 +801,7 @@ class WorkflowBasedAppRunner:
|
||||
node_type=event.node_type,
|
||||
node_title=event.node_title,
|
||||
start_at=event.start_at,
|
||||
node_run_index=workflow_entry.graph_engine.graph_runtime_state.node_run_steps,
|
||||
node_run_index=workflow_entry.graph_engine.runtime_state.node_run_steps,
|
||||
inputs=event.inputs,
|
||||
outputs=event.outputs,
|
||||
metadata=event.metadata,
|
||||
|
||||
@@ -108,7 +108,7 @@ def is_retriever_segment_access_granted(segment_id: str) -> bool:
|
||||
|
||||
|
||||
@contextmanager
|
||||
def bind_file_access_scope(scope: FileAccessScope) -> Generator[None]:
|
||||
def bind_file_access_scope(scope: FileAccessScope | None) -> Generator[None]:
|
||||
token = _current_file_access_scope.set(scope)
|
||||
try:
|
||||
yield
|
||||
|
||||
@@ -12,14 +12,14 @@ from typing import override
|
||||
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent, NodeRunVariableUpdatedEvent
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent, NodeRunVariableUpdatedEvent
|
||||
from services.conversation_variable_updater import ConversationVariableUpdater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConversationVariablePersistenceLayer(GraphEngineLayer):
|
||||
class ConversationVariablePersistenceLayer(Layer):
|
||||
def __init__(self, conversation_variable_updater: ConversationVariableUpdater) -> None:
|
||||
super().__init__()
|
||||
self._conversation_variable_updater = conversation_variable_updater
|
||||
@@ -29,7 +29,7 @@ class ConversationVariablePersistenceLayer(GraphEngineLayer):
|
||||
pass
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
if not isinstance(event, NodeRunVariableUpdatedEvent):
|
||||
return
|
||||
|
||||
@@ -38,7 +38,7 @@ class ConversationVariablePersistenceLayer(GraphEngineLayer):
|
||||
logger.warning("Conversation variable selector invalid. selector=%s", selector)
|
||||
return
|
||||
|
||||
conversation_id = get_system_text(self.graph_runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
conversation_id = get_system_text(self.runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
if conversation_id is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity,
|
||||
from core.repositories.human_input_repository import HumanInputFormSubmissionRepository
|
||||
from core.workflow.nodes.human_input.boundary import enrich_graph_pause_reasons
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent, GraphRunPausedEvent
|
||||
from models.model import AppMode
|
||||
from repositories.api_workflow_run_repository import APIWorkflowRunRepository
|
||||
from repositories.factory import DifyAPIRepositoryFactory
|
||||
@@ -74,7 +74,7 @@ class PauseStateLayerConfig:
|
||||
state_owner_user_id: str
|
||||
|
||||
|
||||
class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
class PauseStatePersistenceLayer(Layer):
|
||||
def __init__(
|
||||
self,
|
||||
session_factory: Engine | sessionmaker[Session],
|
||||
@@ -99,6 +99,7 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
self._state_owner_user_id = state_owner_user_id
|
||||
self._generate_entity = generate_entity
|
||||
self._response_stream_filter = response_stream_filter
|
||||
self._paused_event: GraphRunPausedEvent | None = None
|
||||
|
||||
def _get_repo(self) -> APIWorkflowRunRepository:
|
||||
return DifyAPIRepositoryFactory.create_api_workflow_run_repository(self._session_maker)
|
||||
@@ -114,7 +115,7 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
pass
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
"""
|
||||
Called for every event emitted by the engine.
|
||||
|
||||
@@ -127,7 +128,13 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
Args:
|
||||
event: The event emitted by the engine
|
||||
"""
|
||||
if not isinstance(event, GraphRunPausedEvent):
|
||||
if isinstance(event, GraphRunPausedEvent):
|
||||
self._paused_event = event
|
||||
|
||||
def persist_paused_state(self) -> None:
|
||||
"""Commit the runtime/filter pair after the outer stream has fully flushed."""
|
||||
event = self._paused_event
|
||||
if event is None:
|
||||
return
|
||||
|
||||
entity_wrapper: _GenerateEntityUnion
|
||||
@@ -137,13 +144,13 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
entity_wrapper = _AdvancedChatAppGenerateEntityWrapper(entity=self._generate_entity)
|
||||
|
||||
state = WorkflowResumptionContext(
|
||||
serialized_graph_runtime_state=self.graph_runtime_state.dumps(),
|
||||
serialized_graph_runtime_state=self.runtime_state.dumps(),
|
||||
generate_entity=entity_wrapper,
|
||||
serialized_response_stream_filter_state=self._response_stream_filter.dumps(),
|
||||
)
|
||||
|
||||
workflow_run_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.WORKFLOW_EXECUTION_ID,
|
||||
)
|
||||
assert workflow_run_id is not None
|
||||
@@ -153,7 +160,7 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
pause_reasons = enrich_graph_pause_reasons(
|
||||
reasons=event.reasons,
|
||||
form_repository=HumanInputFormSubmissionRepository(),
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
variable_pool=self.runtime_state.variable_pool,
|
||||
)
|
||||
repo = self._get_repo()
|
||||
repo.create_workflow_pause(
|
||||
@@ -162,6 +169,7 @@ class PauseStatePersistenceLayer(GraphEngineLayer):
|
||||
state=state.dumps(),
|
||||
pause_reasons=pause_reasons,
|
||||
)
|
||||
self._paused_event = None
|
||||
|
||||
@override
|
||||
def on_graph_end(self, error: Exception | None) -> None:
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import override
|
||||
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent, GraphRunPausedEvent
|
||||
|
||||
|
||||
class SuspendLayer(GraphEngineLayer):
|
||||
class SuspendLayer(Layer):
|
||||
""" """
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -16,7 +16,7 @@ class SuspendLayer(GraphEngineLayer):
|
||||
self._paused = False
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent):
|
||||
def on_event(self, event: EngineEvent):
|
||||
"""
|
||||
Handle the paused event, stash runtime state into storage and wait for resume.
|
||||
"""
|
||||
|
||||
@@ -4,16 +4,16 @@ from typing import ClassVar, override
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler # type: ignore
|
||||
|
||||
from graphon.graph_engine.entities.commands import CommandType, GraphEngineCommand
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent
|
||||
from graphon.engine.command import PauseCommand
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent
|
||||
from services.workflow.entities import WorkflowScheduleCFSPlanEntity
|
||||
from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TimeSliceLayer(GraphEngineLayer):
|
||||
class TimeSliceLayer(Layer):
|
||||
"""
|
||||
CFS plan scheduler to control the timeslice of the workflow.
|
||||
"""
|
||||
@@ -51,14 +51,7 @@ class TimeSliceLayer(GraphEngineLayer):
|
||||
return
|
||||
|
||||
# send command to pause the workflow
|
||||
self.command_channel.send_command(
|
||||
GraphEngineCommand(
|
||||
command_type=CommandType.PAUSE,
|
||||
payload={
|
||||
"reason": SchedulerCommand.RESOURCE_LIMIT_REACHED,
|
||||
},
|
||||
)
|
||||
)
|
||||
self.command_channel.send_command(PauseCommand(reason=SchedulerCommand.RESOURCE_LIMIT_REACHED))
|
||||
|
||||
except Exception:
|
||||
logger.exception("scheduler error during check if the workflow need to be suspended")
|
||||
@@ -80,7 +73,7 @@ class TimeSliceLayer(GraphEngineLayer):
|
||||
)
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent):
|
||||
def on_event(self, event: EngineEvent):
|
||||
pass
|
||||
|
||||
@override
|
||||
|
||||
@@ -6,9 +6,9 @@ from pydantic import TypeAdapter
|
||||
|
||||
from core.db.session_factory import session_factory
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import (
|
||||
GraphEngineEvent,
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import (
|
||||
EngineEvent,
|
||||
GraphRunAbortedEvent,
|
||||
GraphRunFailedEvent,
|
||||
GraphRunPausedEvent,
|
||||
@@ -21,12 +21,12 @@ from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntit
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerPostLayer(GraphEngineLayer):
|
||||
class TriggerPostLayer(Layer):
|
||||
"""
|
||||
Trigger post layer.
|
||||
"""
|
||||
|
||||
_STATUS_MAP: ClassVar[dict[type[GraphEngineEvent], WorkflowTriggerStatus]] = {
|
||||
_STATUS_MAP: ClassVar[dict[type[EngineEvent], WorkflowTriggerStatus]] = {
|
||||
GraphRunSucceededEvent: WorkflowTriggerStatus.SUCCEEDED,
|
||||
GraphRunFailedEvent: WorkflowTriggerStatus.FAILED,
|
||||
GraphRunAbortedEvent: WorkflowTriggerStatus.FAILED,
|
||||
@@ -49,7 +49,7 @@ class TriggerPostLayer(GraphEngineLayer):
|
||||
pass
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent):
|
||||
def on_event(self, event: EngineEvent):
|
||||
"""
|
||||
Update trigger log with success or failure.
|
||||
"""
|
||||
@@ -65,16 +65,16 @@ class TriggerPostLayer(GraphEngineLayer):
|
||||
elapsed_time = (datetime.now(UTC) - self.start_time).total_seconds()
|
||||
|
||||
# Extract relevant data from result
|
||||
outputs = self.graph_runtime_state.outputs
|
||||
outputs = self.runtime_state.outputs
|
||||
|
||||
# BASICLY, workflow_execution_id is the same as workflow_run_id
|
||||
workflow_run_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.WORKFLOW_EXECUTION_ID,
|
||||
)
|
||||
assert workflow_run_id, "Workflow run id is not set"
|
||||
|
||||
total_tokens = self.graph_runtime_state.total_tokens
|
||||
total_tokens = self.runtime_state.total_tokens
|
||||
|
||||
# Update trigger log with success
|
||||
trigger_log.status = self._STATUS_MAP[type(event)]
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import time
|
||||
import urllib.parse
|
||||
from collections.abc import Generator
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import TYPE_CHECKING, Literal, override
|
||||
|
||||
from configs import dify_config
|
||||
@@ -18,7 +19,7 @@ from core.workflow.file_reference import parse_file_reference
|
||||
from extensions.ext_storage import storage
|
||||
from graphon.file import FileTransferMethod
|
||||
from graphon.file.protocols import WorkflowFileRuntimeProtocol
|
||||
from graphon.file.runtime import set_workflow_file_runtime
|
||||
from graphon.file.runtime import use_workflow_file_runtime
|
||||
from graphon.http.protocols import HttpResponseProtocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -215,5 +216,6 @@ class DifyWorkflowFileRuntime(WorkflowFileRuntimeProtocol):
|
||||
raise ValueError(f"Tool file {tool_file_id} not found")
|
||||
|
||||
|
||||
def bind_dify_workflow_file_runtime() -> None:
|
||||
set_workflow_file_runtime(DifyWorkflowFileRuntime(file_access_controller=DatabaseFileAccessController()))
|
||||
def bind_dify_workflow_file_runtime() -> AbstractContextManager[None]:
|
||||
"""Bind the host adapter for one explicit execution or rendering scope."""
|
||||
return use_workflow_file_runtime(DifyWorkflowFileRuntime(file_access_controller=DatabaseFileAccessController()))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Workflow-level GraphEngine layers that depend on outer infrastructure."""
|
||||
"""Workflow-level Engine layers that depend on outer infrastructure."""
|
||||
|
||||
from .observability import ObservabilityLayer
|
||||
from .persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Observability layer for GraphEngine.
|
||||
Observability layer for Engine.
|
||||
|
||||
This layer creates OpenTelemetry spans for node execution, enabling distributed
|
||||
tracing of workflow execution. It establishes OTel context during node execution
|
||||
@@ -8,7 +8,8 @@ associates with the node span.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from contextvars import Token
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from typing import cast, final, override
|
||||
|
||||
@@ -25,9 +26,9 @@ from extensions.otel.parser import (
|
||||
)
|
||||
from extensions.otel.runtime import is_instrument_flag_enabled
|
||||
from extensions.otel.semconv import DifySpanAttributes
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent, GraphRunAbortedEvent, NodeEvent
|
||||
from graphon.enums import BuiltinNodeTypes, NodeType
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, GraphRunAbortedEvent
|
||||
from graphon.nodes.base.node import Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -36,11 +37,10 @@ logger = logging.getLogger(__name__)
|
||||
@dataclass(slots=True)
|
||||
class _NodeSpanContext:
|
||||
span: "Span"
|
||||
token: Token[context_api.Context]
|
||||
|
||||
|
||||
@final
|
||||
class ObservabilityLayer(GraphEngineLayer):
|
||||
class ObservabilityLayer(Layer):
|
||||
"""
|
||||
Layer that creates OpenTelemetry spans for node execution.
|
||||
|
||||
@@ -88,8 +88,25 @@ class ObservabilityLayer(GraphEngineLayer):
|
||||
"""Called when graph execution starts."""
|
||||
self._node_contexts.clear()
|
||||
|
||||
@contextmanager
|
||||
@override
|
||||
def node_run_context(self, node: Node, *, parent_execution_id: str | None = None) -> Generator[None]:
|
||||
self._ensure_node_span(node, parent_execution_id=parent_execution_id)
|
||||
node_context = self._node_contexts.get(node.execution_id)
|
||||
if node_context is None:
|
||||
yield
|
||||
return
|
||||
token = context_api.attach(set_span_in_context(node_context.span))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
context_api.detach(token)
|
||||
|
||||
@override
|
||||
def on_node_run_start(self, node: Node) -> None:
|
||||
self._ensure_node_span(node)
|
||||
|
||||
def _ensure_node_span(self, node: Node, *, parent_execution_id: str | None = None) -> None:
|
||||
"""
|
||||
Called when a node starts execution.
|
||||
|
||||
@@ -103,28 +120,24 @@ class ObservabilityLayer(GraphEngineLayer):
|
||||
return
|
||||
|
||||
execution_id = node.execution_id
|
||||
if not execution_id:
|
||||
if not execution_id or execution_id in self._node_contexts:
|
||||
return
|
||||
|
||||
parent_context = context_api.get_current()
|
||||
parent = self._node_contexts.get(parent_execution_id) if parent_execution_id else None
|
||||
parent_context = set_span_in_context(parent.span) if parent else context_api.get_current()
|
||||
span = self._tracer.start_span(
|
||||
f"{node.title}",
|
||||
kind=SpanKind.INTERNAL,
|
||||
context=parent_context,
|
||||
)
|
||||
|
||||
new_context = set_span_in_context(span)
|
||||
token = context_api.attach(new_context)
|
||||
|
||||
self._node_contexts[execution_id] = _NodeSpanContext(span=span, token=token)
|
||||
self._node_contexts[execution_id] = _NodeSpanContext(span=span)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Failed to create OpenTelemetry span for node %s: %s", node.id, e)
|
||||
|
||||
@override
|
||||
def on_node_run_end(
|
||||
self, node: Node, error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
) -> None:
|
||||
def on_node_run_end(self, node: Node, error: Exception | None, result_event: NodeEvent | None = None) -> None:
|
||||
"""
|
||||
Called when a node finishes execution.
|
||||
|
||||
@@ -147,19 +160,13 @@ class ObservabilityLayer(GraphEngineLayer):
|
||||
parser.parse(node=node, span=span, error=error, result_event=result_event)
|
||||
span.end()
|
||||
finally:
|
||||
token = node_context.token
|
||||
if token is not None:
|
||||
try:
|
||||
context_api.detach(token)
|
||||
except Exception:
|
||||
logger.warning("Failed to detach OpenTelemetry token: %s", token)
|
||||
self._node_contexts.pop(execution_id, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Failed to end OpenTelemetry span for node %s: %s", node.id, e)
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
"""Record graph-level observability events."""
|
||||
if self._is_disabled:
|
||||
return
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Workflow persistence layer for GraphEngine.
|
||||
"""Workflow persistence layer for Engine.
|
||||
|
||||
This layer mirrors the former ``WorkflowCycleManager`` responsibilities by
|
||||
listening to ``GraphEngineEvent`` instances directly and persisting workflow
|
||||
listening to ``EngineEvent`` instances directly and persisting workflow
|
||||
and node execution state via the injected repositories.
|
||||
|
||||
The design keeps domain persistence concerns inside the engine thread, while
|
||||
@@ -24,17 +24,9 @@ from core.workflow.node_execution_process_data import preserve_workflow_agent_bi
|
||||
from core.workflow.system_variables import SystemVariableKey
|
||||
from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID
|
||||
from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run
|
||||
from graphon.entities import WorkflowExecution, WorkflowNodeExecution, WorkflowStartReason
|
||||
from graphon.enums import (
|
||||
BuiltinNodeTypes,
|
||||
WorkflowExecutionStatus,
|
||||
WorkflowNodeExecutionMetadataKey,
|
||||
WorkflowNodeExecutionStatus,
|
||||
WorkflowType,
|
||||
)
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import (
|
||||
GraphEngineEvent,
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import (
|
||||
EngineEvent,
|
||||
GraphRunAbortedEvent,
|
||||
GraphRunFailedEvent,
|
||||
GraphRunPartialSucceededEvent,
|
||||
@@ -48,8 +40,17 @@ from graphon.graph_events import (
|
||||
NodeRunStartedEvent,
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
from graphon.entities import WorkflowExecution, WorkflowNodeExecution, WorkflowStartReason
|
||||
from graphon.enums import (
|
||||
BuiltinNodeTypes,
|
||||
WorkflowExecutionStatus,
|
||||
WorkflowNodeExecutionMetadataKey,
|
||||
WorkflowNodeExecutionStatus,
|
||||
WorkflowType,
|
||||
)
|
||||
from graphon.node_events import NodeRunResult
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from services.workflow.event_ancestry import WorkflowEventAncestry
|
||||
from services.workflow.inspector_events import (
|
||||
publish_node_changed as _inspector_publish_node_changed,
|
||||
)
|
||||
@@ -80,8 +81,8 @@ class _NodeRuntimeSnapshot:
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
"""GraphEngine layer that persists workflow and node execution state."""
|
||||
class WorkflowPersistenceLayer(Layer):
|
||||
"""Engine layer that persists workflow and node execution state."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -95,6 +96,7 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
super().__init__()
|
||||
self._application_generate_entity = application_generate_entity
|
||||
self._workflow_info = workflow_info
|
||||
self._event_ancestry = WorkflowEventAncestry(workflow_info.graph_data)
|
||||
self._workflow_execution_repository = workflow_execution_repository
|
||||
self._workflow_node_execution_repository = workflow_node_execution_repository
|
||||
self._trace_manager = trace_manager
|
||||
@@ -103,9 +105,10 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
self._node_execution_cache: dict[str, WorkflowNodeExecution] = {}
|
||||
self._node_snapshots: dict[str, _NodeRuntimeSnapshot] = {}
|
||||
self._node_sequence: int = 0
|
||||
self._pending_pause: GraphRunPausedEvent | None = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GraphEngineLayer lifecycle
|
||||
# Layer lifecycle
|
||||
# ------------------------------------------------------------------
|
||||
@override
|
||||
def on_graph_start(self) -> None:
|
||||
@@ -113,9 +116,10 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
self._node_execution_cache.clear()
|
||||
self._node_snapshots.clear()
|
||||
self._node_sequence = 0
|
||||
self._pending_pause = None
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
match event:
|
||||
case GraphRunStartedEvent():
|
||||
self._handle_graph_run_started(event)
|
||||
@@ -128,7 +132,7 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
case GraphRunAbortedEvent():
|
||||
self._handle_graph_run_aborted(event)
|
||||
case GraphRunPausedEvent():
|
||||
self._handle_graph_run_paused(event)
|
||||
self._pending_pause = event
|
||||
case NodeRunRetryEvent():
|
||||
self._handle_node_retry(event)
|
||||
case NodeRunStartedEvent():
|
||||
@@ -142,6 +146,17 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
case NodeRunPauseRequestedEvent():
|
||||
self._handle_node_pause_requested(event)
|
||||
|
||||
def persist_paused_state(self) -> None:
|
||||
"""Commit PAUSED only after the recoverable runtime/filter snapshot exists."""
|
||||
if self._pending_pause is not None:
|
||||
self._handle_graph_run_paused(self._pending_pause)
|
||||
self._pending_pause = None
|
||||
|
||||
def fail_pause_persistence(self, error: Exception) -> None:
|
||||
if self._pending_pause is not None:
|
||||
self._pending_pause = None
|
||||
self._handle_graph_run_failed(GraphRunFailedEvent(error=str(error)))
|
||||
|
||||
@override
|
||||
def on_graph_end(self, error: Exception | None) -> None:
|
||||
return
|
||||
@@ -227,8 +242,10 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
execution = self._get_workflow_execution()
|
||||
|
||||
metadata = {
|
||||
WorkflowNodeExecutionMetadataKey.ITERATION_ID: event.in_iteration_id,
|
||||
WorkflowNodeExecutionMetadataKey.LOOP_ID: event.in_loop_id,
|
||||
WorkflowNodeExecutionMetadataKey.ITERATION_ID: self._event_ancestry.resolve(
|
||||
event.container_id
|
||||
).iteration_id,
|
||||
WorkflowNodeExecutionMetadataKey.LOOP_ID: self._event_ancestry.resolve(event.container_id).loop_id,
|
||||
}
|
||||
|
||||
domain_execution = WorkflowNodeExecution(
|
||||
@@ -256,8 +273,8 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
node_id=event.node_id,
|
||||
title=event.node_title,
|
||||
predecessor_node_id=event.predecessor_node_id,
|
||||
iteration_id=event.in_iteration_id,
|
||||
loop_id=event.in_loop_id,
|
||||
iteration_id=self._event_ancestry.resolve(event.container_id).iteration_id,
|
||||
loop_id=self._event_ancestry.resolve(event.container_id).loop_id,
|
||||
created_at=event.start_at,
|
||||
)
|
||||
self._node_snapshots[event.id] = snapshot
|
||||
@@ -415,7 +432,7 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
def _populate_completion_statistics(self, execution: WorkflowExecution, *, update_finished: bool = True) -> None:
|
||||
if update_finished:
|
||||
execution.finished_at = naive_utc_now()
|
||||
runtime_state = self.graph_runtime_state
|
||||
runtime_state = self.runtime_state
|
||||
execution.total_tokens = runtime_state.total_tokens
|
||||
execution.total_steps = runtime_state.node_run_steps
|
||||
execution.outputs = execution.outputs or runtime_state.outputs
|
||||
@@ -501,5 +518,5 @@ class WorkflowPersistenceLayer(GraphEngineLayer):
|
||||
self._trace_manager.add_trace_task(trace_task)
|
||||
|
||||
def _system_variables(self) -> Mapping[str, Any]:
|
||||
runtime_state = self.graph_runtime_state
|
||||
runtime_state = self.runtime_state
|
||||
return runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, override
|
||||
from core.plugin.entities.plugin_daemon import TTSAudioChunk
|
||||
from graphon.model_runtime.entities.model_entities import ModelPropertyKey
|
||||
from graphon.model_runtime.errors.invoke import InvokeBadRequestError
|
||||
from graphon.model_runtime.protocols.tts_runtime import TTSChunk
|
||||
from libs.stream import close_stream
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -93,8 +94,10 @@ def _normalize_reported_mime_type(mime_type: object | None, source: str) -> str
|
||||
return normalized_mime_type
|
||||
|
||||
|
||||
def _extract_audio_chunk(chunk: bytes | bytearray | memoryview | TTSAudioChunk) -> tuple[bytes, str | None]:
|
||||
"""Accept old byte chunks and the compatibility carrier for current Graphon."""
|
||||
def _extract_audio_chunk(chunk: bytes | bytearray | memoryview | TTSAudioChunk | TTSChunk) -> tuple[bytes, str | None]:
|
||||
"""Unpack native runtime chunks and plugin byte carriers without losing MIME."""
|
||||
if isinstance(chunk, TTSChunk):
|
||||
return chunk.data, chunk.mime_type
|
||||
if isinstance(chunk, TTSAudioChunk):
|
||||
return bytes(chunk), chunk.mime_type
|
||||
|
||||
@@ -141,7 +144,7 @@ def resolve_audio_mime_type(
|
||||
return normalized_declared_mime_type or DEFAULT_TTS_AUDIO_MIME_TYPE
|
||||
|
||||
|
||||
type _AudioChunk = bytes | bytearray | memoryview | TTSAudioChunk
|
||||
type _AudioChunk = bytes | bytearray | memoryview | TTSAudioChunk | TTSChunk
|
||||
|
||||
|
||||
class AudioStream(Iterator[bytes]):
|
||||
|
||||
@@ -35,6 +35,8 @@ from graphon.model_runtime.model_providers.base.rerank_model import RerankModel
|
||||
from graphon.model_runtime.model_providers.base.speech2text_model import Speech2TextModel
|
||||
from graphon.model_runtime.model_providers.base.text_embedding_model import TextEmbeddingModel
|
||||
from graphon.model_runtime.model_providers.base.tts_model import TTSModel
|
||||
from graphon.model_runtime.protocols.tts_runtime import TTSChunk
|
||||
from libs.stream import close_stream
|
||||
from models.provider import ProviderType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -407,7 +409,7 @@ class ModelInstance:
|
||||
voice: str = "",
|
||||
*,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Iterable[bytes]:
|
||||
) -> Iterable[TTSChunk]:
|
||||
"""
|
||||
Invoke large language tts model
|
||||
|
||||
@@ -794,7 +796,7 @@ class QuotaManagedModelInstance(ModelInstance):
|
||||
voice: str = "",
|
||||
*,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Iterable[bytes]:
|
||||
) -> Iterable[TTSChunk]:
|
||||
return self._invoke_tts_stream(content_text=content_text, voice=voice, request_metadata=request_metadata)
|
||||
|
||||
def _invoke_tts_stream(
|
||||
@@ -803,9 +805,10 @@ class QuotaManagedModelInstance(ModelInstance):
|
||||
content_text: str,
|
||||
voice: str,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Generator[bytes]:
|
||||
) -> Generator[TTSChunk]:
|
||||
effective_request_metadata = self._resolve_request_metadata(request_metadata)
|
||||
reservation = self._reserve_quota_for_request(effective_request_metadata)
|
||||
response: Iterable[TTSChunk] | None = None
|
||||
try:
|
||||
response = super().invoke_tts(
|
||||
content_text=content_text,
|
||||
@@ -816,7 +819,11 @@ class QuotaManagedModelInstance(ModelInstance):
|
||||
reservation.commit()
|
||||
yield chunk
|
||||
finally:
|
||||
self.release_quota_safely(reservation)
|
||||
try:
|
||||
if response is not None:
|
||||
close_stream(response)
|
||||
finally:
|
||||
self.release_quota_safely(reservation)
|
||||
|
||||
|
||||
class ModelManager:
|
||||
|
||||
@@ -32,7 +32,8 @@ from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput
|
||||
from graphon.model_runtime.entities.text_embedding_entities import EmbeddingInputType, EmbeddingResult
|
||||
from graphon.model_runtime.model_providers.base.large_language_model import normalize_non_stream_runtime_result
|
||||
from graphon.model_runtime.protocols.runtime import ModelRuntime
|
||||
from graphon.model_runtime.protocols.tts_runtime import TTSModelVoice
|
||||
from graphon.model_runtime.protocols.tts_runtime import TTSChunk, TTSModelVoice
|
||||
from libs.stream import close_stream
|
||||
from models.provider_ids import ModelProviderID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -638,10 +639,10 @@ class PluginModelRuntime(ModelRuntime):
|
||||
content_text: str,
|
||||
voice: str,
|
||||
request_metadata: Mapping[str, object] | None = None,
|
||||
) -> Iterable[bytes]:
|
||||
) -> Iterable[TTSChunk]:
|
||||
del request_metadata
|
||||
plugin_id, provider_name = self._split_provider(provider)
|
||||
return self.client.invoke_tts(
|
||||
response = self.client.invoke_tts(
|
||||
tenant_id=self.tenant_id,
|
||||
user_id=self.user_id,
|
||||
plugin_id=plugin_id,
|
||||
@@ -652,6 +653,12 @@ class PluginModelRuntime(ModelRuntime):
|
||||
voice=voice,
|
||||
)
|
||||
|
||||
try:
|
||||
for chunk in response:
|
||||
yield TTSChunk(data=bytes(chunk), mime_type=chunk.mime_type)
|
||||
finally:
|
||||
close_stream(response)
|
||||
|
||||
@override
|
||||
def get_tts_model_voices(
|
||||
self,
|
||||
|
||||
@@ -492,9 +492,12 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
|
||||
# Add images first
|
||||
for file in image_files:
|
||||
try:
|
||||
file_content = file_manager.to_prompt_message_content(
|
||||
file, image_detail_config=ImagePromptMessageContent.DETAIL.LOW
|
||||
)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
file_content = file_manager.to_prompt_message_content(
|
||||
file, image_detail_config=ImagePromptMessageContent.DETAIL.LOW
|
||||
)
|
||||
prompt_message_contents.append(file_content)
|
||||
except Exception:
|
||||
logger.warning("Failed to convert image file to prompt message content", exc_info=True)
|
||||
|
||||
@@ -11,7 +11,7 @@ from core.workflow.nodes.human_input.pause_reason import (
|
||||
HumanInputRequired,
|
||||
PauseReason,
|
||||
)
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.variables import ArrayStringSegment
|
||||
from models.human_input import ApprovalChannel, RecipientType
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from collections.abc import Callable, Generator, Sequence
|
||||
from typing import Any, override
|
||||
|
||||
from graphon.file import File
|
||||
from graphon.model_runtime.entities.llm_entities import LLMStructuredOutput
|
||||
from graphon.model_runtime.entities.message_entities import PromptMessage
|
||||
from graphon.node_events.base import NodeEventBase
|
||||
from graphon.node_events.base import NodeEventPayload
|
||||
from graphon.nodes.llm.node import LLMNode
|
||||
from graphon.nodes.llm.runtime_protocols import LLMPollingCapableProtocol
|
||||
|
||||
@@ -31,12 +32,14 @@ class DifyLLMNode(LLMNode):
|
||||
self,
|
||||
*,
|
||||
polling_model: LLMPollingCapableProtocol,
|
||||
file_outputs: list[File],
|
||||
prompt_messages: Sequence[PromptMessage],
|
||||
stop: Sequence[str] | None,
|
||||
) -> Generator[NodeEventBase | LLMStructuredOutput]:
|
||||
) -> Generator[NodeEventPayload | LLMStructuredOutput]:
|
||||
try:
|
||||
yield from super()._invoke_llm_with_polling(
|
||||
polling_model=polling_model,
|
||||
file_outputs=file_outputs,
|
||||
prompt_messages=prompt_messages,
|
||||
stop=stop,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import importlib
|
||||
import pkgutil
|
||||
import secrets
|
||||
from collections.abc import Callable, Iterator, Mapping, MutableMapping, Sequence
|
||||
from copy import copy, deepcopy
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast, final, override, runtime_checkable
|
||||
@@ -80,9 +81,10 @@ from core.workflow.template_rendering import CodeExecutorJinja2TemplateRenderer
|
||||
from extensions.application_services.data_sources import build_data_source_credentials
|
||||
from graphon.entities.base_node_data import BaseNodeData
|
||||
from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter
|
||||
from graphon.enums import BuiltinNodeTypes, NodeType
|
||||
from graphon.enums import BuiltinNodeTypes, NodeExecutionType, NodeType
|
||||
from graphon.file.file_manager import file_manager
|
||||
from graphon.graph.graph import NodeFactory
|
||||
from graphon.graph.scoping import resolve_container_id
|
||||
from graphon.model_runtime.memory import PromptMessageMemory
|
||||
from graphon.model_runtime.model_providers.base.large_language_model import LargeLanguageModel
|
||||
from graphon.nodes.base.node import Node
|
||||
@@ -106,8 +108,7 @@ from services.dify_builder.native_testing.runtime_bridge import NativeRuntimeBri
|
||||
from services.dify_builder.template_execution import MAX_OUTPUT_BYTES, BoundedJinja2TemplateRenderer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import InitParams, RuntimeState
|
||||
|
||||
LATEST_VERSION = "latest"
|
||||
_START_NODE_TYPES: frozenset[NodeType] = frozenset(
|
||||
@@ -119,7 +120,7 @@ _START_NODE_TYPES: frozenset[NodeType] = frozenset(
|
||||
class DifyGraphInitContext:
|
||||
"""Explicit graph-init values owned by the workflow layer.
|
||||
|
||||
Dify is gradually removing direct `GraphInitParams` construction from its
|
||||
Dify is gradually removing direct `InitParams` construction from its
|
||||
production call sites. Keep the translation here until `graphon` exposes an
|
||||
equivalent explicit API.
|
||||
"""
|
||||
@@ -129,10 +130,10 @@ class DifyGraphInitContext:
|
||||
run_context: Mapping[str, Any]
|
||||
call_depth: int
|
||||
|
||||
def to_graph_init_params(self) -> "GraphInitParams":
|
||||
from graphon.entities import GraphInitParams
|
||||
def to_graph_init_params(self) -> "InitParams":
|
||||
from graphon.runtime import InitParams
|
||||
|
||||
return GraphInitParams(
|
||||
return InitParams(
|
||||
workflow_id=self.workflow_id,
|
||||
graph_config=self.graph_config,
|
||||
run_context=self.run_context,
|
||||
@@ -369,7 +370,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
cls,
|
||||
*,
|
||||
graph_init_context: DifyGraphInitContext,
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
runtime_state: "RuntimeState",
|
||||
execution_recorder: BuilderExecutionRecorder | None = None,
|
||||
native_runtime: NativeRuntimeBridge | None = None,
|
||||
) -> "DifyNodeFactory":
|
||||
@@ -377,22 +378,23 @@ class DifyNodeFactory(NodeFactory):
|
||||
return cls(
|
||||
execution_recorder=execution_recorder,
|
||||
native_runtime=native_runtime,
|
||||
graph_init_params=graph_init_context.to_graph_init_params(),
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=graph_init_context.to_graph_init_params(),
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
graph_init_params: "GraphInitParams",
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
init_params: "InitParams",
|
||||
runtime_state: "RuntimeState",
|
||||
*,
|
||||
execution_recorder: BuilderExecutionRecorder | None = None,
|
||||
native_runtime: NativeRuntimeBridge | None = None,
|
||||
_request_hmac_key: bytes | None = None,
|
||||
) -> None:
|
||||
self.graph_init_params = graph_init_params
|
||||
self.graph_runtime_state = graph_runtime_state
|
||||
self._dify_context = self._resolve_dify_context(graph_init_params.run_context)
|
||||
self.init_params = init_params
|
||||
self._admitted_graph_config = deepcopy(init_params.graph_config)
|
||||
self.runtime_state = runtime_state
|
||||
self._dify_context = self._resolve_dify_context(init_params.run_context)
|
||||
self.execution_recorder = execution_recorder
|
||||
self._native_runtime = native_runtime
|
||||
if native_runtime is not None and self._dify_context.native_test_ref is None:
|
||||
@@ -408,9 +410,9 @@ class DifyNodeFactory(NodeFactory):
|
||||
self._dify_context.tenant_id,
|
||||
self._dify_context.app_id,
|
||||
self._dify_context.user_id,
|
||||
graph_init_params.workflow_id,
|
||||
graph_init_params.call_depth,
|
||||
dict(graph_init_params.graph_config),
|
||||
init_params.workflow_id,
|
||||
init_params.call_depth,
|
||||
dict(init_params.graph_config),
|
||||
) != (
|
||||
context.tenant_id,
|
||||
context.app_id,
|
||||
@@ -471,12 +473,12 @@ class DifyNodeFactory(NodeFactory):
|
||||
self._human_input_runtime = DifyHumanInputNodeRuntime(
|
||||
self._dify_context,
|
||||
workflow_execution_id_getter=lambda: get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.WORKFLOW_EXECUTION_ID,
|
||||
),
|
||||
conversation_id_getter=self._conversation_id,
|
||||
)
|
||||
self._tool_runtime = DifyToolNodeRuntime(self._dify_context, workflow_id=self.graph_init_params.workflow_id)
|
||||
self._tool_runtime = DifyToolNodeRuntime(self._dify_context, workflow_id=self.init_params.workflow_id)
|
||||
self._http_request_file_manager = file_manager
|
||||
self._document_extractor_unstructured_api_config = UnstructuredApiConfig(
|
||||
api_url=dify_config.UNSTRUCTURED_API_URL,
|
||||
@@ -508,13 +510,13 @@ class DifyNodeFactory(NodeFactory):
|
||||
self._dify_context.tenant_id,
|
||||
self._dify_context.app_id,
|
||||
self._dify_context.user_id,
|
||||
self.graph_init_params.workflow_id,
|
||||
self.graph_init_params.call_depth,
|
||||
self.init_params.workflow_id,
|
||||
self.init_params.call_depth,
|
||||
) != (context.tenant_id, context.app_id, context.actor_id, context.workflow_id, 0):
|
||||
raise BuilderExecutionPolicyError("execution_owner_mismatch")
|
||||
|
||||
def _admission_snapshot(self) -> RestrictedAdmissionSnapshot:
|
||||
graph = dict(self.graph_init_params.graph_config)
|
||||
graph = dict(self.init_params.graph_config)
|
||||
return RestrictedAdmissionSnapshot(
|
||||
app_mode="workflow",
|
||||
workflow_kind="standard",
|
||||
@@ -559,9 +561,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
raise BuilderExecutionPolicyError("node_binding_mismatch")
|
||||
|
||||
def issue_guard(node_id: str, node_execution_id: str) -> ConfirmedToolGuard:
|
||||
native_run_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID
|
||||
)
|
||||
native_run_id = get_system_text(self.runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
|
||||
if not native_run_id:
|
||||
raise BuilderExecutionPolicyError("execution_launch_mismatch")
|
||||
return recorder.confirmed_guard(
|
||||
@@ -586,7 +586,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
self._validate_json_raw_config(node_config)
|
||||
if admit_json_workflow(snapshot).nodes != context.admitted_nodes:
|
||||
raise BuilderExecutionPolicyError("node_binding_mismatch")
|
||||
native_run_id = get_system_text(self.graph_runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
|
||||
native_run_id = get_system_text(self.runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
|
||||
if not native_run_id:
|
||||
raise BuilderExecutionPolicyError("execution_launch_mismatch")
|
||||
self._json_tool_runtime = QualifiedJsonToolNodeRuntime(
|
||||
@@ -596,14 +596,88 @@ class DifyNodeFactory(NodeFactory):
|
||||
issuer=recorder,
|
||||
)
|
||||
|
||||
def with_runtime_state(self, graph_runtime_state: "GraphRuntimeState") -> "DifyNodeFactory":
|
||||
return DifyNodeFactory(
|
||||
execution_recorder=self.execution_recorder,
|
||||
_request_hmac_key=getattr(self, "_request_hmac_key", None),
|
||||
native_runtime=self._native_runtime,
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
def with_runtime_state(self, runtime_state: "RuntimeState") -> "DifyNodeFactory":
|
||||
"""Copy frame-local state while retaining the launch's guards and HMAC identity."""
|
||||
context = self._dify_context.builder_execution
|
||||
if context is not None:
|
||||
self._require_builder_owner(context)
|
||||
scoped = copy(self)
|
||||
scoped.runtime_state = runtime_state
|
||||
scoped.init_params = self.init_params.model_copy(deep=True)
|
||||
if hasattr(scoped, "_human_input_runtime"):
|
||||
scoped._human_input_runtime = DifyHumanInputNodeRuntime(
|
||||
scoped._dify_context,
|
||||
workflow_execution_id_getter=lambda: get_system_text(
|
||||
scoped.runtime_state.variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID
|
||||
),
|
||||
conversation_id_getter=scoped._conversation_id,
|
||||
)
|
||||
scoped._bound_tool_file_manager_factory = lambda: DifyToolFileManager(
|
||||
scoped._dify_context, conversation_id_getter=scoped._conversation_id
|
||||
)
|
||||
scoped._llm_file_saver = build_dify_llm_file_saver(
|
||||
run_context=scoped._dify_context,
|
||||
http_client=scoped._remote_file_http_client,
|
||||
conversation_id_getter=scoped._conversation_id,
|
||||
)
|
||||
return scoped
|
||||
|
||||
def with_graph_config(self, graph_config: Mapping[str, Any]) -> "DifyNodeFactory":
|
||||
"""Scope node initialization without re-running admission or initializing services."""
|
||||
scoped = copy(self)
|
||||
scoped.init_params = self.init_params.model_copy(update={"graph_config": deepcopy(graph_config)})
|
||||
return scoped
|
||||
|
||||
def _binding_node_config(self, node_config: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Recover only Graphon's verified canonical ownership addition for raw admission.
|
||||
|
||||
Digests bind the persisted graph, not Graphon's materialized DTO. Never
|
||||
discard a caller-supplied field or other changed extras from that boundary.
|
||||
"""
|
||||
config = dict(node_config)
|
||||
data = config.get("data")
|
||||
if isinstance(data, BaseNodeData):
|
||||
data = data.model_dump(mode="json", by_alias=True, exclude_unset=True)
|
||||
if not isinstance(data, Mapping):
|
||||
return config
|
||||
data = dict(data)
|
||||
originals = {node["id"]: node for node in self._admitted_graph_config.get("nodes", [])}
|
||||
original = originals.get(config.get("id"))
|
||||
if original is not None and "container_id" not in original["data"] and "container_id" in data:
|
||||
if data["container_id"] != resolve_container_id(original, nodes_by_id=originals):
|
||||
raise BuilderExecutionPolicyError("execution_binding_mismatch")
|
||||
del data["container_id"]
|
||||
config["data"] = data
|
||||
return config
|
||||
|
||||
def validate_node(self, node_config: Mapping[str, Any]) -> NodeExecutionType:
|
||||
"""Resolve and validate the concrete node and host binding before any construction."""
|
||||
context = self._dify_context.builder_execution
|
||||
if context is not None:
|
||||
node_config = self._binding_node_config(node_config)
|
||||
validator = None
|
||||
if isinstance(context, ConfirmedToolExecutionContext):
|
||||
validator = self._validate_confirmed_raw_config
|
||||
elif isinstance(context, JsonToolExecutionContext):
|
||||
validator = self._validate_json_raw_config
|
||||
elif isinstance(context, BuilderExecutionContext):
|
||||
validator = self._validate_restricted_raw_config
|
||||
elif context is not None:
|
||||
raise BuilderExecutionPolicyError("unsupported_execution_policy")
|
||||
|
||||
def validate_raw_binding(_adapted: Mapping[str, Any], node_class: type[Node] | None) -> None:
|
||||
# Admission binds persisted input, before legacy Tool/HITL DTO adaptation.
|
||||
# Still check the actual resolved class at both validation checkpoints.
|
||||
if validator is not None:
|
||||
validator(node_config, node_class)
|
||||
|
||||
_, _, node_class, _ = _validated_node_config(
|
||||
adapt_node_config_for_graph(node_config),
|
||||
resolve_node_class=self._resolve_node_class,
|
||||
validate_resolved_node_data=self._validate_resolved_node_data,
|
||||
pre_validate_node_data=validate_raw_binding if validator is not None else None,
|
||||
)
|
||||
return node_class.execution_type
|
||||
|
||||
@staticmethod
|
||||
def _resolve_dify_context(run_context: Mapping[str, Any]) -> DifyRunContext:
|
||||
@@ -615,7 +689,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
return DifyRunContext.model_validate(raw_ctx)
|
||||
|
||||
def _conversation_id(self) -> str | None:
|
||||
return get_system_text(self.graph_runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
return get_system_text(self.runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
|
||||
@override
|
||||
def create_node(self, node_config: dict[str, Any] | NodeConfigDict) -> Node:
|
||||
@@ -630,13 +704,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
"""
|
||||
context = self._dify_context.builder_execution
|
||||
if context is not None:
|
||||
# Graph.init supplies its shared DTO. JSON restores native enum spellings
|
||||
# while exclude_unset preserves the original raw config digest boundary.
|
||||
if isinstance(node_config.get("data"), BaseNodeData):
|
||||
node_config = {
|
||||
**node_config,
|
||||
"data": node_config["data"].model_dump(mode="json", by_alias=True, exclude_unset=True),
|
||||
}
|
||||
node_config = self._binding_node_config(node_config)
|
||||
if isinstance(context, ConfirmedToolExecutionContext):
|
||||
return self._create_confirmed_node(node_config)
|
||||
if isinstance(context, JsonToolExecutionContext):
|
||||
@@ -659,13 +727,13 @@ class DifyNodeFactory(NodeFactory):
|
||||
node_id=node_id,
|
||||
node_data=resolved_node_data,
|
||||
node_class=node_class,
|
||||
runtime_state=self.graph_runtime_state,
|
||||
runtime_state=self.runtime_state,
|
||||
)
|
||||
return node_class(
|
||||
node_id=node_id,
|
||||
data=resolved_node_data.model_dump(mode="python", by_alias=True),
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
init_params=self.init_params,
|
||||
runtime_state=self.runtime_state,
|
||||
**native_kwargs,
|
||||
)
|
||||
if isinstance(context, BuilderExecutionContext):
|
||||
@@ -697,8 +765,8 @@ class DifyNodeFactory(NodeFactory):
|
||||
return node_class(
|
||||
node_id=node_id,
|
||||
data=resolved_node_data.model_dump(mode="python", by_alias=True),
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
init_params=self.init_params,
|
||||
runtime_state=self.runtime_state,
|
||||
**kwargs,
|
||||
)
|
||||
node_type = node_data.type
|
||||
@@ -777,8 +845,8 @@ class DifyNodeFactory(NodeFactory):
|
||||
node = node_class(
|
||||
node_id=node_id,
|
||||
data=constructor_node_data,
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
init_params=self.init_params,
|
||||
runtime_state=self.runtime_state,
|
||||
**node_init_kwargs,
|
||||
)
|
||||
return node
|
||||
@@ -801,8 +869,8 @@ class DifyNodeFactory(NodeFactory):
|
||||
return node_class(
|
||||
node_id=node_id,
|
||||
data=resolved_node_data.model_dump(mode="python", by_alias=True),
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
init_params=self.init_params,
|
||||
runtime_state=self.runtime_state,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -823,8 +891,8 @@ class DifyNodeFactory(NodeFactory):
|
||||
return node_class(
|
||||
node_id=node_id,
|
||||
data=resolved.model_dump(mode="python", by_alias=True),
|
||||
graph_init_params=self.graph_init_params,
|
||||
graph_runtime_state=self.graph_runtime_state,
|
||||
init_params=self.init_params,
|
||||
runtime_state=self.runtime_state,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -928,7 +996,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
return node_data
|
||||
|
||||
selector = parse_llm_model_selector(model_selector)
|
||||
variable = self.graph_runtime_state.variable_pool.get(selector)
|
||||
variable = self.runtime_state.variable_pool.get(selector)
|
||||
if not isinstance(variable, ObjectSegment):
|
||||
raise ValueError(f"LLM environment variable '{selector[1]}' was not found or is not an LLM variable")
|
||||
|
||||
@@ -1051,6 +1119,8 @@ class DifyNodeFactory(NodeFactory):
|
||||
if include_jinja2_template_renderer:
|
||||
node_init_kwargs["jinja2_template_renderer"] = self._jinja2_template_renderer
|
||||
if validated_node_data.type == BuiltinNodeTypes.LLM:
|
||||
for legacy_key in ("credentials_provider", "model_factory", "http_client"):
|
||||
node_init_kwargs.pop(legacy_key, None)
|
||||
node_init_kwargs["default_query_selector"] = system_variable_selector(SystemVariableKey.QUERY)
|
||||
node_init_kwargs["polling_finalizer"] = cast(DifyPreparedLLM, node_model_instance).finalize_llm_polling
|
||||
return node_init_kwargs
|
||||
@@ -1089,7 +1159,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
if not context_variable_selector:
|
||||
return False
|
||||
|
||||
context_value = self.graph_runtime_state.variable_pool.get(context_variable_selector)
|
||||
context_value = self.runtime_state.variable_pool.get(context_variable_selector)
|
||||
if not isinstance(context_value, ArrayObjectSegment):
|
||||
return False
|
||||
|
||||
@@ -1124,7 +1194,7 @@ class DifyNodeFactory(NodeFactory):
|
||||
if node_data.memory is None:
|
||||
return None
|
||||
|
||||
conversation_id = get_system_text(self.graph_runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
conversation_id = get_system_text(self.runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
return fetch_memory(
|
||||
conversation_id=conversation_id,
|
||||
app_id=self._dify_context.app_id,
|
||||
|
||||
@@ -6,9 +6,9 @@ from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.node_events import NodeEventBase, NodeRunResult, StreamCompletedEvent
|
||||
from graphon.node_events import NodeEventPayload, NodeRunResult, StreamCompletedEvent
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.nodes.base.variable_template_parser import VariableTemplateParser
|
||||
|
||||
@@ -23,8 +23,7 @@ from .runtime_support import AgentRuntimeSupport
|
||||
from .strategy_protocols import AgentStrategyPresentationProvider, AgentStrategyResolver
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import InitParams, RuntimeState
|
||||
|
||||
|
||||
class AgentNode(Node[AgentNodeData]):
|
||||
@@ -40,8 +39,8 @@ class AgentNode(Node[AgentNodeData]):
|
||||
node_id: str,
|
||||
data: AgentNodeData,
|
||||
*,
|
||||
graph_init_params: GraphInitParams,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
init_params: InitParams,
|
||||
runtime_state: RuntimeState,
|
||||
strategy_resolver: AgentStrategyResolver,
|
||||
presentation_provider: AgentStrategyPresentationProvider,
|
||||
runtime_support: AgentRuntimeSupport,
|
||||
@@ -50,8 +49,8 @@ class AgentNode(Node[AgentNodeData]):
|
||||
super().__init__(
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
self._strategy_resolver = strategy_resolver
|
||||
self._presentation_provider = presentation_provider
|
||||
@@ -77,8 +76,8 @@ class AgentNode(Node[AgentNodeData]):
|
||||
@override
|
||||
@singledispatchmethod
|
||||
def _dispatch( # pyrefly: ignore[missing-override-decorator]
|
||||
self, event: NodeEventBase
|
||||
) -> GraphNodeEventBase:
|
||||
self, event: NodeEventPayload
|
||||
) -> NodeEvent:
|
||||
return super()._dispatch(event)
|
||||
|
||||
@_dispatch.register
|
||||
@@ -98,7 +97,7 @@ class AgentNode(Node[AgentNodeData]):
|
||||
)
|
||||
|
||||
@override
|
||||
def _run(self) -> Generator[NodeEventBase]:
|
||||
def _run(self) -> Generator[NodeEventPayload]:
|
||||
from core.plugin.impl.exc import PluginDaemonClientSideError
|
||||
|
||||
dify_ctx = DifyRunContext.model_validate(self.require_run_context_value(DIFY_RUN_CONTEXT_KEY))
|
||||
@@ -123,7 +122,7 @@ class AgentNode(Node[AgentNodeData]):
|
||||
|
||||
parameters = self._runtime_support.build_parameters(
|
||||
agent_parameters=agent_parameters,
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
variable_pool=self.runtime_state.variable_pool,
|
||||
node_data=self.node_data,
|
||||
strategy=strategy,
|
||||
tenant_id=dify_ctx.tenant_id,
|
||||
@@ -133,7 +132,7 @@ class AgentNode(Node[AgentNodeData]):
|
||||
)
|
||||
parameters_for_log = self._runtime_support.build_parameters(
|
||||
agent_parameters=agent_parameters,
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
variable_pool=self.runtime_state.variable_pool,
|
||||
node_data=self.node_data,
|
||||
strategy=strategy,
|
||||
tenant_id=dify_ctx.tenant_id,
|
||||
@@ -144,7 +143,7 @@ class AgentNode(Node[AgentNodeData]):
|
||||
)
|
||||
credentials = self._runtime_support.build_credentials(parameters=parameters)
|
||||
|
||||
conversation_id = get_system_text(self.graph_runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
conversation_id = get_system_text(self.runtime_state.variable_pool, SystemVariableKey.CONVERSATION_ID)
|
||||
|
||||
try:
|
||||
message_stream = strategy.invoke(
|
||||
|
||||
@@ -3,11 +3,11 @@ from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.node_events import NodeEventBase
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.node_events import NodeEventPayload
|
||||
|
||||
|
||||
class AgentLogEvent(NodeEventBase):
|
||||
class AgentLogEvent(NodeEventPayload):
|
||||
message_id: str = Field(..., description="id")
|
||||
label: str = Field(..., description="label")
|
||||
node_execution_id: str = Field(..., description="node execution id")
|
||||
@@ -19,7 +19,7 @@ class AgentLogEvent(NodeEventBase):
|
||||
node_id: str = Field(..., description="node id")
|
||||
|
||||
|
||||
class GraphAgentNodeEventBase(GraphNodeEventBase):
|
||||
class GraphAgentNodeEventBase(NodeEvent):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from graphon.node_events import (
|
||||
NodeEventBase,
|
||||
NodeEventPayload,
|
||||
NodeRunResult,
|
||||
StreamChunkEvent,
|
||||
StreamCompletedEvent,
|
||||
@@ -45,7 +45,7 @@ class AgentMessageTransformer:
|
||||
node_type: NodeType,
|
||||
node_id: str,
|
||||
node_execution_id: str,
|
||||
) -> Generator[NodeEventBase]:
|
||||
) -> Generator[NodeEventPayload]:
|
||||
from core.plugin.impl.plugin import PluginInstaller
|
||||
|
||||
message_stream = ToolFileMessageTransformer.transform_tool_invoke_messages(
|
||||
|
||||
@@ -24,6 +24,7 @@ from clients.agent_backend import (
|
||||
)
|
||||
from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.engine_events import NodeRunPauseRequestedEvent
|
||||
from graphon.enums import (
|
||||
BuiltinNodeTypes,
|
||||
ErrorStrategy,
|
||||
@@ -31,8 +32,7 @@ from graphon.enums import (
|
||||
WorkflowNodeExecutionMetadataKey,
|
||||
WorkflowNodeExecutionStatus,
|
||||
)
|
||||
from graphon.graph_events import NodeRunPauseRequestedEvent
|
||||
from graphon.node_events import NodeEventBase, NodeRunResult, StreamCompletedEvent
|
||||
from graphon.node_events import NodeEventPayload, NodeRunResult, StreamCompletedEvent
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.nodes.base.variable_template_parser import VariableTemplateParser
|
||||
from models.agent_config_entities import WorkflowNodeJobConfig, WorkflowOutputRoutes
|
||||
@@ -57,8 +57,7 @@ from .runtime_request_builder import (
|
||||
from .session_store import WorkflowAgentSessionScope, WorkflowAgentWorkspaceStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import InitParams, RuntimeState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,8 +86,8 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
node_id: str,
|
||||
data: DifyAgentNodeData,
|
||||
*,
|
||||
graph_init_params: GraphInitParams,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
init_params: InitParams,
|
||||
runtime_state: RuntimeState,
|
||||
binding_resolver: WorkflowAgentBindingResolver,
|
||||
runtime_request_builder: WorkflowAgentRuntimeRequestBuilder,
|
||||
agent_backend_client: AgentBackendRunClient,
|
||||
@@ -101,8 +100,8 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
super().__init__(
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
# The base node parses the factory's serialized payload into node_data.
|
||||
if self.node_data.agent_output_routes.enabled and self.error_strategy == ErrorStrategy.DEFAULT_VALUE:
|
||||
@@ -128,7 +127,7 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
event.extras["agent_node"] = {"version": "2", "agent_node_kind": self.node_data.agent_node_kind}
|
||||
|
||||
@override
|
||||
def _run(self) -> Generator[NodeEventBase | NodeRunPauseRequestedEvent]:
|
||||
def _run(self) -> Generator[NodeEventPayload | NodeRunPauseRequestedEvent]:
|
||||
inputs: dict[str, Any] = {}
|
||||
process_data: dict[str, Any] = {}
|
||||
metadata: dict[str, Any] = {
|
||||
@@ -155,16 +154,16 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
inputs: dict[str, Any],
|
||||
process_data: dict[str, Any],
|
||||
metadata: dict[str, Any],
|
||||
) -> Generator[NodeEventBase | NodeRunPauseRequestedEvent]:
|
||||
) -> Generator[NodeEventPayload | NodeRunPauseRequestedEvent]:
|
||||
dify_ctx = DifyRunContext.model_validate(self.require_run_context_value(DIFY_RUN_CONTEXT_KEY))
|
||||
workflow_id = self.graph_init_params.workflow_id
|
||||
workflow_id = self.init_params.workflow_id
|
||||
workflow_run_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.WORKFLOW_EXECUTION_ID,
|
||||
)
|
||||
# Chatflow sessions also carry their owning conversation.
|
||||
conversation_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.CONVERSATION_ID,
|
||||
)
|
||||
|
||||
@@ -247,7 +246,7 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
workflow_run_id=workflow_run_id,
|
||||
node_id=self._node_id,
|
||||
node_execution_id=self.execution_id,
|
||||
variable_pool=self.graph_runtime_state.variable_pool,
|
||||
variable_pool=self.runtime_state.variable_pool,
|
||||
binding=bundle.binding,
|
||||
agent=bundle.agent,
|
||||
snapshot=bundle.snapshot,
|
||||
@@ -530,9 +529,9 @@ class DifyAgentNode(Node[DifyAgentNodeData]):
|
||||
return cancellation, None
|
||||
|
||||
def _is_graph_aborted(self) -> bool:
|
||||
"""Let Agent SSE consumption observe GraphEngine's cooperative abort state."""
|
||||
"""Let Agent SSE consumption observe Engine's cooperative abort state."""
|
||||
try:
|
||||
return self.graph_runtime_state.graph_execution.aborted
|
||||
return self.runtime_state.graph_execution.aborted
|
||||
except (AttributeError, RuntimeError):
|
||||
return False
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ from typing import override
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext
|
||||
from core.workflow.nodes.agent_v2.session_store import WorkflowAgentWorkspaceStore
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.graph_engine.layers import GraphEngineLayer
|
||||
from graphon.graph_events import (
|
||||
GraphEngineEvent,
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import (
|
||||
EngineEvent,
|
||||
GraphRunAbortedEvent,
|
||||
GraphRunFailedEvent,
|
||||
GraphRunPartialSucceededEvent,
|
||||
@@ -21,7 +21,7 @@ from tasks.collect_agent_resources_task import enqueue_agent_resource_collection
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkflowAgentWorkspaceRetirementLayer(GraphEngineLayer):
|
||||
class WorkflowAgentWorkspaceRetirementLayer(Layer):
|
||||
"""Synchronously retire run Workspaces, then enqueue physical collection."""
|
||||
|
||||
_TERMINAL_EVENTS = (
|
||||
@@ -44,11 +44,11 @@ class WorkflowAgentWorkspaceRetirementLayer(GraphEngineLayer):
|
||||
return
|
||||
|
||||
@override
|
||||
def on_event(self, event: GraphEngineEvent) -> None:
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
if not isinstance(event, self._TERMINAL_EVENTS):
|
||||
return
|
||||
workflow_run_id = get_system_text(
|
||||
self.graph_runtime_state.variable_pool,
|
||||
self.runtime_state.variable_pool,
|
||||
SystemVariableKey.WORKFLOW_EXECUTION_ID,
|
||||
)
|
||||
if not workflow_run_id:
|
||||
|
||||
@@ -21,8 +21,7 @@ from .entities import DatasourceNodeData, DatasourceParameter, OnlineDriveDownlo
|
||||
from .exc import DatasourceNodeError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import InitParams, RuntimeState
|
||||
|
||||
|
||||
class DatasourceNode(Node[DatasourceNodeData]):
|
||||
@@ -38,15 +37,15 @@ class DatasourceNode(Node[DatasourceNodeData]):
|
||||
node_id: str,
|
||||
data: DatasourceNodeData,
|
||||
*,
|
||||
graph_init_params: "GraphInitParams",
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
init_params: "InitParams",
|
||||
runtime_state: "RuntimeState",
|
||||
datasource_credentials: Callable[[str, str, str, str | None], dict[str, Any]],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
self.datasource_manager = DatasourceManager
|
||||
self._datasource_credentials = datasource_credentials
|
||||
@@ -63,7 +62,7 @@ class DatasourceNode(Node[DatasourceNodeData]):
|
||||
"""
|
||||
dify_ctx = DifyRunContext.model_validate(self.require_run_context_value(DIFY_RUN_CONTEXT_KEY))
|
||||
node_data = self.node_data
|
||||
variable_pool = self.graph_runtime_state.variable_pool
|
||||
variable_pool = self.runtime_state.variable_pool
|
||||
datasource_type_segment = get_system_segment(variable_pool, SystemVariableKey.DATASOURCE_TYPE)
|
||||
if not datasource_type_segment:
|
||||
raise DatasourceNodeError("Datasource type is not set")
|
||||
|
||||
@@ -5,17 +5,17 @@ from collections.abc import Iterable, Sequence
|
||||
from core.repositories.human_input_repository import HumanInputFormSubmissionRepository
|
||||
from core.workflow.human_input_policy import resolve_variable_select_input_options
|
||||
from core.workflow.system_variables import SystemVariableKey, get_system_text
|
||||
from graphon.entities.pause_reason import HitlRequired, SchedulingPause
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.filters import GraphEventFilterContext
|
||||
from graphon.graph_events import (
|
||||
GraphEngineEvent,
|
||||
from graphon.engine.filter import EngineEventFilterContext
|
||||
from graphon.engine_events import (
|
||||
EngineEvent,
|
||||
NodeRunHumanInputFormFilledEvent,
|
||||
NodeRunHumanInputFormTimeoutEvent,
|
||||
NodeRunStartedEvent,
|
||||
NodeRunSucceededEvent,
|
||||
)
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.entities.pause_reason import HitlRequired, SchedulingPause
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
|
||||
from .constants import OUTPUT_FIELD_ACTION_ID, OUTPUT_FIELD_ACTION_VALUE, OUTPUT_FIELD_RENDERED_CONTENT, TIMEOUT_HANDLE
|
||||
from .pause_reason import HumanInputRequired, PauseReason
|
||||
@@ -38,11 +38,11 @@ class HumanInputFormEventFilter:
|
||||
def filter_id(self) -> str:
|
||||
return "dify-human-input-form-events"
|
||||
|
||||
def initialize(self, context: GraphEventFilterContext) -> None:
|
||||
def initialize(self, context: EngineEventFilterContext) -> None:
|
||||
self._node_titles.clear()
|
||||
self._app_id = get_system_text(context.runtime_state.variable_pool, SystemVariableKey.APP_ID)
|
||||
|
||||
def on_event(self, event: GraphEngineEvent) -> Iterable[GraphEngineEvent]:
|
||||
def on_event(self, event: EngineEvent) -> Iterable[EngineEvent]:
|
||||
if isinstance(event, NodeRunStartedEvent) and event.node_type == BuiltinNodeTypes.HUMAN_INPUT:
|
||||
self._node_titles[event.id] = event.node_title
|
||||
elif isinstance(event, NodeRunSucceededEvent) and event.node_type == BuiltinNodeTypes.HUMAN_INPUT:
|
||||
@@ -50,7 +50,7 @@ class HumanInputFormEventFilter:
|
||||
|
||||
yield event
|
||||
|
||||
def flush(self) -> Iterable[GraphEngineEvent]:
|
||||
def flush(self) -> Iterable[EngineEvent]:
|
||||
return ()
|
||||
|
||||
def _completion_event(
|
||||
@@ -72,8 +72,7 @@ class HumanInputFormEventFilter:
|
||||
node_type=event.node_type,
|
||||
node_title=node_title,
|
||||
node_version=event.node_version,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
container_id=event.container_id,
|
||||
expiration_time=form.expiration_time,
|
||||
)
|
||||
|
||||
@@ -83,8 +82,7 @@ class HumanInputFormEventFilter:
|
||||
node_type=event.node_type,
|
||||
node_title=node_title,
|
||||
node_version=event.node_version,
|
||||
in_iteration_id=event.in_iteration_id,
|
||||
in_loop_id=event.in_loop_id,
|
||||
container_id=event.container_id,
|
||||
rendered_content=result.outputs[OUTPUT_FIELD_RENDERED_CONTENT].text,
|
||||
action_id=result.outputs[OUTPUT_FIELD_ACTION_ID].text,
|
||||
action_text=result.outputs[OUTPUT_FIELD_ACTION_VALUE].text,
|
||||
|
||||
@@ -11,7 +11,7 @@ from core.repositories.human_input_repository import FormCreateParams, HumanInpu
|
||||
from core.workflow.human_input_adapter import DeliveryChannelConfig
|
||||
from core.workflow.node_runtime import DifyFileReferenceFactory
|
||||
from graphon.nodes.human_input.entities import Completed, Expired, HITLContext, HITLDecision, PauseRequested
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.variables.factory import build_segment
|
||||
from graphon.variables.segments import Segment
|
||||
from graphon.variables.template_resolution import convert_template
|
||||
|
||||
@@ -17,7 +17,7 @@ from graphon.entities.base_node_data import BaseNodeData
|
||||
from graphon.enums import BuiltinNodeTypes, NodeType
|
||||
from graphon.file.enums import FileTransferMethod, FileType
|
||||
from graphon.nodes.base.variable_template_parser import VariableTemplateParser
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.variables.consts import SELECTORS_LENGTH
|
||||
from graphon.variables.segments import Segment
|
||||
|
||||
|
||||
@@ -23,8 +23,7 @@ from .exc import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import InitParams, RuntimeState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_INVOKE_FROM_DEBUGGER = "debugger"
|
||||
@@ -39,14 +38,14 @@ class KnowledgeIndexNode(Node[KnowledgeIndexNodeData]):
|
||||
node_id: str,
|
||||
data: KnowledgeIndexNodeData,
|
||||
*,
|
||||
graph_init_params: "GraphInitParams",
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
init_params: "InitParams",
|
||||
runtime_state: "RuntimeState",
|
||||
) -> None:
|
||||
super().__init__(
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
self.index_processor = IndexProcessor()
|
||||
self.summary_index_service = SummaryIndex()
|
||||
@@ -54,7 +53,7 @@ class KnowledgeIndexNode(Node[KnowledgeIndexNodeData]):
|
||||
@override
|
||||
def _run(self) -> NodeRunResult:
|
||||
node_data = self.node_data
|
||||
variable_pool = self.graph_runtime_state.variable_pool
|
||||
variable_pool = self.runtime_state.variable_pool
|
||||
|
||||
# get dataset id as string
|
||||
dataset_id_segment = get_system_segment(variable_pool, SystemVariableKey.DATASET_ID)
|
||||
|
||||
@@ -17,7 +17,6 @@ from core.db.session_factory import session_factory
|
||||
from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict
|
||||
from core.rag.retrieval.dataset_retrieval import DatasetRetrieval
|
||||
from core.workflow.file_reference import parse_file_reference
|
||||
from graphon.entities import GraphInitParams
|
||||
from graphon.enums import (
|
||||
BuiltinNodeTypes,
|
||||
WorkflowNodeExecutionMetadataKey,
|
||||
@@ -27,6 +26,7 @@ from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from graphon.node_events import NodeRunResult
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.runtime import InitParams
|
||||
from graphon.variables import (
|
||||
ArrayFileSegment,
|
||||
FileSegment,
|
||||
@@ -48,7 +48,7 @@ from .retrieval import KnowledgeRetrievalRequest, Source
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from graphon.file import File
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,15 +77,15 @@ class KnowledgeRetrievalNode(Node[KnowledgeRetrievalNodeData]):
|
||||
node_id: str,
|
||||
data: KnowledgeRetrievalNodeData,
|
||||
*,
|
||||
graph_init_params: "GraphInitParams",
|
||||
graph_runtime_state: "GraphRuntimeState",
|
||||
init_params: "InitParams",
|
||||
runtime_state: "RuntimeState",
|
||||
session_maker=None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
node_id=node_id,
|
||||
data=data,
|
||||
graph_init_params=graph_init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
# LLM file outputs, used for MultiModal outputs.
|
||||
self._file_outputs = []
|
||||
@@ -112,7 +112,7 @@ class KnowledgeRetrievalNode(Node[KnowledgeRetrievalNodeData]):
|
||||
variables: dict[str, Any] = {}
|
||||
# extract variables
|
||||
if self._node_data.query_variable_selector:
|
||||
variable = self.graph_runtime_state.variable_pool.get(self._node_data.query_variable_selector)
|
||||
variable = self.runtime_state.variable_pool.get(self._node_data.query_variable_selector)
|
||||
if not isinstance(variable, StringSegment):
|
||||
return NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.FAILED,
|
||||
@@ -123,7 +123,7 @@ class KnowledgeRetrievalNode(Node[KnowledgeRetrievalNodeData]):
|
||||
variables["query"] = query
|
||||
|
||||
if self._node_data.query_attachment_selector:
|
||||
variable = self.graph_runtime_state.variable_pool.get(self._node_data.query_attachment_selector)
|
||||
variable = self.runtime_state.variable_pool.get(self._node_data.query_attachment_selector)
|
||||
if not isinstance(variable, ArrayFileSegment) and not isinstance(variable, FileSegment):
|
||||
return NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.FAILED,
|
||||
@@ -309,7 +309,7 @@ class KnowledgeRetrievalNode(Node[KnowledgeRetrievalNodeData]):
|
||||
conditions=None,
|
||||
)
|
||||
|
||||
variable_pool = self.graph_runtime_state.variable_pool
|
||||
variable_pool = self.runtime_state.variable_pool
|
||||
resolved_conditions: list[Condition] = []
|
||||
for cond in conditions.conditions or []:
|
||||
value = cond.value
|
||||
|
||||
@@ -56,8 +56,8 @@ class TriggerEventNode(Node[TriggerEventNodeData]):
|
||||
"plugin_unique_identifier": self.node_data.plugin_unique_identifier,
|
||||
},
|
||||
}
|
||||
node_inputs = dict(self.graph_runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
system_inputs = self.graph_runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
node_inputs = dict(self.runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
system_inputs = self.runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
for variable_name, value in system_inputs.items():
|
||||
node_inputs[f"{SYSTEM_VARIABLE_NODE_ID}.{variable_name}"] = value
|
||||
|
||||
@@ -34,8 +34,8 @@ class TriggerScheduleNode(Node[TriggerScheduleNodeData]):
|
||||
|
||||
@override
|
||||
def _run(self) -> NodeRunResult:
|
||||
node_inputs = dict(self.graph_runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
system_inputs = self.graph_runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
node_inputs = dict(self.runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
system_inputs = self.runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
for variable_name, value in system_inputs.items():
|
||||
node_inputs[f"{SYSTEM_VARIABLE_NODE_ID}.{variable_name}"] = value
|
||||
|
||||
@@ -64,11 +64,11 @@ class TriggerWebhookNode(Node[WebhookData]):
|
||||
happens in the trigger controller.
|
||||
"""
|
||||
# Get webhook data from variable pool (injected by Celery task)
|
||||
webhook_inputs = dict(self.graph_runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
webhook_inputs = dict(self.runtime_state.variable_pool.get_by_prefix(self.id))
|
||||
|
||||
# Extract webhook-specific outputs based on node configuration
|
||||
outputs = self._extract_configured_outputs(webhook_inputs)
|
||||
system_inputs = self.graph_runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
system_inputs = self.runtime_state.variable_pool.get_by_prefix(SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
for variable_name, value in system_inputs.items():
|
||||
outputs[f"{SYSTEM_VARIABLE_NODE_ID}.{variable_name}"] = value
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Generator, Mapping, Sequence
|
||||
from collections.abc import Callable, Generator, Mapping, Sequence
|
||||
from contextlib import ExitStack
|
||||
from typing import Any, TypedDict
|
||||
from uuid import uuid4
|
||||
|
||||
from configs import dify_config
|
||||
from context import capture_current_context
|
||||
from core.app.apps.exc import GenerateTaskStoppedError
|
||||
from core.app.entities.app_invoke_entities import (
|
||||
InvokeFrom,
|
||||
@@ -13,7 +13,10 @@ from core.app.entities.app_invoke_entities import (
|
||||
build_dify_run_context,
|
||||
)
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.app.layers.pause_state_persist_layer import PauseStatePersistenceLayer
|
||||
from core.app.workflow.file_runtime import DifyWorkflowFileRuntime, bind_dify_workflow_file_runtime
|
||||
from core.app.workflow.layers.observability import ObservabilityLayer
|
||||
from core.app.workflow.layers.persistence import WorkflowPersistenceLayer
|
||||
from core.credit_usage import CreditUsageAppType
|
||||
from core.repositories.human_input_repository import HumanInputFormSubmissionRepository
|
||||
from core.workflow.node_factory import (
|
||||
@@ -33,32 +36,41 @@ from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add
|
||||
from core.workflow.variable_prefixes import ENVIRONMENT_VARIABLE_NODE_ID
|
||||
from extensions.otel.runtime import is_instrument_flag_enabled
|
||||
from factories import file_factory
|
||||
from graphon.engine import Engine
|
||||
from graphon.engine.command import CommandChannel, InMemoryChannel
|
||||
from graphon.engine.filter import EngineEventFilterContext, ResponseStreamFilter, filter_engine_events
|
||||
from graphon.engine.layer import ExecutionLimitsLayer, Layer
|
||||
from graphon.engine_events import EngineEvent, GraphRunFailedEvent, NodeEvent, is_node_result_event
|
||||
from graphon.entities.graph_config import NodeConfigDictAdapter
|
||||
from graphon.errors import WorkflowNodeRunFailedError
|
||||
from graphon.file import File
|
||||
from graphon.filters import GraphEventFilterContext, ResponseStreamFilter, filter_graph_events
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine import GraphEngine, GraphEngineConfig
|
||||
from graphon.graph_engine.command_channels import CommandChannel, InMemoryChannel
|
||||
from graphon.graph_engine.layers import DebugLoggingLayer, ExecutionLimitsLayer, GraphEngineLayer
|
||||
from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, GraphRunFailedEvent, is_node_result_event
|
||||
from graphon.nodes import BuiltinNodeTypes
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.nodes.container_effects import ContainerAwaitRequest
|
||||
from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
from graphon.runtime import ReadOnlyRuntimeStateWrapper, RuntimeState, VariablePool
|
||||
from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool
|
||||
from models.workflow import Workflow
|
||||
from services.workflow.event_ancestry import ContainerAncestry, WorkflowEventAncestry
|
||||
from services.workflow.execution_lifecycle import (
|
||||
WorkflowDebugLayer,
|
||||
WorkflowExecutionContextLayer,
|
||||
settled_workflow_events,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_file_access_controller = DatabaseFileAccessController()
|
||||
|
||||
|
||||
def iter_dify_graph_engine_events(
|
||||
engine: GraphEngine,
|
||||
engine: Engine,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
) -> Generator[GraphEngineEvent]:
|
||||
*,
|
||||
pause_callbacks: Sequence[Callable[[], None]] = (),
|
||||
pause_failure_callbacks: Sequence[Callable[[Exception], None]] = (),
|
||||
) -> Generator[EngineEvent]:
|
||||
"""
|
||||
Apply Dify's response streaming compatibility filter to GraphEngine events.
|
||||
Apply Dify's response streaming compatibility filter to Engine events.
|
||||
|
||||
Graphon v0.5.0 emits raw variable stream chunks and requires callers to opt
|
||||
into the legacy response-ordered stream behavior that Dify exposes to its
|
||||
@@ -69,15 +81,19 @@ def iter_dify_graph_engine_events(
|
||||
the filter's ``paths_map`` reflects everything the engine has actually
|
||||
streamed for this run.
|
||||
"""
|
||||
yield from filter_graph_events(
|
||||
events = filter_engine_events(
|
||||
engine.run(),
|
||||
context=GraphEventFilterContext.from_engine(engine),
|
||||
context=EngineEventFilterContext.from_engine(engine),
|
||||
filters=[
|
||||
HumanInputFormEventFilter(form_repository=HumanInputFormSubmissionRepository()),
|
||||
response_stream_filter or ResponseStreamFilter(),
|
||||
],
|
||||
)
|
||||
|
||||
yield from settled_workflow_events(
|
||||
events, pause_callbacks=pause_callbacks, pause_failure_callbacks=pause_failure_callbacks
|
||||
)
|
||||
|
||||
|
||||
class _NodeConfigDict(TypedDict):
|
||||
id: str
|
||||
@@ -112,7 +128,7 @@ class WorkflowEntry:
|
||||
invoke_from: InvokeFrom,
|
||||
call_depth: int,
|
||||
variable_pool: VariablePool,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
command_channel: CommandChannel | None = None,
|
||||
response_stream_filter: ResponseStreamFilter | None = None,
|
||||
) -> None:
|
||||
@@ -129,7 +145,7 @@ class WorkflowEntry:
|
||||
:param invoke_from: invoke from
|
||||
:param call_depth: call depth
|
||||
:param variable_pool: variable pool
|
||||
:param graph_runtime_state: pre-created graph runtime state
|
||||
:param runtime_state: pre-created graph runtime state
|
||||
:param command_channel: command channel for external control (optional, defaults to InMemoryChannel)
|
||||
:param response_stream_filter: pre-restored filter for resumed runs (optional, defaults to a fresh
|
||||
ResponseStreamFilter for runs with no prior pause)
|
||||
@@ -146,50 +162,58 @@ class WorkflowEntry:
|
||||
|
||||
self.command_channel = command_channel
|
||||
self._response_stream_filter = response_stream_filter or ResponseStreamFilter()
|
||||
execution_context = capture_current_context()
|
||||
# ponytail: Graphon snapshots omit process-local context; use a public rebind API when Graphon exposes one.
|
||||
graph_runtime_state._execution_context = execution_context
|
||||
self.graph_engine = GraphEngine(
|
||||
workflow_id=workflow_id,
|
||||
self._layers: list[Layer] = []
|
||||
self._event_ancestry = WorkflowEventAncestry(graph_config)
|
||||
self.graph_engine = Engine(
|
||||
graph=graph,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
command_channel=command_channel,
|
||||
config=GraphEngineConfig(
|
||||
min_workers=dify_config.GRAPH_ENGINE_MIN_WORKERS,
|
||||
max_workers=dify_config.GRAPH_ENGINE_MAX_WORKERS,
|
||||
scale_up_threshold=dify_config.GRAPH_ENGINE_SCALE_UP_THRESHOLD,
|
||||
scale_down_idle_time=dify_config.GRAPH_ENGINE_SCALE_DOWN_IDLE_TIME,
|
||||
),
|
||||
workers=dify_config.GRAPH_ENGINE_MAX_WORKERS,
|
||||
file_runtime=DifyWorkflowFileRuntime(file_access_controller=_file_access_controller),
|
||||
)
|
||||
|
||||
# Add debug logging layer when in debug mode
|
||||
self.add_layer(WorkflowExecutionContextLayer())
|
||||
if dify_config.DEBUG:
|
||||
logger.info("Debug mode enabled - adding DebugLoggingLayer to GraphEngine")
|
||||
debug_layer = DebugLoggingLayer(
|
||||
level="DEBUG",
|
||||
include_inputs=True,
|
||||
include_outputs=True,
|
||||
include_process_data=False, # Process data can be very verbose
|
||||
logger_name=f"GraphEngine.Debug.{workflow_id[:8]}", # Use workflow ID prefix for unique logger
|
||||
)
|
||||
self.graph_engine.layer(debug_layer)
|
||||
self.add_layer(WorkflowDebugLayer())
|
||||
|
||||
# Add execution limits layer
|
||||
limits_layer = ExecutionLimitsLayer(
|
||||
max_steps=dify_config.WORKFLOW_MAX_EXECUTION_STEPS, max_time=dify_config.WORKFLOW_MAX_EXECUTION_TIME
|
||||
)
|
||||
self.graph_engine.layer(limits_layer)
|
||||
self.add_layer(limits_layer)
|
||||
|
||||
# Add observability layer when OTel is enabled
|
||||
if dify_config.ENABLE_OTEL or is_instrument_flag_enabled():
|
||||
self.graph_engine.layer(ObservabilityLayer())
|
||||
self.add_layer(ObservabilityLayer())
|
||||
|
||||
def run(self) -> Generator[GraphEngineEvent]:
|
||||
def event_ancestry(self, event: EngineEvent) -> ContainerAncestry:
|
||||
return self._event_ancestry.resolve(event.container_id)
|
||||
|
||||
def add_layer(self, layer: Layer) -> None:
|
||||
self.graph_engine.add_layer(layer)
|
||||
self._layers.append(layer)
|
||||
|
||||
def run(self) -> Generator[EngineEvent]:
|
||||
graph_engine = self.graph_engine
|
||||
|
||||
try:
|
||||
# Preserve Dify's response-stream semantics on top of Graphon 0.5.0.
|
||||
generator = iter_dify_graph_engine_events(graph_engine, self._response_stream_filter)
|
||||
generator = iter_dify_graph_engine_events(
|
||||
graph_engine,
|
||||
self._response_stream_filter,
|
||||
pause_callbacks=tuple(
|
||||
layer.persist_paused_state
|
||||
for layer in self._layers
|
||||
if isinstance(layer, PauseStatePersistenceLayer)
|
||||
)
|
||||
+ tuple(
|
||||
layer.persist_paused_state for layer in self._layers if isinstance(layer, WorkflowPersistenceLayer)
|
||||
),
|
||||
pause_failure_callbacks=tuple(
|
||||
layer.fail_pause_persistence
|
||||
for layer in self._layers
|
||||
if isinstance(layer, WorkflowPersistenceLayer)
|
||||
),
|
||||
)
|
||||
yield from generator
|
||||
except GenerateTaskStoppedError:
|
||||
pass
|
||||
@@ -208,7 +232,7 @@ class WorkflowEntry:
|
||||
user_inputs: Mapping[str, Any],
|
||||
variable_pool: VariablePool,
|
||||
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
|
||||
) -> tuple[Node, Generator[GraphNodeEventBase | ContainerAwaitRequest]]:
|
||||
) -> tuple[Node, Generator[NodeEvent | ContainerAwaitRequest]]:
|
||||
"""
|
||||
Single step run workflow node
|
||||
:param workflow: Workflow instance
|
||||
@@ -246,10 +270,10 @@ class WorkflowEntry:
|
||||
run_context=run_context,
|
||||
call_depth=0,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
runtime_state = RuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
execution_context=capture_current_context(),
|
||||
workflow_id=graph_init_context.workflow_id,
|
||||
)
|
||||
|
||||
if is_start_node_type(node_type):
|
||||
@@ -297,7 +321,7 @@ class WorkflowEntry:
|
||||
# init workflow run state
|
||||
node_factory = DifyNodeFactory.from_graph_init_context(
|
||||
graph_init_context=graph_init_context,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
node = node_factory.create_node(node_config)
|
||||
|
||||
@@ -363,7 +387,7 @@ class WorkflowEntry:
|
||||
@classmethod
|
||||
def run_free_node(
|
||||
cls, node_data: dict[str, Any], node_id: str, tenant_id: str, user_id: str, user_inputs: dict[str, Any]
|
||||
) -> tuple[Node, Generator[GraphNodeEventBase | ContainerAwaitRequest]]:
|
||||
) -> tuple[Node, Generator[NodeEvent | ContainerAwaitRequest]]:
|
||||
"""
|
||||
Run free node
|
||||
|
||||
@@ -406,17 +430,17 @@ class WorkflowEntry:
|
||||
run_context=run_context,
|
||||
call_depth=0,
|
||||
)
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
runtime_state = RuntimeState(
|
||||
variable_pool=variable_pool,
|
||||
start_at=time.perf_counter(),
|
||||
execution_context=capture_current_context(),
|
||||
workflow_id=graph_init_context.workflow_id,
|
||||
)
|
||||
|
||||
# init workflow run state
|
||||
node_config = NodeConfigDictAdapter.validate_python({"id": node_id, "data": node_data})
|
||||
node_factory = DifyNodeFactory.from_graph_init_context(
|
||||
graph_init_context=graph_init_context,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
node = node_factory.create_node(node_config)
|
||||
|
||||
@@ -558,13 +582,13 @@ class WorkflowEntry:
|
||||
variable_pool.add([variable_node_id] + variable_key_list, input_value)
|
||||
|
||||
@staticmethod
|
||||
def _run_node_with_layers(node: Node, *, tenant_id: str) -> Generator[GraphNodeEventBase | ContainerAwaitRequest]:
|
||||
def _run_node_with_layers(node: Node, *, tenant_id: str) -> Generator[NodeEvent | ContainerAwaitRequest]:
|
||||
"""
|
||||
Run a standalone node with the same quota and observability hooks as GraphEngine.
|
||||
Run a standalone node with the same quota and observability hooks as Engine.
|
||||
"""
|
||||
layers: Sequence[GraphEngineLayer] = (ObservabilityLayer(),)
|
||||
layers: Sequence[Layer] = (ObservabilityLayer(),)
|
||||
command_channel = InMemoryChannel()
|
||||
runtime_state = ReadOnlyGraphRuntimeStateWrapper(node.graph_runtime_state)
|
||||
runtime_state = ReadOnlyRuntimeStateWrapper(node.runtime_state)
|
||||
for layer in layers:
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
layer.on_graph_start()
|
||||
@@ -572,8 +596,14 @@ class WorkflowEntry:
|
||||
node.bind_execution_id(str(uuid4()))
|
||||
|
||||
def _gen():
|
||||
with bind_dify_workflow_file_runtime(), ExitStack() as stack:
|
||||
for layer in layers:
|
||||
stack.enter_context(layer.node_run_context(node))
|
||||
yield from _run_scoped()
|
||||
|
||||
def _run_scoped():
|
||||
error: Exception | None = None
|
||||
result_event: GraphNodeEventBase | None = None
|
||||
result_event: NodeEvent | None = None
|
||||
layers_finished = False
|
||||
|
||||
def finish_layers() -> None:
|
||||
@@ -590,7 +620,7 @@ class WorkflowEntry:
|
||||
for layer in layers:
|
||||
layer.on_node_run_start(node)
|
||||
for event in node.run():
|
||||
if isinstance(event, GraphNodeEventBase) and is_node_result_event(event):
|
||||
if isinstance(event, NodeEvent) and is_node_result_event(event):
|
||||
result_event = event
|
||||
finish_layers()
|
||||
yield event
|
||||
|
||||
+36
-1
@@ -1,8 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import sys
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from flask import Flask
|
||||
from flask.ctx import AppContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from extensions.ext_login import DifyLoginManager
|
||||
@@ -12,3 +15,35 @@ class DifyApp(Flask):
|
||||
"""Flask application type with Dify-specific extension attributes."""
|
||||
|
||||
login_manager: DifyLoginManager
|
||||
|
||||
@override
|
||||
def app_context(self) -> AppContext:
|
||||
return _DifyAppContext(self)
|
||||
|
||||
|
||||
class _DifyAppContext(AppContext):
|
||||
"""Own file rendering for requests and background jobs using this application."""
|
||||
|
||||
def __init__(self, app: Flask) -> None:
|
||||
super().__init__(app)
|
||||
self._file_scopes: list[AbstractContextManager[None]] = []
|
||||
|
||||
@override
|
||||
def push(self) -> None:
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
super().push()
|
||||
scope = bind_dify_workflow_file_runtime()
|
||||
try:
|
||||
scope.__enter__()
|
||||
except BaseException:
|
||||
super().pop(sys.exc_info()[1])
|
||||
raise
|
||||
self._file_scopes.append(scope)
|
||||
|
||||
@override
|
||||
def pop(self, exc: BaseException | None = None) -> None:
|
||||
try:
|
||||
super().pop(exc if exc is not None else sys.exc_info()[1])
|
||||
finally:
|
||||
self._file_scopes.pop().__exit__(None, None, None)
|
||||
|
||||
@@ -141,6 +141,3 @@ storage = Storage()
|
||||
|
||||
def init_app(app: DifyApp):
|
||||
storage.init_app(app)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
bind_dify_workflow_file_runtime()
|
||||
|
||||
@@ -17,9 +17,9 @@ from pydantic import BaseModel
|
||||
from configs import dify_config
|
||||
from enums import DeploymentEdition
|
||||
from extensions.otel.semconv.gen_ai import ChainAttributes, GenAIAttributes
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.file import File
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.variables import Segment
|
||||
|
||||
@@ -90,7 +90,7 @@ class NodeOTelParser(Protocol):
|
||||
"""Parser interface for node-specific OpenTelemetry enrichment."""
|
||||
|
||||
def parse(
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: NodeEvent | None = None
|
||||
) -> None: ...
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class DefaultNodeOTelParser:
|
||||
"""Fallback parser used when no node-specific parser is registered."""
|
||||
|
||||
def parse(
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: NodeEvent | None = None
|
||||
) -> None:
|
||||
span.set_attribute("node.id", node.id)
|
||||
if node.execution_id:
|
||||
|
||||
@@ -10,7 +10,7 @@ from opentelemetry.trace import Span
|
||||
|
||||
from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps
|
||||
from extensions.otel.semconv.gen_ai import LLMAttributes
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.nodes.base.node import Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -99,7 +99,7 @@ class LLMNodeOTelParser:
|
||||
self._delegate = DefaultNodeOTelParser()
|
||||
|
||||
def parse(
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: NodeEvent | None = None
|
||||
) -> None:
|
||||
self._delegate.parse(node=node, span=span, error=error, result_event=result_event)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from opentelemetry.trace import Span
|
||||
|
||||
from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps
|
||||
from extensions.otel.semconv.gen_ai import RetrieverAttributes
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.variables import Segment
|
||||
|
||||
@@ -72,7 +72,7 @@ class RetrievalNodeOTelParser:
|
||||
self._delegate = DefaultNodeOTelParser()
|
||||
|
||||
def parse(
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: NodeEvent | None = None
|
||||
) -> None:
|
||||
self._delegate.parse(node=node, span=span, error=error, result_event=result_event)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ from opentelemetry.trace import Span
|
||||
|
||||
from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps
|
||||
from extensions.otel.semconv.gen_ai import ToolAttributes
|
||||
from graphon.engine_events import NodeEvent
|
||||
from graphon.enums import WorkflowNodeExecutionMetadataKey
|
||||
from graphon.graph_events import GraphNodeEventBase
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.nodes.tool.entities import ToolNodeData
|
||||
|
||||
@@ -19,7 +19,7 @@ class ToolNodeOTelParser:
|
||||
self._delegate = DefaultNodeOTelParser()
|
||||
|
||||
def parse(
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: GraphNodeEventBase | None = None
|
||||
self, *, node: Node, span: "Span", error: Exception | None, result_event: NodeEvent | None = None
|
||||
) -> None:
|
||||
self._delegate.parse(node=node, span=span, error=error, result_event=result_event)
|
||||
|
||||
|
||||
+8
-2
@@ -157,7 +157,10 @@ def build_icon_url(icon_type: Any, icon: str | None) -> str | None:
|
||||
icon_type_value = icon_type.value if isinstance(icon_type, IconType) else str(icon_type)
|
||||
if icon_type_value.lower() != IconType.IMAGE:
|
||||
return None
|
||||
return file_helpers.get_signed_file_url(icon)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
return file_helpers.get_signed_file_url(icon)
|
||||
|
||||
|
||||
def build_avatar_url(avatar: str | None) -> str | None:
|
||||
@@ -165,7 +168,10 @@ def build_avatar_url(avatar: str | None) -> str | None:
|
||||
return None
|
||||
if avatar.startswith(("http://", "https://")):
|
||||
return avatar
|
||||
return file_helpers.get_signed_file_url(avatar)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
return file_helpers.get_signed_file_url(avatar)
|
||||
|
||||
|
||||
class TimestampField(fields.Raw):
|
||||
|
||||
@@ -2118,7 +2118,7 @@ class WorkflowPause(DefaultFieldsDCMixin, TypeBase):
|
||||
)
|
||||
|
||||
# state_object_key stores the object key referencing the serialized runtime state
|
||||
# of the `GraphEngine`. This object captures the complete execution context of the
|
||||
# of the `Engine`. This object captures the complete execution context of the
|
||||
# workflow at the moment it was paused, enabling accurate resumption.
|
||||
state_object_key: Mapped[str] = mapped_column(String(length=255), nullable=False)
|
||||
|
||||
|
||||
+3
-1
@@ -40,12 +40,14 @@ dependencies = [
|
||||
"opentelemetry-instrumentation-redis==0.65b0",
|
||||
"opentelemetry-instrumentation-sqlalchemy==0.65b0",
|
||||
"opentelemetry-propagator-b3>=1.45.0,<2.0.0",
|
||||
"pypandoc==1.17; sys_platform == 'win32'",
|
||||
"pypandoc-binary==1.17; sys_platform != 'win32'",
|
||||
"readabilipy==0.3.0",
|
||||
"resend>=2.27.0,<3.0.0",
|
||||
"zstandard==0.25.0",
|
||||
# Emerging: newer and fast-moving, use compatible pins
|
||||
"fastopenapi[flask]==0.7.0",
|
||||
"graphon==0.7.0",
|
||||
"graphon==0.8.0",
|
||||
"httpx-sse==0.4.3",
|
||||
"json-repair==0.60.1",
|
||||
]
|
||||
|
||||
@@ -30,4 +30,7 @@ class SQLAlchemyAccountAvatarFileGateway(AccountAvatarFileGateway):
|
||||
|
||||
if owned_file_id is None:
|
||||
return None
|
||||
return file_helpers.get_signed_file_url(upload_file_id=owned_file_id)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
return file_helpers.get_signed_file_url(upload_file_id=owned_file_id)
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
This service provides centralized logic for task control operations
|
||||
like stopping tasks, handling both legacy Redis flag mechanism and
|
||||
new GraphEngine command channel mechanism.
|
||||
new Engine command channel mechanism.
|
||||
"""
|
||||
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||
from core.app.apps.execution_coordinator import set_app_task_stop_flag
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_redis import RedisClientWrapper, redis_client
|
||||
from graphon.graph_engine.manager import GraphEngineManager
|
||||
from models.model import AppMode
|
||||
from services.workflow.command_service import send_workflow_stop_command
|
||||
|
||||
|
||||
class AppTaskControlService:
|
||||
@@ -30,7 +30,7 @@ class AppTaskControlService:
|
||||
|
||||
This method handles stopping tasks using both mechanisms:
|
||||
1. Legacy Redis flag mechanism (for backward compatibility)
|
||||
2. New GraphEngine command channel (for workflow-based apps)
|
||||
2. New Engine command channel (for workflow-based apps)
|
||||
|
||||
Args:
|
||||
task_id: The task ID to stop
|
||||
@@ -44,19 +44,19 @@ class AppTaskControlService:
|
||||
# Legacy mechanism: Set stop flag in Redis
|
||||
AppQueueManager.set_stop_flag(task_id, invoke_from, user_id, redis=self._redis_client)
|
||||
|
||||
# New mechanism: Send stop command via GraphEngine for workflow-based apps
|
||||
# New mechanism: Send stop command via Engine for workflow-based apps
|
||||
# This ensures proper workflow status recording in the persistence layer
|
||||
if app_mode in (AppMode.ADVANCED_CHAT, AppMode.WORKFLOW):
|
||||
GraphEngineManager(self._redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=self._redis_client)
|
||||
|
||||
def stop_workflow_task_no_user_check(self, *, task_id: str) -> None:
|
||||
"""Stop a workflow after app admission, without consulting the user ownership cache.
|
||||
|
||||
Keep the legacy stop flag before the GraphEngine command, including when
|
||||
Keep the legacy stop flag before the Engine command, including when
|
||||
the latter fails. Callers must authorize access to the workflow app first.
|
||||
"""
|
||||
set_app_task_stop_flag(task_id, redis=self._redis_client)
|
||||
GraphEngineManager(self._redis_client).send_stop_command(task_id)
|
||||
send_workflow_stop_command(task_id=task_id, redis=self._redis_client)
|
||||
|
||||
|
||||
class AppTaskService:
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any, Protocol, override
|
||||
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core.app.entities.app_invoke_entities import DifyRunContext
|
||||
from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler
|
||||
from core.db.session_factory import session_factory
|
||||
from core.tools.entities.tool_entities import ToolInvokeFrom, ToolInvokeMessage, ToolProviderType
|
||||
@@ -182,7 +183,7 @@ class QualifiedJsonToolNodeRuntime(DifyToolNodeRuntime):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
run_context: Mapping[str, Any],
|
||||
run_context: DifyRunContext | Mapping[str, Any],
|
||||
*,
|
||||
native_run_id: str,
|
||||
native_task_id: str,
|
||||
@@ -202,6 +203,7 @@ class QualifiedJsonToolNodeRuntime(DifyToolNodeRuntime):
|
||||
self._healthy = True
|
||||
|
||||
@staticmethod
|
||||
@override
|
||||
def _require_no_transaction(session: Session) -> None:
|
||||
if session.in_transaction():
|
||||
raise BuilderExecutionPolicyError("json_tool_session_in_transaction")
|
||||
|
||||
@@ -163,7 +163,10 @@ class FileService:
|
||||
session.commit()
|
||||
|
||||
if not upload_file.source_url:
|
||||
upload_file.source_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
upload_file.source_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
|
||||
|
||||
return upload_file
|
||||
|
||||
@@ -285,7 +288,10 @@ class FileService:
|
||||
raise NotFound("File not found")
|
||||
except NotFound as exc:
|
||||
raise FileNotExistsError("File reference not found") from exc
|
||||
return file_helpers.get_signed_file_url(upload_file_id=file_id)
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
with bind_dify_workflow_file_runtime():
|
||||
return file_helpers.get_signed_file_url(upload_file_id=file_id)
|
||||
|
||||
def upload_text(self, text: str, text_name: str, user_id: str, tenant_id: str) -> UploadFile:
|
||||
if len(text_name) > 200:
|
||||
|
||||
@@ -30,8 +30,8 @@ from core.workflow.nodes.human_input.entities import (
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus, ValueSourceType
|
||||
from factories.file_factory import build_from_mapping, build_from_mappings
|
||||
from graphon.file import FileUploadConfig
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
from libs.datetime_utils import ensure_naive_utc, naive_utc_now
|
||||
from libs.exception import BaseHTTPException
|
||||
from models.human_input import RecipientType
|
||||
@@ -298,7 +298,7 @@ class HumanInputService:
|
||||
return None
|
||||
|
||||
resumption_context = WorkflowResumptionContext.loads(pause_entity.get_state().decode())
|
||||
runtime_state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
runtime_state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
|
||||
return runtime_state.variable_pool
|
||||
|
||||
|
||||
@@ -45,10 +45,10 @@ from core.workflow.variable_pool_initializer import add_variables_to_pool
|
||||
from core.workflow.workflow_entry import WorkflowEntry
|
||||
from enterprise.telemetry.draft_trace import enqueue_draft_node_execution_trace
|
||||
from extensions.ext_database import db
|
||||
from graphon.engine_events import NodeEvent, NodeRunFailedEvent, NodeRunSucceededEvent
|
||||
from graphon.entities import WorkflowNodeExecution
|
||||
from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeType, WorkflowNodeExecutionStatus
|
||||
from graphon.errors import WorkflowNodeRunFailedError
|
||||
from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent
|
||||
from graphon.node_events import NodeRunResult
|
||||
from graphon.nodes.base.node import Node
|
||||
from graphon.nodes.container_effects import ContainerAwaitRequest
|
||||
@@ -949,7 +949,7 @@ class RagPipelineService:
|
||||
self,
|
||||
getter: Callable[
|
||||
[],
|
||||
tuple[Node, Generator[GraphNodeEventBase | ContainerAwaitRequest]],
|
||||
tuple[Node, Generator[NodeEvent | ContainerAwaitRequest]],
|
||||
],
|
||||
start_at: float,
|
||||
tenant_id: str,
|
||||
@@ -1048,7 +1048,7 @@ class RagPipelineService:
|
||||
workflow_node_execution.status = WorkflowNodeExecutionStatus.FAILED
|
||||
workflow_node_execution.error = error
|
||||
# update document status
|
||||
variable_pool = node_instance.graph_runtime_state.variable_pool
|
||||
variable_pool = node_instance.runtime_state.variable_pool
|
||||
invoke_from = get_system_segment(variable_pool, SystemVariableKey.INVOKE_FROM)
|
||||
if invoke_from:
|
||||
if invoke_from.value == InvokeFrom.PUBLISHED_PIPELINE:
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Iterator
|
||||
from typing import NamedTuple, Protocol
|
||||
|
||||
from constants import IMAGE_EXTENSIONS
|
||||
from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
from graphon.file import helpers as file_helpers
|
||||
from services.errors.file import UnsupportedFileTypeError
|
||||
|
||||
@@ -55,13 +56,14 @@ class UploadFileDeliveryService:
|
||||
nonce: str,
|
||||
sign: str,
|
||||
) -> UploadFileDelivery:
|
||||
if not file_helpers.verify_image_signature(
|
||||
upload_file_id=file_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
sign=sign,
|
||||
):
|
||||
raise UploadFileDeliveryNotFoundError("File not found or signature is invalid")
|
||||
with bind_dify_workflow_file_runtime():
|
||||
if not file_helpers.verify_image_signature(
|
||||
upload_file_id=file_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
sign=sign,
|
||||
):
|
||||
raise UploadFileDeliveryNotFoundError("File not found or signature is invalid")
|
||||
|
||||
file = self._get_file(file_id=file_id)
|
||||
self._ensure_image(file=file)
|
||||
@@ -78,13 +80,14 @@ class UploadFileDeliveryService:
|
||||
nonce: str,
|
||||
sign: str,
|
||||
) -> UploadFileDelivery:
|
||||
if not file_helpers.verify_file_signature(
|
||||
upload_file_id=file_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
sign=sign,
|
||||
):
|
||||
raise UploadFileDeliveryNotFoundError("File not found or signature is invalid")
|
||||
with bind_dify_workflow_file_runtime():
|
||||
if not file_helpers.verify_file_signature(
|
||||
upload_file_id=file_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
sign=sign,
|
||||
):
|
||||
raise UploadFileDeliveryNotFoundError("File not found or signature is invalid")
|
||||
|
||||
file = self._get_file(file_id=file_id)
|
||||
return UploadFileDelivery(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Dify stop-command behavior over the public Graphon command channel."""
|
||||
|
||||
import logging
|
||||
|
||||
from extensions.ext_redis import RedisClientWrapper
|
||||
from graphon.engine.command import AbortCommand, RedisChannel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_workflow_stop_command(*, task_id: str, redis: RedisClientWrapper) -> None:
|
||||
"""Keep stop requests best-effort after the durable legacy stop flag is set."""
|
||||
if not task_id:
|
||||
return
|
||||
try:
|
||||
RedisChannel(redis, f"workflow:{task_id}:commands").send_command(AbortCommand(reason="User requested stop"))
|
||||
except Exception:
|
||||
logger.exception("Failed to send stop command for workflow task %s", task_id)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Project native container ownership into Dify's response metadata."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.graph.scoping import resolve_container_id
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContainerAncestry:
|
||||
iteration_id: str | None = None
|
||||
loop_id: str | None = None
|
||||
|
||||
|
||||
class WorkflowEventAncestry:
|
||||
"""Resolve nearest typed ancestors without conflating occurrence IDs and node IDs.
|
||||
|
||||
Graphon event.id remains the native occurrence key. container_id is topology;
|
||||
traversal frame_id remains the execution frame identity and is never substituted
|
||||
with a container node ID.
|
||||
"""
|
||||
|
||||
def __init__(self, graph_config: Mapping[str, Any]) -> None:
|
||||
self._nodes = {node["id"]: node for node in graph_config.get("nodes", []) if node.get("type") != "custom-note"}
|
||||
|
||||
def resolve(self, container_id: str) -> ContainerAncestry:
|
||||
iteration_id = loop_id = None
|
||||
visited: set[str] = set()
|
||||
while container_id:
|
||||
if container_id in visited or container_id not in self._nodes:
|
||||
raise ValueError("Invalid workflow event container ancestry")
|
||||
visited.add(container_id)
|
||||
node = self._nodes[container_id]
|
||||
node_type = node["data"]["type"]
|
||||
if node_type == BuiltinNodeTypes.ITERATION and iteration_id is None:
|
||||
iteration_id = container_id
|
||||
if node_type == BuiltinNodeTypes.LOOP and loop_id is None:
|
||||
loop_id = container_id
|
||||
container_id = resolve_container_id(node, nodes_by_id=self._nodes)
|
||||
return ContainerAncestry(iteration_id=iteration_id, loop_id=loop_id)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Host execution scopes and the post-filter pause persistence boundary."""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable, Generator, Iterable, Sequence
|
||||
from contextlib import contextmanager
|
||||
from typing import override
|
||||
|
||||
from context import capture_current_context
|
||||
from graphon.engine.layer import Layer
|
||||
from graphon.engine_events import EngineEvent, GraphRunPausedEvent
|
||||
from graphon.file.runtime import peek_workflow_file_runtime, use_workflow_file_runtime
|
||||
from graphon.nodes.base.node import Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkflowExecutionContextLayer(Layer):
|
||||
"""Restore host context separately for every worker segment, including resumes."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._context = capture_current_context()
|
||||
|
||||
@contextmanager
|
||||
@override
|
||||
def node_run_context(self, node: Node, *, parent_execution_id: str | None = None) -> Generator[None]:
|
||||
file_runtime = peek_workflow_file_runtime()
|
||||
with self._context, use_workflow_file_runtime(file_runtime):
|
||||
yield
|
||||
|
||||
|
||||
class WorkflowDebugLayer(Layer):
|
||||
@override
|
||||
def on_event(self, event: EngineEvent) -> None:
|
||||
logger.debug("Workflow event %s", event)
|
||||
|
||||
|
||||
def settled_workflow_events(
|
||||
events: Iterable[EngineEvent],
|
||||
*,
|
||||
pause_callbacks: Sequence[Callable[[], None]],
|
||||
pause_failure_callbacks: Sequence[Callable[[Exception], None]] = (),
|
||||
) -> Generator[EngineEvent]:
|
||||
"""Expose paused only after engine quiescence, filter flush, and durable persistence.
|
||||
|
||||
Callbacks execute outside Graphon's isolated layer hooks so persistence failures
|
||||
reach the host runner. Closing a partially consumed stream never checkpoints it.
|
||||
"""
|
||||
paused: GraphRunPausedEvent | None = None
|
||||
for event in events:
|
||||
if isinstance(event, GraphRunPausedEvent):
|
||||
paused = event
|
||||
else:
|
||||
yield event
|
||||
if paused is not None:
|
||||
try:
|
||||
for callback in pause_callbacks:
|
||||
callback()
|
||||
except Exception as error:
|
||||
for callback in pause_failure_callbacks:
|
||||
callback(error)
|
||||
raise
|
||||
yield paused
|
||||
@@ -40,8 +40,8 @@ from core.workflow.nodes.human_input.pause_reason import (
|
||||
)
|
||||
from graphon.entities import WorkflowStartReason
|
||||
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime.graph_runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.runtime import RuntimeState
|
||||
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
|
||||
from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter
|
||||
from libs.broadcast_channel.exc import SubscriptionClosedError
|
||||
from libs.datetime_utils import to_utc_timestamp
|
||||
@@ -511,7 +511,7 @@ def _load_variable_pool_from_resumption_context(
|
||||
) -> ReadOnlyVariablePool | None:
|
||||
if resumption_context is None:
|
||||
return None
|
||||
state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
|
||||
return state.variable_pool
|
||||
|
||||
@@ -565,7 +565,7 @@ def _build_pause_event(
|
||||
outputs: dict[str, Any] = {}
|
||||
variable_pool: ReadOnlyVariablePool | None = None
|
||||
if resumption_context is not None:
|
||||
state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
outputs = dict(WorkflowRuntimeTypeConverter().to_json_encodable(state.outputs or {}))
|
||||
variable_pool = state.variable_pool
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ from events.app_event import app_draft_workflow_was_synced, app_published_workfl
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_storage import storage
|
||||
from factories.file_factory import build_from_mapping, build_from_mappings
|
||||
from graphon.engine_events import NodeEvent, NodeRunFailedEvent, NodeRunSucceededEvent
|
||||
from graphon.entities import WorkflowNodeExecution
|
||||
from graphon.entities.graph_config import NodeConfigDict
|
||||
from graphon.enums import (
|
||||
@@ -65,7 +66,6 @@ from graphon.enums import (
|
||||
)
|
||||
from graphon.errors import WorkflowNodeRunFailedError
|
||||
from graphon.file import File
|
||||
from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent
|
||||
from graphon.node_events import NodeRunResult
|
||||
from graphon.nodes import BuiltinNodeTypes
|
||||
from graphon.nodes.base.node import Node
|
||||
@@ -1703,7 +1703,7 @@ class WorkflowService:
|
||||
self,
|
||||
invoke_node_fn: Callable[
|
||||
[],
|
||||
tuple[Node, Generator[GraphNodeEventBase | ContainerAwaitRequest]],
|
||||
tuple[Node, Generator[NodeEvent | ContainerAwaitRequest]],
|
||||
],
|
||||
start_at: float,
|
||||
node_id: str,
|
||||
@@ -1743,7 +1743,7 @@ class WorkflowService:
|
||||
self,
|
||||
invoke_node_fn: Callable[
|
||||
[],
|
||||
tuple[Node, Generator[GraphNodeEventBase | ContainerAwaitRequest]],
|
||||
tuple[Node, Generator[NodeEvent | ContainerAwaitRequest]],
|
||||
],
|
||||
) -> tuple[Node, NodeRunResult | None, bool, str | None]:
|
||||
"""
|
||||
|
||||
@@ -25,10 +25,10 @@ from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig, Wor
|
||||
from core.dify_builder.execution_policy import BuilderExecutionContextValue, BuilderExecutionPolicyError
|
||||
from core.repositories import DifyCoreRepositoryFactory
|
||||
from extensions.ext_database import db
|
||||
from graphon.engine.filter import ResponseStreamFilter
|
||||
from graphon.entities import WorkflowStartReason
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.filters import ResponseStreamFilter
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from libs.flask_utils import set_login_user
|
||||
from libs.helper import to_timestamp
|
||||
@@ -526,7 +526,7 @@ def _resume_app_execution(payload: dict[str, Any]) -> None:
|
||||
if generate_entity.builder_execution is not None:
|
||||
raise BuilderExecutionPolicyError("unsupported_execution_transport")
|
||||
|
||||
graph_runtime_state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
runtime_state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
response_stream_filter = resumption_context.get_response_stream_filter()
|
||||
|
||||
conversation = None
|
||||
@@ -619,7 +619,7 @@ def _resume_app_execution(payload: dict[str, Any]) -> None:
|
||||
conversation=conversation,
|
||||
message=message,
|
||||
generate_entity=generate_entity,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
response_stream_filter=response_stream_filter,
|
||||
session_factory=session_factory,
|
||||
pause_state_config=pause_config,
|
||||
@@ -633,7 +633,7 @@ def _resume_app_execution(payload: dict[str, Any]) -> None:
|
||||
workflow=workflow,
|
||||
user=user,
|
||||
generate_entity=generate_entity,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
response_stream_filter=response_stream_filter,
|
||||
session_factory=session_factory,
|
||||
pause_state_config=pause_config,
|
||||
@@ -652,7 +652,7 @@ def _resume_advanced_chat(
|
||||
conversation: Conversation,
|
||||
message: Message,
|
||||
generate_entity: AdvancedChatAppGenerateEntity,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
response_stream_filter: ResponseStreamFilter,
|
||||
session_factory: sessionmaker,
|
||||
pause_state_config: PauseStateLayerConfig,
|
||||
@@ -694,7 +694,7 @@ def _resume_advanced_chat(
|
||||
application_generate_entity=resumed_generate_entity,
|
||||
workflow_execution_repository=workflow_execution_repository,
|
||||
workflow_node_execution_repository=workflow_node_execution_repository,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
pause_state_config=pause_state_config,
|
||||
response_stream_filter=response_stream_filter,
|
||||
session=session,
|
||||
@@ -720,7 +720,7 @@ def _resume_workflow(
|
||||
workflow: Workflow,
|
||||
user: Account | EndUser,
|
||||
generate_entity: WorkflowAppGenerateEntity,
|
||||
graph_runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
response_stream_filter: ResponseStreamFilter,
|
||||
session_factory: sessionmaker,
|
||||
pause_state_config: PauseStateLayerConfig,
|
||||
@@ -759,7 +759,7 @@ def _resume_workflow(
|
||||
workflow=workflow,
|
||||
user=user,
|
||||
application_generate_entity=resumed_generate_entity,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_execution_repository=workflow_execution_repository,
|
||||
workflow_node_execution_repository=workflow_node_execution_repository,
|
||||
pause_state_config=pause_state_config,
|
||||
|
||||
@@ -23,7 +23,7 @@ from core.app.layers.trigger_post_layer import TriggerPostLayer
|
||||
from core.db.session_factory import session_factory
|
||||
from core.repositories import DifyCoreRepositoryFactory
|
||||
from extensions.ext_database import db
|
||||
from graphon.runtime import GraphRuntimeState
|
||||
from graphon.runtime import RuntimeState
|
||||
from models.account import Account
|
||||
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom, WorkflowTriggerStatus
|
||||
from models.model import App, EndUser, Tenant
|
||||
@@ -224,7 +224,7 @@ def resume_workflow_execution(task_data_dict: dict[str, Any]) -> None:
|
||||
)
|
||||
return
|
||||
|
||||
graph_runtime_state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
runtime_state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
response_stream_filter = resumption_context.get_response_stream_filter()
|
||||
|
||||
with session_factory() as session:
|
||||
@@ -285,7 +285,7 @@ def resume_workflow_execution(task_data_dict: dict[str, Any]) -> None:
|
||||
workflow=workflow,
|
||||
user=user,
|
||||
application_generate_entity=generate_entity,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
workflow_execution_repository=workflow_execution_repository,
|
||||
workflow_node_execution_repository=workflow_node_execution_repository,
|
||||
graph_engine_layers=graph_engine_layers,
|
||||
|
||||
@@ -13,7 +13,7 @@ from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext
|
||||
from core.workflow.human_input_adapter import EmailDeliveryConfig, EmailDeliveryMethod
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_mail import mail
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from models.human_input import (
|
||||
DeliveryMethodType,
|
||||
HumanInputDelivery,
|
||||
@@ -139,8 +139,8 @@ def _load_variable_pool(workflow_run_id: str | None) -> VariablePool | None:
|
||||
logger.exception("Failed to load resumption context for workflow run %s", workflow_run_id)
|
||||
return None
|
||||
|
||||
graph_runtime_state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
return graph_runtime_state.variable_pool
|
||||
runtime_state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
return runtime_state.variable_pool
|
||||
|
||||
|
||||
def _open_session(session_factory: sessionmaker | Session | None):
|
||||
|
||||
@@ -4,5 +4,6 @@ from core.app.workflow.file_runtime import bind_dify_workflow_file_runtime
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _bind_workflow_file_runtime() -> None:
|
||||
bind_dify_workflow_file_runtime()
|
||||
def _bind_workflow_file_runtime():
|
||||
with bind_dify_workflow_file_runtime():
|
||||
yield
|
||||
|
||||
@@ -15,7 +15,7 @@ from graphon.node_events import NodeRunResult
|
||||
from graphon.nodes.code.code_node import CodeNode
|
||||
from graphon.nodes.code.entities import CodeNodeData
|
||||
from graphon.nodes.code.limits import CodeNodeLimits
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from tests.workflow_test_utils import build_test_graph_init_params
|
||||
|
||||
pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.code_executor",)
|
||||
@@ -44,14 +44,14 @@ def _init_code_node(code_config: dict) -> CodeNode:
|
||||
)
|
||||
variable_pool.add(["code", "args1"], 1)
|
||||
variable_pool.add(["code", "args2"], 2)
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
node_factory = DifyNodeFactory(graph_init_params=init_params, graph_runtime_state=graph_runtime_state)
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
node_factory = DifyNodeFactory(init_params=init_params, runtime_state=runtime_state)
|
||||
Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
|
||||
return CodeNode(
|
||||
node_id=str(uuid.uuid4()),
|
||||
data=CodeNodeData.model_validate(code_config["data"]),
|
||||
graph_init_params=init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
code_executor=node_factory._code_executor,
|
||||
code_limits=CodeNodeLimits(
|
||||
max_string_length=dify_config.CODE_MAX_STRING_LENGTH,
|
||||
@@ -90,8 +90,8 @@ def _code_config(code: str, output_type: str, *, variables: bool = True) -> dict
|
||||
def test_execute_code(setup_code_executor_mock) -> None:
|
||||
code = "def main(args1: int, args2: int):\n return {'result': args1 + args2}"
|
||||
node = _init_code_node(_code_config(code, "number"))
|
||||
node.graph_runtime_state.variable_pool.add(["1", "args1"], 1)
|
||||
node.graph_runtime_state.variable_pool.add(["1", "args2"], 2)
|
||||
node.runtime_state.variable_pool.add(["1", "args1"], 1)
|
||||
node.runtime_state.variable_pool.add(["1", "args2"], 2)
|
||||
|
||||
result = node._run()
|
||||
|
||||
@@ -106,8 +106,8 @@ def test_execute_code(setup_code_executor_mock) -> None:
|
||||
def test_execute_code_output_validator(setup_code_executor_mock) -> None:
|
||||
code = "def main(args1: int, args2: int):\n return {'result': args1 + args2}"
|
||||
node = _init_code_node(_code_config(code, "string"))
|
||||
node.graph_runtime_state.variable_pool.add(["1", "args1"], 1)
|
||||
node.graph_runtime_state.variable_pool.add(["1", "args2"], 2)
|
||||
node.runtime_state.variable_pool.add(["1", "args1"], 1)
|
||||
node.runtime_state.variable_pool.add(["1", "args2"], 2)
|
||||
|
||||
result = node._run()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus
|
||||
from graphon.file.file_manager import file_manager
|
||||
from graphon.graph import Graph
|
||||
from graphon.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig, HttpRequestNodeData
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from tests.workflow_test_utils import build_test_graph_init_params
|
||||
|
||||
pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.http",)
|
||||
@@ -64,12 +64,12 @@ def init_http_node(config: dict):
|
||||
variable_pool.add(["a", "args1"], 1)
|
||||
variable_pool.add(["a", "args2"], 2)
|
||||
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
# Create node factory
|
||||
node_factory = DifyNodeFactory(
|
||||
graph_init_params=init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
|
||||
@@ -77,8 +77,8 @@ def init_http_node(config: dict):
|
||||
node = HttpRequestNode(
|
||||
node_id=str(uuid.uuid4()),
|
||||
data=HttpRequestNodeData.model_validate(config["data"]),
|
||||
graph_init_params=init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
http_request_config=HTTP_REQUEST_CONFIG,
|
||||
http_client=ssrf_proxy,
|
||||
tool_file_manager_factory=ToolFileManager,
|
||||
@@ -711,12 +711,12 @@ def test_nested_object_variable_selector(setup_http_mock):
|
||||
variable_pool.add(["a", "args2"], 2)
|
||||
variable_pool.add(["a", "args3"], {"nested": "nested_value"}) # Only for this test
|
||||
|
||||
graph_runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
# Create node factory
|
||||
node_factory = DifyNodeFactory(
|
||||
graph_init_params=init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
graph = Graph.init(graph_config=graph_config, node_factory=node_factory, root_node_id="start")
|
||||
@@ -724,8 +724,8 @@ def test_nested_object_variable_selector(setup_http_mock):
|
||||
node = HttpRequestNode(
|
||||
node_id=str(uuid.uuid4()),
|
||||
data=HttpRequestNodeData.model_validate(graph_config["nodes"][1]["data"]),
|
||||
graph_init_params=init_params,
|
||||
graph_runtime_state=graph_runtime_state,
|
||||
init_params=init_params,
|
||||
runtime_state=runtime_state,
|
||||
http_request_config=HTTP_REQUEST_CONFIG,
|
||||
http_client=ssrf_proxy,
|
||||
tool_file_manager_factory=ToolFileManager,
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ from core.workflow.nodes.human_input.entities import (
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus, ValueSourceType
|
||||
from core.workflow.nodes.human_input.pause_reason import HumanInputRequired
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
|
||||
from models.human_input import (
|
||||
@@ -108,7 +108,7 @@ def _build_resumption_context(*, app: App, workflow_run: WorkflowRun, options: l
|
||||
call_depth=0,
|
||||
workflow_execution_id=workflow_run.id,
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state.variable_pool.add(("start", "options"), options)
|
||||
return WorkflowResumptionContext(
|
||||
generate_entity=_WorkflowGenerateEntityWrapper(entity=generate_entity),
|
||||
|
||||
+43
-37
@@ -11,7 +11,7 @@ This test suite covers complete integration scenarios including:
|
||||
- Workflow status transitions in database
|
||||
- Error handling with real database constraints
|
||||
- Multiple pause events in sequence
|
||||
- Integration with real ReadOnlyGraphRuntimeState implementations
|
||||
- Integration with real ReadOnlyRuntimeState implementations
|
||||
|
||||
These tests use TestContainers to spin up real services for integration testing,
|
||||
providing more reliable and realistic test scenarios than mocks.
|
||||
@@ -33,14 +33,13 @@ from core.app.layers.pause_state_persist_layer import (
|
||||
)
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from extensions.ext_storage import storage
|
||||
from graphon.engine.command import Command
|
||||
from graphon.engine.filter import EngineEventFilterContext, ResponseStreamFilter
|
||||
from graphon.engine_events import GraphRunPausedEvent
|
||||
from graphon.entities.pause_reason import SchedulingPause
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.filters import GraphEventFilterContext, ResponseStreamFilter
|
||||
from graphon.graph_engine.entities.commands import GraphEngineCommand
|
||||
from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError
|
||||
from graphon.graph_events import GraphRunPausedEvent
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool
|
||||
from graphon.runtime import ReadOnlyRuntimeState, ReadOnlyRuntimeStateWrapper, RuntimeState, VariablePool
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account
|
||||
from models import WorkflowPause as WorkflowPauseModel
|
||||
@@ -56,14 +55,14 @@ def _create_initialized_response_stream_filter() -> ResponseStreamFilter:
|
||||
"""Build a `ResponseStreamFilter` that has already run `initialize()`.
|
||||
|
||||
`ResponseStreamFilter.dumps()` raises `RuntimeError` unless the filter has
|
||||
processed a `GraphEventFilterContext` first. In production this always
|
||||
processed a `EngineEventFilterContext` first. In production this always
|
||||
happens before any event (including `GraphRunPausedEvent`) reaches
|
||||
`PauseStatePersistenceLayer.on_event`, so tests that exercise `on_event`
|
||||
or a subsequent `dumps()` call need a filter in that same state. A
|
||||
nodeless graph is enough to satisfy the precondition.
|
||||
"""
|
||||
response_stream_filter = ResponseStreamFilter()
|
||||
context = GraphEventFilterContext(graph=Mock(nodes={}), runtime_state=Mock())
|
||||
context = EngineEventFilterContext(graph=Mock(nodes={}), runtime_state=Mock())
|
||||
response_stream_filter.initialize(context)
|
||||
return response_stream_filter
|
||||
|
||||
@@ -72,14 +71,14 @@ class _TestCommandChannelImpl:
|
||||
"""Real implementation of CommandChannel for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self._commands: list[GraphEngineCommand] = []
|
||||
self._commands: list[Command] = []
|
||||
|
||||
def fetch_commands(self) -> list[GraphEngineCommand]:
|
||||
"""Fetch pending commands for this GraphEngine instance."""
|
||||
def fetch_commands(self) -> list[Command]:
|
||||
"""Fetch pending commands for this Engine instance."""
|
||||
return self._commands.copy()
|
||||
|
||||
def send_command(self, command: GraphEngineCommand) -> None:
|
||||
"""Send a command to be processed by this GraphEngine instance."""
|
||||
def send_command(self, command: Command) -> None:
|
||||
"""Send a command to be processed by this Engine instance."""
|
||||
self._commands.append(command)
|
||||
|
||||
|
||||
@@ -229,8 +228,8 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
node_run_steps: int = 0,
|
||||
variables: dict[tuple[str, str], object] | None = None,
|
||||
workflow_run_id: str | None = None,
|
||||
) -> ReadOnlyGraphRuntimeState:
|
||||
"""Create a real GraphRuntimeState for testing."""
|
||||
) -> ReadOnlyRuntimeState:
|
||||
"""Create a real RuntimeState for testing."""
|
||||
start_at = time()
|
||||
|
||||
execution_id = workflow_run_id or getattr(self, "test_workflow_run_id", None) or str(uuid.uuid4())
|
||||
@@ -248,7 +247,8 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
llm_usage.total_tokens = total_tokens
|
||||
|
||||
# Create graph runtime state
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=variable_pool,
|
||||
start_at=start_at,
|
||||
llm_usage=llm_usage,
|
||||
@@ -256,7 +256,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
node_run_steps=node_run_steps,
|
||||
)
|
||||
|
||||
return ReadOnlyGraphRuntimeStateWrapper(graph_runtime_state)
|
||||
return ReadOnlyRuntimeStateWrapper(runtime_state)
|
||||
|
||||
def _create_generate_entity(
|
||||
self,
|
||||
@@ -335,7 +335,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
("node1", "var1"): "string_value",
|
||||
("node2", "var2"): {"complex": "object"},
|
||||
}
|
||||
graph_runtime_state = self._create_graph_runtime_state(
|
||||
runtime_state = self._create_graph_runtime_state(
|
||||
outputs=test_outputs,
|
||||
total_tokens=100,
|
||||
node_run_steps=5,
|
||||
@@ -343,7 +343,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
)
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
# Create pause event
|
||||
event = GraphRunPausedEvent(
|
||||
@@ -353,6 +353,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
|
||||
# Act
|
||||
layer.on_event(event)
|
||||
layer.persist_paused_state()
|
||||
|
||||
# Assert - Verify pause state was saved to database
|
||||
self.session.refresh(self.test_workflow_run)
|
||||
@@ -373,7 +374,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
storage_content = storage.load(pause_model.state_object_key).decode()
|
||||
resumption_context = WorkflowResumptionContext.loads(storage_content)
|
||||
assert resumption_context.version == "1"
|
||||
assert resumption_context.serialized_graph_runtime_state == graph_runtime_state.dumps()
|
||||
assert resumption_context.serialized_graph_runtime_state == runtime_state.dumps()
|
||||
persisted_entity = resumption_context.get_generate_entity()
|
||||
assert isinstance(persisted_entity, WorkflowAppGenerateEntity)
|
||||
assert persisted_entity.workflow_execution_id == self.test_workflow_run_id
|
||||
@@ -396,7 +397,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
("node3", "var3"): [1, 2, 3],
|
||||
}
|
||||
|
||||
graph_runtime_state = self._create_graph_runtime_state(
|
||||
runtime_state = self._create_graph_runtime_state(
|
||||
outputs=complex_outputs,
|
||||
total_tokens=250,
|
||||
node_run_steps=10,
|
||||
@@ -404,12 +405,13 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
)
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
event = GraphRunPausedEvent(reasons=[SchedulingPause(message="test pause")])
|
||||
|
||||
# Act - Save pause state
|
||||
layer.on_event(event)
|
||||
layer.persist_paused_state()
|
||||
|
||||
# Assert - Retrieve and verify
|
||||
pause_entity = self.workflow_run_service._workflow_runs.get_workflow_pause(self.test_workflow_run_id)
|
||||
@@ -419,7 +421,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
|
||||
state_bytes = pause_entity.get_state()
|
||||
resumption_context = WorkflowResumptionContext.loads(state_bytes.decode())
|
||||
retrieved_state = GraphRuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
retrieved_state = RuntimeState.from_snapshot(resumption_context.serialized_graph_runtime_state)
|
||||
|
||||
assert retrieved_state.outputs == complex_outputs
|
||||
assert retrieved_state.total_tokens == 250
|
||||
@@ -430,18 +432,19 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
"""Test that database transactions are handled correctly."""
|
||||
# Arrange
|
||||
layer = self._create_pause_state_persistence_layer()
|
||||
graph_runtime_state = self._create_graph_runtime_state(
|
||||
runtime_state = self._create_graph_runtime_state(
|
||||
outputs={"test": "transaction"},
|
||||
total_tokens=50,
|
||||
)
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
event = GraphRunPausedEvent(reasons=[SchedulingPause(message="test pause")])
|
||||
|
||||
# Act
|
||||
layer.on_event(event)
|
||||
layer.persist_paused_state()
|
||||
|
||||
# Assert - Verify data is committed and accessible in new session
|
||||
with Session(bind=self.session.get_bind(), expire_on_commit=False) as new_session:
|
||||
@@ -464,18 +467,19 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
|
||||
# Create large state data to test storage
|
||||
large_outputs = {"data": "x" * 10000} # 10KB of data
|
||||
graph_runtime_state = self._create_graph_runtime_state(
|
||||
runtime_state = self._create_graph_runtime_state(
|
||||
outputs=large_outputs,
|
||||
total_tokens=1000,
|
||||
)
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
event = GraphRunPausedEvent(reasons=[SchedulingPause(message="test pause")])
|
||||
|
||||
# Act
|
||||
layer.on_event(event)
|
||||
layer.persist_paused_state()
|
||||
|
||||
# Assert - Verify file was uploaded to storage
|
||||
self.session.refresh(self.test_workflow_run)
|
||||
@@ -488,7 +492,7 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
# Verify content in storage
|
||||
storage_content = storage.load(pause_model.state_object_key).decode()
|
||||
resumption_context = WorkflowResumptionContext.loads(storage_content)
|
||||
assert resumption_context.serialized_graph_runtime_state == graph_runtime_state.dumps()
|
||||
assert resumption_context.serialized_graph_runtime_state == runtime_state.dumps()
|
||||
assert resumption_context.get_generate_entity().workflow_execution_id == self.test_workflow_run_id
|
||||
|
||||
def test_workflow_with_different_creators(self, db_session_with_containers: Session):
|
||||
@@ -530,18 +534,19 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
workflow=different_workflow,
|
||||
)
|
||||
|
||||
graph_runtime_state = self._create_graph_runtime_state(
|
||||
runtime_state = self._create_graph_runtime_state(
|
||||
outputs={"creator_test": "different_creator"},
|
||||
workflow_run_id=different_workflow_run.id,
|
||||
)
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
event = GraphRunPausedEvent(reasons=[SchedulingPause(message="test pause")])
|
||||
|
||||
# Act
|
||||
layer.on_event(event)
|
||||
layer.persist_paused_state()
|
||||
|
||||
# Assert - Should use workflow creator (not run creator)
|
||||
self.session.refresh(different_workflow_run)
|
||||
@@ -560,13 +565,13 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
"""Test that layer ignores non-pause events."""
|
||||
# Arrange
|
||||
layer = self._create_pause_state_persistence_layer()
|
||||
graph_runtime_state = self._create_graph_runtime_state()
|
||||
runtime_state = self._create_graph_runtime_state()
|
||||
|
||||
command_channel = _TestCommandChannelImpl()
|
||||
layer.initialize(graph_runtime_state, command_channel)
|
||||
layer.initialize(runtime_state, command_channel)
|
||||
|
||||
# Import other event types
|
||||
from graphon.graph_events import (
|
||||
from graphon.engine_events import (
|
||||
GraphRunFailedEvent,
|
||||
GraphRunStartedEvent,
|
||||
GraphRunSucceededEvent,
|
||||
@@ -590,10 +595,11 @@ class TestPauseStatePersistenceLayerTestContainers:
|
||||
"""Test that layer requires proper initialization before handling events."""
|
||||
# Arrange
|
||||
layer = self._create_pause_state_persistence_layer()
|
||||
# Don't initialize - graph_runtime_state should be uninitialized
|
||||
# Don't initialize - runtime_state should be uninitialized
|
||||
|
||||
event = GraphRunPausedEvent(reasons=[SchedulingPause(message="test pause")])
|
||||
|
||||
# Act & Assert - Should raise GraphEngineLayerNotInitializedError
|
||||
with pytest.raises(GraphEngineLayerNotInitializedError):
|
||||
layer.on_event(event)
|
||||
# Act & Assert - Should raise RuntimeError
|
||||
layer.on_event(event)
|
||||
with pytest.raises(RuntimeError):
|
||||
layer.persist_paused_state()
|
||||
|
||||
+17
-18
@@ -17,16 +17,16 @@ from core.workflow.nodes.human_input.callback import (
|
||||
from core.workflow.nodes.human_input.entities import HumanInputNodeData, UserActionConfig
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormStatus
|
||||
from core.workflow.system_variables import build_system_variables
|
||||
from graphon.engine import Engine
|
||||
from graphon.engine.command import InMemoryChannel
|
||||
from graphon.enums import WorkflowType
|
||||
from graphon.graph import Graph
|
||||
from graphon.graph_engine import GraphEngine
|
||||
from graphon.graph_engine.command_channels import InMemoryChannel
|
||||
from graphon.nodes.end.end_node import EndNode
|
||||
from graphon.nodes.end.entities import EndNodeData
|
||||
from graphon.nodes.human_input.human_input_node import HumanInputNode
|
||||
from graphon.nodes.start.entities import StartNodeData
|
||||
from graphon.nodes.start.start_node import StartNode
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models import Account
|
||||
from models.account import AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
||||
@@ -37,7 +37,7 @@ from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeEx
|
||||
from tests.workflow_test_utils import build_test_graph_init_params
|
||||
|
||||
|
||||
def _build_runtime_state(workflow_execution_id: str, app_id: str, workflow_id: str, user_id: str) -> GraphRuntimeState:
|
||||
def _build_runtime_state(workflow_execution_id: str, app_id: str, workflow_id: str, user_id: str) -> RuntimeState:
|
||||
variable_pool = VariablePool.from_bootstrap(
|
||||
system_variables=build_system_variables(
|
||||
workflow_execution_id=workflow_execution_id,
|
||||
@@ -48,11 +48,11 @@ def _build_runtime_state(workflow_execution_id: str, app_id: str, workflow_id: s
|
||||
user_inputs={},
|
||||
conversation_variables=[],
|
||||
)
|
||||
return GraphRuntimeState(variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
return RuntimeState(workflow_id="test-workflow", variable_pool=variable_pool, start_at=time.perf_counter())
|
||||
|
||||
|
||||
def _build_graph(
|
||||
runtime_state: GraphRuntimeState,
|
||||
runtime_state: RuntimeState,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
workflow_id: str,
|
||||
@@ -75,8 +75,8 @@ def _build_graph(
|
||||
start_node = StartNode(
|
||||
node_id="start",
|
||||
data=start_data,
|
||||
graph_init_params=params,
|
||||
graph_runtime_state=runtime_state,
|
||||
init_params=params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
human_data = HumanInputNodeData(
|
||||
@@ -94,8 +94,8 @@ def _build_graph(
|
||||
human_node = HumanInputNode(
|
||||
node_id="human",
|
||||
data=human_data,
|
||||
graph_init_params=params,
|
||||
graph_runtime_state=runtime_state,
|
||||
init_params=params,
|
||||
runtime_state=runtime_state,
|
||||
hitl_callback=hitl_callback,
|
||||
)
|
||||
|
||||
@@ -107,8 +107,8 @@ def _build_graph(
|
||||
end_node = EndNode(
|
||||
node_id="end",
|
||||
data=end_data,
|
||||
graph_init_params=params,
|
||||
graph_runtime_state=runtime_state,
|
||||
init_params=params,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -271,14 +271,13 @@ class TestHumanInputResumeNodeExecutionIntegration:
|
||||
workflow_node_execution_repository=node_execution_repo,
|
||||
)
|
||||
|
||||
def _run_graph(self, graph: Graph, runtime_state: GraphRuntimeState, execution_id: str) -> None:
|
||||
engine = GraphEngine(
|
||||
workflow_id=self.workflow.id,
|
||||
def _run_graph(self, graph: Graph, runtime_state: RuntimeState, execution_id: str) -> None:
|
||||
engine = Engine(
|
||||
graph=graph,
|
||||
graph_runtime_state=runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
command_channel=InMemoryChannel(),
|
||||
)
|
||||
engine.layer(self._build_persistence_layer(execution_id))
|
||||
engine.add_layer(self._build_persistence_layer(execution_id))
|
||||
for _ in engine.run():
|
||||
continue
|
||||
|
||||
@@ -304,7 +303,7 @@ class TestHumanInputResumeNodeExecutionIntegration:
|
||||
self._run_graph(paused_graph, runtime_state, execution_id)
|
||||
|
||||
snapshot = runtime_state.dumps()
|
||||
resumed_state = GraphRuntimeState.from_snapshot(snapshot)
|
||||
resumed_state = RuntimeState.from_snapshot(snapshot)
|
||||
form = self.session.scalar(
|
||||
select(HumanInputForm).where(
|
||||
HumanInputForm.tenant_id == self.tenant.id,
|
||||
|
||||
@@ -1647,7 +1647,7 @@ class TestWorkflowService:
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from graphon.graph_events import NodeRunSucceededEvent
|
||||
from graphon.engine_events import NodeRunSucceededEvent
|
||||
from graphon.node_events import NodeRunResult
|
||||
|
||||
node = _ExecutedNode(
|
||||
@@ -1716,7 +1716,7 @@ class TestWorkflowService:
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from graphon.graph_events import NodeRunFailedEvent
|
||||
from graphon.engine_events import NodeRunFailedEvent
|
||||
from graphon.node_events import NodeRunResult
|
||||
|
||||
node = _ExecutedNode(
|
||||
@@ -1780,7 +1780,7 @@ class TestWorkflowService:
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from graphon.graph_events import NodeRunFailedEvent
|
||||
from graphon.engine_events import NodeRunFailedEvent
|
||||
from graphon.node_events import NodeRunResult
|
||||
|
||||
node = _ExecutedNode(
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ from core.workflow.nodes.human_input.entities import SelectInputConfig, StringLi
|
||||
from core.workflow.nodes.human_input.enums import HumanInputFormStatus, ValueSourceType
|
||||
from core.workflow.nodes.human_input.pause_reason import HumanInputRequired
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from models.enums import CreatorUserRole
|
||||
from models.human_input import HumanInputForm
|
||||
from models.model import AppMode
|
||||
@@ -79,7 +79,7 @@ def _build_resumption_context(workflow_run_id: str) -> WorkflowResumptionContext
|
||||
call_depth=0,
|
||||
workflow_execution_id=workflow_run_id,
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state.variable_pool.add(("start", "options"), ["approve", "reject"])
|
||||
wrapper = _WorkflowGenerateEntityWrapper(entity=generate_entity)
|
||||
return WorkflowResumptionContext(
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ from core.workflow.human_input_adapter import (
|
||||
from core.workflow.nodes.human_input.entities import HumanInputNodeData
|
||||
from extensions.ext_storage import storage
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom
|
||||
from models.human_input import HumanInputDelivery, HumanInputForm, HumanInputFormRecipient
|
||||
@@ -138,7 +138,7 @@ def _create_workflow_pause_state(
|
||||
)
|
||||
db_session_with_containers.add(workflow_run)
|
||||
|
||||
runtime_state = GraphRuntimeState(variable_pool=variable_pool, start_at=0.0)
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=variable_pool, start_at=0.0)
|
||||
resumption_context = WorkflowResumptionContext(
|
||||
generate_entity={
|
||||
"type": AppMode.WORKFLOW,
|
||||
|
||||
@@ -561,13 +561,13 @@ def _stop_global_redis(monkeypatch: pytest.MonkeyPatch) -> Generator[_StopRedis]
|
||||
redis = _StopRedis(
|
||||
read_error=AssertionError("Must use the injected Redis for ownership reads"),
|
||||
flag_error=AssertionError("Must use the injected Redis for stop flags"),
|
||||
command_error=AssertionError("Must use the injected Redis for GraphEngine commands"),
|
||||
command_error=AssertionError("Must use the injected Redis for Engine commands"),
|
||||
)
|
||||
monkeypatch.setattr(app_queue_module, "redis_client", redis)
|
||||
monkeypatch.setattr(coordinator_module, "redis_client", redis)
|
||||
monkeypatch.setattr(app_task_module, "redis_client", redis)
|
||||
yield redis
|
||||
# GraphEngine catches Redis failures, so inspect the trap even after HTTP success.
|
||||
# Engine catches Redis failures, so inspect the trap even after HTTP success.
|
||||
assert redis.reads == []
|
||||
assert redis.operations == []
|
||||
|
||||
@@ -633,7 +633,7 @@ def test_stop_handlers_preserve_mode_specific_commands_and_response(
|
||||
command_key = f"workflow:{_TASK_ID}:commands"
|
||||
assert set(stop_redis.commands) == {command_key}
|
||||
assert [json.loads(command) for command in stop_redis.commands[command_key]] == [
|
||||
{"command_type": "abort", "payload": None, "reason": "User requested stop"}
|
||||
{"command_type": "abort", "reason": "User requested stop"}
|
||||
]
|
||||
else:
|
||||
assert stop_redis.commands == {}
|
||||
@@ -669,7 +669,7 @@ def test_stop_handlers_preserve_mode_specific_behavior_when_task_ownership_does_
|
||||
if mode == AppMode.ADVANCED_CHAT:
|
||||
assert stop_redis.operations == ["graph_command"]
|
||||
assert [json.loads(command) for command in stop_redis.commands[f"workflow:{_TASK_ID}:commands"]] == [
|
||||
{"command_type": "abort", "payload": None, "reason": "User requested stop"}
|
||||
{"command_type": "abort", "reason": "User requested stop"}
|
||||
]
|
||||
else:
|
||||
assert stop_redis.operations == []
|
||||
|
||||
@@ -598,7 +598,7 @@ def test_draft_node_last_run_raises_not_found_when_execution_missing(
|
||||
|
||||
def test_workflow_task_stop_uses_queue_flag_and_graph_command(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
set_stop_flag = Mock()
|
||||
send_stop_command = Mock()
|
||||
send_command = Mock()
|
||||
monkeypatch.setattr(
|
||||
snippet_workflow_module.AppQueueManager,
|
||||
"set_stop_flag_no_user_check",
|
||||
@@ -606,8 +606,8 @@ def test_workflow_task_stop_uses_queue_flag_and_graph_command(app: Flask, monkey
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
snippet_workflow_module,
|
||||
"GraphEngineManager",
|
||||
Mock(return_value=Mock(send_stop_command=send_stop_command)),
|
||||
"send_workflow_stop_command",
|
||||
send_command,
|
||||
)
|
||||
|
||||
api = snippet_workflow_module.SnippetWorkflowTaskStopApi()
|
||||
@@ -618,4 +618,4 @@ def test_workflow_task_stop_uses_queue_flag_and_graph_command(app: Flask, monkey
|
||||
|
||||
assert result == {"result": "success"}
|
||||
set_stop_flag.assert_called_once_with("task-1")
|
||||
send_stop_command.assert_called_once_with("task-1")
|
||||
send_command.assert_called_once_with(task_id="task-1", redis=snippet_workflow_module.redis_client)
|
||||
|
||||
@@ -267,7 +267,7 @@ def test_stop_task_rejects_other_token_owner(resource_fixture: ResourceFixture)
|
||||
with (
|
||||
patch("controllers.openapi.app_run.redis_client") as redis,
|
||||
patch("controllers.openapi.app_run.AppQueueManager.set_stop_flag_no_user_check") as stop,
|
||||
patch("controllers.openapi.app_run.GraphEngineManager") as engine,
|
||||
patch("controllers.openapi.app_run.send_workflow_stop_command") as engine,
|
||||
):
|
||||
redis.get.return_value = b"end-user-other-user"
|
||||
with pytest.raises(NotFound):
|
||||
|
||||
@@ -74,12 +74,10 @@ def test_chat_payload_normalizes_conversation_id():
|
||||
def test_stop_task_calls_queue_manager_and_graph_engine(app: Flask, monkeypatch: pytest.MonkeyPatch):
|
||||
queue_mock = Mock()
|
||||
graph_mock = Mock()
|
||||
graph_instance = Mock()
|
||||
graph_mock.return_value = graph_instance
|
||||
|
||||
run_module = sys.modules["controllers.openapi.app_run"]
|
||||
monkeypatch.setattr(run_module, "AppQueueManager", queue_mock)
|
||||
monkeypatch.setattr(run_module, "GraphEngineManager", graph_mock)
|
||||
monkeypatch.setattr(run_module, "send_workflow_stop_command", graph_mock)
|
||||
monkeypatch.setattr(run_module, "redis_client", object())
|
||||
|
||||
api = AppRunTaskStopApi()
|
||||
@@ -92,7 +90,7 @@ def test_stop_task_calls_queue_manager_and_graph_engine(app: Flask, monkeypatch:
|
||||
)
|
||||
|
||||
queue_mock.set_stop_flag_no_user_check.assert_called_once_with("task-1")
|
||||
graph_instance.send_stop_command.assert_called_once_with("task-1")
|
||||
graph_mock.assert_called_once_with(task_id="task-1", redis=run_module.redis_client)
|
||||
assert result == TaskStopResponse(result="success")
|
||||
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ from core.workflow.system_variables import build_system_variables
|
||||
from enums import DeploymentEdition
|
||||
from graphon.entities import WorkflowStartReason
|
||||
from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from libs.datetime_utils import to_utc_timestamp
|
||||
from models.account import Account
|
||||
from models.enums import CreatorUserRole, EndUserType, MessageStatus
|
||||
@@ -313,7 +313,7 @@ def _build_resumption_context(task_id: str) -> WorkflowResumptionContext:
|
||||
call_depth=0,
|
||||
workflow_execution_id="run-1",
|
||||
)
|
||||
runtime_state = GraphRuntimeState(variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state = RuntimeState(workflow_id="test-workflow", variable_pool=VariablePool(), start_at=0.0)
|
||||
runtime_state.set_output("result", "value")
|
||||
wrapper = _WorkflowGenerateEntityWrapper(entity=generate_entity)
|
||||
return WorkflowResumptionContext(
|
||||
@@ -504,7 +504,7 @@ class TestHitlServiceApi:
|
||||
pipeline = AdvancedChatAppGenerateTaskPipeline(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=WorkflowSnapshot(id="workflow-id", tenant_id="tenant", features_dict={}),
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, graph_runtime_state=None),
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, runtime_state=None),
|
||||
conversation=ConversationSnapshot(id="conv-id", mode=AppMode.ADVANCED_CHAT),
|
||||
message=MessageSnapshot(
|
||||
id="message-id",
|
||||
@@ -597,7 +597,7 @@ class TestHitlServiceApi:
|
||||
pipeline = WorkflowAppGenerateTaskPipeline(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=_workflow(workflow_id="workflow-id", app_id="app", tenant_id="tenant"),
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, graph_runtime_state=None),
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, runtime_state=None),
|
||||
user=_end_user(user_id="user", app_id="app", tenant_id="tenant"),
|
||||
stream=False,
|
||||
draft_var_saver_factory=lambda **kwargs: None,
|
||||
@@ -691,7 +691,7 @@ class TestHitlServiceApi:
|
||||
responses = converter.workflow_pause_to_stream_response(
|
||||
event=queue_event,
|
||||
task_id="task",
|
||||
graph_runtime_state=runtime_state,
|
||||
runtime_state=runtime_state,
|
||||
)
|
||||
|
||||
assert isinstance(responses[-1], WorkflowPauseStreamResponse)
|
||||
|
||||
@@ -36,7 +36,6 @@ from controllers.service_api.app.error import (
|
||||
)
|
||||
from controllers.service_api.app.workflow import (
|
||||
AppQueueManager,
|
||||
GraphEngineManager,
|
||||
WorkflowAppLogApi,
|
||||
WorkflowLogQuery,
|
||||
WorkflowRunApi,
|
||||
@@ -48,6 +47,7 @@ from controllers.service_api.app.workflow import (
|
||||
)
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from enums import CloudPlan, DeploymentEdition
|
||||
from graphon.engine.command import AbortCommand, RedisChannel
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from graphon.model_runtime.errors.invoke import InvokeRateLimitError as ProviderInvokeRateLimitError
|
||||
from models import Account
|
||||
@@ -727,7 +727,7 @@ class TestWorkflowTaskStopApi:
|
||||
stop_mock = Mock()
|
||||
send_mock = Mock()
|
||||
monkeypatch.setattr(AppQueueManager, "set_stop_flag_no_user_check", stop_mock)
|
||||
monkeypatch.setattr(GraphEngineManager, "send_stop_command", send_mock)
|
||||
monkeypatch.setattr(RedisChannel, "send_command", send_mock)
|
||||
|
||||
api = WorkflowTaskStopApi()
|
||||
handler = unwrap(api.post)
|
||||
@@ -739,7 +739,7 @@ class TestWorkflowTaskStopApi:
|
||||
|
||||
assert response == {"result": "success"}
|
||||
stop_mock.assert_called_once_with("t1")
|
||||
send_mock.assert_called_once_with("t1")
|
||||
send_mock.assert_called_once_with(AbortCommand(reason="User requested stop"))
|
||||
|
||||
|
||||
class TestWorkflowAppLogApi:
|
||||
|
||||
@@ -15,6 +15,7 @@ from controllers.web.error import (
|
||||
)
|
||||
from controllers.web.workflow import WorkflowRunApi, WorkflowTaskStopApi
|
||||
from core.errors.error import ProviderTokenNotInitError, QuotaExceededError
|
||||
from graphon.engine.command import AbortCommand
|
||||
from models.model import App, AppMode, EndUser
|
||||
from services.errors.app import (
|
||||
TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError,
|
||||
@@ -109,7 +110,7 @@ class TestWorkflowTaskStopApi:
|
||||
with pytest.raises(NotWorkflowAppError):
|
||||
WorkflowTaskStopApi().post(_chat_app(), _end_user(), "task-1")
|
||||
|
||||
@patch("controllers.web.workflow.GraphEngineManager.send_stop_command")
|
||||
@patch("services.workflow.command_service.RedisChannel.send_command")
|
||||
@patch("controllers.web.workflow.AppQueueManager.set_stop_flag_no_user_check")
|
||||
def test_stop_calls_both_mechanisms(self, mock_legacy: MagicMock, mock_graph: MagicMock, app: Flask) -> None:
|
||||
with app.test_request_context("/workflows/tasks/task-1/stop", method="POST"):
|
||||
@@ -117,4 +118,4 @@ class TestWorkflowTaskStopApi:
|
||||
|
||||
assert result == {"result": "success"}
|
||||
mock_legacy.assert_called_once_with("task-1")
|
||||
mock_graph.assert_called_once_with("task-1")
|
||||
mock_graph.assert_called_once_with(AbortCommand(reason="User requested stop"))
|
||||
|
||||
@@ -313,7 +313,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
def _fake_generate(**kwargs):
|
||||
nonlocal captured_entity, captured_graph_runtime_state
|
||||
captured_entity = kwargs["application_generate_entity"]
|
||||
captured_graph_runtime_state = kwargs["graph_runtime_state"]
|
||||
captured_graph_runtime_state = kwargs["runtime_state"]
|
||||
return SimpleNamespace(resumed=True)
|
||||
|
||||
monkeypatch.setattr(generator, "_generate", _fake_generate)
|
||||
@@ -328,7 +328,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_runtime_state=SimpleNamespace(),
|
||||
runtime_state=SimpleNamespace(),
|
||||
pause_state_config=None,
|
||||
)
|
||||
|
||||
@@ -733,7 +733,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=None,
|
||||
runtime_state=None,
|
||||
)
|
||||
|
||||
def test_generate_worker_raises_when_app_not_found_for_internal_call(
|
||||
@@ -785,7 +785,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=None,
|
||||
runtime_state=None,
|
||||
)
|
||||
|
||||
def test_generate_worker_handles_stopped_error(
|
||||
@@ -848,7 +848,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=SimpleNamespace(),
|
||||
runtime_state=SimpleNamespace(),
|
||||
)
|
||||
|
||||
queue_manager.publish_error.assert_not_called()
|
||||
@@ -922,7 +922,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=None,
|
||||
runtime_state=None,
|
||||
)
|
||||
|
||||
queue_manager.publish_error.assert_called_once()
|
||||
@@ -991,7 +991,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=None,
|
||||
runtime_state=None,
|
||||
)
|
||||
|
||||
queue_manager.publish_error.assert_called_once()
|
||||
@@ -1170,7 +1170,7 @@ class TestAdvancedChatAppGeneratorInternals:
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_engine_layers=(),
|
||||
graph_runtime_state=None,
|
||||
runtime_state=None,
|
||||
)
|
||||
|
||||
assert queue_manager.publish_error.called
|
||||
@@ -1388,7 +1388,7 @@ class TestAdvancedChatAppGeneratorResume:
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_runtime_state=SimpleNamespace(),
|
||||
runtime_state=SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert result.ok is True
|
||||
@@ -1436,7 +1436,7 @@ class TestAdvancedChatAppGeneratorResume:
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow_execution_repository=SimpleNamespace(),
|
||||
workflow_node_execution_repository=SimpleNamespace(),
|
||||
graph_runtime_state=SimpleNamespace(),
|
||||
runtime_state=SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert result.ok is True
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ def _patch_common_run_deps(runner: AdvancedChatAppRunner):
|
||||
RedisChannel=MagicMock(),
|
||||
redis_client=MagicMock(),
|
||||
WorkflowEntry=MagicMock(**{"return_value.run.return_value": iter([])}),
|
||||
GraphRuntimeState=MagicMock(),
|
||||
RuntimeState=MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ def test_resume_appends_chunks_to_paused_answer(unbound_session: Session) -> Non
|
||||
trace_manager=None,
|
||||
extras={},
|
||||
)
|
||||
queue_manager = SimpleNamespace(graph_runtime_state=None)
|
||||
queue_manager = SimpleNamespace(runtime_state=None)
|
||||
conversation = pipeline_module.ConversationSnapshot(id="conversation-1", mode=AppMode.ADVANCED_CHAT)
|
||||
message = pipeline_module.MessageSnapshot(
|
||||
id="message-1",
|
||||
@@ -209,7 +209,7 @@ def test_emit_message_trace_enqueues_message_trace() -> None:
|
||||
trace_manager=mock_trace_manager,
|
||||
extras={"trace_session_id": "session-trace-1"},
|
||||
)
|
||||
queue_manager = SimpleNamespace(graph_runtime_state=None)
|
||||
queue_manager = SimpleNamespace(runtime_state=None)
|
||||
conversation = pipeline_module.ConversationSnapshot(id="conv-1", mode=AppMode.ADVANCED_CHAT)
|
||||
message = pipeline_module.MessageSnapshot(
|
||||
id="msg-1",
|
||||
@@ -286,7 +286,7 @@ def test_save_message_does_not_emit_trace() -> None:
|
||||
trace_manager=mock_trace_manager,
|
||||
extras={"trace_session_id": "session-trace-1"},
|
||||
)
|
||||
queue_manager = SimpleNamespace(graph_runtime_state=None)
|
||||
queue_manager = SimpleNamespace(runtime_state=None)
|
||||
conversation = pipeline_module.ConversationSnapshot(id="conv-1", mode=AppMode.ADVANCED_CHAT)
|
||||
message = pipeline_module.MessageSnapshot(
|
||||
id="msg-1",
|
||||
@@ -353,7 +353,7 @@ def test_emit_message_trace_skips_when_no_trace_manager() -> None:
|
||||
trace_manager=None,
|
||||
extras={},
|
||||
)
|
||||
queue_manager = SimpleNamespace(graph_runtime_state=None)
|
||||
queue_manager = SimpleNamespace(runtime_state=None)
|
||||
conversation = pipeline_module.ConversationSnapshot(id="conv-1", mode=AppMode.ADVANCED_CHAT)
|
||||
message = pipeline_module.MessageSnapshot(
|
||||
id="msg-1",
|
||||
|
||||
+13
-8
@@ -57,7 +57,7 @@ from core.workflow.system_variables import build_system_variables
|
||||
from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.file import FileTransferMethod, FileType
|
||||
from graphon.model_runtime.entities.llm_entities import LLMUsage
|
||||
from graphon.runtime import GraphRuntimeState, VariablePool
|
||||
from graphon.runtime import RuntimeState, VariablePool
|
||||
from libs.datetime_utils import naive_utc_now
|
||||
from models.enums import MessageStatus
|
||||
from models.model import AppMode, EndUser, Message, MessageFile
|
||||
@@ -101,7 +101,7 @@ def _make_pipeline():
|
||||
pipeline = AdvancedChatAppGenerateTaskPipeline(
|
||||
application_generate_entity=application_generate_entity,
|
||||
workflow=workflow,
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, graph_runtime_state=None),
|
||||
queue_manager=SimpleNamespace(invoke_from=InvokeFrom.WEB_APP, runtime_state=None),
|
||||
conversation=conversation,
|
||||
message=message,
|
||||
user=user,
|
||||
@@ -170,7 +170,8 @@ class TestAdvancedChatGenerateTaskPipeline:
|
||||
pipeline = _make_pipeline()
|
||||
pipeline._task_state.answer = "partial answer"
|
||||
pipeline._workflow_run_id = "run-id"
|
||||
pipeline._graph_runtime_state = GraphRuntimeState(
|
||||
pipeline._graph_runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=build_test_variable_pool(
|
||||
variables=build_system_variables(workflow_execution_id="run-id"),
|
||||
),
|
||||
@@ -300,7 +301,8 @@ class TestAdvancedChatGenerateTaskPipeline:
|
||||
pipeline = _make_pipeline()
|
||||
message = _persist_message(sqlite_session)
|
||||
other_message = _persist_message(sqlite_session, message_id="other-message-id")
|
||||
pipeline._graph_runtime_state = GraphRuntimeState(
|
||||
pipeline._graph_runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=build_test_variable_pool(variables=build_system_variables(workflow_execution_id="run-id")),
|
||||
start_at=0.0,
|
||||
)
|
||||
@@ -480,7 +482,8 @@ class TestAdvancedChatGenerateTaskPipeline:
|
||||
def test_workflow_finish_handlers(self):
|
||||
pipeline = _make_pipeline()
|
||||
pipeline._workflow_run_id = "run-id"
|
||||
pipeline._graph_runtime_state = GraphRuntimeState(
|
||||
pipeline._graph_runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=VariablePool.from_bootstrap(
|
||||
system_variables=build_system_variables(workflow_execution_id="run-id")
|
||||
),
|
||||
@@ -665,14 +668,15 @@ class TestAdvancedChatGenerateTaskPipeline:
|
||||
|
||||
message = _persist_message(sqlite_session)
|
||||
|
||||
graph_runtime_state = GraphRuntimeState(
|
||||
runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=VariablePool.from_bootstrap(
|
||||
system_variables=build_system_variables(workflow_execution_id="run-id")
|
||||
),
|
||||
start_at=0.0,
|
||||
)
|
||||
|
||||
pipeline._save_message(session=sqlite_session, graph_runtime_state=graph_runtime_state)
|
||||
pipeline._save_message(session=sqlite_session, runtime_state=runtime_state)
|
||||
sqlite_session.commit()
|
||||
|
||||
assert message.status == MessageStatus.NORMAL
|
||||
@@ -697,7 +701,8 @@ class TestAdvancedChatGenerateTaskPipeline:
|
||||
|
||||
def test_handle_message_end_event_applies_output_moderation(self):
|
||||
pipeline = _make_pipeline()
|
||||
pipeline._graph_runtime_state = GraphRuntimeState(
|
||||
pipeline._graph_runtime_state = RuntimeState(
|
||||
workflow_id="test-workflow",
|
||||
variable_pool=VariablePool.from_bootstrap(
|
||||
system_variables=build_system_variables(workflow_execution_id="run-id")
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user