diff --git a/api/controllers/service_api_with_auth/app/app.py b/api/controllers/service_api_with_auth/app/app.py index 2227dd21ba..f110a8eb92 100644 --- a/api/controllers/service_api_with_auth/app/app.py +++ b/api/controllers/service_api_with_auth/app/app.py @@ -1,10 +1,9 @@ -from flask_restful import Resource, marshal_with # type: ignore - from controllers.common import fields from controllers.common import helpers as controller_helpers from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import AppUnavailableError from controllers.service_api_with_auth.wraps import validate_app_token +from flask_restful import Resource, marshal_with # type: ignore from models.model import App, AppMode from services.app_service import AppService @@ -15,7 +14,26 @@ class AppParameterApi(Resource): @validate_app_token @marshal_with(fields.parameters_fields) def get(self, app_model: App): - """Retrieve app parameters.""" + """Retrieve app parameters. + --- + tags: + - app/parameters + summary: Get app parameters + description: Retrieve parameters for the current application + security: + - ApiKeyAuth: [] + responses: + 200: + description: Parameters retrieved successfully + schema: + type: object + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: App unavailable + """ if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}: workflow = app_model.workflow if workflow is None: @@ -40,14 +58,53 @@ class AppParameterApi(Resource): class AppMetaApi(Resource): @validate_app_token def get(self, app_model: App): - """Get app meta""" + """Get app meta information. + --- + tags: + - app/meta + summary: Get app meta + description: Retrieve meta information for the current application + security: + - ApiKeyAuth: [] + responses: + 200: + description: Meta information retrieved successfully + schema: + type: object + 401: + description: Invalid or missing token + """ return AppService().get_app_meta(app_model) class AppInfoApi(Resource): @validate_app_token def get(self, app_model: App): - """Get app information""" + """Get app information. + --- + tags: + - app/info + summary: Get app info + description: Retrieve basic information about the current application + security: + - ApiKeyAuth: [] + responses: + 200: + description: App information retrieved successfully + schema: + type: object + properties: + name: + type: string + description: + type: string + tags: + type: array + items: + type: string + 401: + description: Invalid or missing token + """ tags = [tag.name for tag in app_model.tags] return {"name": app_model.name, "description": app_model.description, "tags": tags} diff --git a/api/controllers/service_api_with_auth/app/audio.py b/api/controllers/service_api_with_auth/app/audio.py index 2cde88f440..b6bb979c68 100644 --- a/api/controllers/service_api_with_auth/app/audio.py +++ b/api/controllers/service_api_with_auth/app/audio.py @@ -1,9 +1,5 @@ import logging -from flask import request -from flask_restful import Resource, reqparse # type: ignore -from werkzeug.exceptions import InternalServerError - import services from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import ( @@ -17,9 +13,15 @@ from controllers.service_api_with_auth.app.error import ( ProviderQuotaExceededError, UnsupportedAudioTypeError, ) -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token -from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from controllers.service_api_with_auth.wraps import validate_app_token +from core.errors.error import ( + ModelCurrentlyNotSupportError, + ProviderTokenNotInitError, + QuotaExceededError, +) from core.model_runtime.errors.invoke import InvokeError +from flask import request +from flask_restful import Resource, reqparse # type: ignore from models.model import App, AppMode, EndUser from services.audio_service import AudioService from services.errors.audio import ( @@ -28,11 +30,46 @@ from services.errors.audio import ( ProviderNotSupportSpeechToTextServiceError, UnsupportedAudioTypeServiceError, ) +from werkzeug.exceptions import InternalServerError class AudioApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM)) + @validate_app_token def post(self, app_model: App, end_user: EndUser): + """Transcribe audio to text. + --- + tags: + - app/audio + summary: Transcribe audio + description: Convert audio file to text using speech-to-text + security: + - ApiKeyAuth: [] + consumes: + - multipart/form-data + parameters: + - name: file + in: formData + required: true + type: file + description: The audio file to transcribe + responses: + 200: + description: Audio transcribed successfully + schema: + type: object + properties: + text: + type: string + description: Transcribed text + 400: + description: Invalid request, no audio uploaded, or unsupported audio type + 401: + description: Invalid or missing token + 413: + description: Audio file too large + 500: + description: Provider error or internal server error + """ file = request.files["file"] try: @@ -66,8 +103,51 @@ class AudioApi(Resource): class TextApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) + @validate_app_token def post(self, app_model: App, end_user: EndUser): + """Convert text to speech. + --- + tags: + - app/audio + summary: Text to speech + description: Convert text to speech audio + security: + - ApiKeyAuth: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + required: + - text + properties: + text: + type: string + description: Text to convert to speech + voice: + type: string + description: Voice ID to use for speech synthesis + streaming: + type: boolean + default: false + description: Whether to stream the audio response + responses: + 200: + description: Text converted to speech successfully + schema: + type: object + properties: + audio_url: + type: string + description: URL to the generated audio file + 400: + description: Invalid request + 401: + description: Invalid or missing token + 500: + description: Provider error or internal server error + """ try: parser = reqparse.RequestParser() parser.add_argument("message_id", type=str, required=False, location="json") @@ -87,7 +167,11 @@ class TextApi(Resource): voice = args.get("voice") or text_to_speech.get("voice") else: try: - voice = args.get("voice") or app_model.app_model_config.text_to_speech_dict.get("voice") + voice = args.get("voice") or ( + app_model.app_model_config.text_to_speech_dict.get("voice") + if app_model.app_model_config + else None + ) except Exception: voice = None response = AudioService.transcript_tts( diff --git a/api/controllers/service_api_with_auth/app/completion.py b/api/controllers/service_api_with_auth/app/completion.py index 6b154a9cf4..69d9450915 100644 --- a/api/controllers/service_api_with_auth/app/completion.py +++ b/api/controllers/service_api_with_auth/app/completion.py @@ -1,9 +1,5 @@ import logging -from libs.login import login_required -from flask_restful import Resource, reqparse # type: ignore -from werkzeug.exceptions import InternalServerError, NotFound - import services from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import ( @@ -15,7 +11,11 @@ from controllers.service_api_with_auth.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from controllers.service_api_with_auth.wraps import ( + FetchUserArg, + WhereisUserArg, + validate_app_token, +) from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( @@ -24,15 +24,62 @@ from core.errors.error import ( QuotaExceededError, ) from core.model_runtime.errors.invoke import InvokeError +from flask_restful import Resource, reqparse # type: ignore from libs import helper from libs.helper import uuid_value +from libs.login import login_required from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService +from werkzeug.exceptions import InternalServerError, NotFound class CompletionApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser): + """Generate completion response. + --- + tags: + - app/completion + summary: Generate completion + description: Generate a completion response for the provided inputs + security: + - ApiKeyAuth: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + required: + - inputs + properties: + inputs: + type: object + description: Input variables for the completion + query: + type: string + description: User query text + files: + type: array + description: List of files to process + response_mode: + type: string + enum: [blocking, streaming] + description: Response delivery mode + retriever_from: + type: string + default: dev + description: Source of the retriever + responses: + 200: + description: Completion generated successfully + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: App unavailable or conversation not found + """ if app_model.mode != "completion": raise AppUnavailableError() @@ -82,8 +129,36 @@ class CompletionApi(Resource): class CompletionStopApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser, task_id): + """Stop a running completion task. + --- + tags: + - app/completion + summary: Stop completion task + description: Stop a running completion generation task + security: + - ApiKeyAuth: [] + parameters: + - name: task_id + in: path + required: true + type: string + description: ID of the task to stop + responses: + 200: + description: Task stopped successfully + schema: + type: object + properties: + result: + type: string + example: success + 401: + description: Invalid or missing token + 404: + description: App unavailable + """ if app_model.mode != "completion": raise AppUnavailableError() @@ -93,8 +168,61 @@ class CompletionStopApi(Resource): class ChatApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser): + """Generate chat response. + --- + tags: + - app/chat + summary: Generate chat response + description: Generate a chat response for the provided inputs and query + security: + - ApiKeyAuth: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + required: + - inputs + - query + properties: + inputs: + type: object + description: Input variables for the chat + query: + type: string + description: User query text + files: + type: array + description: List of files to process + response_mode: + type: string + enum: [blocking, streaming] + description: Response delivery mode + conversation_id: + type: string + format: uuid + description: ID of an existing conversation to continue + retriever_from: + type: string + default: dev + description: Source of the retriever + auto_generate_name: + type: boolean + default: true + description: Whether to automatically generate a name for the conversation + responses: + 200: + description: Chat response generated successfully + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: App unavailable or conversation not found + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() @@ -141,8 +269,36 @@ class ChatApi(Resource): class ChatStopApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser, task_id): + """Stop a running chat task. + --- + tags: + - app/chat + summary: Stop chat task + description: Stop a running chat generation task + security: + - ApiKeyAuth: [] + parameters: + - name: task_id + in: path + required: true + type: string + description: ID of the task to stop + responses: + 200: + description: Task stopped successfully + schema: + type: object + properties: + result: + type: string + example: success + 401: + description: Invalid or missing token + 404: + description: App unavailable or not a chat app + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() diff --git a/api/controllers/service_api_with_auth/app/conversation.py b/api/controllers/service_api_with_auth/app/conversation.py index 1993e329b8..5d3e5c7ce2 100644 --- a/api/controllers/service_api_with_auth/app/conversation.py +++ b/api/controllers/service_api_with_auth/app/conversation.py @@ -1,12 +1,11 @@ -from flask_restful import Resource, marshal_with, reqparse # type: ignore -from flask_restful.inputs import int_range # type: ignore -from sqlalchemy.orm import Session -from werkzeug.exceptions import NotFound - import services from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import NotChatAppError -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from controllers.service_api_with_auth.wraps import ( + FetchUserArg, + WhereisUserArg, + validate_app_token, +) from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from fields.conversation_fields import ( @@ -14,15 +13,63 @@ from fields.conversation_fields import ( conversation_infinite_scroll_pagination_fields, simple_conversation_fields, ) +from flask_restful import Resource, marshal_with, reqparse # type: ignore +from flask_restful.inputs import int_range # type: ignore from libs.helper import uuid_value from models.model import App, AppMode, EndUser from services.conversation_service import ConversationService +from sqlalchemy.orm import Session # type: ignore +from werkzeug.exceptions import NotFound class ConversationApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) + @validate_app_token @marshal_with(conversation_infinite_scroll_pagination_fields) def get(self, app_model: App, end_user: EndUser): + """Get conversations list. + --- + tags: + - app/conversation + summary: List conversations + description: Get a paginated list of conversations for the current user + security: + - ApiKeyAuth: [] + parameters: + - name: last_id + in: query + type: string + format: uuid + description: ID of the last conversation for pagination + - name: limit + in: query + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Number of conversations to return + - name: sort_by + in: query + type: string + enum: [created_at, -created_at, updated_at, -updated_at] + default: -updated_at + description: Field to sort by, prefix with - for descending order + responses: + 200: + description: Conversations retrieved successfully + schema: + type: object + properties: + data: + type: array + items: + type: object + has_more: + type: boolean + 401: + description: Invalid or missing token + 404: + description: Not a chat app + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() @@ -56,9 +103,38 @@ class ConversationApi(Resource): class ConversationDetailApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) + @validate_app_token @marshal_with(conversation_delete_fields) def delete(self, app_model: App, end_user: EndUser, c_id): + """Delete a conversation. + --- + tags: + - app/conversation + summary: Delete conversation + description: Delete a specific conversation + security: + - ApiKeyAuth: [] + parameters: + - name: c_id + in: path + required: true + type: string + format: uuid + description: ID of the conversation to delete + responses: + 200: + description: Conversation deleted successfully + schema: + type: object + properties: + result: + type: string + example: success + 401: + description: Invalid or missing token + 404: + description: Conversation not found or not a chat app + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() @@ -73,9 +149,57 @@ class ConversationDetailApi(Resource): class ConversationRenameApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON)) + @validate_app_token @marshal_with(simple_conversation_fields) def post(self, app_model: App, end_user: EndUser, c_id): + """Rename a conversation. + --- + tags: + - app/conversation + summary: Rename conversation + description: Change the name of a specific conversation + security: + - ApiKeyAuth: [] + parameters: + - name: c_id + in: path + required: true + type: string + format: uuid + description: ID of the conversation to rename + - name: body + in: body + required: true + schema: + type: object + required: + - name + properties: + name: + type: string + description: New name for the conversation + auto_generate: + type: boolean + default: false + description: Whether to auto-generate the name + responses: + 200: + description: Conversation renamed successfully + schema: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: Conversation not found or not a chat app + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() diff --git a/api/controllers/service_api_with_auth/app/file.py b/api/controllers/service_api_with_auth/app/file.py index 80aa6c05ce..42f0d29d86 100644 --- a/api/controllers/service_api_with_auth/app/file.py +++ b/api/controllers/service_api_with_auth/app/file.py @@ -1,6 +1,3 @@ -from flask import request -from flask_restful import Resource, marshal_with # type: ignore - import services from controllers.common.errors import FilenameNotExistsError from controllers.service_api_with_auth import api @@ -10,16 +7,66 @@ from controllers.service_api_with_auth.app.error import ( TooManyFilesError, UnsupportedFileTypeError, ) -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from controllers.service_api_with_auth.wraps import ( + FetchUserArg, + WhereisUserArg, + validate_app_token, +) from fields.file_fields import file_fields +from flask import request +from flask_restful import Resource, marshal_with # type: ignore from models.model import App, EndUser from services.file_service import FileService class FileApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.FORM)) + @validate_app_token @marshal_with(file_fields) def post(self, app_model: App, end_user: EndUser): + """Upload a file. + --- + tags: + - app/file + summary: Upload file + description: Upload a file to be used with the application + security: + - ApiKeyAuth: [] + consumes: + - multipart/form-data + parameters: + - name: file + in: formData + required: true + type: file + description: The file to upload + responses: + 201: + description: File uploaded successfully + schema: + type: object + properties: + id: + type: string + name: + type: string + size: + type: integer + extension: + type: string + mime_type: + type: string + url: + type: string + created_at: + type: string + format: date-time + 400: + description: Invalid request, no file uploaded, unsupported file type, or too many files + 401: + description: Invalid or missing token + 413: + description: File too large + """ file = request.files["file"] # check file diff --git a/api/controllers/service_api_with_auth/app/message.py b/api/controllers/service_api_with_auth/app/message.py index d2b68e79f5..a039adb990 100644 --- a/api/controllers/service_api_with_auth/app/message.py +++ b/api/controllers/service_api_with_auth/app/message.py @@ -1,20 +1,23 @@ import logging -from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore -from flask_restful.inputs import int_range # type: ignore -from werkzeug.exceptions import BadRequest, InternalServerError, NotFound - import services from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import NotChatAppError -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from controllers.service_api_with_auth.wraps import ( + FetchUserArg, + WhereisUserArg, + validate_app_token, +) from core.app.entities.app_invoke_entities import InvokeFrom from fields.conversation_fields import message_file_fields from fields.raws import FilesContainedField +from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore +from flask_restful.inputs import int_range # type: ignore from libs.helper import TimestampField, uuid_value from models.model import App, AppMode, EndUser from services.errors.message import SuggestedQuestionsAfterAnswerDisabledError from services.message_service import MessageService +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound class MessageListApi(Resource): @@ -74,9 +77,57 @@ class MessageListApi(Resource): "data": fields.List(fields.Nested(message_fields)), } - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) + @validate_app_token @marshal_with(message_infinite_scroll_pagination_fields) def get(self, app_model: App, end_user: EndUser): + """Get messages list. + --- + tags: + - app/message + summary: List messages + description: Get a paginated list of messages for a conversation + security: + - ApiKeyAuth: [] + parameters: + - name: conversation_id + in: query + required: true + type: string + format: uuid + description: ID of the conversation to get messages for + - name: first_id + in: query + type: string + format: uuid + description: ID of the first message for pagination + - name: limit + in: query + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Number of messages to return + responses: + 200: + description: Messages retrieved successfully + schema: + type: object + properties: + limit: + type: integer + has_more: + type: boolean + data: + type: array + items: + type: object + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: Conversation not found or not a chat app + """ app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: raise NotChatAppError() @@ -98,8 +149,54 @@ class MessageListApi(Resource): class MessageFeedbackApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser, message_id): + """Submit feedback for a message. + --- + tags: + - app/message + summary: Submit message feedback + description: Submit user feedback for a specific message + security: + - ApiKeyAuth: [] + parameters: + - name: message_id + in: path + required: true + type: string + format: uuid + description: ID of the message to provide feedback for + - name: body + in: body + required: true + schema: + type: object + required: + - rating + properties: + rating: + type: string + enum: [like, dislike, null] + description: User's rating of the message + content: + type: string + description: Additional feedback content + responses: + 200: + description: Feedback submitted successfully + schema: + type: object + properties: + result: + type: string + example: success + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: Message not found or not a chat app + """ message_id = str(message_id) parser = reqparse.RequestParser() @@ -122,8 +219,43 @@ class MessageFeedbackApi(Resource): class MessageSuggestedApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY, required=True)) + @validate_app_token def get(self, app_model: App, end_user: EndUser, message_id): + """Get suggested questions for a message. + --- + tags: + - app/message + summary: Get suggested questions + description: Get suggested follow-up questions for a specific message + security: + - ApiKeyAuth: [] + parameters: + - name: message_id + in: path + required: true + type: string + format: uuid + description: ID of the message to get suggestions for + responses: + 200: + description: Suggested questions retrieved successfully + schema: + type: object + properties: + result: + type: string + example: success + data: + type: array + items: + type: string + 400: + description: Invalid request or suggestions disabled + 401: + description: Invalid or missing token + 404: + description: Message not found or not a chat app + """ message_id = str(message_id) app_mode = AppMode.value_of(app_model.mode) if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: diff --git a/api/controllers/service_api_with_auth/app/workflow.py b/api/controllers/service_api_with_auth/app/workflow.py index ff1113677b..7c50da4841 100644 --- a/api/controllers/service_api_with_auth/app/workflow.py +++ b/api/controllers/service_api_with_auth/app/workflow.py @@ -1,9 +1,5 @@ import logging -from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore -from flask_restful.inputs import int_range # type: ignore -from werkzeug.exceptions import InternalServerError - from controllers.service_api_with_auth import api from controllers.service_api_with_auth.app.error import ( CompletionRequestError, @@ -12,7 +8,11 @@ from controllers.service_api_with_auth.app.error import ( ProviderNotInitializeError, ProviderQuotaExceededError, ) -from controllers.service_api_with_auth.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from controllers.service_api_with_auth.wraps import ( + FetchUserArg, + WhereisUserArg, + validate_app_token, +) from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( @@ -23,11 +23,14 @@ from core.errors.error import ( from core.model_runtime.errors.invoke import InvokeError from extensions.ext_database import db from fields.workflow_app_log_fields import workflow_app_log_pagination_fields +from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore +from flask_restful.inputs import int_range # type: ignore from libs import helper from models.model import App, AppMode, EndUser from models.workflow import WorkflowRun from services.app_generate_service import AppGenerateService from services.workflow_app_service import WorkflowAppService +from werkzeug.exceptions import InternalServerError logger = logging.getLogger(__name__) @@ -50,8 +53,55 @@ class WorkflowRunDetailApi(Resource): @validate_app_token @marshal_with(workflow_run_fields) def get(self, app_model: App, workflow_id: str): - """ - Get a workflow task running detail + """Get workflow run details. + --- + tags: + - app/workflow + summary: Get workflow run details + description: Retrieve details of a specific workflow run + security: + - ApiKeyAuth: [] + parameters: + - name: workflow_id + in: path + required: true + type: string + description: ID of the workflow run to retrieve + responses: + 200: + description: Workflow run details retrieved successfully + schema: + type: object + properties: + id: + type: string + workflow_id: + type: string + status: + type: string + inputs: + type: object + outputs: + type: object + error: + type: string + total_steps: + type: integer + total_tokens: + type: integer + created_at: + type: string + format: date-time + finished_at: + type: string + format: date-time + elapsed_time: + type: number + format: float + 401: + description: Invalid or missing token + 404: + description: Workflow run not found or not a workflow app """ app_mode = AppMode.value_of(app_model.mode) if app_mode != AppMode.WORKFLOW: @@ -62,10 +112,41 @@ class WorkflowRunDetailApi(Resource): class WorkflowRunApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser): - """ - Run workflow + """Run a workflow. + --- + tags: + - app/workflow + summary: Run workflow + description: Execute a workflow with the provided inputs + security: + - ApiKeyAuth: [] + parameters: + - name: body + in: body + required: true + schema: + type: object + required: + - inputs + properties: + inputs: + type: object + description: Input variables for the workflow + response_mode: + type: string + enum: [blocking, streaming] + description: Response delivery mode + responses: + 200: + description: Workflow executed successfully + 400: + description: Invalid request + 401: + description: Invalid or missing token + 404: + description: Not a workflow app """ app_mode = AppMode.value_of(app_model.mode) if app_mode != AppMode.WORKFLOW: @@ -101,10 +182,35 @@ class WorkflowRunApi(Resource): class WorkflowTaskStopApi(Resource): - @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON, required=True)) + @validate_app_token def post(self, app_model: App, end_user: EndUser, task_id: str): - """ - Stop workflow task + """Stop a running workflow task. + --- + tags: + - app/workflow + summary: Stop workflow task + description: Stop a running workflow task + security: + - ApiKeyAuth: [] + parameters: + - name: task_id + in: path + required: true + type: string + description: ID of the task to stop + responses: + 200: + description: Task stopped successfully + schema: + type: object + properties: + result: + type: string + example: success + 401: + description: Invalid or missing token + 404: + description: Not a workflow app """ app_mode = AppMode.value_of(app_model.mode) if app_mode != AppMode.WORKFLOW: @@ -119,8 +225,50 @@ class WorkflowAppLogApi(Resource): @validate_app_token @marshal_with(workflow_app_log_pagination_fields) def get(self, app_model: App): - """ - Get workflow app logs + """Get workflow app logs. + --- + tags: + - app/workflow + summary: Get workflow logs + description: Retrieve logs for workflow app executions + security: + - ApiKeyAuth: [] + parameters: + - name: page + in: query + type: integer + minimum: 1 + default: 1 + description: Page number for pagination + - name: limit + in: query + type: integer + minimum: 1 + maximum: 100 + default: 20 + description: Number of logs per page + responses: + 200: + description: Workflow logs retrieved successfully + schema: + type: object + properties: + data: + type: array + items: + type: object + has_more: + type: boolean + limit: + type: integer + total: + type: integer + page: + type: integer + 401: + description: Invalid or missing token + 404: + description: Not a workflow app """ parser = reqparse.RequestParser() parser.add_argument("keyword", type=str, location="args") diff --git a/api/controllers/service_api_with_auth/wraps.py b/api/controllers/service_api_with_auth/wraps.py index 8e69d042a0..f096d89861 100644 --- a/api/controllers/service_api_with_auth/wraps.py +++ b/api/controllers/service_api_with_auth/wraps.py @@ -9,6 +9,7 @@ from flask import current_app, request from flask_login import user_logged_in # type: ignore from flask_restful import Resource # type: ignore from libs.login import _get_user +from libs.passport import PassportService from models.account import Account, Tenant, TenantAccountJoin, TenantStatus from models.model import ApiToken, App, EndUser from pydantic import BaseModel # type: ignore @@ -33,16 +34,34 @@ class FetchUserArg(BaseModel): required: bool = False -# TODO: add auth jwt token check -def validate_app_token( - view: Optional[Callable] = None, *, fetch_user_arg: Optional[FetchUserArg] = None -): +def validate_app_token(view: Optional[Callable] = None): def decorator(view_func): @wraps(view_func) def decorated_view(*args, **kwargs): - api_token = validate_and_get_api_token("app") + # Extract user info from Bearer token + auth_header = request.headers.get("Authorization") + if auth_header is None or " " not in auth_header: + raise Unauthorized("Authorization header must be provided and start with 'Bearer'") + + auth_scheme, auth_token = auth_header.split(None, 1) + auth_scheme = auth_scheme.lower() + + if auth_scheme != "bearer": + raise Unauthorized("Authorization scheme must be 'Bearer'") + + # Decode the JWT token to extract user info + try: + decoded = PassportService().verify(auth_token) + user_id = decoded.get("user_id") + if not user_id: + raise Unauthorized("Invalid token: missing user_id") + except Exception as e: + raise Unauthorized(f"Failed to extract user_id from token: {str(e)}") + + # Get app model using hardcoded ID + app_id = "b278ba96-fa8e-48a8-b3e9-debe34468be0" # TODO: ytqh Replace with your actual hardcoded app ID + app_model = db.session.query(App).filter(App.id == app_id).first() - app_model = db.session.query(App).filter(App.id == api_token.app_id).first() if not app_model: raise Forbidden("The app no longer exists.") @@ -52,11 +71,7 @@ def validate_app_token( if not app_model.enable_api: raise Forbidden("The app's API service has been disabled.") - tenant = ( - db.session.query(Tenant) - .filter(Tenant.id == app_model.tenant_id) - .first() - ) + tenant = db.session.query(Tenant).filter(Tenant.id == app_model.tenant_id).first() if tenant is None: raise ValueError("Tenant does not exist.") if tenant.status == TenantStatus.ARCHIVE: @@ -64,26 +79,7 @@ def validate_app_token( kwargs["app_model"] = app_model - if fetch_user_arg: - if fetch_user_arg.fetch_from == WhereisUserArg.QUERY: - user_id = request.args.get("user") - elif fetch_user_arg.fetch_from == WhereisUserArg.JSON: - user_id = request.get_json().get("user") - elif fetch_user_arg.fetch_from == WhereisUserArg.FORM: - user_id = request.form.get("user") - else: - # use default-user - user_id = None - - if not user_id and fetch_user_arg.required: - raise ValueError("Arg user must be provided.") - - if user_id: - user_id = str(user_id) - - kwargs["end_user"] = create_or_update_end_user_for_user_id( - app_model, user_id - ) + kwargs["end_user"] = create_or_update_end_user_for_user_id(app_model, user_id) return view_func(*args, **kwargs) @@ -108,27 +104,13 @@ def cloud_edition_billing_resource_check(resource: str, api_token_type: str): documents_upload_quota = features.documents_upload_quota if resource == "members" and 0 < members.limit <= members.size: - raise Forbidden( - "The number of members has reached the limit of your subscription." - ) + raise Forbidden("The number of members has reached the limit of your subscription.") elif resource == "apps" and 0 < apps.limit <= apps.size: - raise Forbidden( - "The number of apps has reached the limit of your subscription." - ) - elif ( - resource == "vector_space" - and 0 < vector_space.limit <= vector_space.size - ): - raise Forbidden( - "The capacity of the vector space has reached the limit of your subscription." - ) - elif ( - resource == "documents" - and 0 < documents_upload_quota.limit <= documents_upload_quota.size - ): - raise Forbidden( - "The number of documents has reached the limit of your subscription." - ) + raise Forbidden("The number of apps has reached the limit of your subscription.") + elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size: + raise Forbidden("The capacity of the vector space has reached the limit of your subscription.") + elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size: + raise Forbidden("The number of documents has reached the limit of your subscription.") else: return view(*args, **kwargs) @@ -204,9 +186,7 @@ def validate_and_get_api_token(scope: str | None = None): """ auth_header = request.headers.get("Authorization") if auth_header is None or " " not in auth_header: - raise Unauthorized( - "Authorization header must be provided and start with 'Bearer'" - ) + raise Unauthorized("Authorization header must be provided and start with 'Bearer'") auth_scheme, auth_token = auth_header.split(None, 1) auth_scheme = auth_scheme.lower() @@ -221,10 +201,7 @@ def validate_and_get_api_token(scope: str | None = None): update(ApiToken) .where( ApiToken.token == auth_token, - ( - ApiToken.last_used_at.is_(None) - | (ApiToken.last_used_at < cutoff_time) - ), + (ApiToken.last_used_at.is_(None) | (ApiToken.last_used_at < cutoff_time)), ApiToken.type == scope, ) .values(last_used_at=current_time) @@ -234,9 +211,7 @@ def validate_and_get_api_token(scope: str | None = None): api_token = result.scalar_one_or_none() if not api_token: - stmt = select(ApiToken).where( - ApiToken.token == auth_token, ApiToken.type == scope - ) + stmt = select(ApiToken).where(ApiToken.token == auth_token, ApiToken.type == scope) api_token = session.scalar(stmt) if not api_token: raise Unauthorized("Access token is invalid") @@ -246,9 +221,7 @@ def validate_and_get_api_token(scope: str | None = None): return api_token -def create_or_update_end_user_for_user_id( - app_model: App, user_id: Optional[str] = None -) -> EndUser: +def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str] = None) -> EndUser: """ Create or update session terminal based on user ID. """