This commit is contained in:
QuantumGhost
2025-11-12 05:28:56 +08:00
parent 8b914d9116
commit 4f48b8a57d
23 changed files with 1108 additions and 1828 deletions

View File

@ -16,6 +16,10 @@ from extensions.otel import AppGenerateHandler, trace_span
from models.model import Account, App, AppMode, EndUser
from models.workflow import Workflow
from services.errors.app import InvokeRateLimitError, QuotaExceededError, WorkflowIdFormatError, WorkflowNotFoundError
from models.model import Account, App, AppMode, EndUser
from models.workflow import Workflow, WorkflowRun
from services.errors.app import WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
from services.workflow_service import WorkflowService
from tasks.app_generate.workflow_execute_task import ChatflowExecutionParams, chatflow_execute_task
@ -246,3 +250,19 @@ class AppGenerateService:
raise ValueError("Workflow not published")
return workflow
@classmethod
def get_response_generator(
cls,
app_model: App,
workflow_run: WorkflowRun,
):
if workflow_run.status.is_ended():
# TODO(QuantumGhost): handled the ended scenario.
return
generator = AdvancedChatAppGenerator()
return generator.convert_to_event_stream(
generator.retrieve_events(app_model.mode, workflow_run.id),
)

View File

@ -1,272 +0,0 @@
"""
Service for managing human input forms using domain models.
This service layer operates on domain models and uses repositories for persistence,
keeping the business logic clean and independent of database concerns.
"""
import logging
from typing import Any, Optional
from core.workflow.entities.human_input_form import HumanInputForm, HumanInputFormStatus, HumanInputSubmissionType
from core.workflow.repositories.human_input_form_repository import HumanInputFormRepository
from services.errors.base import BaseServiceError
logger = logging.getLogger(__name__)
class HumanInputFormNotFoundError(BaseServiceError):
"""Raised when a human input form is not found."""
def __init__(self, identifier: str):
super().__init__(f"Human input form not found: {identifier}")
self.identifier = identifier
class HumanInputFormExpiredError(BaseServiceError):
"""Raised when a human input form has expired."""
def __init__(self):
super().__init__("Human input form has expired")
class HumanInputFormAlreadySubmittedError(BaseServiceError):
"""Raised when trying to operate on an already submitted form."""
def __init__(self):
super().__init__("Human input form has already been submitted")
class InvalidFormDataError(BaseServiceError):
"""Raised when form submission data is invalid."""
def __init__(self, message: str):
super().__init__(f"Invalid form data: {message}")
self.message = message
class HumanInputFormDomainService:
"""Service for managing human input forms using domain models."""
def __init__(self, repository: HumanInputFormRepository):
"""
Initialize the service with a repository.
Args:
repository: Repository for human input form persistence
"""
self._repository = repository
def create_form(
self,
*,
form_id: str,
workflow_run_id: str,
form_definition: dict[str, Any],
rendered_content: str,
web_app_token: Optional[str] = None,
) -> HumanInputForm:
"""
Create a new human input form.
Args:
form_id: Unique identifier for the form
workflow_run_id: ID of the associated workflow run
form_definition: Form definition as a dictionary
rendered_content: Rendered HTML content of the form
web_app_token: Optional token for web app access
Returns:
Created HumanInputForm domain model
"""
form = HumanInputForm.create(
id_=form_id,
workflow_run_id=workflow_run_id,
form_definition=form_definition,
rendered_content=rendered_content,
web_app_token=web_app_token,
)
self._repository.save(form)
logger.info("Created human input form %s", form_id)
return form
def get_form_by_id(self, form_id: str) -> HumanInputForm:
"""
Get a form by its ID.
Args:
form_id: The ID of the form to retrieve
Returns:
HumanInputForm domain model
Raises:
HumanInputFormNotFoundError: If the form is not found
"""
try:
return self._repository.get_by_id(form_id)
except ValueError as e:
raise HumanInputFormNotFoundError(form_id) from e
def get_form_by_token(self, web_app_token: str) -> HumanInputForm:
"""
Get a form by its web app token.
Args:
web_app_token: The web app token to search for
Returns:
HumanInputForm domain model
Raises:
HumanInputFormNotFoundError: If the form is not found
"""
try:
return self._repository.get_by_web_app_token(web_app_token)
except ValueError as e:
raise HumanInputFormNotFoundError(web_app_token) from e
def get_form_definition(
self,
identifier: str,
is_token: bool = False,
include_site_info: bool = False,
app_id: Optional[str] = None,
) -> dict[str, Any]:
"""
Get form definition for display.
Args:
identifier: Form ID or web app token
is_token: True if identifier is a web app token, False if it's a form ID
include_site_info: Whether to include site information in the response
app_id: App ID for site information (if include_site_info is True)
Returns:
Form definition dictionary for display
Raises:
HumanInputFormNotFoundError: If the form is not found
HumanInputFormExpiredError: If the form has expired
HumanInputFormAlreadySubmittedError: If the form has already been submitted
"""
if is_token:
form = self.get_form_by_token(identifier)
else:
form = self.get_form_by_id(identifier)
try:
form_definition = form.get_form_definition_for_display(include_site_info=include_site_info)
except ValueError as e:
if "expired" in str(e).lower():
raise HumanInputFormExpiredError() from e
elif "submitted" in str(e).lower():
raise HumanInputFormAlreadySubmittedError() from e
else:
raise InvalidFormDataError(str(e)) from e
# Add site info if requested and app_id is provided
if include_site_info and app_id and "site" in form_definition:
form_definition["site"]["app_id"] = app_id
return form_definition
def submit_form(
self,
identifier: str,
form_data: dict[str, Any],
action: str,
is_token: bool = False,
submission_type: HumanInputSubmissionType = HumanInputSubmissionType.web_form,
submission_user_id: Optional[str] = None,
submission_end_user_id: Optional[str] = None,
) -> HumanInputForm:
"""
Submit a form.
Args:
identifier: Form ID or web app token
form_data: The submitted form data
action: The action taken by the user
is_token: True if identifier is a web app token, False if it's a form ID
submission_type: Type of submission (web_form, web_app, email)
submission_user_id: ID of the user who submitted (for console submissions)
submission_end_user_id: ID of the end user who submitted (for webapp submissions)
Returns:
Updated HumanInputForm domain model
Raises:
HumanInputFormNotFoundError: If the form is not found
HumanInputFormExpiredError: If the form has expired
HumanInputFormAlreadySubmittedError: If the form has already been submitted
InvalidFormDataError: If the submission data is invalid
"""
if is_token:
form = self.get_form_by_token(identifier)
else:
form = self.get_form_by_id(identifier)
if form.is_expired:
raise HumanInputFormExpiredError()
if form.is_submitted:
raise HumanInputFormAlreadySubmittedError()
try:
form.submit(
data=form_data,
action=action,
submission_type=submission_type,
submission_user_id=submission_user_id,
submission_end_user_id=submission_end_user_id,
)
except ValueError as e:
raise InvalidFormDataError(str(e)) from e
self._repository.save(form)
logger.info("Form %s submitted with action %s", form.id_, action)
return form
def cleanup_expired_forms(self, expiry_hours: int = 48) -> int:
"""
Clean up expired forms.
Args:
expiry_hours: Number of hours after which forms should be expired
Returns:
Number of forms cleaned up
"""
count = self._repository.mark_expired_forms(expiry_hours)
logger.info("Cleaned up %d expired forms", count)
return count
def get_pending_forms_for_workflow_run(self, workflow_run_id: str) -> list[HumanInputForm]:
"""
Get all pending human input forms for a workflow run.
Args:
workflow_run_id: The workflow run ID to filter by
Returns:
List of pending HumanInputForm domain models
"""
return self._repository.get_pending_forms_for_workflow_run(workflow_run_id)
def form_exists(self, identifier: str, is_token: bool = False) -> bool:
"""
Check if a form exists.
Args:
identifier: Form ID or web app token
is_token: True if identifier is a web app token, False if it's a form ID
Returns:
True if the form exists, False otherwise
"""
if is_token:
return self._repository.exists_by_web_app_token(identifier)
else:
return self._repository.exists_by_id(identifier)

