From 5d0576de0a4cedd44cae2ab41c9607483c38d092 Mon Sep 17 00:00:00 2001 From: chariri Date: Thu, 2 Jul 2026 13:43:58 +0900 Subject: [PATCH] refactor(api): migrate console app chat endpoints to BaseModel (#37952) Co-authored-by: Asuka Minato Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/app/annotation.py | 100 ++++++------ api/controllers/console/app/audio.py | 43 ++--- api/controllers/console/app/completion.py | 15 +- api/controllers/console/app/conversation.py | 34 ++-- .../console/app/conversation_variables.py | 8 +- api/controllers/console/app/message.py | 73 ++------- api/controllers/console/app/statistic.py | 98 ++++++------ api/fields/annotation_fields.py | 10 ++ api/fields/conversation_variable_fields.py | 13 +- api/openapi/markdown/console-openapi.md | 120 ++++++++------ .../controllers/console/app/test_audio.py | 12 +- .../controllers/console/app/test_audio_api.py | 149 ------------------ .../console/app/test_message_api.py | 46 +++++- .../console/app/test_statistic_api.py | 23 +-- .../generated/api/console/agent/types.gen.ts | 16 +- .../generated/api/console/agent/zod.gen.ts | 16 +- .../generated/api/console/apps/types.gen.ts | 94 ++++++----- .../generated/api/console/apps/zod.gen.ts | 115 ++++++++------ 18 files changed, 457 insertions(+), 528 deletions(-) delete mode 100644 api/tests/unit_tests/controllers/console/app/test_audio_api.py diff --git a/api/controllers/console/app/annotation.py b/api/controllers/console/app/annotation.py index d03bd15046b..d14c7d2a7dc 100644 --- a/api/controllers/console/app/annotation.py +++ b/api/controllers/console/app/annotation.py @@ -1,7 +1,7 @@ from typing import Any, Literal from uuid import UUID -from flask import abort, make_response, request +from flask import abort, request from flask_restx import Resource from pydantic import BaseModel, Field, TypeAdapter, field_validator from sqlalchemy import select @@ -28,10 +28,12 @@ from fields.annotation_fields import ( AnnotationExportList, AnnotationHitHistory, AnnotationHitHistoryList, + AnnotationJobStatusDetailResponse, + AnnotationJobStatusResponse, AnnotationList, ) from fields.base import ResponseModel -from libs.helper import uuid_value +from libs.helper import dump_response, uuid_value from libs.login import current_account_with_tenant, login_required from models.model import App from services.annotation_service import ( @@ -113,23 +115,23 @@ class AnnotationFilePayload(BaseModel): return uuid_value(value) -class AnnotationJobStatusResponse(ResponseModel): - job_id: str | None = None - job_status: str | None = None - error_msg: str | None = None - record_count: int | None = None - - -class AnnotationEmbeddingModelResponse(ResponseModel): +class AnnotationSettingEmbeddingModelResponse(ResponseModel): embedding_provider_name: str | None = None embedding_model_name: str | None = None class AnnotationSettingResponse(ResponseModel): - id: str | None = None enabled: bool + id: str | None = None score_threshold: float | None = None - embedding_model: AnnotationEmbeddingModelResponse | None = None + embedding_model: AnnotationSettingEmbeddingModelResponse | None = None + + +class AnnotationBatchImportResponse(ResponseModel): + job_id: str | None = None + job_status: str | None = None + record_count: int | None = None + error_msg: str | None = None register_schema_models( @@ -156,7 +158,10 @@ register_response_schema_models( AnnotationHitHistory, AnnotationHitHistoryList, AnnotationJobStatusResponse, + AnnotationJobStatusDetailResponse, + AnnotationSettingEmbeddingModelResponse, AnnotationSettingResponse, + AnnotationBatchImportResponse, ) @@ -186,7 +191,7 @@ class AnnotationReplyActionApi(Resource): result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id)) case "disable": result = AppAnnotationService.disable_app_annotation(str(app_id)) - return result, 200 + return dump_response(AnnotationJobStatusResponse, result), 200 @console_ns.route("/apps//annotation-setting") @@ -207,7 +212,7 @@ class AppAnnotationSettingDetailApi(Resource): @rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT) def get(self, app_id: UUID): result = AppAnnotationService.get_app_annotation_setting_by_app_id(str(app_id)) - return result, 200 + return dump_response(AnnotationSettingResponse, result), 200 @console_ns.route("/apps//annotation-settings/") @@ -232,7 +237,7 @@ class AppAnnotationSettingUpdateApi(Resource): result = AppAnnotationService.update_app_annotation_setting( str(app_id), annotation_setting_id_str, setting_args ) - return result, 200 + return dump_response(AnnotationSettingResponse, result), 200 @console_ns.route("/apps//annotation-reply//status/") @@ -241,9 +246,7 @@ class AnnotationReplyActionStatusApi(Resource): @console_ns.doc(description="Get status of annotation reply action job") @console_ns.doc(params={"app_id": "Application ID", "job_id": "Job ID", "action": "Action type"}) @console_ns.response( - 200, - "Job status retrieved successfully", - console_ns.models[AnnotationJobStatusResponse.__name__], + 200, "Job status retrieved successfully", console_ns.models[AnnotationJobStatusDetailResponse.__name__] ) @console_ns.response(403, "Insufficient permissions") @setup_required @@ -265,7 +268,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 @console_ns.route("/apps//annotations") @@ -289,14 +294,9 @@ class AnnotationApi(Resource): annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(str(app_id), page, limit, keyword) annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True) - response = AnnotationList( - data=annotation_models, - has_more=len(annotation_list) == limit, - limit=limit, - total=total, - page=page, - ) - return response.model_dump(mode="json"), 200 + return AnnotationList( + data=annotation_models, has_more=len(annotation_list) == limit, limit=limit, total=total, page=page + ).model_dump(mode="json"), 200 @console_ns.doc("create_annotation") @console_ns.doc(description="Create a new annotation for an app") @@ -322,7 +322,7 @@ class AnnotationApi(Resource): if args.question is not None: upsert_args["question"] = args.question annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id)) - return Annotation.model_validate(annotation, from_attributes=True).model_dump(mode="json") + return dump_response(Annotation, annotation), 201 @setup_required @login_required @@ -372,14 +372,14 @@ class AnnotationExportApi(Resource): def get(self, app_id: UUID): annotation_list = AppAnnotationService.export_annotation_list_by_app_id(str(app_id)) annotation_models = TypeAdapter(list[Annotation]).validate_python(annotation_list, from_attributes=True) - response_data = AnnotationExportList(data=annotation_models).model_dump(mode="json") - - # Create response with secure headers for CSV export - response = make_response(response_data, 200) - response.headers["Content-Type"] = "application/json; charset=utf-8" - response.headers["X-Content-Type-Options"] = "nosniff" - - return response + return ( + AnnotationExportList(data=annotation_models).model_dump(mode="json"), + 200, + { + "Content-Type": "application/json; charset=utf-8", + "X-Content-Type-Options": "nosniff", + }, + ) @console_ns.route("/apps//annotations/") @@ -428,9 +428,7 @@ class AnnotationBatchImportApi(Resource): @console_ns.doc(description="Batch import annotations from CSV file with rate limiting and security checks") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.response( - 200, - "Batch import started successfully", - console_ns.models[AnnotationJobStatusResponse.__name__], + 200, "Batch import started successfully", console_ns.models[AnnotationBatchImportResponse.__name__] ) @console_ns.response(403, "Insufficient permissions") @console_ns.response(400, "No file uploaded or too many files") @@ -477,7 +475,10 @@ class AnnotationBatchImportApi(Resource): if file_size == 0: raise ValueError("The uploaded file is empty") - return AppAnnotationService.batch_import_app_annotations(str(app_id), file) + return dump_response( + AnnotationBatchImportResponse, + AppAnnotationService.batch_import_app_annotations(str(app_id), file), + ) @console_ns.route("/apps//annotations/batch-import-status/") @@ -486,9 +487,7 @@ class AnnotationBatchImportStatusApi(Resource): @console_ns.doc(description="Get status of batch import job") @console_ns.doc(params={"app_id": "Application ID", "job_id": "Job ID"}) @console_ns.response( - 200, - "Job status retrieved successfully", - console_ns.models[AnnotationJobStatusResponse.__name__], + 200, "Job status retrieved successfully", console_ns.models[AnnotationJobStatusDetailResponse.__name__] ) @console_ns.response(403, "Insufficient permissions") @setup_required @@ -508,7 +507,9 @@ class AnnotationBatchImportStatusApi(Resource): indexing_error_msg_key = f"app_annotation_batch_import_error_msg_{str(job_id)}" error_msg = redis_client.get(indexing_error_msg_key).decode() - return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200 + return AnnotationJobStatusDetailResponse( + job_id=str(job_id), job_status=job_status, error_msg=error_msg + ).model_dump(mode="json"), 200 @console_ns.route("/apps//annotations//hit-histories") @@ -541,11 +542,6 @@ class AnnotationHitHistoryListApi(Resource): history_models = TypeAdapter(list[AnnotationHitHistory]).validate_python( annotation_hit_history_list, from_attributes=True ) - response = AnnotationHitHistoryList( - data=history_models, - has_more=len(annotation_hit_history_list) == limit, - limit=limit, - total=total, - page=page, - ) - return response.model_dump(mode="json") + return AnnotationHitHistoryList( + data=history_models, has_more=len(annotation_hit_history_list) == limit, limit=limit, total=total, page=page + ).model_dump(mode="json") diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index 33e0efeef51..c6cd71f30f1 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -1,5 +1,4 @@ import logging -from typing import Any from flask import request from flask_restx import Resource @@ -7,7 +6,6 @@ from pydantic import BaseModel, Field, RootModel from werkzeug.exceptions import InternalServerError import services -from controllers.common.fields import AudioBinaryResponse from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.error import ( @@ -31,7 +29,9 @@ from controllers.console.wraps import ( ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from extensions.ext_database import db +from fields.base import ResponseModel from graphon.model_runtime.errors.invoke import InvokeError +from libs.helper import dump_response from libs.login import current_user, login_required from models import App, AppMode from services.app_ref_service import AppRefService @@ -57,16 +57,27 @@ class TextToSpeechVoiceQuery(BaseModel): language: str = Field(..., description="Language code") -class AudioTranscriptResponse(BaseModel): +class AudioTranscriptResponse(ResponseModel): text: str = Field(description="Transcribed text from audio") -class TextToSpeechVoiceListResponse(RootModel[list[dict[str, Any]]]): - root: list[dict[str, Any]] +class TextToSpeechVoiceResponse(ResponseModel): + # see api/core/plugin/impl/model.py + name: str = Field(description="Voice display name") + value: str = Field(description="Voice identifier") -register_schema_models(console_ns, AudioTranscriptResponse, TextToSpeechPayload, TextToSpeechVoiceQuery) -register_response_schema_models(console_ns, AudioBinaryResponse, TextToSpeechVoiceListResponse) +class TextToSpeechVoiceListResponse(RootModel[list[TextToSpeechVoiceResponse]]): + root: list[TextToSpeechVoiceResponse] = Field(description="Available voices") + + +register_schema_models(console_ns, TextToSpeechPayload, TextToSpeechVoiceQuery) +register_response_schema_models( + console_ns, + AudioTranscriptResponse, + TextToSpeechVoiceResponse, + TextToSpeechVoiceListResponse, +) @console_ns.route("/apps//audio-to-text") @@ -95,7 +106,7 @@ class ChatMessageAudioApi(Resource): end_user=None, ) - return response + return dump_response(AudioTranscriptResponse, response) except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() @@ -128,11 +139,8 @@ class ChatMessageTextApi(Resource): @console_ns.doc(description="Convert text to speech for chat messages") @console_ns.doc(params={"app_id": "App ID"}) @console_ns.expect(console_ns.models[TextToSpeechPayload.__name__]) - @console_ns.response( - 200, - "Text to speech conversion successful", - console_ns.models[AudioBinaryResponse.__name__], - ) + # TTS returns provider audio bytes, so the success response is intentionally schema-less. + @console_ns.response(200, "Text to speech conversion successful") @console_ns.response(400, "Bad request - Invalid parameters") @setup_required @login_required @@ -150,7 +158,8 @@ class ChatMessageTextApi(Resource): account_id=current_user.id, ) - response = AudioService.transcript_tts( + # response-contract:ignore + return AudioService.transcript_tts( app_model=app_model, session=db.session, text=payload.text, @@ -158,7 +167,6 @@ class ChatMessageTextApi(Resource): message_ref=message_ref, is_draft=True, ) - return response except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() @@ -189,8 +197,7 @@ class ChatMessageTextApi(Resource): class TextModesApi(Resource): @console_ns.doc("get_text_to_speech_voices") @console_ns.doc(description="Get available TTS voices for a specific language") - @console_ns.doc(params={"app_id": "App ID"}) - @console_ns.doc(params=query_params_from_model(TextToSpeechVoiceQuery)) + @console_ns.doc(params={"app_id": "App ID", **query_params_from_model(TextToSpeechVoiceQuery)}) @console_ns.response( 200, "TTS voices retrieved successfully", @@ -211,7 +218,7 @@ class TextModesApi(Resource): language=args.language, ) - return response + return dump_response(TextToSpeechVoiceListResponse, response) except services.errors.audio.ProviderNotSupportTextToSpeechLanageServiceError: raise AppUnavailableError("Text to audio voices language parameter loss.") except NoAudioUploadedServiceError: diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index 2862312c1e3..0d9c6009329 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, field_validator 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.console import console_ns from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model @@ -136,7 +136,7 @@ After config persistence completes, respond FINISHED.""" register_schema_models(console_ns, CompletionMessagePayload, ChatMessagePayload) -register_response_schema_models(console_ns, GeneratedAppResponse, SimpleResultResponse) +register_response_schema_models(console_ns, SimpleResultResponse) # define completion message api for user @@ -146,7 +146,7 @@ class CompletionMessageApi(Resource): @console_ns.doc(description="Generate completion message for debugging") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[CompletionMessagePayload.__name__]) - @console_ns.response(200, "Completion generated successfully", console_ns.models[GeneratedAppResponse.__name__]) + @console_ns.response(200, "Completion generated successfully") @console_ns.response(400, "Invalid request parameters") @console_ns.response(404, "App not found") @setup_required @@ -167,6 +167,7 @@ class CompletionMessageApi(Resource): app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming ) + # response-contract:ignore compact_generate_response return helper.compact_generate_response(response) except services.errors.conversation.ConversationNotExistsError: raise NotFound("Conversation Not Exists.") @@ -210,7 +211,7 @@ class CompletionMessageStopApi(Resource): app_mode=AppMode.value_of(app_model.mode), ) - return {"result": "success"}, 200 + return SimpleResultResponse(result="success").model_dump(mode="json"), 200 @console_ns.route("/apps//chat-messages") @@ -219,7 +220,7 @@ class ChatMessageApi(Resource): @console_ns.doc(description="Generate chat message for debugging") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[ChatMessagePayload.__name__]) - @console_ns.response(200, "Chat message generated successfully", console_ns.models[GeneratedAppResponse.__name__]) + @console_ns.response(200, "Chat message generated successfully") @console_ns.response(400, "Invalid request parameters") @console_ns.response(404, "App or conversation not found") @setup_required @@ -240,7 +241,7 @@ class AgentChatMessageApi(Resource): @console_ns.doc(description="Generate an Agent App chat message for debugging") @console_ns.doc(params={"agent_id": "Agent ID"}) @console_ns.expect(console_ns.models[ChatMessagePayload.__name__]) - @console_ns.response(200, "Chat message generated successfully", console_ns.models[GeneratedAppResponse.__name__]) + @console_ns.response(200, "Chat message generated successfully") @console_ns.response(400, "Invalid request parameters") @console_ns.response(404, "Agent or conversation not found") @setup_required @@ -520,4 +521,4 @@ def _stop_chat_message(*, current_user_id: str, app_model: App, task_id: str): app_mode=AppMode.value_of(app_model.mode), ) - return {"result": "success"}, 200 + return SimpleResultResponse(result="success").model_dump(mode="json"), 200 diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index ec34c26fedd..6387ea441e5 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -9,7 +9,7 @@ from sqlalchemy import func, or_ from sqlalchemy.orm import selectinload from werkzeug.exceptions import NotFound -from controllers.common.schema import query_params_from_model, register_schema_models +from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import ( @@ -39,6 +39,7 @@ from fields.conversation_fields import ( ConversationWithSummaryPagination as ConversationWithSummaryPaginationResponse, ) from libs.datetime_utils import naive_utc_now, parse_time_range +from libs.helper import dump_response from libs.login import login_required from models import Conversation, EndUser, Message, MessageAnnotation from models.account import Account @@ -79,13 +80,14 @@ register_schema_models( console_ns, CompletionConversationQuery, ChatConversationQuery, +) +register_response_schema_models( + console_ns, ConversationResponse, ConversationPaginationResponse, ConversationMessageDetailResponse, ConversationWithSummaryPaginationResponse, ConversationDetailResponse, - CompletionConversationQuery, - ChatConversationQuery, ) @@ -93,8 +95,7 @@ register_schema_models( class CompletionConversationApi(Resource): @console_ns.doc("list_completion_conversations") @console_ns.doc(description="Get completion conversations with pagination and filtering") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(CompletionConversationQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(CompletionConversationQuery)}) @console_ns.response(200, "Success", console_ns.models[ConversationPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required @@ -157,9 +158,7 @@ class CompletionConversationApi(Resource): conversations = db.paginate(query, page=args.page, per_page=args.limit, error_out=False) - return ConversationPaginationResponse.model_validate(conversations, from_attributes=True).model_dump( - mode="json" - ) + return dump_response(ConversationPaginationResponse, conversations) @console_ns.route("/apps//completion-conversations/") @@ -179,9 +178,9 @@ class CompletionConversationDetailApi(Resource): @get_app_model(mode=AppMode.COMPLETION) def get(self, current_user: Account, app_model: App, conversation_id: UUID): conversation_id_str = str(conversation_id) - return ConversationMessageDetailResponse.model_validate( - _get_conversation(current_user, app_model, conversation_id_str), from_attributes=True - ).model_dump(mode="json") + return dump_response( + ConversationMessageDetailResponse, _get_conversation(current_user, app_model, conversation_id_str) + ) @console_ns.doc("delete_completion_conversation") @console_ns.doc(description="Delete a completion conversation") @@ -211,8 +210,7 @@ class CompletionConversationDetailApi(Resource): class ChatConversationApi(Resource): @console_ns.doc("list_chat_conversations") @console_ns.doc(description="Get chat conversations with pagination, filtering and summary") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(ChatConversationQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(ChatConversationQuery)}) @console_ns.response(200, "Success", console_ns.models[ConversationWithSummaryPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required @@ -314,9 +312,7 @@ class ChatConversationApi(Resource): conversations = db.paginate(query, page=args.page, per_page=args.limit, error_out=False) - return ConversationWithSummaryPaginationResponse.model_validate(conversations, from_attributes=True).model_dump( - mode="json" - ) + return dump_response(ConversationWithSummaryPaginationResponse, conversations) @console_ns.route("/apps//chat-conversations/") @@ -336,9 +332,9 @@ class ChatConversationDetailApi(Resource): @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT]) def get(self, current_user: Account, app_model: App, conversation_id: UUID): conversation_id_str = str(conversation_id) - return ConversationDetailResponse.model_validate( - _get_conversation(current_user, app_model, conversation_id_str), from_attributes=True - ).model_dump(mode="json") + return dump_response( + ConversationDetailResponse, _get_conversation(current_user, app_model, conversation_id_str) + ) @console_ns.doc("delete_chat_conversation") @console_ns.doc(description="Delete a chat conversation") diff --git a/api/controllers/console/app/conversation_variables.py b/api/controllers/console/app/conversation_variables.py index 3069dd3011c..aa8090f0440 100644 --- a/api/controllers/console/app/conversation_variables.py +++ b/api/controllers/console/app/conversation_variables.py @@ -22,7 +22,7 @@ from controllers.console.wraps import ( from extensions.ext_database import db from fields._value_type_serializer import serialize_value_type from fields.base import ResponseModel -from libs.helper import to_timestamp +from libs.helper import dump_response, to_timestamp from libs.login import login_required from models import ConversationVariable from models.model import App, AppMode @@ -119,7 +119,8 @@ class ConversationVariablesApi(Resource): with sessionmaker(db.engine, expire_on_commit=False).begin() as session: rows = session.scalars(stmt).all() - response = PaginatedConversationVariableResponse.model_validate( + return dump_response( + PaginatedConversationVariableResponse, { "page": page, "limit": page_size, @@ -135,6 +136,5 @@ class ConversationVariablesApi(Resource): ) for row in rows ], - } + }, ) - return response.model_dump(mode="json") diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index 3e24dbdcff0..6a44ca3db8a 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from typing import Literal from uuid import UUID @@ -38,16 +37,10 @@ from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotIni from extensions.ext_database import db from fields.base import ResponseModel from fields.conversation_fields import ( - AgentThought, - ConversationAnnotation, - ConversationAnnotationHitHistory, - Feedback, - JSONValue, - MessageFile, - format_files_contained, + MessageDetail as BaseMessageDetailResponse, ) from graphon.model_runtime.errors.invoke import InvokeError -from libs.helper import to_timestamp, uuid_value +from libs.helper import dump_response, uuid_value from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.login import login_required from models.account import Account @@ -111,49 +104,16 @@ class FeedbackExportQuery(BaseModel): raise ValueError("has_comment must be a boolean value") -class AnnotationCountResponse(BaseModel): +class AnnotationCountResponse(ResponseModel): count: int = Field(description="Number of annotations") -class SuggestedQuestionsResponse(BaseModel): +class SuggestedQuestionsResponse(ResponseModel): data: list[str] = Field(description="Suggested question") -class MessageDetailResponse(ResponseModel): - id: str - conversation_id: str - inputs: dict[str, JSONValue] - query: str - message: JSONValue | None = None - message_tokens: int | None = None - answer: str = Field(validation_alias="re_sign_file_url_answer") - answer_tokens: int | None = None - provider_response_latency: float | None = None - from_source: str - from_end_user_id: str | None = None - from_account_id: str | None = None - feedbacks: list[Feedback] = Field(default_factory=list) - workflow_run_id: str | None = None - annotation: ConversationAnnotation | None = None - annotation_hit_history: ConversationAnnotationHitHistory | None = None - created_at: int | None = None - agent_thoughts: list[AgentThought] = Field(default_factory=list) - message_files: list[MessageFile] = Field(default_factory=list) +class MessageDetailResponse(BaseMessageDetailResponse): extra_contents: list[ExecutionExtraContentDomainModel] = Field(default_factory=list) - metadata: JSONValue | None = Field(default=None, validation_alias="message_metadata_dict") - status: str - error: str | None = None - parent_message_id: str | None = None - - @field_validator("inputs", mode="before") - @classmethod - def _normalize_inputs(cls, value: JSONValue) -> JSONValue: - return format_files_contained(value) - - @field_validator("created_at", mode="before") - @classmethod - def _normalize_created_at(cls, value: datetime | int | None) -> int | None: - return to_timestamp(value) class MessageInfiniteScrollPaginationResponse(ResponseModel): @@ -183,8 +143,7 @@ register_response_schema_models( class ChatMessageListApi(Resource): @console_ns.doc("list_chat_messages") @console_ns.doc(description="Get chat messages for a conversation with pagination") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(ChatMessagesQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(ChatMessagesQuery)}) @console_ns.response(200, "Success", console_ns.models[MessageInfiniteScrollPaginationResponse.__name__]) @console_ns.response(404, "Conversation not found") @login_required @@ -274,7 +233,7 @@ class MessageAnnotationCountApi(Resource): select(func.count(MessageAnnotation.id)).where(MessageAnnotation.app_id == app_model.id) ) - return {"count": count} + return AnnotationCountResponse(count=count or 0).model_dump(mode="json") @console_ns.route("/apps//chat-messages//suggested-questions") @@ -323,13 +282,12 @@ class AgentMessageSuggestedQuestionApi(Resource): class MessageFeedbackExportApi(Resource): @console_ns.doc("export_feedbacks") @console_ns.doc(description="Export user feedback data for Google Sheets") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(FeedbackExportQuery)) @console_ns.response( 200, "Feedback data exported successfully", console_ns.models[TextFileResponse.__name__], ) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(FeedbackExportQuery)}) @console_ns.response(400, "Invalid parameters") @console_ns.response(500, "Internal server error") @setup_required @@ -354,7 +312,6 @@ class MessageFeedbackExportApi(Resource): end_date=args.end_date, format_type=args.format, ) - return export_data except ValueError as e: @@ -465,16 +422,16 @@ def _list_chat_messages(*, app_model: App, current_user: Account | None = None): history_messages = list(reversed(history_messages)) attach_message_extra_contents(history_messages) - return MessageInfiniteScrollPaginationResponse.model_validate( + return dump_response( + MessageInfiniteScrollPaginationResponse, InfiniteScrollPagination(data=history_messages, limit=args.limit, has_more=has_more), - from_attributes=True, - ).model_dump(mode="json") + ) def _update_message_feedback(*, current_user: Account, app_model: App): args = MessageFeedbackPayload.model_validate(console_ns.payload) - message_id = str(args.message_id) + message_id = args.message_id message = db.session.scalar( select(Message).where(Message.id == message_id, Message.app_id == app_model.id).limit(1) @@ -509,7 +466,7 @@ def _update_message_feedback(*, current_user: Account, app_model: App): db.session.commit() - return {"result": "success"} + return SimpleResultResponse(result="success").model_dump(mode="json") def _get_message_suggested_questions(*, current_user: Account, app_model: App, message_id: UUID): @@ -537,7 +494,7 @@ def _get_message_suggested_questions(*, current_user: Account, app_model: App, m logger.exception("internal server error.") raise InternalServerError() - return {"data": questions} + return dump_response(SuggestedQuestionsResponse, {"data": questions}) def _get_message_detail(*, app_model: App, message_id: UUID): @@ -551,4 +508,4 @@ def _get_message_detail(*, app_model: App, message_id: UUID): raise NotFound("Message Not Exists.") attach_message_extra_contents([message]) - return MessageDetailResponse.model_validate(message, from_attributes=True).model_dump(mode="json") + return dump_response(MessageDetailResponse, message) diff --git a/api/controllers/console/app/statistic.py b/api/controllers/console/app/statistic.py index fbb6d3e987f..c7c3c9e64b6 100644 --- a/api/controllers/console/app/statistic.py +++ b/api/controllers/console/app/statistic.py @@ -1,7 +1,7 @@ from decimal import Decimal import sqlalchemy as sa -from flask import abort, jsonify, request +from flask import abort, request from flask_restx import Resource from pydantic import BaseModel, Field, field_validator @@ -20,7 +20,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from fields.base import ResponseModel from libs.datetime_utils import parse_time_range -from libs.helper import convert_datetime_to_date +from libs.helper import convert_datetime_to_date, dump_response from libs.login import login_required from models import AppMode from models.account import Account @@ -44,8 +44,15 @@ class DailyMessageStatisticItem(ResponseModel): message_count: int -class DailyMessageStatisticResponse(ResponseModel): - data: list[DailyMessageStatisticItem] +register_schema_models(console_ns, StatisticTimeRangeQuery) + + +class StatisticDataResponse[T](ResponseModel): + data: list[T] + + +class DailyMessageStatisticResponse(StatisticDataResponse[DailyMessageStatisticItem]): + pass class DailyConversationStatisticItem(ResponseModel): @@ -53,8 +60,8 @@ class DailyConversationStatisticItem(ResponseModel): conversation_count: int -class DailyConversationStatisticResponse(ResponseModel): - data: list[DailyConversationStatisticItem] +class DailyConversationStatisticResponse(StatisticDataResponse[DailyConversationStatisticItem]): + pass class DailyTerminalStatisticItem(ResponseModel): @@ -62,19 +69,19 @@ class DailyTerminalStatisticItem(ResponseModel): terminal_count: int -class DailyTerminalStatisticResponse(ResponseModel): - data: list[DailyTerminalStatisticItem] +class DailyTerminalStatisticResponse(StatisticDataResponse[DailyTerminalStatisticItem]): + pass class DailyTokenCostStatisticItem(ResponseModel): date: str - token_count: int - total_price: str | float - currency: str + token_count: int | None = None + total_price: Decimal | None = None + currency: str | None = None -class DailyTokenCostStatisticResponse(ResponseModel): - data: list[DailyTokenCostStatisticItem] +class DailyTokenCostStatisticResponse(StatisticDataResponse[DailyTokenCostStatisticItem]): + pass class AverageSessionInteractionStatisticItem(ResponseModel): @@ -82,8 +89,8 @@ class AverageSessionInteractionStatisticItem(ResponseModel): interactions: float -class AverageSessionInteractionStatisticResponse(ResponseModel): - data: list[AverageSessionInteractionStatisticItem] +class AverageSessionInteractionStatisticResponse(StatisticDataResponse[AverageSessionInteractionStatisticItem]): + pass class UserSatisfactionRateStatisticItem(ResponseModel): @@ -91,8 +98,8 @@ class UserSatisfactionRateStatisticItem(ResponseModel): rate: float -class UserSatisfactionRateStatisticResponse(ResponseModel): - data: list[UserSatisfactionRateStatisticItem] +class UserSatisfactionRateStatisticResponse(StatisticDataResponse[UserSatisfactionRateStatisticItem]): + pass class AverageResponseTimeStatisticItem(ResponseModel): @@ -100,8 +107,8 @@ class AverageResponseTimeStatisticItem(ResponseModel): latency: float -class AverageResponseTimeStatisticResponse(ResponseModel): - data: list[AverageResponseTimeStatisticItem] +class AverageResponseTimeStatisticResponse(StatisticDataResponse[AverageResponseTimeStatisticItem]): + pass class TokensPerSecondStatisticItem(ResponseModel): @@ -109,20 +116,27 @@ class TokensPerSecondStatisticItem(ResponseModel): tps: float -class TokensPerSecondStatisticResponse(ResponseModel): - data: list[TokensPerSecondStatisticItem] +class TokensPerSecondStatisticResponse(StatisticDataResponse[TokensPerSecondStatisticItem]): + pass -register_schema_models(console_ns, StatisticTimeRangeQuery) register_response_schema_models( console_ns, + DailyMessageStatisticItem, DailyMessageStatisticResponse, + DailyConversationStatisticItem, DailyConversationStatisticResponse, + DailyTerminalStatisticItem, DailyTerminalStatisticResponse, + DailyTokenCostStatisticItem, DailyTokenCostStatisticResponse, + AverageSessionInteractionStatisticItem, AverageSessionInteractionStatisticResponse, + UserSatisfactionRateStatisticItem, UserSatisfactionRateStatisticResponse, + AverageResponseTimeStatisticItem, AverageResponseTimeStatisticResponse, + TokensPerSecondStatisticItem, TokensPerSecondStatisticResponse, ) @@ -131,8 +145,7 @@ register_response_schema_models( class DailyMessageStatistic(Resource): @console_ns.doc("get_daily_message_statistics") @console_ns.doc(description="Get daily message statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Daily message statistics retrieved successfully", @@ -185,15 +198,14 @@ WHERE for i in rs: response_data.append({"date": str(i.date), "message_count": i.message_count}) - return jsonify({"data": response_data}) + return dump_response(DailyMessageStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/daily-conversations") class DailyConversationStatistic(Resource): @console_ns.doc("get_daily_conversation_statistics") @console_ns.doc(description="Get daily conversation statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Daily conversation statistics retrieved successfully", @@ -245,15 +257,14 @@ WHERE for i in rs: response_data.append({"date": str(i.date), "conversation_count": i.conversation_count}) - return jsonify({"data": response_data}) + return dump_response(DailyConversationStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/daily-end-users") class DailyTerminalsStatistic(Resource): @console_ns.doc("get_daily_terminals_statistics") @console_ns.doc(description="Get daily terminal/end-user statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Daily terminal statistics retrieved successfully", @@ -306,15 +317,14 @@ WHERE for i in rs: response_data.append({"date": str(i.date), "terminal_count": i.terminal_count}) - return jsonify({"data": response_data}) + return dump_response(DailyTerminalStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/token-costs") class DailyTokenCostStatistic(Resource): @console_ns.doc("get_daily_token_cost_statistics") @console_ns.doc(description="Get daily token cost statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Daily token cost statistics retrieved successfully", @@ -370,15 +380,14 @@ WHERE {"date": str(i.date), "token_count": i.token_count, "total_price": i.total_price, "currency": "USD"} ) - return jsonify({"data": response_data}) + return dump_response(DailyTokenCostStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/average-session-interactions") class AverageSessionInteractionStatistic(Resource): @console_ns.doc("get_average_session_interaction_statistics") @console_ns.doc(description="Get average session interaction statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Average session interaction statistics retrieved successfully", @@ -450,15 +459,14 @@ ORDER BY {"date": str(i.date), "interactions": float(i.interactions.quantize(Decimal("0.01")))} ) - return jsonify({"data": response_data}) + return dump_response(AverageSessionInteractionStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/user-satisfaction-rate") class UserSatisfactionRateStatistic(Resource): @console_ns.doc("get_user_satisfaction_rate_statistics") @console_ns.doc(description="Get user satisfaction rate statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "User satisfaction rate statistics retrieved successfully", @@ -520,15 +528,14 @@ WHERE } ) - return jsonify({"data": response_data}) + return dump_response(UserSatisfactionRateStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/average-response-time") class AverageResponseTimeStatistic(Resource): @console_ns.doc("get_average_response_time_statistics") @console_ns.doc(description="Get average response time statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Average response time statistics retrieved successfully", @@ -581,15 +588,14 @@ WHERE for i in rs: response_data.append({"date": str(i.date), "latency": round(i.latency * 1000, 4)}) - return jsonify({"data": response_data}) + return dump_response(AverageResponseTimeStatisticResponse, {"data": response_data}) @console_ns.route("/apps//statistics/tokens-per-second") class TokensPerSecondStatistic(Resource): @console_ns.doc("get_tokens_per_second_statistics") @console_ns.doc(description="Get tokens per second statistics for an application") - @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.doc(params=query_params_from_model(StatisticTimeRangeQuery)) + @console_ns.doc(params={"app_id": "Application ID", **query_params_from_model(StatisticTimeRangeQuery)}) @console_ns.response( 200, "Tokens per second statistics retrieved successfully", @@ -645,4 +651,4 @@ WHERE for i in rs: response_data.append({"date": str(i.date), "tps": round(i.tokens_per_second, 4)}) - return jsonify({"data": response_data}) + return dump_response(TokensPerSecondStatisticResponse, {"data": response_data}) diff --git a/api/fields/annotation_fields.py b/api/fields/annotation_fields.py index 4546a051cce..86a13a32bd2 100644 --- a/api/fields/annotation_fields.py +++ b/api/fields/annotation_fields.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Literal from pydantic import Field, field_validator @@ -29,6 +30,15 @@ class AnnotationList(ResponseModel): page: int +class AnnotationJobStatusResponse(ResponseModel): + job_id: str + job_status: Literal["waiting", "processing", "completed", "error"] | str + + +class AnnotationJobStatusDetailResponse(AnnotationJobStatusResponse): + error_msg: str = "" + + class AnnotationExportList(ResponseModel): data: list[Annotation] diff --git a/api/fields/conversation_variable_fields.py b/api/fields/conversation_variable_fields.py index 4d5de84fd98..6618475f2f3 100644 --- a/api/fields/conversation_variable_fields.py +++ b/api/fields/conversation_variable_fields.py @@ -3,25 +3,14 @@ from __future__ import annotations from datetime import datetime from typing import Any -from flask_restx import fields from pydantic import field_validator from fields.base import ResponseModel from graphon.variables.types import SegmentType -from libs.helper import TimestampField, to_timestamp +from libs.helper import to_timestamp from ._value_type_serializer import serialize_value_type -conversation_variable_fields = { - "id": fields.String, - "name": fields.String, - "value_type": fields.String(attribute=serialize_value_type), - "value": fields.String, - "description": fields.String, - "created_at": TimestampField, - "updated_at": TimestampField, -} - class ConversationVariableResponse(ResponseModel): id: str diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 7ad855860d6..ec296e26091 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -2371,7 +2371,7 @@ Get status of annotation reply action job | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Job status retrieved successfully | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Job status retrieved successfully | **application/json**: [AnnotationJobStatusDetailResponse](#annotationjobstatusdetailresponse)
| | 403 | Insufficient permissions | | ### [GET] /apps/{app_id}/annotation-setting @@ -2480,7 +2480,7 @@ Batch import annotations from CSV file with rate limiting and security checks | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Batch import started successfully | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Batch import started successfully | **application/json**: [AnnotationBatchImportResponse](#annotationbatchimportresponse)
| | 400 | No file uploaded or too many files | | | 403 | Insufficient permissions | | | 413 | File too large | | @@ -2500,7 +2500,7 @@ Get status of batch import job | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Job status retrieved successfully | **application/json**: [AnnotationJobStatusResponse](#annotationjobstatusresponse)
| +| 200 | Job status retrieved successfully | **application/json**: [AnnotationJobStatusDetailResponse](#annotationjobstatusdetailresponse)
| | 403 | Insufficient permissions | | ### [GET] /apps/{app_id}/annotations/count @@ -2816,11 +2816,11 @@ Generate completion message for debugging #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Completion generated successfully | **application/json**: [GeneratedAppResponse](#generatedappresponse)
| -| 400 | Invalid request parameters | | -| 404 | App not found | | +| Code | Description | +| ---- | ----------- | +| 200 | Completion generated successfully | +| 400 | Invalid request parameters | +| 404 | App not found | ### [POST] /apps/{app_id}/completion-messages/{task_id}/stop Stop a running completion message generation @@ -3379,10 +3379,10 @@ Convert text to speech for chat messages #### Responses -| Code | Description | Schema | -| ---- | ----------- | ------ | -| 200 | Text to speech conversion successful | **application/json**: [AudioBinaryResponse](#audiobinaryresponse)
| -| 400 | Bad request - Invalid parameters | | +| Code | Description | +| ---- | ----------- | +| 200 | Text to speech conversion successful | +| 400 | Bad request - Invalid parameters | ### [GET] /apps/{app_id}/text-to-audio/voices Get available TTS voices for a specific language @@ -14644,19 +14644,21 @@ Soft lifecycle state for Agent records. | id | string | | Yes | | question | string | | No | +#### AnnotationBatchImportResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| error_msg | string | | No | +| job_id | string | | No | +| job_status | string | | No | +| record_count | integer | | No | + #### AnnotationCountResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | count | integer | Number of annotations | Yes | -#### AnnotationEmbeddingModelResponse - -| Name | Type | Description | Required | -| ---- | ---- | ----------- | -------- | -| embedding_model_name | string | | No | -| embedding_provider_name | string | | No | - #### AnnotationExportList | Name | Type | Description | Required | @@ -14698,14 +14700,20 @@ Soft lifecycle state for Agent records. | limit | integer,
**Default:** 20 | Page size | No | | page | integer,
**Default:** 1 | Page number | No | -#### AnnotationJobStatusResponse +#### AnnotationJobStatusDetailResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | error_msg | string | | No | -| job_id | string | | No | -| job_status | string | | No | -| record_count | integer | | No | +| job_id | string | | Yes | +| job_status | string
string | | Yes | + +#### AnnotationJobStatusResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| job_id | string | | Yes | +| job_status | string
string | | Yes | #### AnnotationList @@ -14739,11 +14747,18 @@ Soft lifecycle state for Agent records. | ---- | ---- | ----------- | -------- | | action | string,
**Available values:** "disable", "enable" | *Enum:* `"disable"`, `"enable"` | Yes | +#### AnnotationSettingEmbeddingModelResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| embedding_model_name | string | | No | +| embedding_provider_name | string | | No | + #### AnnotationSettingResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| embedding_model | [AnnotationEmbeddingModelResponse](#annotationembeddingmodelresponse) | | No | +| embedding_model | [AnnotationSettingEmbeddingModelResponse](#annotationsettingembeddingmodelresponse) | | No | | enabled | boolean | | Yes | | id | string | | No | | score_threshold | number | | No | @@ -15712,13 +15727,13 @@ Enum class for configurate method of provider model. | admin_feedback_stats | [FeedbackStat](#feedbackstat) | | No | | annotation | [ConversationAnnotation](#conversationannotation) | | No | | created_at | integer | | No | -| first_message | [SimpleMessageDetail](#simplemessagedetail) | | No | | from_account_id | string | | No | | from_account_name | string | | No | | from_end_user_id | string | | No | | from_end_user_session_id | string | | No | | from_source | string | | Yes | | id | string | | Yes | +| message | [SimpleMessageDetail](#simplemessagedetail) | | No | | model_config | [SimpleModelConfig](#simplemodelconfig) | | No | | read_at | integer | | No | | status | string | | Yes | @@ -15782,11 +15797,11 @@ Enum class for configurate method of provider model. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | created_at | integer | | No | -| first_message | [MessageDetail](#messagedetail) | | No | | from_account_id | string | | No | | from_end_user_id | string | | No | | from_source | string | | Yes | | id | string | | Yes | +| message | [MessageDetail](#messagedetail) | | No | | model_config | [ModelConfig](#modelconfig) | | No | | status | string | | Yes | @@ -15794,10 +15809,10 @@ Enum class for configurate method of provider model. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| has_next | boolean | | Yes | -| items | [ [Conversation](#conversation) ] | | Yes | +| data | [ [Conversation](#conversation) ] | | Yes | +| has_more | boolean | | Yes | +| limit | integer | | Yes | | page | integer | | Yes | -| per_page | integer | | Yes | | total | integer | | Yes | #### ConversationRenamePayload @@ -15860,7 +15875,7 @@ Enum class for configurate method of provider model. | read_at | integer | | No | | status | string | | Yes | | status_count | [StatusCount](#statuscount) | | No | -| summary_or_query | string | | Yes | +| summary | string | | Yes | | updated_at | integer | | No | | user_feedback_stats | [FeedbackStat](#feedbackstat) | | No | @@ -15868,10 +15883,10 @@ Enum class for configurate method of provider model. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| has_next | boolean | | Yes | -| items | [ [ConversationWithSummary](#conversationwithsummary) ] | | Yes | +| data | [ [ConversationWithSummary](#conversationwithsummary) ] | | Yes | +| has_more | boolean | | Yes | +| limit | integer | | Yes | | page | integer | | Yes | -| per_page | integer | | Yes | | total | integer | | Yes | #### ConvertToWorkflowPayload @@ -16044,10 +16059,10 @@ Model class for provider custom model configuration. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| currency | string | | Yes | +| currency | string | | No | | date | string | | Yes | -| token_count | integer | | Yes | -| total_price | string
number | | Yes | +| token_count | integer | | No | +| total_price | string | | No | #### DailyTokenCostStatisticResponse @@ -18344,6 +18359,7 @@ Enum class for large language model mode. | agent_thoughts | [ [AgentThought](#agentthought) ] | | Yes | | annotation | [ConversationAnnotation](#conversationannotation) | | No | | annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No | +| answer | string | | Yes | | answer_tokens | integer | | Yes | | conversation_id | string | | Yes | | created_at | integer | | No | @@ -18356,12 +18372,11 @@ Enum class for large language model mode. | inputs | object | | Yes | | message | [JSONValue](#jsonvalue) | | Yes | | message_files | [ [MessageFile](#messagefile) ] | | Yes | -| message_metadata_dict | [JSONValue](#jsonvalue) | | Yes | | message_tokens | integer | | Yes | +| metadata | [JSONValue](#jsonvalue) | | Yes | | parent_message_id | string | | No | | provider_response_latency | number | | Yes | | query | string | | Yes | -| re_sign_file_url_answer | string | | Yes | | status | string | | Yes | | workflow_run_id | string | | No | @@ -18369,27 +18384,27 @@ Enum class for large language model mode. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| agent_thoughts | [ [AgentThought](#agentthought) ] | | No | +| agent_thoughts | [ [AgentThought](#agentthought) ] | | Yes | | annotation | [ConversationAnnotation](#conversationannotation) | | No | | annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No | | answer | string | | Yes | -| answer_tokens | integer | | No | +| answer_tokens | integer | | Yes | | conversation_id | string | | Yes | | created_at | integer | | No | | error | string | | No | | extra_contents | [ [HumanInputContent](#humaninputcontent) ] | | No | -| feedbacks | [ [Feedback](#feedback) ] | | No | +| feedbacks | [ [Feedback](#feedback) ] | | Yes | | from_account_id | string | | No | | from_end_user_id | string | | No | | from_source | string | | Yes | | id | string | | Yes | | inputs | object | | Yes | -| message | [JSONValue](#jsonvalue) | | No | -| message_files | [ [MessageFile](#messagefile) ] | | No | -| message_tokens | integer | | No | -| metadata | [JSONValue](#jsonvalue) | | No | +| message | [JSONValue](#jsonvalue) | | Yes | +| message_files | [ [MessageFile](#messagefile) ] | | Yes | +| message_tokens | integer | | Yes | +| metadata | [JSONValue](#jsonvalue) | | Yes | | parent_message_id | string | | No | -| provider_response_latency | number | | No | +| provider_response_latency | number | | Yes | | query | string | | Yes | | status | string | | Yes | | workflow_run_id | string | | No | @@ -20612,7 +20627,7 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| model_dict | [JSONValue](#jsonvalue) | | No | +| model | [JSONValue](#jsonvalue) | | No | | pre_prompt | string | | No | #### SimpleProviderEntityResponse @@ -21235,9 +21250,11 @@ Tag type #### TextToSpeechVoiceListResponse +Available voices + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| TextToSpeechVoiceListResponse | array | | | +| TextToSpeechVoiceListResponse | array | Available voices | | #### TextToSpeechVoiceQuery @@ -21245,6 +21262,13 @@ Tag type | ---- | ---- | ----------- | -------- | | language | string | Language code | Yes | +#### TextToSpeechVoiceResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| name | string | Voice display name | Yes | +| value | string | Voice identifier | Yes | + #### TokensPerSecondStatisticItem | Name | Type | Description | Required | diff --git a/api/tests/unit_tests/controllers/console/app/test_audio.py b/api/tests/unit_tests/controllers/console/app/test_audio.py index 5dfc5b271eb..138683169c7 100644 --- a/api/tests/unit_tests/controllers/console/app/test_audio.py +++ b/api/tests/unit_tests/controllers/console/app/test_audio.py @@ -148,7 +148,8 @@ def test_console_text_api_error_mapping(app: Flask, monkeypatch: pytest.MonkeyPa def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(AudioService, "transcript_tts_voices", lambda **_kwargs: ["voice-1"]) + expected_voices = [{"name": "Voice 1", "value": "voice-1"}] + monkeypatch.setattr(AudioService, "transcript_tts_voices", lambda **_kwargs: expected_voices) api = TextModesApi() handler = unwrap(api.get) @@ -157,7 +158,7 @@ def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"): response = handler(api, app_model=app_model) - assert response == ["voice-1"] + assert response == expected_voices def test_console_text_modes_language_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -242,7 +243,8 @@ def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatc api = TextModesApi() method = unwrap(api.get) - monkeypatch.setattr(AudioService, "transcript_tts_voices", lambda **_kwargs: ["voice-1"]) + expected_voices = [{"name": "Voice 1", "value": "voice-1"}] + monkeypatch.setattr(AudioService, "transcript_tts_voices", lambda **_kwargs: expected_voices) app_model = SimpleNamespace(tenant_id="tenant-1") @@ -253,7 +255,7 @@ def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatc ): response = method(api, app_model=app_model) - assert response == ["voice-1"] + assert response == expected_voices def test_audio_to_text_with_invalid_file(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -300,7 +302,7 @@ def test_text_to_audio_voices_with_language_filter(app: Flask, monkeypatch: pyte monkeypatch.setattr( AudioService, "transcript_tts_voices", - lambda **_kwargs: [{"id": "voice-1", "name": "Voice 1"}], + lambda **_kwargs: [{"name": "Voice 1", "value": "voice-1"}], ) app_model = SimpleNamespace(tenant_id="tenant-1") diff --git a/api/tests/unit_tests/controllers/console/app/test_audio_api.py b/api/tests/unit_tests/controllers/console/app/test_audio_api.py deleted file mode 100644 index 40e6be11417..00000000000 --- a/api/tests/unit_tests/controllers/console/app/test_audio_api.py +++ /dev/null @@ -1,149 +0,0 @@ -from __future__ import annotations - -import io -from inspect import unwrap -from types import SimpleNamespace - -import pytest -from flask import Flask - -from controllers.console.app import audio as audio_module -from controllers.console.app.error import AudioTooLargeError -from services.errors.audio import AudioTooLargeServiceError - - -def test_audio_to_text_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.ChatMessageAudioApi() - method = unwrap(api.post) - - response_payload = {"text": "hello"} - monkeypatch.setattr(audio_module.AudioService, "transcript_asr", lambda **_kwargs: response_payload) - - app_model = SimpleNamespace(id="app-1") - - data = {"file": (io.BytesIO(b"x"), "sample.wav")} - with app.test_request_context( - "/console/api/apps/app-1/audio-to-text", - method="POST", - data=data, - content_type="multipart/form-data", - ): - response = method(api, app_model=app_model) - - assert response == response_payload - - -def test_audio_to_text_maps_audio_too_large(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.ChatMessageAudioApi() - method = unwrap(api.post) - - monkeypatch.setattr( - audio_module.AudioService, - "transcript_asr", - lambda **_kwargs: (_ for _ in ()).throw(AudioTooLargeServiceError("too large")), - ) - - app_model = SimpleNamespace(id="app-1") - - data = {"file": (io.BytesIO(b"x"), "sample.wav")} - with app.test_request_context( - "/console/api/apps/app-1/audio-to-text", - method="POST", - data=data, - content_type="multipart/form-data", - ): - with pytest.raises(AudioTooLargeError): - method(api, app_model=app_model) - - -def test_text_to_audio_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.ChatMessageTextApi() - method = unwrap(api.post) - - monkeypatch.setattr(audio_module.AudioService, "transcript_tts", lambda **_kwargs: {"audio": "ok"}) - - app_model = SimpleNamespace(id="app-1") - - with app.test_request_context( - "/console/api/apps/app-1/text-to-audio", - method="POST", - json={"text": "hello"}, - ): - response = method(api, app_model=app_model) - - assert response == {"audio": "ok"} - - -def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.TextModesApi() - method = unwrap(api.get) - - monkeypatch.setattr(audio_module.AudioService, "transcript_tts_voices", lambda **_kwargs: ["voice-1"]) - - app_model = SimpleNamespace(tenant_id="tenant-1") - - with app.test_request_context( - "/console/api/apps/app-1/text-to-audio/voices", - method="GET", - query_string={"language": "en-US"}, - ): - response = method(api, app_model=app_model) - - assert response == ["voice-1"] - - -def test_audio_to_text_with_invalid_file(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.ChatMessageAudioApi() - method = unwrap(api.post) - - monkeypatch.setattr(audio_module.AudioService, "transcript_asr", lambda **_kwargs: {"text": "test"}) - - app_model = SimpleNamespace(id="app-1") - - data = {"file": (io.BytesIO(b"invalid"), "sample.xyz")} - with app.test_request_context( - "/console/api/apps/app-1/audio-to-text", - method="POST", - data=data, - content_type="multipart/form-data", - ): - # Should not raise, AudioService is mocked - response = method(api, app_model=app_model) - assert response == {"text": "test"} - - -def test_text_to_audio_with_language_param(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.ChatMessageTextApi() - method = unwrap(api.post) - - monkeypatch.setattr(audio_module.AudioService, "transcript_tts", lambda **_kwargs: {"audio": "test"}) - - app_model = SimpleNamespace(id="app-1") - - with app.test_request_context( - "/console/api/apps/app-1/text-to-audio", - method="POST", - json={"text": "hello", "language": "en-US"}, - ): - response = method(api, app_model=app_model) - assert response == {"audio": "test"} - - -def test_text_to_audio_voices_with_language_filter(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: - api = audio_module.TextModesApi() - method = unwrap(api.get) - - monkeypatch.setattr( - audio_module.AudioService, - "transcript_tts_voices", - lambda **_kwargs: [{"id": "voice-1", "name": "Voice 1"}], - ) - - app_model = SimpleNamespace(tenant_id="tenant-1") - - with app.test_request_context( - "/console/api/apps/app-1/text-to-audio/voices?language=en-US", - method="GET", - ): - response = method(api, app_model=app_model) - assert isinstance(response, list) diff --git a/api/tests/unit_tests/controllers/console/app/test_message_api.py b/api/tests/unit_tests/controllers/console/app/test_message_api.py index 067edc6fd68..20187da6159 100644 --- a/api/tests/unit_tests/controllers/console/app/test_message_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_message_api.py @@ -125,13 +125,57 @@ def test_message_detail_response_normalizes_aliases_and_timestamp(app: Flask, mo "conversation_id": "550e8400-e29b-41d4-a716-446655440001", "inputs": {"foo": "bar"}, "query": "hello", - "re_sign_file_url_answer": "world", + "message": [{"text": "hello"}], + "message_tokens": 7, + "answer": "world", + "answer_tokens": 11, + "provider_response_latency": 1.25, "from_source": "user", + "from_end_user_id": None, + "from_account_id": "550e8400-e29b-41d4-a716-446655440002", + "feedbacks": [], + "workflow_run_id": None, + "annotation": None, + "annotation_hit_history": None, "status": "normal", "created_at": created_at, + "agent_thoughts": [], + "message_files": [], "message_metadata_dict": {"token_usage": 3}, + "error": None, + "parent_message_id": None, + "extra_contents": [], } ) assert response.answer == "world" + assert response.message_tokens == 7 + assert response.answer_tokens == 11 + assert response.provider_response_latency == 1.25 assert response.metadata == {"token_usage": 3} assert response.created_at == int(created_at.timestamp()) + assert response.model_dump(mode="json") == { + "id": "550e8400-e29b-41d4-a716-446655440000", + "conversation_id": "550e8400-e29b-41d4-a716-446655440001", + "inputs": {"foo": "bar"}, + "query": "hello", + "message": [{"text": "hello"}], + "message_tokens": 7, + "answer": "world", + "answer_tokens": 11, + "provider_response_latency": 1.25, + "from_source": "user", + "from_end_user_id": None, + "from_account_id": "550e8400-e29b-41d4-a716-446655440002", + "feedbacks": [], + "workflow_run_id": None, + "annotation": None, + "annotation_hit_history": None, + "created_at": int(created_at.timestamp()), + "agent_thoughts": [], + "message_files": [], + "metadata": {"token_usage": 3}, + "status": "normal", + "error": None, + "parent_message_id": None, + "extra_contents": [], + } diff --git a/api/tests/unit_tests/controllers/console/app/test_statistic_api.py b/api/tests/unit_tests/controllers/console/app/test_statistic_api.py index b0506e348fc..c51a38ad798 100644 --- a/api/tests/unit_tests/controllers/console/app/test_statistic_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_statistic_api.py @@ -3,6 +3,7 @@ from __future__ import annotations from decimal import Decimal from inspect import unwrap from types import SimpleNamespace +from typing import Any import pytest from flask import Flask @@ -39,6 +40,10 @@ def _install_common(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(statistic_module, "convert_datetime_to_date", lambda field: field) +def _json_payload(response: Any) -> dict[str, Any]: + return response if isinstance(response, dict) else response.get_json() + + def test_daily_message_statistic_returns_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: api = statistic_module.DailyMessageStatistic() method = unwrap(api.get) @@ -50,7 +55,7 @@ def test_daily_message_statistic_returns_rows(app: Flask, monkeypatch: pytest.Mo with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - assert response.get_json() == {"data": [{"date": "2024-01-01", "message_count": 3}]} + assert _json_payload(response) == {"data": [{"date": "2024-01-01", "message_count": 3}]} def test_daily_conversation_statistic_returns_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -64,7 +69,7 @@ def test_daily_conversation_statistic_returns_rows(app: Flask, monkeypatch: pyte with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - assert response.get_json() == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} + assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} def test_daily_token_cost_statistic_returns_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -78,11 +83,11 @@ def test_daily_token_cost_statistic_returns_rows(app: Flask, monkeypatch: pytest with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - data = response.get_json() + data = _json_payload(response) assert len(data["data"]) == 1 assert data["data"][0]["date"] == "2024-01-03" assert data["data"][0]["token_count"] == 10 - assert data["data"][0]["total_price"] == 0.25 + assert Decimal(data["data"][0]["total_price"]) == Decimal("0.25") def test_daily_terminals_statistic_returns_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -96,7 +101,7 @@ def test_daily_terminals_statistic_returns_rows(app: Flask, monkeypatch: pytest. with app.test_request_context("/console/api/apps/app-1/statistics/daily-end-users", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - assert response.get_json() == {"data": [{"date": "2024-01-04", "terminal_count": 7}]} + assert _json_payload(response) == {"data": [{"date": "2024-01-04", "terminal_count": 7}]} def test_average_session_interaction_statistic_requires_chat_mode(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -139,7 +144,7 @@ def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.M with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - data = response.get_json() + data = _json_payload(response) assert len(data["data"]) == 3 @@ -153,7 +158,7 @@ def test_daily_message_statistic_empty_result(app: Flask, monkeypatch: pytest.Mo with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - assert response.get_json() == {"data": []} + assert _json_payload(response) == {"data": []} def test_daily_conversation_statistic_with_time_range(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -172,7 +177,7 @@ def test_daily_conversation_statistic_with_time_range(app: Flask, monkeypatch: p with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - assert response.get_json() == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} + assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]} def test_daily_token_cost_with_multiple_currencies(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: @@ -189,5 +194,5 @@ def test_daily_token_cost_with_multiple_currencies(app: Flask, monkeypatch: pyte with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"): response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1")) - data = response.get_json() + data = _json_payload(response) assert len(data["data"]) == 2 diff --git a/packages/contracts/generated/api/console/agent/types.gen.ts b/packages/contracts/generated/api/console/agent/types.gen.ts index 0f922efdab4..114703ff92f 100644 --- a/packages/contracts/generated/api/console/agent/types.gen.ts +++ b/packages/contracts/generated/api/console/agent/types.gen.ts @@ -378,16 +378,16 @@ export type AgentLogMessageListResponse = { } export type MessageDetailResponse = { - agent_thoughts?: Array + agent_thoughts: Array annotation?: ConversationAnnotation | null annotation_hit_history?: ConversationAnnotationHitHistory | null answer: string - answer_tokens?: number | null + answer_tokens: number conversation_id: string created_at?: number | null error?: string | null extra_contents?: Array - feedbacks?: Array + feedbacks: Array from_account_id?: string | null from_end_user_id?: string | null from_source: string @@ -395,12 +395,12 @@ export type MessageDetailResponse = { inputs: { [key: string]: JsonValue } - message?: JsonValue | null - message_files?: Array - message_tokens?: number | null - metadata?: JsonValue | null + message: JsonValue + message_files: Array + message_tokens: number + metadata: JsonValue parent_message_id?: string | null - provider_response_latency?: number | null + provider_response_latency: number query: string status: string workflow_run_id?: string | null diff --git a/packages/contracts/generated/api/console/agent/zod.gen.ts b/packages/contracts/generated/api/console/agent/zod.gen.ts index 8712cf52c17..5d2c30b2637 100644 --- a/packages/contracts/generated/api/console/agent/zod.gen.ts +++ b/packages/contracts/generated/api/console/agent/zod.gen.ts @@ -2525,27 +2525,27 @@ export const zHumanInputContent = z.object({ * MessageDetailResponse */ export const zMessageDetailResponse = z.object({ - agent_thoughts: z.array(zAgentThought).optional(), + agent_thoughts: z.array(zAgentThought), annotation: zConversationAnnotation.nullish(), annotation_hit_history: zConversationAnnotationHitHistory.nullish(), answer: z.string(), - answer_tokens: z.int().nullish(), + answer_tokens: z.int(), conversation_id: z.string(), created_at: z.int().nullish(), error: z.string().nullish(), extra_contents: z.array(zHumanInputContent).optional(), - feedbacks: z.array(zFeedback).optional(), + 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.nullish(), - message_files: z.array(zMessageFile).optional(), - message_tokens: z.int().nullish(), - metadata: zJsonValue.nullish(), + message: zJsonValue, + message_files: z.array(zMessageFile), + message_tokens: z.int(), + metadata: zJsonValue, parent_message_id: z.string().nullish(), - provider_response_latency: z.number().nullish(), + provider_response_latency: z.number(), query: z.string(), status: z.string(), workflow_run_id: z.string().nullish(), diff --git a/packages/contracts/generated/api/console/apps/types.gen.ts b/packages/contracts/generated/api/console/apps/types.gen.ts index 6f593dbd15b..f880dbd9004 100644 --- a/packages/contracts/generated/api/console/apps/types.gen.ts +++ b/packages/contracts/generated/api/console/apps/types.gen.ts @@ -329,14 +329,18 @@ export type AnnotationReplyPayload = { } export type AnnotationJobStatusResponse = { - error_msg?: string | null - job_id?: string | null - job_status?: string | null - record_count?: number | null + job_id: string + job_status: 'completed' | 'error' | 'processing' | 'waiting' | string +} + +export type AnnotationJobStatusDetailResponse = { + error_msg?: string + job_id: string + job_status: 'completed' | 'error' | 'processing' | 'waiting' | string } export type AnnotationSettingResponse = { - embedding_model?: AnnotationEmbeddingModelResponse | null + embedding_model?: AnnotationSettingEmbeddingModelResponse | null enabled: boolean id?: string | null score_threshold?: number | null @@ -372,6 +376,13 @@ export type Annotation = { question?: string | null } +export type AnnotationBatchImportResponse = { + error_msg?: string | null + job_id?: string | null + job_status?: string | null + record_count?: number | null +} + export type AnnotationCountResponse = { count: number } @@ -429,10 +440,10 @@ export type AudioTranscriptResponse = { } export type ConversationWithSummaryPagination = { - has_next: boolean - items: Array + data: Array + has_more: boolean + limit: number page: number - per_page: number total: number } @@ -467,20 +478,20 @@ export type SimpleResultResponse = { } export type ConversationPagination = { - has_next: boolean - items: Array + data: Array + has_more: boolean + limit: number page: number - per_page: number total: number } export type ConversationMessageDetail = { created_at?: number | null - first_message?: MessageDetail | null from_account_id?: string | null from_end_user_id?: string | null from_source: string id: string + message?: MessageDetail | null model_config?: ModelConfig | null status: string } @@ -555,16 +566,16 @@ export type AppIconPayload = { } export type MessageDetailResponse = { - agent_thoughts?: Array + agent_thoughts: Array annotation?: ConversationAnnotation | null annotation_hit_history?: ConversationAnnotationHitHistory | null answer: string - answer_tokens?: number | null + answer_tokens: number conversation_id: string created_at?: number | null error?: string | null extra_contents?: Array - feedbacks?: Array + feedbacks: Array from_account_id?: string | null from_end_user_id?: string | null from_source: string @@ -572,12 +583,12 @@ export type MessageDetailResponse = { inputs: { [key: string]: JsonValue } - message?: JsonValue | null - message_files?: Array - message_tokens?: number | null - metadata?: JsonValue | null + message: JsonValue + message_files: Array + message_tokens: number + metadata: JsonValue parent_message_id?: string | null - provider_response_latency?: number | null + provider_response_latency: number query: string status: string workflow_run_id?: string | null @@ -736,11 +747,7 @@ export type TextToSpeechPayload = { voice?: string | null } -export type AudioBinaryResponse = Blob | File - -export type TextToSpeechVoiceListResponse = Array<{ - [key: string]: unknown -}> +export type TextToSpeechVoiceListResponse = Array export type AppTraceResponse = { enabled?: boolean @@ -1544,7 +1551,7 @@ export type CliToolSuggestion = { name: string } -export type AnnotationEmbeddingModelResponse = { +export type AnnotationSettingEmbeddingModelResponse = { embedding_model_name?: string | null embedding_provider_name?: string | null } @@ -1575,7 +1582,7 @@ export type ConversationWithSummary = { read_at?: number | null status: string status_count?: StatusCount | null - summary_or_query: string + summary: string updated_at?: number | null user_feedback_stats?: FeedbackStat | null } @@ -1589,13 +1596,13 @@ export type Conversation = { admin_feedback_stats?: FeedbackStat | null annotation?: ConversationAnnotation | null created_at?: number | null - first_message?: SimpleMessageDetail | null from_account_id?: string | null from_account_name?: string | null from_end_user_id?: string | null from_end_user_session_id?: string | null from_source: string id: string + message?: SimpleMessageDetail | null model_config?: SimpleModelConfig | null read_at?: number | null status: string @@ -1607,6 +1614,7 @@ export type MessageDetail = { agent_thoughts: Array annotation?: ConversationAnnotation | null annotation_hit_history?: ConversationAnnotationHitHistory | null + answer: string answer_tokens: number conversation_id: string created_at?: number | null @@ -1621,12 +1629,11 @@ export type MessageDetail = { } message: JsonValue message_files: Array - message_metadata_dict: JsonValue message_tokens: number + metadata: JsonValue parent_message_id?: string | null provider_response_latency: number query: string - re_sign_file_url_answer: string status: string workflow_run_id?: string | null } @@ -1725,10 +1732,10 @@ export type DailyMessageStatisticItem = { } export type DailyTokenCostStatisticItem = { - currency: string + currency?: string | null date: string - token_count: number - total_price: string | number + token_count?: number | null + total_price?: string | null } export type TokensPerSecondStatisticItem = { @@ -1741,6 +1748,11 @@ export type UserSatisfactionRateStatisticItem = { rate: number } +export type TextToSpeechVoiceResponse = { + name: string + value: string +} + export type WorkflowAppLogPartialResponse = { created_at?: number | null created_by_account?: SimpleAccount | null @@ -2224,7 +2236,7 @@ export type EnvSuggestion = { } export type SimpleModelConfig = { - model_dict?: JsonValue | null + model?: JsonValue | null pre_prompt?: string | null } @@ -4045,7 +4057,7 @@ export type GetAppsByAppIdAnnotationReplyByActionStatusByJobIdErrors = { } export type GetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponses = { - 200: AnnotationJobStatusResponse + 200: AnnotationJobStatusDetailResponse } export type GetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponse @@ -4169,7 +4181,7 @@ export type PostAppsByAppIdAnnotationsBatchImportErrors = { } export type PostAppsByAppIdAnnotationsBatchImportResponses = { - 200: AnnotationJobStatusResponse + 200: AnnotationBatchImportResponse } export type PostAppsByAppIdAnnotationsBatchImportResponse @@ -4190,7 +4202,7 @@ export type GetAppsByAppIdAnnotationsBatchImportStatusByJobIdErrors = { } export type GetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponses = { - 200: AnnotationJobStatusResponse + 200: AnnotationJobStatusDetailResponse } export type GetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponse @@ -4556,7 +4568,9 @@ export type PostAppsByAppIdCompletionMessagesErrors = { } export type PostAppsByAppIdCompletionMessagesResponses = { - 200: GeneratedAppResponse + 200: { + [key: string]: unknown + } } export type PostAppsByAppIdCompletionMessagesResponse @@ -5130,7 +5144,9 @@ export type PostAppsByAppIdTextToAudioErrors = { } export type PostAppsByAppIdTextToAudioResponses = { - 200: AudioBinaryResponse + 200: { + [key: string]: unknown + } } export type PostAppsByAppIdTextToAudioResponse diff --git a/packages/contracts/generated/api/console/apps/zod.gen.ts b/packages/contracts/generated/api/console/apps/zod.gen.ts index d1930a5b01e..74185ad0dfa 100644 --- a/packages/contracts/generated/api/console/apps/zod.gen.ts +++ b/packages/contracts/generated/api/console/apps/zod.gen.ts @@ -188,10 +188,17 @@ export const zAnnotationReplyPayload = z.object({ * AnnotationJobStatusResponse */ export const zAnnotationJobStatusResponse = z.object({ - error_msg: z.string().nullish(), - job_id: z.string().nullish(), - job_status: z.string().nullish(), - record_count: z.int().nullish(), + job_id: z.string(), + job_status: z.union([z.enum(['completed', 'error', 'processing', 'waiting']), 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()]), }) /** @@ -234,6 +241,16 @@ export const zAnnotationList = z.object({ total: z.int(), }) +/** + * AnnotationBatchImportResponse + */ +export const zAnnotationBatchImportResponse = z.object({ + error_msg: z.string().nullish(), + job_id: z.string().nullish(), + job_status: z.string().nullish(), + record_count: z.int().nullish(), +}) + /** * AnnotationCountResponse */ @@ -449,16 +466,6 @@ export const zTextToSpeechPayload = z.object({ voice: z.string().nullish(), }) -/** - * AudioBinaryResponse - */ -export const zAudioBinaryResponse = z.custom() - -/** - * TextToSpeechVoiceListResponse - */ -export const zTextToSpeechVoiceListResponse = z.array(z.record(z.string(), z.unknown())) - /** * AppTraceResponse */ @@ -1251,9 +1258,9 @@ export const zAgentSkillUploadResponse = z.object({ }) /** - * AnnotationEmbeddingModelResponse + * AnnotationSettingEmbeddingModelResponse */ -export const zAnnotationEmbeddingModelResponse = z.object({ +export const zAnnotationSettingEmbeddingModelResponse = z.object({ embedding_model_name: z.string().nullish(), embedding_provider_name: z.string().nullish(), }) @@ -1262,7 +1269,7 @@ export const zAnnotationEmbeddingModelResponse = z.object({ * AnnotationSettingResponse */ export const zAnnotationSettingResponse = z.object({ - embedding_model: zAnnotationEmbeddingModelResponse.nullish(), + embedding_model: zAnnotationSettingEmbeddingModelResponse.nullish(), enabled: z.boolean(), id: z.string().nullish(), score_threshold: z.number().nullish(), @@ -1456,10 +1463,13 @@ export const zDailyMessageStatisticResponse = z.object({ * DailyTokenCostStatisticItem */ export const zDailyTokenCostStatisticItem = z.object({ - currency: z.string(), + currency: z.string().nullish(), date: z.string(), - token_count: z.int(), - total_price: z.union([z.string(), z.number()]), + token_count: z.int().nullish(), + total_price: z + .string() + .regex(/^(?![-+.]*$)[+-]?\d*(?:\.\d*)?$/) + .nullish(), }) /** @@ -1499,6 +1509,21 @@ export const zUserSatisfactionRateStatisticResponse = z.object({ data: z.array(zUserSatisfactionRateStatisticItem), }) +/** + * TextToSpeechVoiceResponse + */ +export const zTextToSpeechVoiceResponse = z.object({ + name: z.string(), + value: z.string(), +}) + +/** + * TextToSpeechVoiceListResponse + * + * Available voices + */ +export const zTextToSpeechVoiceListResponse = z.array(zTextToSpeechVoiceResponse) + /** * SimpleAccount */ @@ -1574,6 +1599,7 @@ export const zMessageDetail = z.object({ agent_thoughts: z.array(zAgentThought), annotation: zConversationAnnotation.nullish(), annotation_hit_history: zConversationAnnotationHitHistory.nullish(), + answer: z.string(), answer_tokens: z.int(), conversation_id: z.string(), created_at: z.int().nullish(), @@ -1586,12 +1612,11 @@ export const zMessageDetail = z.object({ inputs: z.record(z.string(), zJsonValue), message: zJsonValue, message_files: z.array(zMessageFile), - message_metadata_dict: zJsonValue, message_tokens: z.int(), + metadata: zJsonValue, 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(), }) @@ -2375,11 +2400,11 @@ export const zConversationDetail = z.object({ */ 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(), + message: zMessageDetail.nullish(), model_config: zModelConfig.nullish(), status: z.string(), }) @@ -2525,7 +2550,7 @@ export const zSkillToolInferenceResult = z.object({ * SimpleModelConfig */ export const zSimpleModelConfig = z.object({ - model_dict: zJsonValue.nullish(), + model: zJsonValue.nullish(), pre_prompt: z.string().nullish(), }) @@ -2558,7 +2583,7 @@ export const zConversationWithSummary = z.object({ read_at: z.int().nullish(), status: z.string(), status_count: zStatusCount.nullish(), - summary_or_query: z.string(), + summary: z.string(), updated_at: z.int().nullish(), user_feedback_stats: zFeedbackStat.nullish(), }) @@ -2567,10 +2592,10 @@ export const zConversationWithSummary = z.object({ * ConversationWithSummaryPagination */ export const zConversationWithSummaryPagination = z.object({ - has_next: z.boolean(), - items: z.array(zConversationWithSummary), + data: z.array(zConversationWithSummary), + has_more: z.boolean(), + limit: z.int(), page: z.int(), - per_page: z.int(), total: z.int(), }) @@ -2591,13 +2616,13 @@ export const zConversation = z.object({ admin_feedback_stats: zFeedbackStat.nullish(), annotation: zConversationAnnotation.nullish(), created_at: z.int().nullish(), - first_message: zSimpleMessageDetail.nullish(), from_account_id: z.string().nullish(), from_account_name: z.string().nullish(), from_end_user_id: z.string().nullish(), from_end_user_session_id: z.string().nullish(), from_source: z.string(), id: z.string(), + message: zSimpleMessageDetail.nullish(), model_config: zSimpleModelConfig.nullish(), read_at: z.int().nullish(), status: z.string(), @@ -2609,10 +2634,10 @@ export const zConversation = z.object({ * ConversationPagination */ export const zConversationPagination = z.object({ - has_next: z.boolean(), - items: z.array(zConversation), + data: z.array(zConversation), + has_more: z.boolean(), + limit: z.int(), page: z.int(), - per_page: z.int(), total: z.int(), }) @@ -3847,27 +3872,27 @@ export const zHumanInputContent = z.object({ * MessageDetailResponse */ export const zMessageDetailResponse = z.object({ - agent_thoughts: z.array(zAgentThought).optional(), + agent_thoughts: z.array(zAgentThought), annotation: zConversationAnnotation.nullish(), annotation_hit_history: zConversationAnnotationHitHistory.nullish(), answer: z.string(), - answer_tokens: z.int().nullish(), + answer_tokens: z.int(), conversation_id: z.string(), created_at: z.int().nullish(), error: z.string().nullish(), extra_contents: z.array(zHumanInputContent).optional(), - feedbacks: z.array(zFeedback).optional(), + 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.nullish(), - message_files: z.array(zMessageFile).optional(), - message_tokens: z.int().nullish(), - metadata: zJsonValue.nullish(), + message: zJsonValue, + message_files: z.array(zMessageFile), + message_tokens: z.int(), + metadata: zJsonValue, parent_message_id: z.string().nullish(), - provider_response_latency: z.number().nullish(), + provider_response_latency: z.number(), query: z.string(), status: z.string(), workflow_run_id: z.string().nullish(), @@ -4886,7 +4911,7 @@ export const zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdPath = z.object( * Job status retrieved successfully */ export const zGetAppsByAppIdAnnotationReplyByActionStatusByJobIdResponse - = zAnnotationJobStatusResponse + = zAnnotationJobStatusDetailResponse export const zGetAppsByAppIdAnnotationSettingPath = z.object({ app_id: z.uuid(), @@ -4953,7 +4978,7 @@ export const zPostAppsByAppIdAnnotationsBatchImportPath = z.object({ /** * Batch import started successfully */ -export const zPostAppsByAppIdAnnotationsBatchImportResponse = zAnnotationJobStatusResponse +export const zPostAppsByAppIdAnnotationsBatchImportResponse = zAnnotationBatchImportResponse export const zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdPath = z.object({ app_id: z.uuid(), @@ -4964,7 +4989,7 @@ export const zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdPath = z.object({ * Job status retrieved successfully */ export const zGetAppsByAppIdAnnotationsBatchImportStatusByJobIdResponse - = zAnnotationJobStatusResponse + = zAnnotationJobStatusDetailResponse export const zGetAppsByAppIdAnnotationsCountPath = z.object({ app_id: z.uuid(), @@ -5165,7 +5190,7 @@ export const zPostAppsByAppIdCompletionMessagesPath = z.object({ /** * Completion generated successfully */ -export const zPostAppsByAppIdCompletionMessagesResponse = zGeneratedAppResponse +export const zPostAppsByAppIdCompletionMessagesResponse = z.record(z.string(), z.unknown()) export const zPostAppsByAppIdCompletionMessagesByTaskIdStopPath = z.object({ app_id: z.uuid(), @@ -5509,7 +5534,7 @@ export const zPostAppsByAppIdTextToAudioPath = z.object({ /** * Text to speech conversion successful */ -export const zPostAppsByAppIdTextToAudioResponse = zAudioBinaryResponse +export const zPostAppsByAppIdTextToAudioResponse = z.record(z.string(), z.unknown()) export const zGetAppsByAppIdTextToAudioVoicesPath = z.object({ app_id: z.uuid(),