import logging from collections.abc import Callable, Generator from contextlib import contextmanager from typing import TYPE_CHECKING if TYPE_CHECKING: from services.dify_builder.native_testing.host_execution import NativeHostLaunch from sqlalchemy.orm import Session, sessionmaker from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.common.graph_runtime_state_support import GraphRuntimeStateSupport from core.app.apps.draft_variable_saver import DraftVariableSaverFactory, NoopDraftVariableSaver from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity, get_credit_usage_app_type from core.app.entities.queue_entities import ( AppQueueEvent, MessageQueueMessage, QueueAgentLogEvent, QueueErrorEvent, QueueHumanInputFormFilledEvent, QueueHumanInputFormTimeoutEvent, QueueIterationCompletedEvent, QueueIterationNextEvent, QueueIterationStartEvent, QueueLoopCompletedEvent, QueueLoopNextEvent, QueueLoopStartEvent, QueueNodeExceptionEvent, QueueNodeFailedEvent, QueueNodeRetryEvent, QueueNodeStartedEvent, QueueNodeSucceededEvent, QueuePingEvent, QueueReasoningChunkEvent, QueueStopEvent, QueueTextChunkEvent, QueueWorkflowFailedEvent, QueueWorkflowPartialSuccessEvent, QueueWorkflowPausedEvent, QueueWorkflowStartedEvent, QueueWorkflowSucceededEvent, WorkflowQueueMessage, ) from core.app.entities.task_entities import ( ErrorStreamResponse, HumanInputRequiredPauseReasonPayload, HumanInputRequiredResponse, MessageAudioEndStreamResponse, MessageAudioStreamResponse, PingStreamResponse, ReasoningChunkStreamResponse, StreamResponse, TextChunkStreamResponse, WorkflowAppBlockingResponse, WorkflowAppPausedBlockingResponse, WorkflowAppStreamResponse, WorkflowFinishStreamResponse, WorkflowPauseStreamResponse, WorkflowStartStreamResponse, ) from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.base.tts import AppGeneratorTTSPublisher from core.ops.ops_trace_manager import TraceQueueManager 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 RuntimeState from models import Account from models.enums import CreatorUserRole from models.model import EndUser from models.workflow import Workflow, WorkflowAppLog, WorkflowAppLogCreatedFrom from services.dify_builder.native_testing.observation_adapters import NativeObservationGuard from services.dify_builder.native_testing.observation_stream import ( project_native_queue_event, require_native_observation, ) from services.workflow_runtime.response_converter import WorkflowResponseConverter from services.workflow_runtime.workflow_queue import WorkflowAppQueueManager logger = logging.getLogger(__name__) class WorkflowAppGenerateTaskPipeline(GraphRuntimeStateSupport): """ WorkflowAppGenerateTaskPipeline is a class that generate stream output and state management for Application. """ def __init__( self, application_generate_entity: WorkflowAppGenerateEntity, workflow: Workflow, queue_manager: AppQueueManager, user: Account | EndUser, stream: bool, draft_var_saver_factory: DraftVariableSaverFactory, *, native_observation: NativeObservationGuard | None = None, native_host: "NativeHostLaunch | None" = None, ): require_native_observation(guard=native_observation, entity=application_generate_entity) if native_host is not None: from services.dify_builder.native_testing.runtime import validate_projection_host validate_projection_host( host=native_host, guard=native_observation, entity=application_generate_entity, queue=queue_manager ) self._native_host = native_host self._native_queue = queue_manager self._native_observation = native_observation if native_observation is not None: with native_observation.persistence_scope(): if ( not isinstance(queue_manager, WorkflowAppQueueManager) or queue_manager.native_observation is not native_observation or not isinstance(user, Account) or application_generate_entity.files or application_generate_entity.trace_manager is not None or application_generate_entity.app_config.sensitive_word_avoidance ): native_observation.fail() self._base_task_pipeline = BasedGenerateTaskPipeline( application_generate_entity=application_generate_entity, queue_manager=queue_manager, stream=stream, ) if isinstance(user, EndUser): self._user_id = user.id user_session_id = user.session_id self._created_by_role = CreatorUserRole.END_USER else: self._user_id = user.id user_session_id = user.id self._created_by_role = CreatorUserRole.ACCOUNT self._application_generate_entity = application_generate_entity self._workflow_features_dict = workflow.features_dict self._workflow_execution_id = "" self._invoke_from = queue_manager.invoke_from self._draft_var_saver_factory = draft_var_saver_factory self._workflow = workflow self._workflow_system_variables = build_system_variables( files=application_generate_entity.files, user_id=user_session_id, app_id=application_generate_entity.app_config.app_id, workflow_id=workflow.id, workflow_execution_id=application_generate_entity.workflow_execution_id, ) self._workflow_response_converter = WorkflowResponseConverter( application_generate_entity=application_generate_entity, user=user, system_variables=self._workflow_system_variables, native_observation=native_observation, native_host=native_host, native_queue=queue_manager, ) self._graph_runtime_state: RuntimeState | None = self._base_task_pipeline.queue_manager.graph_runtime_state def process( self, ) -> WorkflowAppBlockingResponse | WorkflowAppPausedBlockingResponse | Generator[WorkflowAppStreamResponse]: """ Process generate task pipeline. :return: """ generator = self._wrapper_process_stream_response(trace_manager=self._application_generate_entity.trace_manager) if self._base_task_pipeline.stream: return self._to_stream_response(generator) else: return self._to_blocking_response(generator) def _to_blocking_response( self, generator: Generator[StreamResponse] ) -> WorkflowAppBlockingResponse | WorkflowAppPausedBlockingResponse: """ To blocking response. :return: """ human_input_responses: list[HumanInputRequiredResponse] = [] for stream_response in generator: match stream_response: case ErrorStreamResponse(): raise stream_response.err case HumanInputRequiredResponse(): human_input_responses.append(stream_response) case WorkflowPauseStreamResponse(): return WorkflowAppPausedBlockingResponse( task_id=self._application_generate_entity.task_id, workflow_run_id=stream_response.data.workflow_run_id, data=WorkflowAppPausedBlockingResponse.Data( id=stream_response.data.workflow_run_id, workflow_id=self._workflow.id, status=stream_response.data.status, outputs=stream_response.data.outputs or {}, error=None, elapsed_time=stream_response.data.elapsed_time, total_tokens=stream_response.data.total_tokens, total_steps=stream_response.data.total_steps, created_at=stream_response.data.created_at, finished_at=None, paused_nodes=stream_response.data.paused_nodes, reasons=stream_response.data.reasons, ), ) case WorkflowFinishStreamResponse(): return WorkflowAppBlockingResponse( task_id=self._application_generate_entity.task_id, workflow_run_id=stream_response.data.id, data=WorkflowAppBlockingResponse.Data( id=stream_response.data.id, workflow_id=stream_response.data.workflow_id, status=stream_response.data.status, outputs=stream_response.data.outputs, error=stream_response.data.error, elapsed_time=stream_response.data.elapsed_time, total_tokens=stream_response.data.total_tokens, total_steps=stream_response.data.total_steps, created_at=int(stream_response.data.created_at), finished_at=int(stream_response.data.finished_at) if stream_response.data.finished_at else None, ), ) case _: continue if human_input_responses: return self._build_paused_blocking_response_from_human_input(human_input_responses) raise ValueError("queue listening stopped unexpectedly.") def _build_paused_blocking_response_from_human_input( self, human_input_responses: list[HumanInputRequiredResponse] ) -> WorkflowAppPausedBlockingResponse: runtime_state = self._resolve_graph_runtime_state() paused_nodes = list(dict.fromkeys(response.data.node_id for response in human_input_responses)) created_at = int(runtime_state.start_at) reasons = [ HumanInputRequiredPauseReasonPayload.from_response_data(response.data).model_dump(mode="json") for response in human_input_responses ] return WorkflowAppPausedBlockingResponse( task_id=self._application_generate_entity.task_id, workflow_run_id=human_input_responses[-1].workflow_run_id, data=WorkflowAppPausedBlockingResponse.Data( id=human_input_responses[-1].workflow_run_id, workflow_id=self._workflow.id, status=WorkflowExecutionStatus.PAUSED, outputs={}, error=None, elapsed_time=self._application_generate_entity.workflow_execution_timing.finish(), total_tokens=runtime_state.total_tokens, total_steps=runtime_state.node_run_steps, created_at=created_at, finished_at=None, paused_nodes=paused_nodes, reasons=reasons, ), ) def _to_stream_response(self, generator: Generator[StreamResponse]) -> Generator[WorkflowAppStreamResponse]: """ To stream response. :return: """ workflow_run_id = None for stream_response in generator: if isinstance(stream_response, WorkflowStartStreamResponse): workflow_run_id = stream_response.workflow_run_id yield WorkflowAppStreamResponse(workflow_run_id=workflow_run_id, stream_response=stream_response) def _listen_audio_msg(self, publisher: AppGeneratorTTSPublisher | None, task_id: str): if not publisher: return None audio_msg = publisher.check_and_get_audio() if audio_msg is None: return None if audio_msg.status == "responding": return MessageAudioStreamResponse(audio=audio_msg.audio, audio_type=audio_msg.audio_type, task_id=task_id) if audio_msg.status in {"finish", "error"}: return None raise RuntimeError(f"TTS publisher returned an unknown status: {audio_msg.status}") def _wrapper_process_stream_response( self, trace_manager: TraceQueueManager | None = None ) -> Generator[StreamResponse]: if self._native_observation is not None: self._native_observation.require_healthy() if trace_manager is not None or self._workflow_features_dict.get("text_to_speech", {}).get("enabled"): self._native_observation.fail() tts_publisher = None task_id = self._application_generate_entity.task_id tenant_id = self._application_generate_entity.app_config.tenant_id features_dict = self._workflow_features_dict if ( self._base_task_pipeline.stream and features_dict.get("text_to_speech") and features_dict["text_to_speech"].get("enabled") and features_dict["text_to_speech"].get("autoPlay") == "enabled" ): tts_publisher = AppGeneratorTTSPublisher( tenant_id, features_dict["text_to_speech"].get("voice"), features_dict["text_to_speech"].get("language"), get_credit_usage_app_type(self._application_generate_entity.app_config.app_mode), ) try: for response in self._process_stream_response(tts_publisher=tts_publisher, trace_manager=trace_manager): while audio_response := self._listen_audio_msg(publisher=tts_publisher, task_id=task_id): yield audio_response if tts_publisher and isinstance(response, ErrorStreamResponse): tts_publisher.cancel() yield MessageAudioEndStreamResponse(audio="", task_id=task_id) yield response return yield response if tts_publisher is None: return tts_publisher.publish(None) while True: audio_trunk = tts_publisher.check_and_get_audio(block=True) assert audio_trunk is not None if audio_trunk.status == "responding": yield MessageAudioStreamResponse( audio=audio_trunk.audio, audio_type=audio_trunk.audio_type, task_id=task_id ) continue if audio_trunk.status not in {"finish", "error"}: raise RuntimeError(f"TTS publisher returned an unknown status: {audio_trunk.status}") yield MessageAudioEndStreamResponse(audio="", task_id=task_id) if audio_trunk.status == "error": if audio_trunk.error is None: raise RuntimeError("TTS publisher returned an error terminal without an exception") yield ErrorStreamResponse(err=audio_trunk.error, task_id=task_id) return finally: if tts_publisher: tts_publisher.cancel() @contextmanager def _database_session(self): """Context manager for database sessions.""" with sessionmaker(bind=db.engine, expire_on_commit=False).begin() as session: yield session def _ensure_workflow_initialized(self): """Fluent validation for workflow state.""" if not self._workflow_execution_id: raise ValueError("workflow run not initialized.") def _handle_ping_event(self, event: QueuePingEvent, **kwargs) -> Generator[PingStreamResponse]: """Handle ping events.""" yield self._base_task_pipeline.ping_stream_response() def _handle_error_event(self, event: QueueErrorEvent, **kwargs) -> Generator[ErrorStreamResponse]: """Handle error events.""" if self._native_observation is not None: event = project_native_queue_event( guard=self._native_observation, event=event, native_host=self._native_host, queue=self._native_queue ) err = self._base_task_pipeline.handle_error(event=event) yield self._base_task_pipeline.error_to_stream_response(err) def _handle_workflow_started_event(self, event: QueueWorkflowStartedEvent, **kwargs) -> Generator[StreamResponse]: """Handle workflow started events.""" runtime_state = self._resolve_graph_runtime_state() run_id = self._extract_workflow_run_id(runtime_state) self._workflow_execution_id = run_id if event.reason == WorkflowStartReason.INITIAL and self._native_observation is None: with self._database_session() as session: self._save_workflow_app_log(session=session, workflow_run_id=self._workflow_execution_id) start_resp = self._workflow_response_converter.workflow_start_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_run_id=run_id, workflow_id=self._workflow.id, reason=event.reason, node_execution_snapshots=event.node_execution_snapshots, ) yield start_resp def _handle_node_retry_event(self, event: QueueNodeRetryEvent, **kwargs) -> Generator[StreamResponse]: """Handle node retry events.""" self._ensure_workflow_initialized() response = self._workflow_response_converter.workflow_node_retry_to_stream_response( event=event, task_id=self._application_generate_entity.task_id, ) if response: yield response def _handle_node_started_event(self, event: QueueNodeStartedEvent, **kwargs) -> Generator[StreamResponse]: """Handle node started events.""" self._ensure_workflow_initialized() node_start_response = self._workflow_response_converter.workflow_node_start_to_stream_response( event=event, task_id=self._application_generate_entity.task_id, ) if node_start_response: yield node_start_response def _handle_node_succeeded_event(self, event: QueueNodeSucceededEvent, **kwargs) -> Generator[StreamResponse]: """Handle node succeeded events.""" node_success_response = self._workflow_response_converter.workflow_node_finish_to_stream_response( event=event, task_id=self._application_generate_entity.task_id, ) self._save_output_for_event(event, event.node_execution_id) if node_success_response: yield node_success_response def _handle_node_failed_events( self, event: QueueNodeFailedEvent | QueueNodeExceptionEvent, **kwargs, ) -> Generator[StreamResponse]: """Handle various node failure events.""" node_failed_response = self._workflow_response_converter.workflow_node_finish_to_stream_response( event=event, task_id=self._application_generate_entity.task_id, ) if isinstance(event, QueueNodeExceptionEvent): self._save_output_for_event(event, event.node_execution_id) if node_failed_response: yield node_failed_response def _handle_iteration_start_event(self, event: QueueIterationStartEvent, **kwargs) -> Generator[StreamResponse]: """Handle iteration start events.""" self._ensure_workflow_initialized() iter_start_resp = self._workflow_response_converter.workflow_iteration_start_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield iter_start_resp def _handle_iteration_next_event(self, event: QueueIterationNextEvent, **kwargs) -> Generator[StreamResponse]: """Handle iteration next events.""" self._ensure_workflow_initialized() iter_next_resp = self._workflow_response_converter.workflow_iteration_next_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield iter_next_resp def _handle_iteration_completed_event( self, event: QueueIterationCompletedEvent, **kwargs ) -> Generator[StreamResponse]: """Handle iteration completed events.""" self._ensure_workflow_initialized() iter_finish_resp = self._workflow_response_converter.workflow_iteration_completed_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield iter_finish_resp def _handle_loop_start_event(self, event: QueueLoopStartEvent, **kwargs) -> Generator[StreamResponse]: """Handle loop start events.""" self._ensure_workflow_initialized() loop_start_resp = self._workflow_response_converter.workflow_loop_start_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield loop_start_resp def _handle_loop_next_event(self, event: QueueLoopNextEvent, **kwargs) -> Generator[StreamResponse]: """Handle loop next events.""" self._ensure_workflow_initialized() loop_next_resp = self._workflow_response_converter.workflow_loop_next_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield loop_next_resp def _handle_loop_completed_event(self, event: QueueLoopCompletedEvent, **kwargs) -> Generator[StreamResponse]: """Handle loop completed events.""" self._ensure_workflow_initialized() loop_finish_resp = self._workflow_response_converter.workflow_loop_completed_to_stream_response( task_id=self._application_generate_entity.task_id, workflow_execution_id=self._workflow_execution_id, event=event, ) yield loop_finish_resp def _handle_workflow_succeeded_event( self, event: QueueWorkflowSucceededEvent, *, trace_manager: TraceQueueManager | None = None, **kwargs, ) -> Generator[StreamResponse]: """Handle workflow succeeded events.""" _ = trace_manager self._ensure_workflow_initialized() validated_state = self._ensure_graph_runtime_initialized() 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.SUCCEEDED, graph_runtime_state=validated_state, ) yield workflow_finish_resp def _handle_workflow_partial_success_event( self, event: QueueWorkflowPartialSuccessEvent, *, trace_manager: TraceQueueManager | None = None, **kwargs, ) -> Generator[StreamResponse]: """Handle workflow partial success events.""" _ = trace_manager self._ensure_workflow_initialized() validated_state = self._ensure_graph_runtime_initialized() 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.PARTIAL_SUCCEEDED, graph_runtime_state=validated_state, exceptions_count=event.exceptions_count, ) yield workflow_finish_resp def _handle_workflow_paused_event( self, event: QueueWorkflowPausedEvent, **kwargs, ) -> Generator[StreamResponse]: """Handle workflow paused events.""" self._ensure_workflow_initialized() validated_state = self._ensure_graph_runtime_initialized() 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, ) yield from responses def _handle_workflow_failed_and_stop_events( self, event: QueueWorkflowFailedEvent | QueueStopEvent, *, trace_manager: TraceQueueManager | None = None, **kwargs, ) -> Generator[StreamResponse]: """Handle workflow failed and stop events.""" _ = trace_manager self._ensure_workflow_initialized() validated_state = self._ensure_graph_runtime_initialized() if isinstance(event, QueueWorkflowFailedEvent): status = WorkflowExecutionStatus.FAILED error = event.error exceptions_count = event.exceptions_count else: status = WorkflowExecutionStatus.STOPPED error = event.get_stop_reason() exceptions_count = 0 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=status, graph_runtime_state=validated_state, error=error, exceptions_count=exceptions_count, ) yield workflow_finish_resp def _handle_text_chunk_event( self, event: QueueTextChunkEvent, *, tts_publisher: AppGeneratorTTSPublisher | None = None, queue_message: WorkflowQueueMessage | MessageQueueMessage | None = None, **kwargs, ) -> Generator[StreamResponse]: """Handle text chunk events.""" if self._native_observation is not None: if tts_publisher is not None: self._native_observation.fail() event = project_native_queue_event( guard=self._native_observation, event=event, native_host=self._native_host, queue=self._native_queue ) delta_text = event.text if delta_text is None: return # only publish tts message at text chunk streaming if tts_publisher and queue_message: tts_publisher.publish(queue_message) yield self._text_chunk_to_stream_response(delta_text, from_variable_selector=event.from_variable_selector) def _handle_reasoning_chunk_event(self, event: QueueReasoningChunkEvent, **kwargs) -> Generator[StreamResponse]: """Handle reasoning chunk events.""" # is_final with empty reasoning is still forwarded as the "thinking finished" signal if not event.reasoning and not event.is_final: return yield ReasoningChunkStreamResponse( task_id=self._application_generate_entity.task_id, data=ReasoningChunkStreamResponse.Data( reasoning=event.reasoning, node_id=event.from_node_id, is_final=event.is_final, ), ) def _handle_agent_log_event(self, event: QueueAgentLogEvent, **kwargs) -> Generator[StreamResponse]: """Handle agent log events.""" yield self._workflow_response_converter.handle_agent_log( task_id=self._application_generate_entity.task_id, event=event ) def _handle_human_input_form_filled_event( self, event: QueueHumanInputFormFilledEvent, **kwargs ) -> Generator[StreamResponse]: """Handle human input form filled events.""" yield self._workflow_response_converter.human_input_form_filled_to_stream_response( event=event, task_id=self._application_generate_entity.task_id ) def _handle_human_input_form_timeout_event( self, event: QueueHumanInputFormTimeoutEvent, **kwargs ) -> Generator[StreamResponse]: """Handle human input form timeout events.""" yield self._workflow_response_converter.human_input_form_timeout_to_stream_response( event=event, task_id=self._application_generate_entity.task_id ) def _get_event_handlers(self) -> dict[type, Callable]: """Get mapping of event types to their handlers using fluent pattern.""" return { # Basic events QueuePingEvent: self._handle_ping_event, QueueErrorEvent: self._handle_error_event, QueueTextChunkEvent: self._handle_text_chunk_event, QueueReasoningChunkEvent: self._handle_reasoning_chunk_event, # Workflow events QueueWorkflowStartedEvent: self._handle_workflow_started_event, QueueWorkflowSucceededEvent: self._handle_workflow_succeeded_event, QueueWorkflowPartialSuccessEvent: self._handle_workflow_partial_success_event, QueueWorkflowPausedEvent: self._handle_workflow_paused_event, # Node events QueueNodeRetryEvent: self._handle_node_retry_event, QueueNodeStartedEvent: self._handle_node_started_event, QueueNodeSucceededEvent: self._handle_node_succeeded_event, # Iteration events QueueIterationStartEvent: self._handle_iteration_start_event, QueueIterationNextEvent: self._handle_iteration_next_event, QueueIterationCompletedEvent: self._handle_iteration_completed_event, # Loop events QueueLoopStartEvent: self._handle_loop_start_event, QueueLoopNextEvent: self._handle_loop_next_event, QueueLoopCompletedEvent: self._handle_loop_completed_event, # Agent events QueueAgentLogEvent: self._handle_agent_log_event, QueueHumanInputFormFilledEvent: self._handle_human_input_form_filled_event, QueueHumanInputFormTimeoutEvent: self._handle_human_input_form_timeout_event, } def _dispatch_event( self, event: AppQueueEvent, *, tts_publisher: AppGeneratorTTSPublisher | None = None, trace_manager: TraceQueueManager | None = None, queue_message: WorkflowQueueMessage | MessageQueueMessage | None = None, ) -> Generator[StreamResponse]: """Dispatch events using elegant pattern matching.""" if self._native_observation is not None: if tts_publisher is not None or trace_manager is not None: self._native_observation.fail() event = project_native_queue_event( guard=self._native_observation, event=event, native_host=self._native_host, queue=self._native_queue ) handlers = self._get_event_handlers() event_type = type(event) # Direct handler lookup if handler := handlers.get(event_type): yield from handler( event, tts_publisher=tts_publisher, trace_manager=trace_manager, queue_message=queue_message, ) return # Handle node failure events with isinstance check if isinstance( event, ( QueueNodeFailedEvent, QueueNodeExceptionEvent, ), ): yield from self._handle_node_failed_events( event, tts_publisher=tts_publisher, trace_manager=trace_manager, queue_message=queue_message, ) return # Handle workflow failed and stop events with isinstance check if isinstance(event, (QueueWorkflowFailedEvent, QueueStopEvent)): yield from self._handle_workflow_failed_and_stop_events( event, tts_publisher=tts_publisher, trace_manager=trace_manager, queue_message=queue_message, ) return # For unhandled events, we continue (original behavior) return def _process_stream_response( self, tts_publisher: AppGeneratorTTSPublisher | None = None, trace_manager: TraceQueueManager | None = None, ) -> Generator[StreamResponse]: """ Process stream response using elegant Fluent Python patterns. Maintains exact same functionality as original 44-if-statement version. """ if self._native_observation is not None and (tts_publisher is not None or trace_manager is not None): self._native_observation.fail() for queue_message in self._base_task_pipeline.queue_manager.listen(): event = queue_message.event if self._native_observation is not None: event = project_native_queue_event( guard=self._native_observation, event=event, native_host=self._native_host, queue=self._native_queue ) match event: case QueueWorkflowStartedEvent(): self._resolve_graph_runtime_state() yield from self._handle_workflow_started_event(event) case QueueTextChunkEvent(): yield from self._handle_text_chunk_event( event, tts_publisher=tts_publisher, queue_message=queue_message ) case QueueErrorEvent(): yield from self._handle_error_event(event) break case QueueWorkflowFailedEvent(): yield from self._handle_workflow_failed_and_stop_events(event) break case QueueWorkflowPausedEvent(): yield from self._handle_workflow_paused_event(event) break case QueueStopEvent(): yield from self._handle_workflow_failed_and_stop_events(event) break # Handle all other events through elegant dispatch case _: if responses := list( self._dispatch_event( event, tts_publisher=tts_publisher, trace_manager=trace_manager, queue_message=queue_message, ) ): yield from responses def _save_workflow_app_log(self, *, session: Session, workflow_run_id: str | None): invoke_from = self._application_generate_entity.invoke_from match invoke_from: case InvokeFrom.SERVICE_API: created_from = WorkflowAppLogCreatedFrom.SERVICE_API case InvokeFrom.OPENAPI: created_from = WorkflowAppLogCreatedFrom.OPENAPI case InvokeFrom.EXPLORE: created_from = WorkflowAppLogCreatedFrom.INSTALLED_APP case InvokeFrom.WEB_APP: created_from = WorkflowAppLogCreatedFrom.WEB_APP case InvokeFrom.DEBUGGER | InvokeFrom.TRIGGER | InvokeFrom.PUBLISHED_PIPELINE | InvokeFrom.VALIDATION: # not save log for debugging return if not workflow_run_id: return workflow_app_log = WorkflowAppLog( tenant_id=self._application_generate_entity.app_config.tenant_id, app_id=self._application_generate_entity.app_config.app_id, workflow_id=self._workflow.id, workflow_run_id=workflow_run_id, created_from=created_from, created_by_role=self._created_by_role, created_by=self._user_id, ) session.add(workflow_app_log) def _text_chunk_to_stream_response( self, text: str, from_variable_selector: list[str] | None = None ) -> TextChunkStreamResponse: """ Handle completed event. :param text: text :return: """ response = TextChunkStreamResponse( task_id=self._application_generate_entity.task_id, data=TextChunkStreamResponse.Data(text=text, from_variable_selector=from_variable_selector), ) return response def _save_output_for_event(self, event: QueueNodeSucceededEvent | QueueNodeExceptionEvent, node_execution_id: str): if self._native_observation is not None: self._native_observation.require_healthy() NoopDraftVariableSaver().save(None, None) return saver = self._draft_var_saver_factory( app_id=self._application_generate_entity.app_config.app_id, node_id=event.node_id, node_type=event.node_type, node_execution_id=node_execution_id, enclosing_node_id=event.in_loop_id or event.in_iteration_id, ) saver.save(event.process_data, event.outputs)