View File

@ -1,352 +0,0 @@
"""
Service for managing human input forms.
This service maintains backward compatibility while internally using domain models
and repositories for better architecture.
"""
import json
import logging
from datetime import datetime, timedelta
from typing import Any, Optional
from sqlalchemy import and_, select
from sqlalchemy.orm import Session
from core.repositories.factory import DifyCoreRepositoryFactory
from core.workflow.entities.human_input_form import HumanInputForm as DomainHumanInputForm, HumanInputSubmissionType
from core.workflow.repositories.human_input_form_repository import HumanInputFormRepository
from models.human_input import (
HumanInputForm,
HumanInputFormStatus,
HumanInputSubmissionType as DBHumanInputSubmissionType,
)
from services.errors.base import BaseServiceError
from services.human_input_form_domain_service import (
HumanInputFormDomainService,
HumanInputFormNotFoundError as DomainNotFoundError,
HumanInputFormExpiredError as DomainExpiredError,
HumanInputFormAlreadySubmittedError as DomainAlreadySubmittedError,
InvalidFormDataError as DomainInvalidFormDataError,
)
logger = logging.getLogger(__name__)
class HumanInputFormNotFoundError(BaseServiceError):
"""Raised when a human input form is not found."""
def __init__(self, identifier: str):
super().__init__(f"Human input form not found: {identifier}")
self.identifier = identifier
class HumanInputFormExpiredError(BaseServiceError):
"""Raised when a human input form has expired."""
def __init__(self):
super().__init__("Human input form has expired")
class HumanInputFormAlreadySubmittedError(BaseServiceError):
"""Raised when trying to operate on an already submitted form."""
def __init__(self):
super().__init__("Human input form has already been submitted")
class InvalidFormDataError(BaseServiceError):
"""Raised when form submission data is invalid."""
def __init__(self, message: str):
super().__init__(f"Invalid form data: {message}")
self.message = message
class HumanInputFormService:
"""Service for managing human input forms using domain models internally."""
def __init__(self, session: Session):
"""
Initialize the service with a database session.
Args:
session: SQLAlchemy session
"""
self._session = session
# For backward compatibility, we need user and app_id context
# These would typically be available from the request context
# For now, we'll extract them from the session or use defaults
self._user = None # This should be set from request context
self._app_id = None # This should be set from request context
self._domain_service = None
def _get_domain_service(self) -> HumanInputFormDomainService:
"""
Get the domain service instance.
Note: This requires user and app_id context to be properly set.
In a real implementation, these would be extracted from the request context.
"""
if self._domain_service is None:
if not self._user:
# For backward compatibility, we need to handle this case
# In practice, the user should be available from the request context
raise ValueError("User context is required for domain operations")
repository = DifyCoreRepositoryFactory.create_human_input_form_repository(
session_factory=self._session,
user=self._user,
app_id=self._app_id or "",
)
self._domain_service = HumanInputFormDomainService(repository)
return self._domain_service
def _domain_to_db_model(self, domain_form: DomainHumanInputForm) -> HumanInputForm:
"""Convert domain model to database model for backward compatibility."""
# Find existing DB model or create new one
db_model = self._session.get(HumanInputForm, domain_form.id_)
if db_model is None:
db_model = HumanInputForm()
db_model.id = domain_form.id_
# Set tenant_id and app_id from context
if hasattr(self._user, "current_tenant_id"):
db_model.tenant_id = self._user.current_tenant_id
elif hasattr(self._user, "tenant_id"):
db_model.tenant_id = self._user.tenant_id
if self._app_id:
db_model.app_id = self._app_id
# Update fields
db_model.workflow_run_id = domain_form.workflow_run_id
db_model.form_definition = json.dumps(domain_form.form_definition)
db_model.rendered_content = domain_form.rendered_content
db_model.status = HumanInputFormStatus(domain_form.status.value)
db_model.web_app_token = domain_form.web_app_token
db_model.created_at = domain_form.created_at
# Handle submission data
if domain_form.submission:
db_model.submitted_data = json.dumps(domain_form.submission.data)
db_model.submitted_at = domain_form.submission.submitted_at
db_model.submission_type = DBHumanInputSubmissionType(domain_form.submission.submission_type.value)
db_model.submission_user_id = domain_form.submission.submission_user_id
db_model.submission_end_user_id = domain_form.submission.submission_end_user_id
# Note: submitter_email is not in the current DB model schema
return db_model
def set_context(self, user, app_id: Optional[str] = None) -> None:
"""
Set user and app context for the service.
Args:
user: User object (Account or EndUser)
app_id: Application ID
"""
self._user = user
self._app_id = app_id
self._domain_service = None # Reset to force recreation with new context
def create_form(
self,
*,
form_id: str,
workflow_run_id: str,
tenant_id: str,
app_id: str,
form_definition: str,
rendered_content: str,
web_app_token: Optional[str] = None,
) -> HumanInputForm:
"""Create a new human input form."""
# Set context for this operation
self._app_id = app_id
try:
domain_service = self._get_domain_service()
domain_form = domain_service.create_form(
form_id=form_id,
workflow_run_id=workflow_run_id,
form_definition=json.loads(form_definition),
rendered_content=rendered_content,
web_app_token=web_app_token,
)
# Convert back to DB model for backward compatibility
db_model = self._domain_to_db_model(domain_form)
self._session.add(db_model)
self._session.commit()
logger.info("Created human input form %s", form_id)
return db_model
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
# Convert domain exceptions to service exceptions
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def get_form_by_id(self, form_id: str) -> HumanInputForm:
"""Get a form by its ID."""
try:
domain_service = self._get_domain_service()
domain_form = domain_service.get_form_by_id(form_id)
return self._domain_to_db_model(domain_form)
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def get_form_by_token(self, web_app_token: str) -> HumanInputForm:
"""Get a form by its web app token."""
try:
domain_service = self._get_domain_service()
domain_form = domain_service.get_form_by_token(web_app_token)
return self._domain_to_db_model(domain_form)
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def get_form_definition(
self,
identifier: str,
is_token: bool = False,
include_site_info: bool = False,
) -> dict[str, Any]:
"""
Get form definition for display.
Args:
identifier: Form ID or web app token
is_token: True if identifier is a web app token, False if it's a form ID
include_site_info: Whether to include site information in the response
"""
try:
domain_service = self._get_domain_service()
return domain_service.get_form_definition(
identifier=identifier,
is_token=is_token,
include_site_info=include_site_info,
app_id=self._app_id,
)
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def submit_form(
self,
identifier: str,
form_data: dict[str, Any],
action: str,
is_token: bool = False,
submission_type: HumanInputSubmissionType = HumanInputSubmissionType.web_form,
submission_user_id: Optional[str] = None,
submission_end_user_id: Optional[str] = None,
) -> HumanInputForm:
"""
Submit a form.
Args:
identifier: Form ID or web app token
form_data: The submitted form data
action: The action taken by the user
is_token: True if identifier is a web app token, False if it's a form ID
submission_type: Type of submission (web_form, web_app, email)
submission_user_id: ID of the user who submitted (for console submissions)
submission_end_user_id: ID of the end user who submitted (for webapp submissions)
"""
try:
domain_service = self._get_domain_service()
domain_form = domain_service.submit_form(
identifier=identifier,
form_data=form_data,
action=action,
is_token=is_token,
submission_type=submission_type,
submission_user_id=submission_user_id,
submission_end_user_id=submission_end_user_id,
)
# Convert back to DB model for backward compatibility
db_model = self._domain_to_db_model(domain_form)
self._session.merge(db_model)
self._session.commit()
return db_model
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def _validate_submission(self, form: HumanInputForm, form_data: dict[str, Any], action: str) -> None:
"""Validate form submission data."""
form_definition = json.loads(form.form_definition)
# Check that the action is valid
valid_actions = {act.get("id") for act in form_definition.get("user_actions", [])}
if action not in valid_actions:
raise InvalidFormDataError(f"Invalid action: {action}")
# Note: We don't validate required inputs here as the original implementation
# allows extra inputs and doesn't strictly enforce all inputs to be present
def cleanup_expired_forms(self) -> int:
"""Clean up expired forms. Returns the number of forms cleaned up."""
try:
domain_service = self._get_domain_service()
return domain_service.cleanup_expired_forms()
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e
def get_pending_forms_for_workflow_run(self, workflow_run_id: str) -> list[HumanInputForm]:
"""Get all pending human input forms for a workflow run."""
try:
domain_service = self._get_domain_service()
domain_forms = domain_service.get_pending_forms_for_workflow_run(workflow_run_id)
return [self._domain_to_db_model(domain_form) for domain_form in domain_forms]
except (DomainNotFoundError, DomainExpiredError, DomainAlreadySubmittedError, DomainInvalidFormDataError) as e:
if isinstance(e, DomainNotFoundError):
raise HumanInputFormNotFoundError(e.identifier) from e
elif isinstance(e, DomainExpiredError):
raise HumanInputFormExpiredError() from e
elif isinstance(e, DomainAlreadySubmittedError):
raise HumanInputFormAlreadySubmittedError() from e
elif isinstance(e, DomainInvalidFormDataError):
raise InvalidFormDataError(e.message) from e

