diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index 8a57ec9818a..947b252d748 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -12,8 +12,13 @@ from controllers.service_api import service_api_ns from controllers.service_api.wraps import validate_app_token from extensions.ext_database import db from extensions.ext_redis import redis_client -from fields.annotation_fields import Annotation, AnnotationList -from fields.base import ResponseModel +from fields.annotation_fields import ( + Annotation, + AnnotationJobStatusDetailResponse, + AnnotationJobStatusResponse, + AnnotationList, +) +from libs.helper import dump_response from models.model import App from services.annotation_service import ( AppAnnotationService, @@ -45,12 +50,6 @@ class AnnotationListQuery(BaseModel): keyword: str = Field(default="", description="Keyword to filter annotations by question or answer content.") -class AnnotationJobStatusResponse(ResponseModel): - job_id: str - job_status: str - error_msg: str | None = None - - ANNOTATION_REPLY_ACTION_PARAM = { "description": "Action to perform: `enable` or `disable`.", "enum": ["enable", "disable"], @@ -66,7 +65,13 @@ register_schema_models( Annotation, AnnotationList, ) -register_response_schema_models(service_api_ns, AnnotationJobStatusResponse) +register_response_schema_models( + service_api_ns, + Annotation, + AnnotationList, + AnnotationJobStatusResponse, + AnnotationJobStatusDetailResponse, +) @service_api_ns.route("/apps/annotation-reply/") @@ -112,7 +117,7 @@ class AnnotationReplyActionApi(Resource): result = AppAnnotationService.enable_app_annotation(enable_args, app_model.id) case "disable": result = AppAnnotationService.disable_app_annotation(app_model.id) - return result, 200 + return dump_response(AnnotationJobStatusResponse, result), 200 @service_api_ns.route("/apps/annotation-reply//status/") @@ -150,7 +155,7 @@ class AnnotationReplyActionStatusApi(Resource): @service_api_ns.response( 200, "Job status retrieved successfully", - service_api_ns.models[AnnotationJobStatusResponse.__name__], + service_api_ns.models[AnnotationJobStatusDetailResponse.__name__], ) @validate_app_token def get(self, app_model: App, job_id: UUID, action: str): @@ -167,7 +172,9 @@ class AnnotationReplyActionStatusApi(Resource): app_annotation_error_key = f"{action}_app_annotation_error_{job_id_str}" error_msg = redis_client.get(app_annotation_error_key).decode() - return {"job_id": job_id_str, "job_status": job_status, "error_msg": error_msg}, 200 + return AnnotationJobStatusDetailResponse( + job_id=job_id_str, job_status=job_status, error_msg=error_msg + ).model_dump(mode="json"), 200 @service_api_ns.route("/apps/annotations") @@ -203,14 +210,13 @@ class AnnotationListApi(Resource): app_model.id, query.page, query.limit, query.keyword ) annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True) - response = AnnotationList( + return AnnotationList( data=annotation_models, has_more=len(annotation_list) == query.limit, limit=query.limit, total=total, page=query.page, - ) - return response.model_dump(mode="json") + ).model_dump(mode="json") @service_api_ns.doc( summary="Create Annotation", @@ -243,8 +249,7 @@ class AnnotationListApi(Resource): payload = AnnotationCreatePayload.model_validate(service_api_ns.payload or {}) insert_args: InsertAnnotationArgs = {"question": payload.question, "answer": payload.answer} annotation = AppAnnotationService.insert_app_annotation_directly(insert_args, app_model.id) - response = Annotation.model_validate(annotation, from_attributes=True) - return response.model_dump(mode="json"), HTTPStatus.CREATED + return dump_response(Annotation, annotation), HTTPStatus.CREATED @service_api_ns.route("/apps/annotations/") @@ -285,8 +290,7 @@ class AnnotationUpdateDeleteApi(Resource): annotation = AppAnnotationService.update_app_annotation_directly( update_args, app_model.id, str(annotation_id), db.session ) - response = Annotation.model_validate(annotation, from_attributes=True) - return response.model_dump(mode="json") + return dump_response(Annotation, annotation) @service_api_ns.doc( summary="Delete Annotation", diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index 59ed4b4a4b1..1ec0a372d4d 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -25,6 +25,7 @@ from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from extensions.ext_database import db from graphon.model_runtime.errors.invoke import InvokeError +from libs.helper import dump_response from models.model import App, EndUser from services.audio_service import AudioService from services.errors.audio import ( @@ -101,7 +102,7 @@ class AudioApi(Resource): try: response = AudioService.transcript_asr(app_model=app_model, file=file, end_user=end_user.id) - return response + return dump_response(AudioTranscriptResponse, response) except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() @@ -164,6 +165,7 @@ class TextApi(Resource): 500: "Internal server error", } ) + # TTS returns provider audio bytes, so the success response is intentionally schema-less. @service_api_ns.response(200, "Text successfully converted to audio") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) def post(self, app_model: App, end_user: EndUser): @@ -177,7 +179,7 @@ class TextApi(Resource): message_id = payload.message_id text = payload.text voice = payload.voice - response = AudioService.transcript_tts( + return AudioService.transcript_tts( app_model=app_model, session=db.session, text=text, @@ -185,8 +187,6 @@ class TextApi(Resource): end_user=end_user.external_user_id, message_id=message_id, ) - - return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 1468f3d776f..873911ec50e 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -9,7 +9,7 @@ from pydantic.json_schema import SkipJsonSchema from werkzeug.exceptions import BadRequest, InternalServerError, NotFound import services -from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse +from controllers.common.fields import SimpleResultResponse from controllers.common.schema import register_response_schema_models, register_schema_models from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( @@ -154,7 +154,7 @@ class ChatRequestPayload(BaseModel): register_schema_models(service_api_ns, CompletionRequestPayload, ChatRequestPayload) -register_response_schema_models(service_api_ns, GeneratedAppResponse, SimpleResultResponse) +register_response_schema_models(service_api_ns, SimpleResultResponse) @service_api_ns.route("/completion-messages") @@ -197,11 +197,7 @@ class CompletionApi(Resource): 500: "Internal server error", } ) - @service_api_ns.response( - 200, - "Completion created successfully", - service_api_ns.models[GeneratedAppResponse.__name__], - ) + @service_api_ns.response(200, "Completion created successfully") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) def post(self, app_model: App, end_user: EndUser): """Create a completion for the given prompt. @@ -236,6 +232,7 @@ class CompletionApi(Resource): streaming=streaming, ) + # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -296,7 +293,7 @@ class CompletionStopApi(Resource): app_mode=AppMode.value_of(app_model.mode), ) - return {"result": "success"}, 200 + return SimpleResultResponse(result="success").model_dump(mode="json"), 200 @service_api_ns.route("/chat-messages") @@ -346,11 +343,7 @@ class ChatApi(Resource): 500: "Internal server error", } ) - @service_api_ns.response( - 200, - "Message sent successfully", - service_api_ns.models[GeneratedAppResponse.__name__], - ) + @service_api_ns.response(200, "Message sent successfully") @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) def post(self, app_model: App, end_user: EndUser): """Send a message in a chat conversation. @@ -379,6 +372,7 @@ class ChatApi(Resource): app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=streaming ) + # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except WorkflowNotFoundError as ex: raise NotFound(str(ex)) @@ -448,4 +442,4 @@ class ChatStopApi(Resource): app_mode=app_mode, ) - return {"result": "success"}, 200 + return SimpleResultResponse(result="success").model_dump(mode="json"), 200 diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 9b5533ea07a..73153ec72bd 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -24,7 +24,7 @@ from fields.conversation_fields import ( SimpleConversation, ) from graphon.variables.types import SegmentType -from libs.helper import UUIDStrOrEmpty, to_timestamp +from libs.helper import UUIDStrOrEmpty, dump_response, to_timestamp from models.model import App, AppMode, EndUser from services.conversation_service import ConversationService @@ -142,15 +142,13 @@ register_schema_models( ConversationRenamePayload, ConversationVariablesQuery, ConversationVariableUpdatePayload, - ConversationVariableResponse, - ConversationVariableInfiniteScrollPaginationResponse, ) register_response_schema_models( service_api_ns, - ConversationInfiniteScrollPagination, - SimpleConversation, ConversationVariableResponse, ConversationVariableInfiniteScrollPaginationResponse, + ConversationInfiniteScrollPagination, + SimpleConversation, ) @@ -166,9 +164,9 @@ class ConversationApi(Resource): 404: "`not_found` : Last conversation does not exist (invalid `last_id`).", }, ) - @service_api_ns.doc(params=query_params_from_model(ConversationListQuery)) @service_api_ns.doc("list_conversations") @service_api_ns.doc(description="List all conversations for the current user") + @service_api_ns.doc(params=query_params_from_model(ConversationListQuery)) @service_api_ns.doc( responses={ 200: "Conversations retrieved successfully", @@ -192,7 +190,7 @@ class ConversationApi(Resource): raise NotChatAppError() query_args = ConversationListQuery.model_validate(request.args.to_dict()) - last_id = str(query_args.last_id) if query_args.last_id else None + last_id = query_args.last_id or None try: with sessionmaker(db.engine).begin() as session: @@ -208,9 +206,7 @@ class ConversationApi(Resource): adapter = TypeAdapter(SimpleConversation) conversations = [adapter.validate_python(item, from_attributes=True) for item in pagination.data] return ConversationInfiniteScrollPagination( - limit=pagination.limit, - has_more=pagination.has_more, - data=conversations, + limit=pagination.limit, has_more=pagination.has_more, data=conversations ).model_dump(mode="json") except services.errors.conversation.LastConversationNotExistsError: raise NotFound("Last Conversation Not Exists.") @@ -301,11 +297,7 @@ class ConversationRenameApi(Resource): conversation = ConversationService.rename( app_model, conversation_id, end_user, payload.name, payload.auto_generate ) - return ( - TypeAdapter(SimpleConversation) - .validate_python(conversation, from_attributes=True) - .model_dump(mode="json") - ) + return dump_response(SimpleConversation, conversation) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -322,10 +314,9 @@ class ConversationVariablesApi(Resource): 404: "`not_found` : Conversation does not exist.", }, ) - @service_api_ns.doc(params=query_params_from_model(ConversationVariablesQuery)) @service_api_ns.doc("list_conversation_variables") @service_api_ns.doc(description="List all variables for a conversation") - @service_api_ns.doc(params={"c_id": "Conversation ID."}) + @service_api_ns.doc(params={"c_id": "Conversation ID.", **query_params_from_model(ConversationVariablesQuery)}) @service_api_ns.doc( responses={ 200: "Variables retrieved successfully", @@ -352,15 +343,13 @@ class ConversationVariablesApi(Resource): conversation_id = str(c_id) query_args = ConversationVariablesQuery.model_validate(request.args.to_dict()) - last_id = str(query_args.last_id) if query_args.last_id else None + last_id = query_args.last_id or None try: pagination = ConversationService.get_conversational_variable( app_model, conversation_id, end_user, query_args.limit, last_id, query_args.variable_name ) - return ConversationVariableInfiniteScrollPaginationResponse.model_validate( - pagination, from_attributes=True - ).model_dump(mode="json") + return dump_response(ConversationVariableInfiniteScrollPaginationResponse, pagination) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -419,7 +408,7 @@ class ConversationVariableDetailApi(Resource): variable = ConversationService.update_conversation_variable( app_model, conversation_id, variable_id_str, end_user, payload.value ) - return ConversationVariableResponse.model_validate(variable, from_attributes=True).model_dump(mode="json") + return dump_response(ConversationVariableResponse, variable) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") except services.errors.conversation.ConversationVariableNotExistsError: diff --git a/api/controllers/service_api/app/file.py b/api/controllers/service_api/app/file.py index 9210c60adeb..aff2e0c1163 100644 --- a/api/controllers/service_api/app/file.py +++ b/api/controllers/service_api/app/file.py @@ -16,6 +16,7 @@ from controllers.service_api.schema import multipart_file_params from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from extensions.ext_database import db from fields.file_fields import FileResponse +from libs.helper import dump_response from models import App, EndUser from services.file_service import FileService @@ -87,5 +88,4 @@ class FileApi(Resource): except services.errors.file.UnsupportedFileTypeError: raise UnsupportedFileTypeError() - response = FileResponse.model_validate(upload_file, from_attributes=True) - return response.model_dump(mode="json"), 201 + return dump_response(FileResponse, upload_file), 201 diff --git a/api/controllers/service_api/app/message.py b/api/controllers/service_api/app/message.py index 18d1c5d3254..2b1a8230efb 100644 --- a/api/controllers/service_api/app/message.py +++ b/api/controllers/service_api/app/message.py @@ -59,12 +59,14 @@ register_response_schema_models( ResultResponse, SimpleResultStringListResponse, MessageInfiniteScrollPagination, + MessageListItem, AppFeedbackListResponse, ) @service_api_ns.route("/messages") class MessageListApi(Resource): + @service_api_ns.doc("list_messages") @service_api_ns.doc( summary="List Conversation Messages", description=( @@ -75,15 +77,15 @@ class MessageListApi(Resource): responses={ 200: "Successfully retrieved conversation history.", 400: "`not_chat_app` : App mode does not match the API route.", - 404: ("- `not_found` : Conversation does not exist.\n- `not_found` : First message does not exist."), + 404: "- `not_found` : Conversation does not exist.\n- `not_found` : First message does not exist.", }, ) @service_api_ns.doc(params=query_params_from_model(MessageListQuery)) - @service_api_ns.doc("list_messages") @service_api_ns.doc(description="List messages in a conversation") @service_api_ns.doc( responses={ 200: "Messages retrieved successfully", + 400: "`not_chat_app` : App mode does not match the API route.", 401: "Unauthorized - invalid API token", 404: "Conversation or first message not found", } @@ -104,8 +106,8 @@ class MessageListApi(Resource): raise NotChatAppError() query_args = MessageListQuery.model_validate(request.args.to_dict()) - conversation_id = str(query_args.conversation_id) - first_id = str(query_args.first_id) if query_args.first_id else None + conversation_id = query_args.conversation_id + first_id = query_args.first_id or None try: pagination = MessageService.pagination_by_first_id( @@ -114,9 +116,7 @@ class MessageListApi(Resource): adapter = TypeAdapter(MessageListItem) items = [adapter.validate_python(message, from_attributes=True) for message in pagination.data] return MessageInfiniteScrollPagination( - limit=pagination.limit, - has_more=pagination.has_more, - data=items, + limit=pagination.limit, has_more=pagination.has_more, data=items ).model_dump(mode="json") except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -126,21 +126,20 @@ class MessageListApi(Resource): @service_api_ns.route("/messages//feedbacks") class MessageFeedbackApi(Resource): + @expect_with_user(service_api_ns, MessageFeedbackPayload) + @service_api_ns.response(200, "Feedback submitted successfully", service_api_ns.models[ResultResponse.__name__]) + @service_api_ns.doc("create_message_feedback") @service_api_ns.doc( summary="Submit Message Feedback", description=( "Submit feedback for a message. End users can rate messages as `like` or `dislike`, and " - "optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted " - "feedback." + "optionally provide text feedback. Pass `null` for `rating` to revoke previously submitted feedback." ), tags=["Feedback"], responses={ 404: "`not_found` : Message does not exist.", }, ) - @expect_with_user(service_api_ns, MessageFeedbackPayload) - @service_api_ns.response(200, "Feedback submitted successfully", service_api_ns.models[ResultResponse.__name__]) - @service_api_ns.doc("create_message_feedback") @service_api_ns.doc(description="Submit feedback for a message") @service_api_ns.doc(params={"message_id": "Message ID."}) @service_api_ns.doc( @@ -176,11 +175,12 @@ class MessageFeedbackApi(Resource): @service_api_ns.route("/app/feedbacks") class AppGetFeedbacksApi(Resource): + @service_api_ns.doc("get_app_feedbacks") @service_api_ns.doc( summary="List App Feedbacks", description=( - "Retrieve a paginated list of all feedback submitted for messages in this application, " - "including both end-user and admin feedback." + "Retrieve a paginated list of all feedback submitted for messages in this application, including both " + "end-user and admin feedback." ), tags=["Feedback"], responses={ @@ -188,7 +188,6 @@ class AppGetFeedbacksApi(Resource): }, ) @service_api_ns.doc(params=query_params_from_model(FeedbackListQuery)) - @service_api_ns.doc("get_app_feedbacks") @service_api_ns.doc(description="Get all feedbacks for the application") @service_api_ns.doc( responses={ @@ -209,11 +208,12 @@ class AppGetFeedbacksApi(Resource): """ query_args = FeedbackListQuery.model_validate(request.args.to_dict()) feedbacks = MessageService.get_all_messages_feedbacks(app_model, page=query_args.page, limit=query_args.limit) - return {"data": feedbacks} + return AppFeedbackListResponse(data=feedbacks).model_dump(mode="json") @service_api_ns.route("/messages//suggested") class MessageSuggestedApi(Resource): + @service_api_ns.doc("get_suggested_questions") @service_api_ns.doc( summary="Get Next Suggested Questions", description="Get next questions suggestions for the current message.", @@ -233,7 +233,6 @@ class MessageSuggestedApi(Resource): "Suggested questions retrieved successfully", service_api_ns.models[SimpleResultStringListResponse.__name__], ) - @service_api_ns.doc("get_suggested_questions") @service_api_ns.doc(description="Get suggested follow-up questions for a message") @service_api_ns.doc(params={"message_id": "Message ID"}) @service_api_ns.doc( @@ -268,4 +267,4 @@ class MessageSuggestedApi(Resource): logger.exception("internal server error.") raise InternalServerError() - return {"result": "success", "data": questions} + return SimpleResultStringListResponse(result="success", data=questions).model_dump(mode="json") diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index 091b79fefbd..d685172d6d2 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -1,19 +1,24 @@ import logging from collections.abc import Mapping from datetime import datetime -from typing import Literal, override +from typing import Literal from dateutil.parser import isoparse from flask import request -from flask_restx import Resource, fields -from pydantic import BaseModel, Field, field_validator +from flask_restx import Resource +from pydantic import BaseModel, Field, field_validator, model_validator from pydantic.json_schema import SkipJsonSchema from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from controllers.common.controller_schemas import WorkflowRunPayload as WorkflowRunPayloadBase -from controllers.common.fields import GeneratedAppResponse, SimpleResultResponse -from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models +from controllers.common.fields import SimpleResultResponse +from controllers.common.schema import ( + query_params_from_model, + query_params_from_request, + register_response_schema_models, + register_schema_models, +) from controllers.service_api import service_api_ns from controllers.service_api.app.error import ( CompletionRequestError, @@ -41,14 +46,13 @@ from extensions.ext_database import db from extensions.ext_redis import redis_client from fields.base import ResponseModel from fields.end_user_fields import SimpleEndUser -from fields.member_fields import SimpleAccount +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.helper import to_timestamp +from libs.helper import dump_response, to_timestamp from models.model import App, AppMode, EndUser -from models.workflow import WorkflowRun from repositories.factory import DifyAPIRepositoryFactory from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError @@ -97,36 +101,19 @@ class WorkflowLogQuery(BaseModel): register_schema_models(service_api_ns, WorkflowRunPayload, WorkflowLogQuery) -register_response_schema_models(service_api_ns, GeneratedAppResponse, SimpleResultResponse) +register_response_schema_models(service_api_ns, SimpleResultResponse) def _enum_value(value): return getattr(value, "value", value) -class WorkflowRunStatusField(fields.Raw): - @override - def output(self, key, obj: WorkflowRun, **kwargs): - return _enum_value(obj.status) - - -class WorkflowRunOutputsField(fields.Raw): - @override - def output(self, key, obj: WorkflowRun, **kwargs): - status = _enum_value(obj.status) - if status == WorkflowExecutionStatus.PAUSED.value: - return {} - - outputs = obj.outputs_dict - return outputs or {} - - class WorkflowRunResponse(ResponseModel): id: str workflow_id: str status: str inputs: dict | list | str | int | float | bool | None = Field(default=None) - outputs: dict = Field(default_factory=dict) + outputs: dict = Field(default_factory=dict, validation_alias="outputs_dict") error: str | None = None total_steps: int | None = None total_tokens: int | None = None @@ -134,11 +121,33 @@ class WorkflowRunResponse(ResponseModel): finished_at: int | None = None elapsed_time: float | int | None = None + @field_validator("status", mode="before") + @classmethod + def _normalize_enum(cls, value): + return _enum_value(value) + + @field_validator("outputs", mode="before") + @classmethod + def _normalize_outputs(cls, value): + if value is None: + return {} + if isinstance(value, dict): + return value + if isinstance(value, Mapping): + return dict(value) + return {} + @field_validator("created_at", "finished_at", mode="before") @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) + @model_validator(mode="after") + def _clear_paused_outputs(self): + if self.status == WorkflowExecutionStatus.PAUSED.value: + self.outputs = {} + return self + class WorkflowRunForLogResponse(ResponseModel): id: str @@ -170,7 +179,7 @@ class WorkflowAppLogPartialResponse(ResponseModel): details: dict | list | str | int | float | bool | None = Field(default=None) created_from: str | None = None created_by_role: str | None = None - created_by_account: SimpleAccount | None = None + created_by_account: SimpleAccountResponse | None = None created_by_end_user: SimpleEndUser | None = None created_at: int | None = None @@ -202,39 +211,6 @@ register_response_schema_models( ) -def _serialize_workflow_run(workflow_run: WorkflowRun) -> dict: - status = _enum_value(workflow_run.status) - raw_outputs = workflow_run.outputs_dict - match raw_outputs: - case _ if status == WorkflowExecutionStatus.PAUSED.value or raw_outputs is None: - outputs: dict = {} - case dict(): - outputs = raw_outputs - case _ if isinstance(raw_outputs, Mapping): - outputs = dict(raw_outputs) - case _: - outputs = {} - return WorkflowRunResponse.model_validate( - { - "id": workflow_run.id, - "workflow_id": workflow_run.workflow_id, - "status": status, - "inputs": workflow_run.inputs, - "outputs": outputs, - "error": workflow_run.error, - "total_steps": workflow_run.total_steps, - "total_tokens": workflow_run.total_tokens, - "created_at": workflow_run.created_at, - "finished_at": workflow_run.finished_at, - "elapsed_time": workflow_run.elapsed_time, - } - ).model_dump(mode="json") - - -def _serialize_workflow_log_pagination(pagination) -> dict: - return WorkflowAppLogPaginationResponse.model_validate(pagination, from_attributes=True).model_dump(mode="json") - - @service_api_ns.route("/workflows/run/") class WorkflowRunDetailApi(Resource): @service_api_ns.doc( @@ -287,7 +263,7 @@ class WorkflowRunDetailApi(Resource): ) if not workflow_run: raise NotFound("Workflow run not found.") - return _serialize_workflow_run(workflow_run) + return dump_response(WorkflowRunResponse, workflow_run) @service_api_ns.route("/workflows/run") @@ -338,7 +314,6 @@ class WorkflowRunApi(Resource): @service_api_ns.response( 200, "Workflow executed successfully", - service_api_ns.models[GeneratedAppResponse.__name__], ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) def post(self, app_model: App, end_user: EndUser): @@ -366,6 +341,7 @@ class WorkflowRunApi(Resource): app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=streaming ) + # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) @@ -445,7 +421,6 @@ class WorkflowRunByIdApi(Resource): @service_api_ns.response( 200, "Workflow executed successfully", - service_api_ns.models[GeneratedAppResponse.__name__], ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) def post(self, app_model: App, end_user: EndUser, workflow_id: str): @@ -476,6 +451,7 @@ class WorkflowRunByIdApi(Resource): app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=streaming ) + # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except WorkflowNotFoundError as ex: raise NotFound(str(ex)) @@ -541,7 +517,7 @@ class WorkflowTaskStopApi(Resource): # New graph engine command channel mechanism GraphEngineManager(redis_client).send_stop_command(task_id) - return {"result": "success"} + return SimpleResultResponse(result="success").model_dump() @service_api_ns.route("/workflows/logs") @@ -574,7 +550,7 @@ class WorkflowAppLogApi(Resource): Returns paginated workflow execution logs with filtering options. """ - args = WorkflowLogQuery.model_validate(request.args.to_dict()) + args = query_params_from_request(WorkflowLogQuery) status = WorkflowExecutionStatus(args.status) if args.status else None created_at_before = isoparse(args.created_at__before) if args.created_at__before else None @@ -596,4 +572,4 @@ class WorkflowAppLogApi(Resource): created_by_account=args.created_by_account, ) - return _serialize_workflow_log_pagination(workflow_app_log_pagination) + return dump_response(WorkflowAppLogPaginationResponse, workflow_app_log_pagination) diff --git a/api/fields/annotation_fields.py b/api/fields/annotation_fields.py index 4546a051cce..86a13a32bd2 100644 --- a/api/fields/annotation_fields.py +++ b/api/fields/annotation_fields.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Literal from pydantic import Field, field_validator @@ -29,6 +30,15 @@ class AnnotationList(ResponseModel): page: int +class AnnotationJobStatusResponse(ResponseModel): + job_id: str + job_status: Literal["waiting", "processing", "completed", "error"] | str + + +class AnnotationJobStatusDetailResponse(AnnotationJobStatusResponse): + error_msg: str = "" + + class AnnotationExportList(ResponseModel): data: list[Annotation] diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index 9bbcbef8429..80b93f0a24b 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -15,13 +15,13 @@ simple_account_fields = { } -class SimpleAccount(ResponseModel): +class SimpleAccountResponse(ResponseModel): id: str name: str email: str -class _AccountAvatar(ResponseModel): +class _AccountAvatarResponseMixin(ResponseModel): avatar: str | None = None @computed_field(return_type=str | None) # type: ignore[prop-decorator] @@ -30,7 +30,7 @@ class _AccountAvatar(ResponseModel): return build_avatar_url(self.avatar) -class Account(_AccountAvatar): +class AccountResponse(_AccountAvatarResponseMixin): id: str name: str email: str @@ -48,7 +48,7 @@ class Account(_AccountAvatar): return to_timestamp(value) -class AccountWithRole(_AccountAvatar): +class AccountWithRoleResponse(_AccountAvatarResponseMixin): id: str name: str email: str @@ -65,5 +65,11 @@ class AccountWithRole(_AccountAvatar): return to_timestamp(value) -class AccountWithRoleList(ResponseModel): - accounts: list[AccountWithRole] +class AccountWithRoleListResponse(ResponseModel): + accounts: list[AccountWithRoleResponse] + + +SimpleAccount = SimpleAccountResponse +Account = AccountResponse +AccountWithRole = AccountWithRoleResponse +AccountWithRoleList = AccountWithRoleListResponse diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index b3a0b8a6a71..e9c2d0ff775 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -38,7 +38,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/change-email #### Request Body @@ -77,7 +77,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/change-email/validity #### Request Body @@ -198,7 +198,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/interface-theme #### Request Body @@ -211,7 +211,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/name #### Request Body @@ -224,7 +224,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/password #### Request Body @@ -237,14 +237,14 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [GET] /account/profile #### Responses | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /account/timezone #### Request Body @@ -257,7 +257,7 @@ Get account avatar url | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [Account](#account)
| +| 200 | Success | **application/json**: [AccountResponse](#accountresponse)
| ### [POST] /activate Activate account with invitation token @@ -9268,7 +9268,7 @@ Increment snippet use count by 1 | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccountWithRoleList](#accountwithrolelist)
| +| 200 | Success | **application/json**: [AccountWithRoleListResponse](#accountwithrolelistresponse)
| ### [GET] /workspaces/current/default-model #### Parameters @@ -9477,7 +9477,7 @@ Update a plugin endpoint | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [AccountWithRoleList](#accountwithrolelist)
| +| 200 | Success | **application/json**: [AccountWithRoleListResponse](#accountwithrolelistresponse)
| ### [POST] /workspaces/current/members/invite-email #### Request Body @@ -11928,23 +11928,6 @@ Default namespace | role_name | string | | No | | tenant_id | string | | No | -#### Account - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| avatar | string | | No | -| avatar_url | string | | Yes | -| created_at | integer | | No | -| email | string | | Yes | -| id | string | | Yes | -| interface_language | string | | No | -| interface_theme | string | | No | -| is_password_set | boolean | | Yes | -| last_login_at | integer | | No | -| last_login_ip | string | | No | -| name | string | | Yes | -| timezone | string | | No | - #### AccountAvatarPayload | Name | Type | Description | Required | @@ -12020,13 +12003,36 @@ Default namespace | password | string | | No | | repeat_new_password | string | | Yes | +#### AccountResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| avatar | string | | No | +| avatar_url | string | | Yes | +| created_at | integer | | No | +| email | string | | Yes | +| id | string | | Yes | +| interface_language | string | | No | +| interface_theme | string | | No | +| is_password_set | boolean | | Yes | +| last_login_at | integer | | No | +| last_login_ip | string | | No | +| name | string | | Yes | +| timezone | string | | No | + #### AccountTimezonePayload | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | timezone | string | | Yes | -#### AccountWithRole +#### AccountWithRoleListResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| accounts | [ [AccountWithRoleResponse](#accountwithroleresponse) ] | | Yes | + +#### AccountWithRoleResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | @@ -12041,12 +12047,6 @@ Default namespace | roles | [ object ] | | No | | status | string | | Yes | -#### AccountWithRoleList - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| accounts | [ [AccountWithRole](#accountwithrole) ] | | Yes | - #### ActivateCheckQuery | Name | Type | Description | Required | @@ -12095,7 +12095,7 @@ Default namespace | ---- | ---- | ----------- | -------- | | conversation_id | string | | No | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | elapsed_time | number | | No | | exceptions_count | integer | | No | | finished_at | integer | | No | @@ -19151,6 +19151,14 @@ Model class for provider quota configuration. | id | string | | Yes | | name | string | | Yes | +#### SimpleAccountResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | | Yes | +| id | string | | Yes | +| name | string | | Yes | + #### SimpleConversation | Name | Type | Description | Required | @@ -19458,7 +19466,7 @@ Query parameters for listing snippet published workflows. | ---- | ---- | ----------- | -------- | | conversation_variables | [ [WorkflowConversationVariableResponse](#workflowconversationvariableresponse) ] | | Yes | | created_at | integer | | Yes | -| created_by | [SimpleAccount](#simpleaccount) | | No | +| created_by | [SimpleAccountResponse](#simpleaccountresponse) | | No | | environment_variables | [ [WorkflowEnvironmentVariableResponse](#workflowenvironmentvariableresponse) ] | | Yes | | features | object | | Yes | | graph | object | | Yes | @@ -19470,7 +19478,7 @@ Query parameters for listing snippet published workflows. | rag_pipeline_variables | [ [PipelineVariableResponse](#pipelinevariableresponse) ] | | Yes | | tool_published | boolean | | Yes | | updated_at | integer | | Yes | -| updated_by | [SimpleAccount](#simpleaccount) | | No | +| updated_by | [SimpleAccountResponse](#simpleaccountresponse) | | No | | version | string | | Yes | #### StarredAppListQuery @@ -20390,7 +20398,7 @@ How a workflow node is bound to an Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | created_by_end_user | [SimpleEndUser](#simpleenduser) | | No | | created_by_role | string | | No | | created_from | string | | No | @@ -20427,7 +20435,7 @@ How a workflow node is bound to an Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | created_by_end_user | [SimpleEndUser](#simpleenduser) | | No | | id | string | | Yes | | trigger_metadata | | | No | @@ -20528,7 +20536,7 @@ How a workflow node is bound to an Agent. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| users | [ [AccountWithRole](#accountwithrole) ] | | Yes | +| users | [ [AccountWithRoleResponse](#accountwithroleresponse) ] | | Yes | #### WorkflowCommentReply @@ -20877,7 +20885,7 @@ can reuse its existing handler. | ---- | ---- | ----------- | -------- | | conversation_variables | [ [WorkflowConversationVariableResponse](#workflowconversationvariableresponse) ] | | Yes | | created_at | integer | | Yes | -| created_by | [SimpleAccount](#simpleaccount) | | No | +| created_by | [SimpleAccountResponse](#simpleaccountresponse) | | No | | environment_variables | [ [WorkflowEnvironmentVariableResponse](#workflowenvironmentvariableresponse) ] | | Yes | | features | object | | Yes | | graph | object | | Yes | @@ -20888,7 +20896,7 @@ can reuse its existing handler. | rag_pipeline_variables | [ [PipelineVariableResponse](#pipelinevariableresponse) ] | | Yes | | tool_published | boolean | | Yes | | updated_at | integer | | Yes | -| updated_by | [SimpleAccount](#simpleaccount) | | No | +| updated_by | [SimpleAccountResponse](#simpleaccountresponse) | | No | | version | string | | Yes | #### WorkflowRestoreResponse @@ -20923,7 +20931,7 @@ can reuse its existing handler. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | created_by_end_user | [SimpleEndUser](#simpleenduser) | | No | | created_by_role | string | | No | | elapsed_time | number | | No | @@ -20962,7 +20970,7 @@ can reuse its existing handler. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | elapsed_time | number | | No | | exceptions_count | integer | | No | | finished_at | integer | | No | @@ -21009,7 +21017,7 @@ can reuse its existing handler. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | created_by_end_user | [SimpleEndUser](#simpleenduser) | | No | | created_by_role | string | | No | | elapsed_time | number | | No | diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 8fc5e75e3cf..064dbfd4576 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -237,7 +237,7 @@ Retrieves the status of an asynchronous annotation reply configuration job start | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Successfully retrieved task status. | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Successfully retrieved task status. | **application/json**: [AnnotationJobStatusDetailResponse](#annotationjobstatusdetailresponse)
| | 400 | `invalid_param` : The specified job does not exist. | | | 401 | Unauthorized - invalid API token | | | 403 | Forbidden - token scope, app, dataset, or workspace access denied | | @@ -392,15 +392,15 @@ Send a request to the chat application. #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -| 404 | `not_found` : Conversation does not exist. | | -| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | -| 500 | `internal_server_error` : Internal server error. | | +| Code | Description | +| ---- | ----------- | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | `not_found` : Conversation does not exist. | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | +| 500 | `internal_server_error` : Internal server error. | ### [POST] /chat-messages/{task_id}/stop **Stop Chat Message Generation** @@ -539,15 +539,15 @@ Send a request to the chat application. #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -| 404 | `not_found` : Conversation does not exist. | | -| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | -| 500 | `internal_server_error` : Internal server error. | | +| Code | Description | +| ---- | ----------- | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. | +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `not_chat_app` : App mode does not match the API route. - `conversation_completed` : The conversation has ended. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | `not_found` : Conversation does not exist. | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | +| 500 | `internal_server_error` : Internal server error. | ### [POST] /chat-messages/{task_id}/stop **Stop Chat Message Generation** @@ -615,15 +615,15 @@ Send a request to the text generation application. #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkCompletionEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -| 404 | Conversation not found | | -| 429 | `too_many_requests` : Too many concurrent requests for this app. | | -| 500 | `internal_server_error` : Internal server error. | | +| Code | Description | +| ---- | ----------- | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkCompletionEvent` objects. | +| 400 | - `app_unavailable` : App unavailable or misconfigured. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Text generation failed. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | Conversation not found | +| 429 | `too_many_requests` : Too many concurrent requests for this app. | +| 500 | `internal_server_error` : Internal server error. | ### [POST] /completion-messages/{task_id}/stop **Stop Completion Message Generation** @@ -2220,15 +2220,15 @@ Execute a workflow. Cannot be executed without a published workflow. #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | - `not_workflow_app` : App mode does not match the API route. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Invalid parameter value. | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -| 404 | Workflow not found | | -| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | -| 500 | `internal_server_error` : Internal server error. | | +| Code | Description | +| ---- | ----------- | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | +| 400 | - `not_workflow_app` : App mode does not match the API route. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Invalid parameter value. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | Workflow not found | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | +| 500 | `internal_server_error` : Internal server error. | ### [GET] /workflows/run/{workflow_run_id} **Get Workflow Run Detail** @@ -2297,15 +2297,15 @@ Execute a specific workflow version identified by its ID. Useful for running a p #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | **application/json**: [GeneratedAppResponse](#generatedappresponse)
**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | - `not_workflow_app` : App mode does not match the API route. - `bad_request` : Workflow is a draft or has an invalid ID format. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Required parameter missing or invalid. | | -| 401 | Unauthorized - invalid API token | | -| 403 | Forbidden - token scope, app, dataset, or workspace access denied | | -| 404 | `not_found` : Workflow not found. | | -| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | | -| 500 | `internal_server_error` : Internal server error. | | +| Code | Description | +| ---- | ----------- | +| 200 | Successful response. The content type and structure depend on the `response_mode` parameter in the request. - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. | +| 400 | - `not_workflow_app` : App mode does not match the API route. - `bad_request` : Workflow is a draft or has an invalid ID format. - `provider_not_initialize` : No valid model provider credentials found. - `provider_quota_exceeded` : Model provider quota exhausted. - `model_currently_not_support` : Current model unavailable. - `completion_request_error` : Workflow execution request failed. - `invalid_param` : Required parameter missing or invalid. | +| 401 | Unauthorized - invalid API token | +| 403 | Forbidden - token scope, app, dataset, or workspace access denied | +| 404 | `not_found` : Workflow not found. | +| 429 | - `too_many_requests` : Too many concurrent requests for this app. - `rate_limit_error` : The upstream model provider rate limit was exceeded. | +| 500 | `internal_server_error` : Internal server error. | --- ## default @@ -2351,7 +2351,7 @@ Retrieve the list of available models by type. Primarily used to query `text-emb | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| content | string | | No | +| answer | string | | No | | created_at | integer | | No | | hit_count | integer | | No | | id | string | | Yes | @@ -2364,13 +2364,20 @@ Retrieve the list of available models by type. Primarily used to query `text-emb | answer | string | Annotation answer. | Yes | | question | string | Annotation question. | Yes | -#### AnnotationJobStatusResponse +#### AnnotationJobStatusDetailResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | error_msg | string | | No | | job_id | string | | Yes | -| job_status | string | | Yes | +| job_status | string
string | | Yes | + +#### AnnotationJobStatusResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| job_id | string | | Yes | +| job_status | string
string | | Yes | #### AnnotationList @@ -3937,7 +3944,7 @@ Model class for provider with models response. | output_variable_name | string | | Yes | | type | string | | No | -#### SimpleAccount +#### SimpleAccountResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | @@ -4147,7 +4154,7 @@ in form definiton, or a variable while the workflow is running. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| created_by_account | [SimpleAccount](#simpleaccount) | | No | +| created_by_account | [SimpleAccountResponse](#simpleaccountresponse) | | No | | created_by_end_user | [SimpleEndUser](#simpleenduser) | | No | | created_by_role | string | | No | | created_from | string | | No | diff --git a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py index b4dd5e957c1..94128e27614 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_annotation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_annotation.py @@ -141,14 +141,14 @@ class TestAppModelPatterns: assert app.id is not None assert app.status == "normal" - assert app.enable_api is True + assert app.enable_api def test_app_model_disabled_api(self): """Test app with disabled API access.""" app = Mock(spec=App) app.enable_api = False - assert app.enable_api is False + assert not app.enable_api def test_app_model_archived_status(self): """Test app with archived status.""" @@ -183,7 +183,7 @@ class TestAnnotationErrorPatterns: class TestAnnotationReplyActionApi: def test_enable(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - enable_mock = Mock() + enable_mock = Mock(return_value={"job_id": "job-1", "job_status": "waiting"}) monkeypatch.setattr(AppAnnotationService, "enable_app_annotation", enable_mock) api = AnnotationReplyActionApi() @@ -198,10 +198,11 @@ class TestAnnotationReplyActionApi: response, status = handler(api, app_model=app_model, action="enable") assert status == 200 + assert response == {"job_id": "job-1", "job_status": "waiting"} enable_mock.assert_called_once() def test_disable(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - disable_mock = Mock() + disable_mock = Mock(return_value={"job_id": "job-1", "job_status": "waiting"}) monkeypatch.setattr(AppAnnotationService, "disable_app_annotation", disable_mock) api = AnnotationReplyActionApi() @@ -216,6 +217,7 @@ class TestAnnotationReplyActionApi: response, status = handler(api, app_model=app_model, action="disable") assert status == 200 + assert response == {"job_id": "job-1", "job_status": "waiting"} disable_mock.assert_called_once() diff --git a/api/tests/unit_tests/controllers/service_api/app/test_message.py b/api/tests/unit_tests/controllers/service_api/app/test_message.py index d8d5c61bcb3..bd07c558475 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_message.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_message.py @@ -51,7 +51,7 @@ class TestMessageListQuery: """Test conversation_id is required.""" conversation_id = str(uuid.uuid4()) query = MessageListQuery(conversation_id=conversation_id) - assert str(query.conversation_id) == conversation_id + assert query.conversation_id == conversation_id def test_query_with_defaults(self): """Test query with default values.""" @@ -87,13 +87,13 @@ class TestMessageListQuery: """Test query rejects limit < 1.""" conversation_id = str(uuid.uuid4()) with pytest.raises(ValueError): - MessageListQuery(conversation_id=conversation_id, limit=0) + MessageListQuery(conversation_id=conversation_id, limit=0) # pyrefly: ignore[bad-argument-type] def test_query_rejects_limit_above_maximum(self): """Test query rejects limit > 100.""" conversation_id = str(uuid.uuid4()) with pytest.raises(ValueError): - MessageListQuery(conversation_id=conversation_id, limit=101) + MessageListQuery(conversation_id=conversation_id, limit=101) # pyrefly: ignore[bad-argument-type] class TestMessageFeedbackPayload: @@ -131,6 +131,7 @@ class TestMessageFeedbackPayload: """Test payload with long feedback content.""" long_content = "A" * 1000 payload = MessageFeedbackPayload(content=long_content) + assert payload.content is not None assert len(payload.content) == 1000 def test_payload_with_unicode_content(self): @@ -163,7 +164,7 @@ class TestFeedbackListQuery: def test_query_rejects_page_below_minimum(self): """Test query rejects page < 1.""" with pytest.raises(ValueError): - FeedbackListQuery(page=0) + FeedbackListQuery(page=0) # pyrefly: ignore[bad-argument-type] def test_query_limit_boundaries(self): """Test query limit boundaries.""" @@ -176,12 +177,12 @@ class TestFeedbackListQuery: def test_query_rejects_limit_below_minimum(self): """Test query rejects limit < 1.""" with pytest.raises(ValueError): - FeedbackListQuery(limit=0) + FeedbackListQuery(limit=0) # pyrefly: ignore[bad-argument-type] def test_query_rejects_limit_above_maximum(self): """Test query rejects limit > 101.""" with pytest.raises(ValueError): - FeedbackListQuery(limit=102) + FeedbackListQuery(limit=102) # pyrefly: ignore[bad-argument-type] class TestMessageAppModeValidation: @@ -449,7 +450,20 @@ class TestMessageFeedbackApi: class TestAppGetFeedbacksApi: def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(MessageService, "get_all_messages_feedbacks", lambda *_args, **_kwargs: ["f1"]) + feedback = { + "id": "feedback-1", + "app_id": "app-1", + "conversation_id": "conversation-1", + "message_id": "message-1", + "rating": "like", + "content": "helpful answer", + "from_source": "user", + "from_end_user_id": "end-user-1", + "from_account_id": None, + "created_at": "2024-01-02T03:04:05", + "updated_at": "2024-01-02T03:04:06", + } + monkeypatch.setattr(MessageService, "get_all_messages_feedbacks", lambda *_args, **_kwargs: [feedback]) api = AppGetFeedbacksApi() handler = unwrap(api.get) @@ -458,7 +472,7 @@ class TestAppGetFeedbacksApi: with app.test_request_context("/app/feedbacks?page=1&limit=20", method="GET"): response = handler(api, app_model=app_model) - assert response == {"data": ["f1"]} + assert response == {"data": [feedback]} class TestMessageSuggestedApi: diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index 4f88ae69c2d..63076dfe81f 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -13,15 +13,17 @@ Focus on: - Service method interfaces """ +import json import sys import uuid +from dataclasses import dataclass, field from datetime import UTC, datetime from inspect import unwrap -from types import SimpleNamespace from unittest.mock import Mock, patch import pytest from flask import Flask +from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound from controllers.service_api.app.error import NotWorkflowAppError @@ -35,31 +37,175 @@ from controllers.service_api.app.workflow import ( WorkflowRunByIdApi, WorkflowRunDetailApi, WorkflowRunPayload, + WorkflowRunResponse, WorkflowTaskStopApi, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from core.app.entities.app_invoke_entities import InvokeFrom from graphon.enums import WorkflowExecutionStatus -from models.model import App, AppMode +from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom +from models.model import App, AppMode, EndUser +from models.workflow import WorkflowAppLog, WorkflowAppLogCreatedFrom, WorkflowRun, WorkflowType from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError -from services.workflow_app_service import WorkflowAppService +from services.workflow_app_service import LogView, LogViewDetails, WorkflowAppService -def _make_mock_workflow_run(run_id: str = "run-1"): - run = Mock() - run.id = run_id - run.workflow_id = "wf-1" - run.status = WorkflowExecutionStatus.SUCCEEDED - run.inputs = {"input": "value"} - run.outputs_dict = {"output": "value"} - run.error = None - run.total_steps = 1 - run.total_tokens = 10 - run.created_at = datetime(2026, 1, 1, tzinfo=UTC) - run.finished_at = datetime(2026, 1, 1, tzinfo=UTC) - run.elapsed_time = 0.1 - return run +def _default_workflow_inputs() -> dict[str, object]: + return {"input": "value"} + + +def _default_log_details() -> LogViewDetails: + return {"trigger_metadata": {"node": "answer", "latency": 1.25}} + + +class _DbSessionStub: + def get(self, *args: object, **kwargs: object) -> None: + return None + + +@dataclass +class _DbStub: + engine: object = field(default_factory=object) + session: _DbSessionStub = field(default_factory=_DbSessionStub) + + +@dataclass +class _WorkflowRunRepositoryStub: + run: WorkflowRun | None + + def get_workflow_run_by_id(self, *, tenant_id: str, app_id: str, run_id: str) -> WorkflowRun | None: + return self.run if tenant_id and app_id and run_id else None + + def get_workflow_run_by_id_without_tenant(self, *, run_id: str) -> WorkflowRun | None: + return self.run if run_id else None + + +class _BeginStub: + def __enter__(self) -> object: + return object() + + def __exit__(self, exc_type: object, exc: object, tb: object) -> bool: + return False + + +class _SessionMakerStub: + def __init__(self, *args: object, **kwargs: object) -> None: + pass + + def begin(self) -> _BeginStub: + return _BeginStub() + + +def _make_workflow_run( + run_id: str = "run-1", + *, + workflow_id: str = "wf-1", + inputs: dict[str, object] | None = None, + outputs: dict[str, object] | None = None, + created_at: datetime | None = None, + finished_at: datetime | None = None, +) -> WorkflowRun: + return WorkflowRun( + id=run_id, + tenant_id="tenant-1", + app_id="app-1", + workflow_id=workflow_id, + type=WorkflowType.WORKFLOW, + triggered_from=WorkflowRunTriggeredFrom.APP_RUN, + version="2026-01-01", + graph=json.dumps({"nodes": [], "edges": []}), + inputs=json.dumps(inputs if inputs is not None else _default_workflow_inputs()), + outputs=json.dumps(outputs if outputs is not None else {"output": "value"}), + status=WorkflowExecutionStatus.SUCCEEDED, + error=None, + elapsed_time=0.1, + total_tokens=10, + total_steps=1, + created_by_role=CreatorUserRole.END_USER, + created_by="end-user-1", + created_at=created_at or datetime(2026, 1, 1, tzinfo=UTC), + finished_at=finished_at or datetime(2026, 1, 1, tzinfo=UTC), + exceptions_count=0, + ) + + +def _make_workflow_app_log() -> WorkflowAppLog: + log = WorkflowAppLog( + tenant_id="tenant-1", + app_id="app-1", + workflow_id="wf-1", + workflow_run_id="log-run-1", + created_from=WorkflowAppLogCreatedFrom.SERVICE_API, + created_by_role=CreatorUserRole.ACCOUNT, + created_by="account-1", + ) + log.id = "app-log-1" + log.created_at = datetime(2026, 1, 1, 1, 0, 3, tzinfo=UTC) + return log + + +def _make_workflow_log_page() -> dict[str, object]: + return { + "page": 1, + "limit": 20, + "total": 1, + "has_more": False, + "data": [LogView(_make_workflow_app_log(), _default_log_details())], + } + + +def _make_app_model( + *, + app_id: str = "app-1", + tenant_id: str = "tenant-1", + mode: AppMode = AppMode.WORKFLOW, +) -> App: + app = App() + app.id = app_id + app.tenant_id = tenant_id + app.mode = mode + return app + + +def _make_end_user(user_id: str = "end-user-1") -> EndUser: + end_user = EndUser() + end_user.id = user_id + return end_user + + +def _expected_workflow_log_pagination_payload() -> dict[str, object]: + return { + "page": 1, + "limit": 20, + "total": 1, + "has_more": False, + "data": [ + { + "id": "app-log-1", + "workflow_run": { + "id": "log-run-1", + "version": "2026-01-01", + "status": "succeeded", + "triggered_from": "app-run", + "error": None, + "elapsed_time": 0.1, + "total_tokens": 10, + "total_steps": 1, + "created_at": 1767229200, + "finished_at": 1767229202, + "exceptions_count": 0, + }, + "details": {"trigger_metadata": {"node": "answer", "latency": 1.25}}, + "created_from": "service-api", + "created_by_role": "account", + "created_by_account": None, + "created_by_end_user": None, + "created_at": 1767229203, + } + ], + } class TestWorkflowRunPayload: @@ -108,6 +254,7 @@ class TestWorkflowRunPayload: {"type": "audio", "url": "http://example.com/audio.mp3"}, ] payload = WorkflowRunPayload(inputs={}, files=files) + assert payload.files is not None assert len(payload.files) == 3 @@ -170,22 +317,22 @@ class TestWorkflowLogQuery: def test_query_rejects_page_below_minimum(self): """Test query rejects page < 1.""" with pytest.raises(ValueError): - WorkflowLogQuery(page=0) + WorkflowLogQuery.model_validate({"page": 0}) def test_query_rejects_page_above_maximum(self): """Test query rejects page > 99999.""" with pytest.raises(ValueError): - WorkflowLogQuery(page=100000) + WorkflowLogQuery.model_validate({"page": 100000}) def test_query_rejects_limit_below_minimum(self): """Test query rejects limit < 1.""" with pytest.raises(ValueError): - WorkflowLogQuery(limit=0) + WorkflowLogQuery.model_validate({"limit": 0}) def test_query_rejects_limit_above_maximum(self): """Test query rejects limit > 100.""" with pytest.raises(ValueError): - WorkflowLogQuery(limit=101) + WorkflowLogQuery.model_validate({"limit": 101}) def test_query_with_keyword_search(self): """Test query with keyword filter.""" @@ -199,6 +346,29 @@ class TestWorkflowLogQuery: assert query.created_at__after == "2024-01-01T00:00:00Z" +class TestWorkflowRunResponse: + def test_validates_workflow_run_object_shape_and_clears_paused_outputs(self): + run = _make_workflow_run(run_id="run-paused") + run.status = WorkflowExecutionStatus.PAUSED + run.outputs = json.dumps({"should": "not leak"}) + + result = WorkflowRunResponse.model_validate(run, from_attributes=True).model_dump(mode="json") + + assert result == { + "id": "run-paused", + "workflow_id": "wf-1", + "status": "paused", + "inputs": '{"input": "value"}', + "outputs": {}, + "error": None, + "total_steps": 1, + "total_tokens": 10, + "created_at": 1767225600, + "finished_at": 1767225600, + "elapsed_time": 0.1, + } + + class TestWorkflowAppService: """Test WorkflowAppService interface.""" @@ -215,17 +385,13 @@ class TestWorkflowAppService: @patch.object(WorkflowAppService, "get_paginate_workflow_app_logs") def test_get_paginate_workflow_app_logs_returns_pagination(self, mock_get_logs): """Test get_paginate_workflow_app_logs returns paginated result.""" - mock_pagination = Mock() - mock_pagination.data = [] - mock_pagination.page = 1 - mock_pagination.limit = 20 - mock_pagination.total = 0 - mock_get_logs.return_value = mock_pagination + pagination = _make_workflow_log_page() + mock_get_logs.return_value = pagination service = WorkflowAppService() result = service.get_paginate_workflow_app_logs( session=Mock(), - app_model=Mock(spec=App), + app_model=_make_app_model(), keyword=None, status=None, created_at_before=None, @@ -236,8 +402,7 @@ class TestWorkflowAppService: created_by_account=None, ) - assert result.page == 1 - assert result.limit == 20 + assert result == pagination class TestWorkflowExecutionStatus: @@ -268,10 +433,10 @@ class TestAppGenerateServiceWorkflow: mock_generate.return_value = {"result": "success"} result = AppGenerateService.generate( - app_model=Mock(spec=App), - user=Mock(), + app_model=_make_app_model(), + user=_make_end_user(), args={"inputs": {"key": "value"}, "workflow_id": "workflow_123"}, - invoke_from=Mock(), + invoke_from=InvokeFrom.SERVICE_API, streaming=False, ) @@ -285,10 +450,10 @@ class TestAppGenerateServiceWorkflow: with pytest.raises(WorkflowNotFoundError): AppGenerateService.generate( - app_model=Mock(spec=App), - user=Mock(), + app_model=_make_app_model(), + user=_make_end_user(), args={"workflow_id": "invalid_id"}, - invoke_from=Mock(), + invoke_from=InvokeFrom.SERVICE_API, streaming=False, ) @@ -299,10 +464,10 @@ class TestAppGenerateServiceWorkflow: with pytest.raises(IsDraftWorkflowError): AppGenerateService.generate( - app_model=Mock(spec=App), - user=Mock(), + app_model=_make_app_model(), + user=_make_end_user(), args={"workflow_id": "draft_workflow"}, - invoke_from=Mock(), + invoke_from=InvokeFrom.SERVICE_API, streaming=False, ) @@ -313,10 +478,10 @@ class TestAppGenerateServiceWorkflow: mock_generate.return_value = mock_stream result = AppGenerateService.generate( - app_model=Mock(spec=App), - user=Mock(), + app_model=_make_app_model(), + user=_make_end_user(), args={"inputs": {}, "response_mode": "streaming"}, - invoke_from=Mock(), + invoke_from=InvokeFrom.SERVICE_API, streaming=True, ) @@ -351,37 +516,33 @@ class TestWorkflowRunRepository: @patch("repositories.factory.DifyAPIRepositoryFactory.create_api_workflow_run_repository") def test_workflow_run_repository_get_by_id(self, mock_factory): """Test workflow run repository get_workflow_run_by_id method.""" - mock_repo = Mock() - mock_run = Mock() - mock_run.id = str(uuid.uuid4()) - mock_run.status = "succeeded" - mock_repo.get_workflow_run_by_id.return_value = mock_run - mock_factory.return_value = mock_repo + run = _make_workflow_run(run_id=str(uuid.uuid4())) + mock_factory.return_value = _WorkflowRunRepositoryStub(run=run) from repositories.factory import DifyAPIRepositoryFactory - repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(Mock()) + repo = DifyAPIRepositoryFactory.create_api_workflow_run_repository(sessionmaker()) result = repo.get_workflow_run_by_id(tenant_id="tenant_123", app_id="app_456", run_id="run_789") - assert result.status == "succeeded" + assert result == run class TestWorkflowRunDetailApi: def test_not_workflow_app(self, app: Flask) -> None: api = WorkflowRunDetailApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) + app_model = _make_app_model(mode=AppMode.CHAT) with app.test_request_context("/workflows/run/1", method="GET"): with pytest.raises(NotWorkflowAppError): handler(api, app_model=app_model, workflow_run_id="run") def test_success(self, monkeypatch: pytest.MonkeyPatch) -> None: - run = _make_mock_workflow_run(run_id="run") - repo = SimpleNamespace(get_workflow_run_by_id=lambda **_kwargs: run) + run = _make_workflow_run(run_id="run") + repo = _WorkflowRunRepositoryStub(run=run) workflow_module = sys.modules["controllers.service_api.app.workflow"] - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + monkeypatch.setattr(workflow_module, "db", _DbStub()) monkeypatch.setattr( DifyAPIRepositoryFactory, "create_api_workflow_run_repository", @@ -390,7 +551,7 @@ class TestWorkflowRunDetailApi: api = WorkflowRunDetailApi() handler = unwrap(api.get) - app_model = SimpleNamespace(mode=AppMode.WORKFLOW, tenant_id="t1", id="a1") + app_model = _make_app_model(app_id="a1", tenant_id="t1") result = handler(api, app_model=app_model, workflow_run_id="run") assert result["id"] == "run" @@ -402,8 +563,8 @@ class TestWorkflowRunApi: def test_not_workflow_app(self, app: Flask) -> None: api = WorkflowRunApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _make_app_model(mode=AppMode.CHAT) + end_user = _make_end_user() with app.test_request_context("/workflows/run", method="POST", json={"inputs": {}}): with pytest.raises(NotWorkflowAppError): @@ -418,8 +579,8 @@ class TestWorkflowRunApi: api = WorkflowRunApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.WORKFLOW) - end_user = SimpleNamespace() + app_model = _make_app_model() + end_user = _make_end_user() with app.test_request_context("/workflows/run", method="POST", json={"inputs": {}}): with pytest.raises(InvokeRateLimitHttpError): @@ -436,8 +597,8 @@ class TestWorkflowRunByIdApi: api = WorkflowRunByIdApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.WORKFLOW) - end_user = SimpleNamespace() + app_model = _make_app_model() + end_user = _make_end_user() with app.test_request_context("/workflows/1/run", method="POST", json={"inputs": {}}): with pytest.raises(NotFound): @@ -452,8 +613,8 @@ class TestWorkflowRunByIdApi: api = WorkflowRunByIdApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.WORKFLOW) - end_user = SimpleNamespace() + app_model = _make_app_model() + end_user = _make_end_user() with app.test_request_context("/workflows/1/run", method="POST", json={"inputs": {}}): with pytest.raises(BadRequest): @@ -464,8 +625,8 @@ class TestWorkflowTaskStopApi: def test_wrong_mode(self, app: Flask) -> None: api = WorkflowTaskStopApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.CHAT.value) - end_user = SimpleNamespace() + app_model = _make_app_model(mode=AppMode.CHAT) + end_user = _make_end_user() with app.test_request_context("/workflows/tasks/1/stop", method="POST"): with pytest.raises(NotWorkflowAppError): @@ -479,8 +640,8 @@ class TestWorkflowTaskStopApi: api = WorkflowTaskStopApi() handler = unwrap(api.post) - app_model = SimpleNamespace(mode=AppMode.WORKFLOW) - end_user = SimpleNamespace(id="u1") + app_model = _make_app_model() + end_user = _make_end_user(user_id="u1") with app.test_request_context("/workflows/tasks/1/stop", method="POST"): response = handler(api, app_model=app_model, end_user=end_user, task_id="t1") @@ -492,37 +653,36 @@ class TestWorkflowTaskStopApi: class TestWorkflowAppLogApi: def test_success(self, app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - class _BeginStub: - def __enter__(self): - return SimpleNamespace() - - def __exit__(self, exc_type, exc, tb): - return False - - class _SessionMakerStub: - def __init__(self, *args, **kwargs): - pass - - def begin(self): - return _BeginStub() - workflow_module = sys.modules["controllers.service_api.app.workflow"] - monkeypatch.setattr(workflow_module, "db", SimpleNamespace(engine=object())) + workflow_model_module = sys.modules["models.workflow"] + monkeypatch.setattr(workflow_module, "db", _DbStub()) + monkeypatch.setattr(workflow_model_module, "db", _DbStub()) monkeypatch.setattr(workflow_module, "sessionmaker", _SessionMakerStub) monkeypatch.setattr( WorkflowAppService, "get_paginate_workflow_app_logs", - lambda *_args, **_kwargs: {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}, + lambda *_args, **_kwargs: _make_workflow_log_page(), + ) + monkeypatch.setattr( + DifyAPIRepositoryFactory, + "create_api_workflow_run_repository", + lambda *_args, **_kwargs: _WorkflowRunRepositoryStub( + run=_make_workflow_run( + run_id="log-run-1", + created_at=datetime(2026, 1, 1, 1, tzinfo=UTC), + finished_at=datetime(2026, 1, 1, 1, 0, 2, tzinfo=UTC), + ) + ), ) api = WorkflowAppLogApi() handler = unwrap(api.get) - app_model = SimpleNamespace(id="a1") + app_model = _make_app_model(app_id="a1") with app.test_request_context("/workflows/logs", method="GET"): response = handler(api, app_model=app_model) - assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []} + assert response == _expected_workflow_log_pagination_payload() # ============================================================================= @@ -536,12 +696,8 @@ class TestWorkflowAppLogApi: @pytest.fixture -def mock_workflow_app(): - app = Mock(spec=App) - app.id = str(uuid.uuid4()) - app.tenant_id = str(uuid.uuid4()) - app.mode = AppMode.WORKFLOW - return app +def workflow_app() -> App: + return _make_app_model(app_id=str(uuid.uuid4()), tenant_id=str(uuid.uuid4())) class TestWorkflowRunDetailApiGet: @@ -558,38 +714,46 @@ class TestWorkflowRunDetailApiGet: mock_db, mock_repo_factory, app: Flask, - mock_workflow_app, + workflow_app: App, ): """Test successful workflow run detail retrieval.""" - mock_run = _make_mock_workflow_run(run_id="run-1") - mock_repo = Mock() - mock_repo.get_workflow_run_by_id.return_value = mock_run - mock_repo_factory.create_api_workflow_run_repository.return_value = mock_repo + run = _make_workflow_run(run_id="run-1") + mock_repo_factory.create_api_workflow_run_repository.return_value = _WorkflowRunRepositoryStub(run=run) from controllers.service_api.app.workflow import WorkflowRunDetailApi with app.test_request_context( - f"/workflows/run/{mock_run.id}", + f"/workflows/run/{run.id}", method="GET", ): api = WorkflowRunDetailApi() - result = unwrap(api.get)(api, app_model=mock_workflow_app, workflow_run_id=mock_run.id) + result = unwrap(api.get)(api, app_model=workflow_app, workflow_run_id=run.id) - assert result["id"] == mock_run.id - assert result["status"] == "succeeded" + assert result == { + "id": "run-1", + "workflow_id": "wf-1", + "status": "succeeded", + "inputs": '{"input": "value"}', + "outputs": {"output": "value"}, + "error": None, + "total_steps": 1, + "total_tokens": 10, + "created_at": 1767225600, + "finished_at": 1767225600, + "elapsed_time": 0.1, + } @patch("controllers.service_api.app.workflow.db") def test_get_workflow_run_wrong_app_mode(self, mock_db, app: Flask): """Test NotWorkflowAppError when app mode is not workflow or advanced_chat.""" from controllers.service_api.app.workflow import WorkflowRunDetailApi - mock_app = Mock(spec=App) - mock_app.mode = AppMode.CHAT.value + app_model = _make_app_model(mode=AppMode.CHAT) with app.test_request_context("/workflows/run/run-1", method="GET"): api = WorkflowRunDetailApi() with pytest.raises(NotWorkflowAppError): - unwrap(api.get)(api, app_model=mock_app, workflow_run_id="run-1") + unwrap(api.get)(api, app_model=app_model, workflow_run_id="run-1") class TestWorkflowTaskStopApiPost: @@ -605,7 +769,7 @@ class TestWorkflowTaskStopApiPost: mock_queue_mgr, mock_graph_mgr, app: Flask, - mock_workflow_app, + workflow_app: App, ): """Test successful workflow task stop.""" from controllers.service_api.app.workflow import WorkflowTaskStopApi @@ -614,8 +778,8 @@ class TestWorkflowTaskStopApiPost: api = WorkflowTaskStopApi() result = unwrap(api.post)( api, - app_model=mock_workflow_app, - end_user=Mock(), + app_model=workflow_app, + end_user=_make_end_user(), task_id="task-1", ) @@ -628,13 +792,12 @@ class TestWorkflowTaskStopApiPost: """Test NotWorkflowAppError when app mode is not workflow.""" from controllers.service_api.app.workflow import WorkflowTaskStopApi - mock_app = Mock(spec=App) - mock_app.mode = AppMode.COMPLETION.value + app_model = _make_app_model(mode=AppMode.COMPLETION) with app.test_request_context("/workflows/tasks/task-1/stop", method="POST"): api = WorkflowTaskStopApi() with pytest.raises(NotWorkflowAppError): - unwrap(api.post)(api, app_model=mock_app, end_user=Mock(), task_id="task-1") + unwrap(api.post)(api, app_model=app_model, end_user=_make_end_user(), task_id="task-1") class TestWorkflowAppLogApiGet: @@ -650,27 +813,23 @@ class TestWorkflowAppLogApiGet: mock_db, mock_wf_svc_cls, app: Flask, - mock_workflow_app, + workflow_app: App, ): """Test successful workflow log retrieval.""" - mock_pagination = Mock() - mock_pagination.page = 1 - mock_pagination.limit = 20 - mock_pagination.total = 0 - mock_pagination.has_more = False - mock_pagination.data = [] mock_svc_instance = Mock() - mock_svc_instance.get_paginate_workflow_app_logs.return_value = mock_pagination + mock_svc_instance.get_paginate_workflow_app_logs.return_value = _make_workflow_log_page() mock_wf_svc_cls.return_value = mock_svc_instance + mock_repo = _WorkflowRunRepositoryStub( + run=_make_workflow_run( + run_id="log-run-1", + created_at=datetime(2026, 1, 1, 1, tzinfo=UTC), + finished_at=datetime(2026, 1, 1, 1, 0, 2, tzinfo=UTC), + ) + ) # Mock sessionmaker(...).begin() context manager - mock_session = Mock() - mock_db.engine = Mock() - mock_begin = Mock() - mock_begin.__enter__ = Mock(return_value=mock_session) - mock_begin.__exit__ = Mock(return_value=False) - mock_session_factory = Mock() - mock_session_factory.begin.return_value = mock_begin + mock_db.engine = object() + mock_db.session.get.return_value = None from controllers.service_api.app.workflow import WorkflowAppLogApi @@ -678,8 +837,15 @@ class TestWorkflowAppLogApiGet: "/workflows/logs?page=1&limit=20", method="GET", ): - with patch("controllers.service_api.app.workflow.sessionmaker", return_value=mock_session_factory): + with ( + patch("controllers.service_api.app.workflow.sessionmaker", _SessionMakerStub), + patch("models.workflow.db", _DbStub()), + patch( + "repositories.factory.DifyAPIRepositoryFactory.create_api_workflow_run_repository", + return_value=mock_repo, + ), + ): api = WorkflowAppLogApi() - result = unwrap(api.get)(api, app_model=mock_workflow_app) + result = unwrap(api.get)(api, app_model=workflow_app) - assert result == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []} + assert result == _expected_workflow_log_pagination_payload() diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py index eda270258d5..caa6ddc0d93 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py @@ -1,25 +1,36 @@ -from types import SimpleNamespace - -from controllers.service_api.app.workflow import WorkflowRunOutputsField, WorkflowRunStatusField +from controllers.service_api.app.workflow import WorkflowRunResponse from graphon.enums import WorkflowExecutionStatus +from libs.helper import dump_response +from models.workflow import WorkflowRun -def test_workflow_run_status_field_with_enum() -> None: - field = WorkflowRunStatusField() - obj = SimpleNamespace(status=WorkflowExecutionStatus.PAUSED) - - assert field.output("status", obj) == "paused" +def _workflow_run(status: WorkflowExecutionStatus, outputs: str | None = '{"foo": "bar"}') -> WorkflowRun: + return WorkflowRun( + id="run-id", + workflow_id="workflow-id", + status=status, + inputs="{}", + outputs=outputs, + error=None, + total_steps=1, + total_tokens=2, + elapsed_time=3.5, + ) -def test_workflow_run_outputs_field_paused_returns_empty() -> None: - field = WorkflowRunOutputsField() - obj = SimpleNamespace(status=WorkflowExecutionStatus.PAUSED, outputs_dict={"foo": "bar"}) +def test_workflow_run_serializer_normalizes_status_enum() -> None: + response = dump_response(WorkflowRunResponse, _workflow_run(WorkflowExecutionStatus.PAUSED)) - assert field.output("outputs", obj) == {} + assert response["status"] == "paused" -def test_workflow_run_outputs_field_running_returns_outputs() -> None: - field = WorkflowRunOutputsField() - obj = SimpleNamespace(status=WorkflowExecutionStatus.RUNNING, outputs_dict={"foo": "bar"}) +def test_workflow_run_serializer_paused_returns_empty_outputs() -> None: + response = dump_response(WorkflowRunResponse, _workflow_run(WorkflowExecutionStatus.PAUSED)) - assert field.output("outputs", obj) == {"foo": "bar"} + assert response["outputs"] == {} + + +def test_workflow_run_serializer_running_returns_outputs() -> None: + response = dump_response(WorkflowRunResponse, _workflow_run(WorkflowExecutionStatus.RUNNING)) + + assert response["outputs"] == {"foo": "bar"} diff --git a/packages/contracts/generated/api/console/account/types.gen.ts b/packages/contracts/generated/api/console/account/types.gen.ts index cdd45925fb2..1a7d1644f8f 100644 --- a/packages/contracts/generated/api/console/account/types.gen.ts +++ b/packages/contracts/generated/api/console/account/types.gen.ts @@ -12,7 +12,7 @@ export type AccountAvatarPayload = { avatar: string } -export type Account = { +export type AccountResponse = { avatar?: string | null readonly avatar_url: string | null created_at?: number | null @@ -140,7 +140,7 @@ export type AccountIntegrateResponse = { provider: string } -export type AccountWritable = { +export type AccountResponseWritable = { avatar?: string | null created_at?: number | null email: string @@ -177,7 +177,7 @@ export type PostAccountAvatarData = { } export type PostAccountAvatarResponses = { - 200: Account + 200: AccountResponse } export type PostAccountAvatarResponse = PostAccountAvatarResponses[keyof PostAccountAvatarResponses] @@ -218,7 +218,7 @@ export type PostAccountChangeEmailResetData = { } export type PostAccountChangeEmailResetResponses = { - 200: Account + 200: AccountResponse } export type PostAccountChangeEmailResetResponse @@ -374,7 +374,7 @@ export type PostAccountInterfaceLanguageData = { } export type PostAccountInterfaceLanguageResponses = { - 200: Account + 200: AccountResponse } export type PostAccountInterfaceLanguageResponse @@ -388,7 +388,7 @@ export type PostAccountInterfaceThemeData = { } export type PostAccountInterfaceThemeResponses = { - 200: Account + 200: AccountResponse } export type PostAccountInterfaceThemeResponse @@ -402,7 +402,7 @@ export type PostAccountNameData = { } export type PostAccountNameResponses = { - 200: Account + 200: AccountResponse } export type PostAccountNameResponse = PostAccountNameResponses[keyof PostAccountNameResponses] @@ -415,7 +415,7 @@ export type PostAccountPasswordData = { } export type PostAccountPasswordResponses = { - 200: Account + 200: AccountResponse } export type PostAccountPasswordResponse @@ -429,7 +429,7 @@ export type GetAccountProfileData = { } export type GetAccountProfileResponses = { - 200: Account + 200: AccountResponse } export type GetAccountProfileResponse = GetAccountProfileResponses[keyof GetAccountProfileResponses] @@ -442,7 +442,7 @@ export type PostAccountTimezoneData = { } export type PostAccountTimezoneResponses = { - 200: Account + 200: AccountResponse } export type PostAccountTimezoneResponse diff --git a/packages/contracts/generated/api/console/account/zod.gen.ts b/packages/contracts/generated/api/console/account/zod.gen.ts index 9951efc8d9f..cba539b0316 100644 --- a/packages/contracts/generated/api/console/account/zod.gen.ts +++ b/packages/contracts/generated/api/console/account/zod.gen.ts @@ -17,9 +17,9 @@ export const zAccountAvatarPayload = z.object({ }) /** - * Account + * AccountResponse */ -export const zAccount = z.object({ +export const zAccountResponse = z.object({ avatar: z.string().nullish(), avatar_url: z.string().nullable(), created_at: z.int().nullish(), @@ -212,9 +212,9 @@ export const zAccountIntegrateListResponse = z.object({ }) /** - * Account + * AccountResponse */ -export const zAccountWritable = z.object({ +export const zAccountResponseWritable = z.object({ avatar: z.string().nullish(), created_at: z.int().nullish(), email: z.string(), @@ -242,7 +242,7 @@ export const zPostAccountAvatarBody = zAccountAvatarPayload /** * Success */ -export const zPostAccountAvatarResponse = zAccount +export const zPostAccountAvatarResponse = zAccountResponse export const zPostAccountChangeEmailBody = zChangeEmailSendPayload @@ -263,7 +263,7 @@ export const zPostAccountChangeEmailResetBody = zChangeEmailResetPayload /** * Success */ -export const zPostAccountChangeEmailResetResponse = zAccount +export const zPostAccountChangeEmailResetResponse = zAccountResponse export const zPostAccountChangeEmailValidityBody = zChangeEmailValidityPayload @@ -336,37 +336,37 @@ export const zPostAccountInterfaceLanguageBody = zAccountInterfaceLanguagePayloa /** * Success */ -export const zPostAccountInterfaceLanguageResponse = zAccount +export const zPostAccountInterfaceLanguageResponse = zAccountResponse export const zPostAccountInterfaceThemeBody = zAccountInterfaceThemePayload /** * Success */ -export const zPostAccountInterfaceThemeResponse = zAccount +export const zPostAccountInterfaceThemeResponse = zAccountResponse export const zPostAccountNameBody = zAccountNamePayload /** * Success */ -export const zPostAccountNameResponse = zAccount +export const zPostAccountNameResponse = zAccountResponse export const zPostAccountPasswordBody = zAccountPasswordPayload /** * Success */ -export const zPostAccountPasswordResponse = zAccount +export const zPostAccountPasswordResponse = zAccountResponse /** * Success */ -export const zGetAccountProfileResponse = zAccount +export const zGetAccountProfileResponse = zAccountResponse export const zPostAccountTimezoneBody = zAccountTimezonePayload /** * Success */ -export const zPostAccountTimezoneResponse = zAccount +export const zPostAccountTimezoneResponse = zAccountResponse diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 9e79518f3cd..3e7826b7d6f 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -731,7 +731,7 @@ export type WorkflowRunPaginationResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -799,7 +799,7 @@ export type WorkflowCommentCreate = { } export type WorkflowCommentMentionUsersPayload = { - users: Array + users: Array } export type WorkflowCommentDetail = { @@ -887,7 +887,7 @@ export type DefaultBlockConfigResponse = { export type WorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount | null + created_by?: SimpleAccountResponse | null environment_variables: Array features: { [key: string]: unknown @@ -902,7 +902,7 @@ export type WorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount | null + updated_by?: SimpleAccountResponse | null version: string } @@ -1029,7 +1029,7 @@ export type AgentComposerValidateResponse = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -1289,7 +1289,7 @@ export type WorkflowOnlineUsersByApp = { export type AdvancedChatWorkflowRunForListResponse = { conversation_id?: string | null created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -1596,7 +1596,7 @@ export type UserSatisfactionRateStatisticItem = { export type WorkflowAppLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null created_from?: string | null @@ -1607,7 +1607,7 @@ export type WorkflowAppLogPartialResponse = { export type WorkflowArchivedLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null id: string trigger_metadata?: unknown @@ -1616,7 +1616,7 @@ export type WorkflowArchivedLogPartialResponse = { export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -1628,7 +1628,7 @@ export type WorkflowRunForListResponse = { version?: string | null } -export type SimpleAccount = { +export type SimpleAccountResponse = { email: string id: string name: string @@ -1671,7 +1671,7 @@ export type WorkflowCommentBasic = { updated_at?: number | null } -export type AccountWithRole = { +export type AccountWithRoleResponse = { avatar?: string | null created_at?: number | null email: string @@ -2054,6 +2054,12 @@ export type SimpleMessageDetail = { query: string } +export type SimpleAccount = { + email: string + id: string + name: string +} + export type HumanInputFormDefinition = { actions?: Array display_in_ui?: boolean diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index 9b86fda0a62..5265e907333 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -1319,9 +1319,9 @@ export const zUserSatisfactionRateStatisticResponse = z.object({ }) /** - * SimpleAccount + * SimpleAccountResponse */ -export const zSimpleAccount = z.object({ +export const zSimpleAccountResponse = z.object({ email: z.string(), id: z.string(), name: z.string(), @@ -1333,7 +1333,7 @@ export const zSimpleAccount = z.object({ export const zAdvancedChatWorkflowRunForListResponse = z.object({ conversation_id: z.string().nullish(), created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -1355,72 +1355,12 @@ export const zAdvancedChatWorkflowRunPaginationResponse = z.object({ limit: z.int(), }) -/** - * ConversationAnnotation - */ -export const zConversationAnnotation = z.object({ - account: zSimpleAccount.nullish(), - content: z.string(), - created_at: z.int().nullish(), - id: z.string(), - question: z.string().nullish(), -}) - -/** - * ConversationAnnotationHitHistory - */ -export const zConversationAnnotationHitHistory = z.object({ - annotation_create_account: zSimpleAccount.nullish(), - created_at: z.int().nullish(), - id: z.string(), -}) - -/** - * Feedback - */ -export const zFeedback = z.object({ - content: z.string().nullish(), - from_account: zSimpleAccount.nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - rating: z.string(), -}) - -/** - * MessageDetail - */ -export const zMessageDetail = z.object({ - agent_thoughts: z.array(zAgentThought), - annotation: zConversationAnnotation.nullish(), - annotation_hit_history: zConversationAnnotationHitHistory.nullish(), - answer_tokens: z.int(), - conversation_id: z.string(), - created_at: z.int().nullish(), - error: z.string().nullish(), - feedbacks: z.array(zFeedback), - from_account_id: z.string().nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - id: z.string(), - inputs: z.record(z.string(), zJsonValue), - message: zJsonValue, - message_files: z.array(zMessageFile), - message_metadata_dict: zJsonValue, - message_tokens: z.int(), - parent_message_id: z.string().nullish(), - provider_response_latency: z.number(), - query: z.string(), - re_sign_file_url_answer: z.string(), - status: z.string(), - workflow_run_id: z.string().nullish(), -}) - /** * WorkflowRunForListResponse */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -1456,7 +1396,7 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -1478,7 +1418,7 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -1544,9 +1484,9 @@ export const zSandboxUploadResponse = z.object({ }) /** - * AccountWithRole + * AccountWithRoleResponse */ -export const zAccountWithRole = z.object({ +export const zAccountWithRoleResponse = z.object({ avatar: z.string().nullish(), created_at: z.int().nullish(), email: z.string(), @@ -1563,7 +1503,7 @@ export const zAccountWithRole = z.object({ * WorkflowCommentMentionUsersPayload */ export const zWorkflowCommentMentionUsersPayload = z.object({ - users: z.array(zAccountWithRole), + users: z.array(zAccountWithRoleResponse), }) /** @@ -1752,7 +1692,7 @@ export const zPipelineVariableResponse = z.object({ export const zWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.nullish(), + created_by: zSimpleAccountResponse.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -1763,7 +1703,7 @@ export const zWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.nullish(), + updated_by: zSimpleAccountResponse.nullish(), version: z.string(), }) @@ -2152,20 +2092,6 @@ export const zConversationDetail = z.object({ user_feedback_stats: zFeedbackStat.nullish(), }) -/** - * ConversationMessageDetail - */ -export const zConversationMessageDetail = z.object({ - created_at: z.int().nullish(), - first_message: zMessageDetail.nullish(), - from_account_id: z.string().nullish(), - from_end_user_id: z.string().nullish(), - from_source: z.string(), - id: z.string(), - model_config: zModelConfig.nullish(), - status: z.string(), -}) - /** * Type */ @@ -2366,6 +2292,26 @@ export const zSimpleMessageDetail = z.object({ query: z.string(), }) +/** + * SimpleAccount + */ +export const zSimpleAccount = z.object({ + email: z.string(), + id: z.string(), + name: z.string(), +}) + +/** + * ConversationAnnotation + */ +export const zConversationAnnotation = z.object({ + account: zSimpleAccount.nullish(), + content: z.string(), + created_at: z.int().nullish(), + id: z.string(), + question: z.string().nullish(), +}) + /** * Conversation */ @@ -2398,6 +2344,69 @@ export const zConversationPagination = z.object({ total: z.int(), }) +/** + * ConversationAnnotationHitHistory + */ +export const zConversationAnnotationHitHistory = z.object({ + annotation_create_account: zSimpleAccount.nullish(), + created_at: z.int().nullish(), + id: z.string(), +}) + +/** + * Feedback + */ +export const zFeedback = z.object({ + content: z.string().nullish(), + from_account: zSimpleAccount.nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + rating: z.string(), +}) + +/** + * MessageDetail + */ +export const zMessageDetail = z.object({ + agent_thoughts: z.array(zAgentThought), + annotation: zConversationAnnotation.nullish(), + annotation_hit_history: zConversationAnnotationHitHistory.nullish(), + answer_tokens: z.int(), + conversation_id: z.string(), + created_at: z.int().nullish(), + error: z.string().nullish(), + feedbacks: z.array(zFeedback), + from_account_id: z.string().nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + id: z.string(), + inputs: z.record(z.string(), zJsonValue), + message: zJsonValue, + message_files: z.array(zMessageFile), + message_metadata_dict: zJsonValue, + message_tokens: z.int(), + parent_message_id: z.string().nullish(), + provider_response_latency: z.number(), + query: z.string(), + re_sign_file_url_answer: z.string(), + status: z.string(), + workflow_run_id: z.string().nullish(), +}) + +/** + * ConversationMessageDetail + */ +export const zConversationMessageDetail = z.object({ + created_at: z.int().nullish(), + first_message: zMessageDetail.nullish(), + from_account_id: z.string().nullish(), + from_end_user_id: z.string().nullish(), + from_source: z.string(), + id: z.string(), + model_config: zModelConfig.nullish(), + status: z.string(), +}) + /** * ExecutionContentType */ @@ -2425,7 +2434,7 @@ export const zWorkflowRunForLogResponse = z.object({ */ export const zWorkflowAppLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), created_from: z.string().nullish(), @@ -2461,7 +2470,7 @@ export const zWorkflowRunForArchivedLogResponse = z.object({ */ export const zWorkflowArchivedLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), id: z.string(), trigger_metadata: z.unknown().optional(), diff --git a/packages/contracts/generated/api/console/rag/types.gen.ts b/packages/contracts/generated/api/console/rag/types.gen.ts index b9862a8d1e8..5a6c903c383 100644 --- a/packages/contracts/generated/api/console/rag/types.gen.ts +++ b/packages/contracts/generated/api/console/rag/types.gen.ts @@ -119,7 +119,7 @@ export type SimpleResultResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -158,7 +158,7 @@ export type DefaultBlockConfigResponse = { export type WorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount | null + created_by?: SimpleAccountResponse | null environment_variables: Array features: { [key: string]: unknown @@ -173,7 +173,7 @@ export type WorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount | null + updated_by?: SimpleAccountResponse | null version: string } @@ -221,7 +221,7 @@ export type DatasourceVariablesPayload = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -421,7 +421,7 @@ export type PluginDependency = { export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -433,7 +433,7 @@ export type WorkflowRunForListResponse = { version?: string | null } -export type SimpleAccount = { +export type SimpleAccountResponse = { email: string id: string name: string diff --git a/packages/contracts/generated/api/console/rag/zod.gen.ts b/packages/contracts/generated/api/console/rag/zod.gen.ts index 717db30baa7..5edc412926a 100644 --- a/packages/contracts/generated/api/console/rag/zod.gen.ts +++ b/packages/contracts/generated/api/console/rag/zod.gen.ts @@ -322,9 +322,9 @@ export const zPipelineTemplateListResponse = z.object({ }) /** - * SimpleAccount + * SimpleAccountResponse */ -export const zSimpleAccount = z.object({ +export const zSimpleAccountResponse = z.object({ email: z.string(), id: z.string(), name: z.string(), @@ -335,7 +335,7 @@ export const zSimpleAccount = z.object({ */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -371,7 +371,7 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -393,7 +393,7 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -471,7 +471,7 @@ export const zPipelineVariableResponse = z.object({ export const zWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.nullish(), + created_by: zSimpleAccountResponse.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -482,7 +482,7 @@ export const zWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.nullish(), + updated_by: zSimpleAccountResponse.nullish(), version: z.string(), }) diff --git a/packages/contracts/generated/api/console/snippets/types.gen.ts b/packages/contracts/generated/api/console/snippets/types.gen.ts index 631da7bace8..2fc256bcbb4 100644 --- a/packages/contracts/generated/api/console/snippets/types.gen.ts +++ b/packages/contracts/generated/api/console/snippets/types.gen.ts @@ -16,7 +16,7 @@ export type SimpleResultResponse = { export type WorkflowRunDetailResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -51,7 +51,7 @@ export type DefaultBlockConfigsResponse = Array<{ export type SnippetWorkflowResponse = { conversation_variables: Array created_at: number - created_by?: SimpleAccount | null + created_by?: SimpleAccountResponse | null environment_variables: Array features: { [key: string]: unknown @@ -69,7 +69,7 @@ export type SnippetWorkflowResponse = { rag_pipeline_variables: Array tool_published: boolean updated_at: number - updated_by?: SimpleAccount | null + updated_by?: SimpleAccountResponse | null version: string } @@ -120,7 +120,7 @@ export type SnippetLoopNodeRunPayload = { export type WorkflowRunNodeExecutionResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null elapsed_time?: number | null @@ -210,7 +210,7 @@ export type WorkflowPublishResponse = { export type WorkflowRunForListResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null elapsed_time?: number | null exceptions_count?: number | null finished_at?: number | null @@ -222,7 +222,7 @@ export type WorkflowRunForListResponse = { version?: string | null } -export type SimpleAccount = { +export type SimpleAccountResponse = { email: string id: string name: string diff --git a/packages/contracts/generated/api/console/snippets/zod.gen.ts b/packages/contracts/generated/api/console/snippets/zod.gen.ts index 85e5b961547..303fb07c66a 100644 --- a/packages/contracts/generated/api/console/snippets/zod.gen.ts +++ b/packages/contracts/generated/api/console/snippets/zod.gen.ts @@ -134,9 +134,9 @@ export const zWorkflowPublishResponse = z.object({ }) /** - * SimpleAccount + * SimpleAccountResponse */ -export const zSimpleAccount = z.object({ +export const zSimpleAccountResponse = z.object({ email: z.string(), id: z.string(), name: z.string(), @@ -147,7 +147,7 @@ export const zSimpleAccount = z.object({ */ export const zWorkflowRunForListResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), elapsed_time: z.number().nullish(), exceptions_count: z.int().nullish(), finished_at: z.int().nullish(), @@ -183,7 +183,7 @@ export const zSimpleEndUser = z.object({ */ export const zWorkflowRunDetailResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -205,7 +205,7 @@ export const zWorkflowRunDetailResponse = z.object({ */ export const zWorkflowRunNodeExecutionResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), elapsed_time: z.number().nullish(), @@ -283,7 +283,7 @@ export const zPipelineVariableResponse = z.object({ export const zSnippetWorkflowResponse = z.object({ conversation_variables: z.array(zWorkflowConversationVariableResponse), created_at: z.int(), - created_by: zSimpleAccount.nullish(), + created_by: zSimpleAccountResponse.nullish(), environment_variables: z.array(zWorkflowEnvironmentVariableResponse), features: z.record(z.string(), z.unknown()), graph: z.record(z.string(), z.unknown()), @@ -295,7 +295,7 @@ export const zSnippetWorkflowResponse = z.object({ rag_pipeline_variables: z.array(zPipelineVariableResponse), tool_published: z.boolean(), updated_at: z.int(), - updated_by: zSimpleAccount.nullish(), + updated_by: zSimpleAccountResponse.nullish(), version: z.string(), }) diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 29f23567e95..f1c9ca32a61 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -104,8 +104,8 @@ export type SnippetUseCountResponse = { use_count: number } -export type AccountWithRoleList = { - accounts: Array +export type AccountWithRoleListResponse = { + accounts: Array } export type DefaultModelDataResponse = { @@ -921,7 +921,7 @@ export type AnonymousInlineModel7B8B49Ca164e = { type?: string } -export type AccountWithRole = { +export type AccountWithRoleResponse = { avatar?: string | null created_at?: number | null email: string @@ -1777,7 +1777,7 @@ export type GetWorkspacesCurrentDatasetOperatorsData = { } export type GetWorkspacesCurrentDatasetOperatorsResponses = { - 200: AccountWithRoleList + 200: AccountWithRoleListResponse } export type GetWorkspacesCurrentDatasetOperatorsResponse @@ -2004,7 +2004,7 @@ export type GetWorkspacesCurrentMembersData = { } export type GetWorkspacesCurrentMembersResponses = { - 200: AccountWithRoleList + 200: AccountWithRoleListResponse } export type GetWorkspacesCurrentMembersResponse diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index fb6f643d7a9..35a6dede889 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -889,9 +889,9 @@ export const zSnippetPagination = z.object({ }) /** - * AccountWithRole + * AccountWithRoleResponse */ -export const zAccountWithRole = z.object({ +export const zAccountWithRoleResponse = z.object({ avatar: z.string().nullish(), created_at: z.int().nullish(), email: z.string(), @@ -905,10 +905,10 @@ export const zAccountWithRole = z.object({ }) /** - * AccountWithRoleList + * AccountWithRoleListResponse */ -export const zAccountWithRoleList = z.object({ - accounts: z.array(zAccountWithRole), +export const zAccountWithRoleListResponse = z.object({ + accounts: z.array(zAccountWithRoleResponse), }) /** @@ -2274,7 +2274,7 @@ export const zPostWorkspacesCurrentCustomizedSnippetsBySnippetIdUseCountIncremen /** * Success */ -export const zGetWorkspacesCurrentDatasetOperatorsResponse = zAccountWithRoleList +export const zGetWorkspacesCurrentDatasetOperatorsResponse = zAccountWithRoleListResponse export const zGetWorkspacesCurrentDefaultModelQuery = z.object({ model_type: z.enum(['llm', 'moderation', 'rerank', 'speech2text', 'text-embedding', 'tts']), @@ -2378,7 +2378,7 @@ export const zPatchWorkspacesCurrentEndpointsByIdResponse = zEndpointUpdateRespo /** * Success */ -export const zGetWorkspacesCurrentMembersResponse = zAccountWithRoleList +export const zGetWorkspacesCurrentMembersResponse = zAccountWithRoleListResponse export const zPostWorkspacesCurrentMembersInviteEmailBody = zMemberInvitePayload diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index 97921643514..78096915f08 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -19,7 +19,7 @@ export type AgentThought = { } export type Annotation = { - content?: string | null + answer?: string | null created_at?: number | null hit_count?: number | null id: string @@ -31,10 +31,15 @@ export type AnnotationCreatePayload = { question: string } -export type AnnotationJobStatusResponse = { - error_msg?: string | null +export type AnnotationJobStatusDetailResponse = { + error_msg?: string job_id: string - job_status: string + job_status: 'completed' | 'error' | 'processing' | 'waiting' | string +} + +export type AnnotationJobStatusResponse = { + job_id: string + job_status: 'completed' | 'error' | 'processing' | 'waiting' | string } export type AnnotationList = { @@ -1414,7 +1419,7 @@ export type SelectInputConfig = { type?: 'select' } -export type SimpleAccount = { +export type SimpleAccountResponse = { email: string id: string name: string @@ -1572,7 +1577,7 @@ export type WorkflowAppLogPaginationResponse = { export type WorkflowAppLogPartialResponse = { created_at?: number | null - created_by_account?: SimpleAccount | null + created_by_account?: SimpleAccountResponse | null created_by_end_user?: SimpleEndUser | null created_by_role?: string | null created_from?: string | null @@ -1768,7 +1773,7 @@ export type GetAppsAnnotationReplyByActionStatusByJobIdErrors = { } export type GetAppsAnnotationReplyByActionStatusByJobIdResponses = { - 200: AnnotationJobStatusResponse + 200: AnnotationJobStatusDetailResponse } export type GetAppsAnnotationReplyByActionStatusByJobIdResponse @@ -1893,16 +1898,32 @@ export type PostChatMessagesData = { } export type PostChatMessagesErrors = { - 400: unknown - 401: unknown - 403: unknown - 404: unknown - 429: unknown - 500: unknown + 400: { + [key: string]: unknown + } + 401: { + [key: string]: unknown + } + 403: { + [key: string]: unknown + } + 404: { + [key: string]: unknown + } + 429: { + [key: string]: unknown + } + 500: { + [key: string]: unknown + } } +export type PostChatMessagesError = PostChatMessagesErrors[keyof PostChatMessagesErrors] + export type PostChatMessagesResponses = { - 200: GeneratedAppResponse + 200: { + [key: string]: unknown + } } export type PostChatMessagesResponse = PostChatMessagesResponses[keyof PostChatMessagesResponses] @@ -1938,16 +1959,33 @@ export type PostCompletionMessagesData = { } export type PostCompletionMessagesErrors = { - 400: unknown - 401: unknown - 403: unknown - 404: unknown - 429: unknown - 500: unknown + 400: { + [key: string]: unknown + } + 401: { + [key: string]: unknown + } + 403: { + [key: string]: unknown + } + 404: { + [key: string]: unknown + } + 429: { + [key: string]: unknown + } + 500: { + [key: string]: unknown + } } +export type PostCompletionMessagesError + = PostCompletionMessagesErrors[keyof PostCompletionMessagesErrors] + export type PostCompletionMessagesResponses = { - 200: GeneratedAppResponse + 200: { + [key: string]: unknown + } } export type PostCompletionMessagesResponse @@ -3622,16 +3660,32 @@ export type PostWorkflowsRunData = { } export type PostWorkflowsRunErrors = { - 400: unknown - 401: unknown - 403: unknown - 404: unknown - 429: unknown - 500: unknown + 400: { + [key: string]: unknown + } + 401: { + [key: string]: unknown + } + 403: { + [key: string]: unknown + } + 404: { + [key: string]: unknown + } + 429: { + [key: string]: unknown + } + 500: { + [key: string]: unknown + } } +export type PostWorkflowsRunError = PostWorkflowsRunErrors[keyof PostWorkflowsRunErrors] + export type PostWorkflowsRunResponses = { - 200: GeneratedAppResponse + 200: { + [key: string]: unknown + } } export type PostWorkflowsRunResponse = PostWorkflowsRunResponses[keyof PostWorkflowsRunResponses] @@ -3692,16 +3746,33 @@ export type PostWorkflowsByWorkflowIdRunData = { } export type PostWorkflowsByWorkflowIdRunErrors = { - 400: unknown - 401: unknown - 403: unknown - 404: unknown - 429: unknown - 500: unknown + 400: { + [key: string]: unknown + } + 401: { + [key: string]: unknown + } + 403: { + [key: string]: unknown + } + 404: { + [key: string]: unknown + } + 429: { + [key: string]: unknown + } + 500: { + [key: string]: unknown + } } +export type PostWorkflowsByWorkflowIdRunError + = PostWorkflowsByWorkflowIdRunErrors[keyof PostWorkflowsByWorkflowIdRunErrors] + export type PostWorkflowsByWorkflowIdRunResponses = { - 200: GeneratedAppResponse + 200: { + [key: string]: unknown + } } export type PostWorkflowsByWorkflowIdRunResponse diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 6ccc5671cb2..ae97b3961e6 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -6,7 +6,7 @@ import * as z from 'zod' * Annotation */ export const zAnnotation = z.object({ - content: z.string().nullish(), + answer: z.string().nullish(), created_at: z.int().nullish(), hit_count: z.int().nullish(), id: z.string(), @@ -21,13 +21,21 @@ export const zAnnotationCreatePayload = z.object({ question: z.string(), }) +/** + * AnnotationJobStatusDetailResponse + */ +export const zAnnotationJobStatusDetailResponse = z.object({ + error_msg: z.string().optional().default(''), + job_id: z.string(), + job_status: z.union([z.enum(['completed', 'error', 'processing', 'waiting']), z.string()]), +}) + /** * AnnotationJobStatusResponse */ export const zAnnotationJobStatusResponse = z.object({ - error_msg: z.string().nullish(), job_id: z.string(), - job_status: z.string(), + job_status: z.union([z.enum(['completed', 'error', 'processing', 'waiting']), z.string()]), }) /** @@ -1676,9 +1684,9 @@ export const zProcessRule = z.object({ }) /** - * SimpleAccount + * SimpleAccountResponse */ -export const zSimpleAccount = z.object({ +export const zSimpleAccountResponse = z.object({ email: z.string(), id: z.string(), name: z.string(), @@ -2196,7 +2204,7 @@ export const zWorkflowRunForLogResponse = z.object({ */ export const zWorkflowAppLogPartialResponse = z.object({ created_at: z.int().nullish(), - created_by_account: zSimpleAccount.nullish(), + created_by_account: zSimpleAccountResponse.nullish(), created_by_end_user: zSimpleEndUser.nullish(), created_by_role: z.string().nullish(), created_from: z.string().nullish(), @@ -2351,7 +2359,8 @@ export const zGetAppsAnnotationReplyByActionStatusByJobIdPath = z.object({ /** * Successfully retrieved task status. */ -export const zGetAppsAnnotationReplyByActionStatusByJobIdResponse = zAnnotationJobStatusResponse +export const zGetAppsAnnotationReplyByActionStatusByJobIdResponse + = zAnnotationJobStatusDetailResponse export const zGetAppsAnnotationsQuery = z.object({ keyword: z.string().optional().default(''), @@ -2409,7 +2418,7 @@ export const zPostChatMessagesBody = zChatRequestPayloadWithUser * - If `response_mode` is `blocking`, returns `application/json` with a `ChatCompletionResponse` object. * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of Server-Sent Events. */ -export const zPostChatMessagesResponse = zGeneratedAppResponse +export const zPostChatMessagesResponse = z.record(z.string(), z.unknown()) export const zPostChatMessagesByTaskIdStopBody = zRequiredServiceApiUserPayload @@ -2430,7 +2439,7 @@ export const zPostCompletionMessagesBody = zCompletionRequestPayloadWithUser * - If `response_mode` is `blocking`, returns `application/json` with a `CompletionResponse` object. * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkCompletionEvent` objects. */ -export const zPostCompletionMessagesResponse = zGeneratedAppResponse +export const zPostCompletionMessagesResponse = z.record(z.string(), z.unknown()) export const zPostCompletionMessagesByTaskIdStopBody = zRequiredServiceApiUserPayload @@ -3250,7 +3259,7 @@ export const zPostWorkflowsRunBody = zWorkflowRunPayloadWithUser * - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. */ -export const zPostWorkflowsRunResponse = zGeneratedAppResponse +export const zPostWorkflowsRunResponse = z.record(z.string(), z.unknown()) export const zGetWorkflowsRunByWorkflowRunIdPath = z.object({ workflow_run_id: z.string(), @@ -3284,7 +3293,7 @@ export const zPostWorkflowsByWorkflowIdRunPath = z.object({ * - If `response_mode` is `blocking`, returns `application/json` with a `WorkflowBlockingResponse` object. * - If `response_mode` is `streaming`, returns `text/event-stream` with a stream of `ChunkWorkflowEvent` objects. */ -export const zPostWorkflowsByWorkflowIdRunResponse = zGeneratedAppResponse +export const zPostWorkflowsByWorkflowIdRunResponse = z.record(z.string(), z.unknown()) export const zGetWorkspacesCurrentModelsModelTypesByModelTypePath = z.object({ model_type: z.enum(['llm', 'moderation', 'rerank', 'speech2text', 'text-embedding', 'tts']), diff --git a/packages/contracts/openapi-ts.api.config.ts b/packages/contracts/openapi-ts.api.config.ts index 8fce8a25bd3..1adbf4fda8e 100644 --- a/packages/contracts/openapi-ts.api.config.ts +++ b/packages/contracts/openapi-ts.api.config.ts @@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & { $ref?: string } +type OpenApiMediaType = JsonObject & { + schema?: SwaggerSchema +} + +type OpenApiResponse = JsonObject & { + content?: Record +} + type OpenApiComponents = JsonObject & { schemas?: Record } type SwaggerOperation = JsonObject & { operationId?: string - responses?: Record + responses?: Record } type SwaggerDocument = JsonObject & { @@ -52,6 +60,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url)) const apiOpenApiDir = path.resolve(currentDir, 'openapi') const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put']) +const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$' +const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$' + +const opaqueJsonContent = (): Record => ({ + 'application/json': { + schema: { + additionalProperties: true, + type: 'object', + }, + }, +}) const apiSpecs: ApiSpec[] = [ { filename: 'console-openapi.json', name: 'console' }, @@ -182,6 +201,46 @@ const addOperationIds = (document: SwaggerDocument) => { } } +const isOpaqueContractResponse = (response: OpenApiResponse) => { + const content = response.content + if (!isObject(content)) + return false + + return Object.entries(content).some(([mediaType, media]) => { + if (!isObject(media)) + return false + + return (mediaType === 'application/json' || mediaType === 'text/event-stream') && !('schema' in media) + }) +} + +const hasOpaqueContractSuccessResponse = (operation: SwaggerOperation) => { + return Object.entries(operation.responses ?? {}).some(([status, response]) => { + return /^2\d\d$/.test(status) && isObject(response) && isOpaqueContractResponse(response) + }) +} + +const normalizeOpaqueContractResponses = (document: SwaggerDocument) => { + // Some backend endpoints has no schema (e.g. external) and will trap heyapi here + // So we forge an opaque schema here + for (const pathItem of Object.values(document.paths ?? {})) { + for (const [method, operation] of Object.entries(pathItem)) { + if (!operationMethods.has(method) || !isObject(operation)) + continue + + const swaggerOperation = operation as SwaggerOperation + if (!hasOpaqueContractSuccessResponse(swaggerOperation)) + continue + + Object.values(swaggerOperation.responses ?? {}) + .filter(response => isObject(response) && isOpaqueContractResponse(response)) + .forEach((response) => { + response.content = opaqueJsonContent() + }) + } + } +} + const hasSuccessResponse = (operation: SwaggerOperation) => { return Object.entries(operation.responses ?? {}).some(([status, response]) => { if (!/^2\d\d$/.test(status)) @@ -215,6 +274,7 @@ const filterContractOperations = (document: SwaggerDocument) => { } const normalizeApiSwagger = (document: SwaggerDocument) => { + normalizeOpaqueContractResponses(document) filterContractOperations(document) addOperationIds(document) @@ -380,10 +440,20 @@ const createApiConfig = (job: ApiJob): UserConfig => ({ 'name': 'zod', '~resolvers': { string: (ctx) => { - if (ctx.schema.format !== 'binary') - return undefined + if (ctx.schema.format === 'binary') + return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File'))) - return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File'))) + if (ctx.schema.pattern === pydanticDecimalStringPattern) { + // the pydantic generated regex will emit error like + // regexp/no-useless-assertions, so patch the regex here + return $(ctx.symbols.z) + .attr('string') + .call() + .attr('regex') + .call($.regexp(codegenSafeDecimalStringPattern)) + } + + return undefined }, }, },