diff --git a/api/controllers/console/files.py b/api/controllers/console/files.py index 24c3a5978e1..5e680c83702 100644 --- a/api/controllers/console/files.py +++ b/api/controllers/console/files.py @@ -27,6 +27,7 @@ from controllers.console.wraps import ( ) from extensions.ext_database import db from fields.file_fields import FileResponse, UploadConfig +from libs.helper import dump_response from libs.login import login_required from models import Account from services.file_service import FileService @@ -117,8 +118,7 @@ class FileApi(Resource): except services.errors.file.BlockedFileExtensionError as blocked_extension_error: raise BlockedFileExtensionError(blocked_extension_error.description) - response = FileResponse.model_validate(upload_file, from_attributes=True) - return response.model_dump(mode="json"), 201 + return dump_response(FileResponse, upload_file), 201 @console_ns.route("/files//preview") @@ -131,7 +131,7 @@ class FilePreviewApi(Resource): def get(self, current_tenant_id: str, file_id: UUID): file_id_str = str(file_id) text = FileService(db.engine).get_file_preview(file_id_str, current_tenant_id) - return {"content": text} + return TextContentResponse(content=text).model_dump(mode="json") @console_ns.route("/files/support-type") @@ -141,4 +141,4 @@ class FileSupportTypeApi(Resource): @account_initialization_required @console_ns.response(200, "Success", console_ns.models[AllowedExtensionsResponse.__name__]) def get(self): - return {"allowed_extensions": list(DOCUMENT_EXTENSIONS)} + return AllowedExtensionsResponse(allowed_extensions=list(DOCUMENT_EXTENSIONS)).model_dump(mode="json") diff --git a/api/controllers/console/spec.py b/api/controllers/console/spec.py index 27b07b4dd81..70e0d1d14ae 100644 --- a/api/controllers/console/spec.py +++ b/api/controllers/console/spec.py @@ -1,8 +1,9 @@ import logging +from collections.abc import Mapping from typing import Any from flask_restx import Resource -from pydantic import RootModel +from pydantic import Field, RootModel from controllers.common.schema import register_response_schema_models from controllers.console.wraps import ( @@ -10,6 +11,7 @@ from controllers.console.wraps import ( setup_required, ) from core.schemas.schema_manager import SchemaManager +from fields.base import ResponseModel from libs.login import login_required from . import console_ns @@ -17,11 +19,17 @@ from . import console_ns logger = logging.getLogger(__name__) -class SchemaDefinitionsResponse(RootModel[Any]): - root: Any +class SchemaDefinitionItemResponse(ResponseModel): + name: str + label: str + schema_: Mapping[str, Any] = Field(alias="schema") -register_response_schema_models(console_ns, SchemaDefinitionsResponse) +class SchemaDefinitionsResponse(RootModel[list[SchemaDefinitionItemResponse]]): + pass + + +register_response_schema_models(console_ns, SchemaDefinitionItemResponse, SchemaDefinitionsResponse) @console_ns.route("/spec/schema-definitions") diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 380ca470c47..45a5a4e6899 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -2,11 +2,11 @@ from typing import Any, Union from flask import Response, request from flask_restx import Resource -from pydantic import BaseModel, Field, ValidationError +from pydantic import BaseModel, Field, RootModel, ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker -from controllers.common.schema import register_schema_model +from controllers.common.schema import register_response_schema_models, register_schema_model from controllers.mcp import mcp_ns from core.mcp import types as mcp_types from core.mcp.server.streamable_http import handle_mcp_request, negotiate_protocol_version @@ -33,7 +33,12 @@ class MCPRequestPayload(BaseModel): id: int | str | None = Field(default=None, description="Request ID for tracking responses") +class MCPJSONRPCResponse(RootModel[mcp_types.JSONRPCResponse | mcp_types.JSONRPCError]): + pass + + register_schema_model(mcp_ns, MCPRequestPayload) +register_response_schema_models(mcp_ns, MCPJSONRPCResponse) @mcp_ns.route("/server//mcp") @@ -42,13 +47,10 @@ class MCPAppApi(Resource): @mcp_ns.doc("handle_mcp_request") @mcp_ns.doc(description="Handle Model Context Protocol (MCP) requests for a specific server") @mcp_ns.doc(params={"server_code": "Unique identifier for the MCP server"}) - @mcp_ns.doc( - responses={ - 200: "MCP response successfully processed", - 400: "Invalid MCP request or parameters", - 404: "Server or app not found", - } - ) + @mcp_ns.response(200, "MCP JSON-RPC response", mcp_ns.models[MCPJSONRPCResponse.__name__]) + @mcp_ns.response(202, "MCP notification accepted") + @mcp_ns.response(400, "Invalid MCP request or parameters") + @mcp_ns.response(404, "Server or app not found") def post(self, server_code: str): """Handle MCP requests for a specific server. @@ -64,6 +66,7 @@ class MCPAppApi(Resource): Raises: ValidationError: Invalid request format or parameters """ + # response-contract:ignore MCP route returns Flask Response from JSON-RPC handler args = MCPRequestPayload.model_validate(mcp_ns.payload or {}) request_id: Union[int, str] | None = args.id mcp_request = self._parse_mcp_request(args.model_dump(exclude_none=True)) diff --git a/api/core/rag/extractor/extract_processor.py b/api/core/rag/extractor/extract_processor.py index 4d11ebe5005..36d879427a5 100644 --- a/api/core/rag/extractor/extract_processor.py +++ b/api/core/rag/extractor/extract_processor.py @@ -1,7 +1,7 @@ import re import tempfile from pathlib import Path -from typing import Union +from typing import Literal, overload from urllib.parse import unquote from configs import dify_config @@ -40,10 +40,22 @@ USER_AGENT = ( class ExtractProcessor: + @overload + @classmethod + def load_from_upload_file( + cls, upload_file: UploadFile, return_text: Literal[True], is_automatic: bool = False + ) -> str: ... + + @overload + @classmethod + def load_from_upload_file( + cls, upload_file: UploadFile, return_text: Literal[False] = False, is_automatic: bool = False + ) -> list[Document]: ... + @classmethod def load_from_upload_file( cls, upload_file: UploadFile, return_text: bool = False, is_automatic: bool = False - ) -> Union[list[Document], str]: + ) -> list[Document] | str: extract_setting = ExtractSetting( datasource_type=DatasourceType.FILE, upload_file=upload_file, document_model="text_model" ) @@ -53,8 +65,16 @@ class ExtractProcessor: else: return cls.extract(extract_setting, is_automatic) + @overload @classmethod - def load_from_url(cls, url: str, return_text: bool = False) -> Union[list[Document], str]: + def load_from_url(cls, url: str, return_text: Literal[True]) -> str: ... + + @overload + @classmethod + def load_from_url(cls, url: str, return_text: Literal[False] = False) -> list[Document]: ... + + @classmethod + def load_from_url(cls, url: str, return_text: bool = False) -> list[Document] | str: response = remote_fetcher.make_request("GET", url, headers={"User-Agent": USER_AGENT}) with tempfile.TemporaryDirectory() as temp_dir: diff --git a/api/fields/raws.py b/api/fields/raws.py deleted file mode 100644 index c7e047626f1..00000000000 --- a/api/fields/raws.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import override - -from flask_restx import fields - -from graphon.file import File - - -class FilesContainedField(fields.Raw): - @override - def format(self, value): - return self._format_file_object(value) - - def _format_file_object(self, v): - if isinstance(v, File): - return v.model_dump() - if isinstance(v, dict): - return {k: self._format_file_object(vv) for k, vv in v.items()} - if isinstance(v, list): - return [self._format_file_object(vv) for vv in v] - return v diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 40ed4ed809c..23e02fac98c 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -20681,11 +20681,19 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs. | last_id | string | | No | | limit | integer,
**Default:** 20 | | No | +#### SchemaDefinitionItemResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| label | string | | Yes | +| name | string | | Yes | +| schema | object | | Yes | + #### SchemaDefinitionsResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| SchemaDefinitionsResponse | | | | +| SchemaDefinitionsResponse | array | | | #### SegmentAttachmentResponse diff --git a/api/services/summary_index_service.py b/api/services/summary_index_service.py index 3adc18dd2d5..a960b219334 100644 --- a/api/services/summary_index_service.py +++ b/api/services/summary_index_service.py @@ -1424,6 +1424,7 @@ class SummaryIndexService: - generating: Number of summaries being generated - error: Number of summaries with errors - not_started: Number of segments without summary records + - timeout: Number of summaries that timed out - summaries: List of summary records with status and content preview """ from services.dataset_service import SegmentService diff --git a/api/tests/unit_tests/controllers/console/test_spec.py b/api/tests/unit_tests/controllers/console/test_spec.py index 58d7027751b..44fb8345928 100644 --- a/api/tests/unit_tests/controllers/console/test_spec.py +++ b/api/tests/unit_tests/controllers/console/test_spec.py @@ -11,7 +11,17 @@ class TestSpecSchemaDefinitionsApi: api = spec_module.SpecSchemaDefinitionsApi() method = unwrap(api.get) - schema_definitions = [{"type": "string"}] + schema_definitions = [ + { + "name": "conversation-variable", + "label": "Conversation variable", + "schema": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + } + ] with patch.object( spec_module, @@ -23,6 +33,12 @@ class TestSpecSchemaDefinitionsApi: assert status == 200 assert resp == schema_definitions + assert spec_module.SchemaDefinitionsResponse.model_validate(resp).model_dump(mode="json") == schema_definitions + + def test_get_documents_tight_response_model(self): + response = spec_module.SpecSchemaDefinitionsApi.get.__apidoc__["responses"]["200"] + + assert response[1].name == spec_module.SchemaDefinitionsResponse.__name__ def test_get_exception_returns_empty_list(self, caplog: pytest.LogCaptureFixture): api = spec_module.SpecSchemaDefinitionsApi() diff --git a/packages/contracts/generated/api/console/spec/types.gen.ts b/packages/contracts/generated/api/console/spec/types.gen.ts index e7b9c0ea9b2..853fb2cccb1 100644 --- a/packages/contracts/generated/api/console/spec/types.gen.ts +++ b/packages/contracts/generated/api/console/spec/types.gen.ts @@ -4,7 +4,15 @@ export type ClientOptions = { baseUrl: `${string}://${string}/console/api` | (string & {}) } -export type SchemaDefinitionsResponse = unknown +export type SchemaDefinitionsResponse = Array + +export type SchemaDefinitionItemResponse = { + label: string + name: string + schema: { + [key: string]: unknown + } +} export type GetSpecSchemaDefinitionsData = { body?: never diff --git a/packages/contracts/generated/api/console/spec/zod.gen.ts b/packages/contracts/generated/api/console/spec/zod.gen.ts index 8fc487d5ac4..b1135d7f34d 100644 --- a/packages/contracts/generated/api/console/spec/zod.gen.ts +++ b/packages/contracts/generated/api/console/spec/zod.gen.ts @@ -2,10 +2,19 @@ import * as z from 'zod' +/** + * SchemaDefinitionItemResponse + */ +export const zSchemaDefinitionItemResponse = z.object({ + label: z.string(), + name: z.string(), + schema: z.record(z.string(), z.unknown()), +}) + /** * SchemaDefinitionsResponse */ -export const zSchemaDefinitionsResponse = z.unknown() +export const zSchemaDefinitionsResponse = z.array(zSchemaDefinitionItemResponse) /** * Success