View File

@ -0,0 +1,63 @@
import abc
from collections.abc import Mapping
from typing import Any
from sqlalchemy import Engine
from sqlalchemy.orm import sessionmaker
from core.workflow.nodes.human_input.entities import FormDefinition
from libs.exception import BaseHTTPException
from models.human_input import RecipientType
class Form(abc.ABC):
@abc.abstractmethod
def get_definition(self) -> FormDefinition:
pass
@abc.abstractmethod
def submitted(self) -> bool:
pass
class HumanInputError(Exception):
pass
class FormSubmittedError(HumanInputError, BaseHTTPException):
error_code = "human_input_form_submitted"
description = "This form has already been submitted by another user, form_id={form_id}"
code = 412
def __init__(self, form_id: str):
description = self.description.format(form_id=form_id)
super().__init__(description=description)
class FormNotFoundError(HumanInputError, BaseException):
error_code = "human_input_form_not_found"
code = 404
class HumanInputService:
def __init__(
self,
session_factory: sessionmaker | Engine,
):
if isinstance(session_factory, Engine):
session_factory = sessionmaker(bind=session_factory)
self._session_factory = session_factory
def get_form_definition_by_token(self, recipient_type: RecipientType, form_token: str) -> Form:
pass
def get_form_definition_by_id(self, form_id: str) -> Form | None:
pass
def submit_form_by_id(self, form_id: str, selected_action_id: str, form_data: Mapping[str, Any]):
pass
def submit_form_by_token(
self, recipient_type: RecipientType, form_token: str, selected_action_id: str, form_data: Mapping[str, Any]
):
pass