This commit is contained in:
chariri 2026-06-25 18:39:32 +00:00 committed by GitHub
commit 8d7eaa4129
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 982 additions and 640 deletions

View File

@ -167,12 +167,16 @@ register_schema_models(
ChatMessagesQuery,
MessageFeedbackPayload,
FeedbackExportQuery,
)
register_response_schema_models(
console_ns,
AnnotationCountResponse,
SuggestedQuestionsResponse,
MessageDetailResponse,
MessageInfiniteScrollPaginationResponse,
SimpleResultResponse,
TextFileResponse,
)
register_response_schema_models(console_ns, SimpleResultResponse, TextFileResponse)
@console_ns.route("/apps/<uuid:app_id>/chat-messages")

View File

@ -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/<string:action>")
@ -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/<string:action>/status/<uuid:job_id>")
@ -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/<uuid:annotation_id>")
@ -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",

View File

@ -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()

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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/<uuid:message_id>/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/<uuid:message_id>/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")

View File

@ -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/<string:workflow_run_id>")
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)

View File

@ -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]

View File

@ -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

View File

@ -38,7 +38,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/change-email
#### Request Body
@ -77,7 +77,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/change-email/validity
#### Request Body
@ -198,7 +198,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/interface-theme
#### Request Body
@ -211,7 +211,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/name
#### Request Body
@ -224,7 +224,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/password
#### Request Body
@ -237,14 +237,14 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [GET] /account/profile
#### Responses
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [POST] /account/timezone
#### Request Body
@ -257,7 +257,7 @@ Get account avatar url
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [Account](#account)<br> |
| 200 | Success | **application/json**: [AccountResponse](#accountresponse)<br> |
### [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)<br> |
| 200 | Success | **application/json**: [AccountWithRoleListResponse](#accountwithrolelistresponse)<br> |
### [GET] /workspaces/current/default-model
#### Parameters
@ -9477,7 +9477,7 @@ Update a plugin endpoint
| Code | Description | Schema |
| ---- | ----------- | ------ |
| 200 | Success | **application/json**: [AccountWithRoleList](#accountwithrolelist)<br> |
| 200 | Success | **application/json**: [AccountWithRoleListResponse](#accountwithrolelistresponse)<br> |
### [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 |
@ -13429,7 +13429,6 @@ Soft lifecycle state for Agent records.
| created_at | integer | | No |
| files | [ string ] | | Yes |
| id | string | | Yes |
| message_chain_id | string | | No |
| message_id | string | | Yes |
| observation | string | | No |
| position | integer | | Yes |
@ -14540,8 +14539,8 @@ Enum class for configurate method of provider model.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| annotation_create_account | [SimpleAccount](#simpleaccount) | | No |
| annotation_id | string | | Yes |
| created_at | integer | | No |
| id | string | | Yes |
#### ConversationDetail
@ -17079,6 +17078,7 @@ Enum class for large language model mode.
| agent_thoughts | [ [AgentThought](#agentthought) ] | | No |
| annotation | [ConversationAnnotation](#conversationannotation) | | No |
| annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No |
| answer | string | | Yes |
| answer_tokens | integer | | No |
| conversation_id | string | | Yes |
| created_at | integer | | No |
@ -17092,12 +17092,11 @@ Enum class for large language model mode.
| inputs | object | | Yes |
| message | [JSONValue](#jsonvalue) | | No |
| message_files | [ [MessageFile](#messagefile) ] | | No |
| message_metadata_dict | [JSONValue](#jsonvalue) | | No |
| message_tokens | integer | | No |
| metadata | [JSONValue](#jsonvalue) | | No |
| parent_message_id | string | | No |
| provider_response_latency | number | | No |
| query | string | | Yes |
| re_sign_file_url_answer | string | | Yes |
| status | string | | Yes |
| workflow_run_id | string | | No |
@ -19151,6 +19150,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 +19465,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 +19477,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 +20397,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 +20434,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 +20535,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 +20884,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 +20895,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 +20930,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 +20969,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 +21016,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 |

View File

@ -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)<br> |
| 200 | Successfully retrieved task status. | **application/json**: [AnnotationJobStatusDetailResponse](#annotationjobstatusdetailresponse)<br> |
| 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)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 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)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 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)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 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)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 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)<br>**text/event-stream**: [GeneratedAppResponse](#generatedappresponse)<br> |
| 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<br>string | | Yes |
#### AnnotationJobStatusResponse
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| job_id | string | | Yes |
| job_status | string<br>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 |

View File

@ -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()

View File

@ -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:

View File

@ -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()

View File

@ -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"}

View File

@ -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

View File

@ -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

View File

@ -269,6 +269,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -284,12 +285,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -723,7 +723,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -743,8 +742,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {

View File

@ -570,7 +570,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1056,8 +1055,8 @@ export const zConversationAnnotation = z.object({
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
id: z.string(),
})
/**
@ -2035,6 +2034,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -2048,12 +2048,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

@ -472,6 +472,7 @@ export type MessageDetailResponse = {
agent_thoughts?: Array<AgentThought>
annotation?: ConversationAnnotation | null
annotation_hit_history?: ConversationAnnotationHitHistory | null
answer: string
answer_tokens?: number | null
conversation_id: string
created_at?: number | null
@ -487,12 +488,11 @@ export type MessageDetailResponse = {
}
message?: JsonValue | null
message_files?: Array<MessageFile>
message_metadata_dict?: JsonValue | null
message_tokens?: number | null
metadata?: JsonValue | null
parent_message_id?: string | null
provider_response_latency?: number | null
query: string
re_sign_file_url_answer: string
status: string
workflow_run_id?: string | null
}
@ -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<AccountWithRole>
users: Array<AccountWithRoleResponse>
}
export type WorkflowCommentDetail = {
@ -887,7 +887,7 @@ export type DefaultBlockConfigResponse = {
export type WorkflowResponse = {
conversation_variables: Array<WorkflowConversationVariableResponse>
created_at: number
created_by?: SimpleAccount | null
created_by?: SimpleAccountResponse | null
environment_variables: Array<WorkflowEnvironmentVariableResponse>
features: {
[key: string]: unknown
@ -902,7 +902,7 @@ export type WorkflowResponse = {
rag_pipeline_variables: Array<PipelineVariableResponse>
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
@ -1498,7 +1498,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number
@ -1518,8 +1517,8 @@ export type ConversationAnnotation = {
export type ConversationAnnotationHitHistory = {
annotation_create_account?: SimpleAccount | null
annotation_id: string
created_at?: number | null
id: string
}
export type HumanInputContent = {
@ -1596,7 +1595,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 +1606,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 +1615,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 +1627,7 @@ export type WorkflowRunForListResponse = {
version?: string | null
}
export type SimpleAccount = {
export type SimpleAccountResponse = {
email: string
id: string
name: string
@ -1671,7 +1670,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 +2053,12 @@ export type SimpleMessageDetail = {
query: string
}
export type SimpleAccount = {
email: string
id: string
name: string
}
export type HumanInputFormDefinition = {
actions?: Array<UserActionConfig>
display_in_ui?: boolean

View File

@ -1150,7 +1150,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),
@ -1319,9 +1318,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 +1332,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 +1354,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 +1395,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 +1417,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 +1483,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 +1502,7 @@ export const zAccountWithRole = z.object({
* WorkflowCommentMentionUsersPayload
*/
export const zWorkflowCommentMentionUsersPayload = z.object({
users: z.array(zAccountWithRole),
users: z.array(zAccountWithRoleResponse),
})
/**
@ -1752,7 +1691,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 +1702,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 +2091,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 +2291,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 +2343,69 @@ export const zConversationPagination = z.object({
total: z.int(),
})
/**
* ConversationAnnotationHitHistory
*/
export const zConversationAnnotationHitHistory = z.object({
annotation_create_account: zSimpleAccount.nullish(),
annotation_id: z.string(),
created_at: z.int().nullish(),
})
/**
* 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 +2433,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 +2469,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(),
@ -3455,6 +3463,7 @@ export const zMessageDetailResponse = z.object({
agent_thoughts: z.array(zAgentThought).optional(),
annotation: zConversationAnnotation.nullish(),
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
answer: z.string(),
answer_tokens: z.int().nullish(),
conversation_id: z.string(),
created_at: z.int().nullish(),
@ -3468,12 +3477,11 @@ export const zMessageDetailResponse = z.object({
inputs: z.record(z.string(), zJsonValue),
message: zJsonValue.nullish(),
message_files: z.array(zMessageFile).optional(),
message_metadata_dict: zJsonValue.nullish(),
message_tokens: z.int().nullish(),
metadata: zJsonValue.nullish(),
parent_message_id: z.string().nullish(),
provider_response_latency: z.number().nullish(),
query: z.string(),
re_sign_file_url_answer: z.string(),
status: z.string(),
workflow_run_id: z.string().nullish(),
})

View File

@ -246,7 +246,6 @@ export type AgentThought = {
created_at?: number | null
files: Array<string>
id: string
message_chain_id?: string | null
message_id: string
observation?: string | null
position: number

View File

@ -266,7 +266,6 @@ export const zAgentThought = z.object({
created_at: z.int().nullish(),
files: z.array(z.string()),
id: z.string(),
message_chain_id: z.string().nullish(),
message_id: z.string(),
observation: z.string().nullish(),
position: z.int(),

View File

@ -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<WorkflowConversationVariableResponse>
created_at: number
created_by?: SimpleAccount | null
created_by?: SimpleAccountResponse | null
environment_variables: Array<WorkflowEnvironmentVariableResponse>
features: {
[key: string]: unknown
@ -173,7 +173,7 @@ export type WorkflowResponse = {
rag_pipeline_variables: Array<PipelineVariableResponse>
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

View File

@ -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(),
})

View File

@ -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<WorkflowConversationVariableResponse>
created_at: number
created_by?: SimpleAccount | null
created_by?: SimpleAccountResponse | null
environment_variables: Array<WorkflowEnvironmentVariableResponse>
features: {
[key: string]: unknown
@ -69,7 +69,7 @@ export type SnippetWorkflowResponse = {
rag_pipeline_variables: Array<PipelineVariableResponse>
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

View File

@ -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(),
})

View File

@ -104,8 +104,8 @@ export type SnippetUseCountResponse = {
use_count: number
}
export type AccountWithRoleList = {
accounts: Array<AccountWithRole>
export type AccountWithRoleListResponse = {
accounts: Array<AccountWithRoleResponse>
}
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

View File

@ -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

View File

@ -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

View File

@ -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']),

View File

@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & {
$ref?: string
}
type OpenApiMediaType = JsonObject & {
schema?: SwaggerSchema
}
type OpenApiResponse = JsonObject & {
content?: Record<string, OpenApiMediaType>
}
type OpenApiComponents = JsonObject & {
schemas?: Record<string, SwaggerSchema>
}
type SwaggerOperation = JsonObject & {
operationId?: string
responses?: Record<string, unknown>
responses?: Record<string, OpenApiResponse>
}
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<string, OpenApiMediaType> => ({
'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
},
},
},

View File

@ -217,14 +217,8 @@ const toFeedback = (feedback: NonNullable<MessageDetailResponse['feedbacks']>[nu
}
}
type AgentDebugMessageWithLegacyAnswer = MessageDetailResponse & {
answer?: string | null
}
const getAgentDebugMessageAnswer = (message: MessageDetailResponse) => {
const legacyAnswer = (message as AgentDebugMessageWithLegacyAnswer).answer
return message.re_sign_file_url_answer ?? legacyAnswer ?? ''
return message.answer ?? ''
}
function getFormattedAgentDebugChatTree(messages: MessageDetailResponse[]): ChatItemInTree[] {