mirror of
https://github.com/langgenius/dify.git
synced 2026-06-26 06:41:10 +08:00
Merge cc795d13a2 into a246dc8b17
This commit is contained in:
commit
2465ea46e2
@ -167,12 +167,16 @@ register_schema_models(
|
||||
ChatMessagesQuery,
|
||||
MessageFeedbackPayload,
|
||||
FeedbackExportQuery,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
AnnotationCountResponse,
|
||||
SuggestedQuestionsResponse,
|
||||
MessageDetailResponse,
|
||||
MessageInfiniteScrollPaginationResponse,
|
||||
SimpleResultResponse,
|
||||
TextFileResponse,
|
||||
)
|
||||
register_response_schema_models(console_ns, SimpleResultResponse, TextFileResponse)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/chat-messages")
|
||||
|
||||
@ -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
|
||||
@ -100,8 +101,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/<uuid:file_id>/preview")
|
||||
@ -114,7 +114,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")
|
||||
@ -124,4 +124,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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -2,11 +2,11 @@ from typing import Any, Union
|
||||
|
||||
from flask import Response
|
||||
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
|
||||
@ -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/<string:server_code>/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))
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
@ -13429,7 +13429,6 @@ Soft lifecycle state for Agent records.
|
||||
| created_at | integer | | No |
|
||||
| files | [ string ] | | Yes |
|
||||
| id | string | | Yes |
|
||||
| message_chain_id | string | | No |
|
||||
| message_id | string | | Yes |
|
||||
| observation | string | | No |
|
||||
| position | integer | | Yes |
|
||||
@ -14540,8 +14539,8 @@ Enum class for configurate method of provider model.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| annotation_create_account | [SimpleAccount](#simpleaccount) | | No |
|
||||
| annotation_id | string | | Yes |
|
||||
| created_at | integer | | No |
|
||||
| id | string | | Yes |
|
||||
|
||||
#### ConversationDetail
|
||||
|
||||
@ -17079,6 +17078,7 @@ Enum class for large language model mode.
|
||||
| agent_thoughts | [ [AgentThought](#agentthought) ] | | No |
|
||||
| annotation | [ConversationAnnotation](#conversationannotation) | | No |
|
||||
| annotation_hit_history | [ConversationAnnotationHitHistory](#conversationannotationhithistory) | | No |
|
||||
| answer | string | | Yes |
|
||||
| answer_tokens | integer | | No |
|
||||
| conversation_id | string | | Yes |
|
||||
| created_at | integer | | No |
|
||||
@ -17092,12 +17092,11 @@ Enum class for large language model mode.
|
||||
| inputs | object | | Yes |
|
||||
| message | [JSONValue](#jsonvalue) | | No |
|
||||
| message_files | [ [MessageFile](#messagefile) ] | | No |
|
||||
| message_metadata_dict | [JSONValue](#jsonvalue) | | No |
|
||||
| message_tokens | integer | | No |
|
||||
| metadata | [JSONValue](#jsonvalue) | | No |
|
||||
| parent_message_id | string | | No |
|
||||
| provider_response_latency | number | | No |
|
||||
| query | string | | Yes |
|
||||
| re_sign_file_url_answer | string | | Yes |
|
||||
| status | string | | Yes |
|
||||
| workflow_run_id | string | | No |
|
||||
|
||||
@ -19026,11 +19025,19 @@ Model class for provider quota configuration.
|
||||
| last_id | string | | No |
|
||||
| limit | integer, <br>**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
|
||||
|
||||
|
||||
@ -1423,6 +1423,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
|
||||
|
||||
@ -9,7 +9,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,
|
||||
@ -21,6 +31,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):
|
||||
api = spec_module.SpecSchemaDefinitionsApi()
|
||||
|
||||
@ -269,6 +269,7 @@ export type MessageDetailResponse = {
|
||||
agent_thoughts?: Array<AgentThought>
|
||||
annotation?: ConversationAnnotation | null
|
||||
annotation_hit_history?: ConversationAnnotationHitHistory | null
|
||||
answer: string
|
||||
answer_tokens?: number | null
|
||||
conversation_id: string
|
||||
created_at?: number | null
|
||||
@ -284,12 +285,11 @@ export type MessageDetailResponse = {
|
||||
}
|
||||
message?: JsonValue | null
|
||||
message_files?: Array<MessageFile>
|
||||
message_metadata_dict?: JsonValue | null
|
||||
message_tokens?: number | null
|
||||
metadata?: JsonValue | null
|
||||
parent_message_id?: string | null
|
||||
provider_response_latency?: number | null
|
||||
query: string
|
||||
re_sign_file_url_answer: string
|
||||
status: string
|
||||
workflow_run_id?: string | null
|
||||
}
|
||||
@ -723,7 +723,6 @@ export type AgentThought = {
|
||||
created_at?: number | null
|
||||
files: Array<string>
|
||||
id: string
|
||||
message_chain_id?: string | null
|
||||
message_id: string
|
||||
observation?: string | null
|
||||
position: number
|
||||
@ -743,8 +742,8 @@ export type ConversationAnnotation = {
|
||||
|
||||
export type ConversationAnnotationHitHistory = {
|
||||
annotation_create_account?: SimpleAccount | null
|
||||
annotation_id: string
|
||||
created_at?: number | null
|
||||
id: string
|
||||
}
|
||||
|
||||
export type HumanInputContent = {
|
||||
|
||||
@ -570,7 +570,6 @@ export const zAgentThought = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
files: z.array(z.string()),
|
||||
id: z.string(),
|
||||
message_chain_id: z.string().nullish(),
|
||||
message_id: z.string(),
|
||||
observation: z.string().nullish(),
|
||||
position: z.int(),
|
||||
@ -1056,8 +1055,8 @@ export const zConversationAnnotation = z.object({
|
||||
*/
|
||||
export const zConversationAnnotationHitHistory = z.object({
|
||||
annotation_create_account: zSimpleAccount.nullish(),
|
||||
annotation_id: z.string(),
|
||||
created_at: z.int().nullish(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
@ -2035,6 +2034,7 @@ export const zMessageDetailResponse = z.object({
|
||||
agent_thoughts: z.array(zAgentThought).optional(),
|
||||
annotation: zConversationAnnotation.nullish(),
|
||||
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
|
||||
answer: z.string(),
|
||||
answer_tokens: z.int().nullish(),
|
||||
conversation_id: z.string(),
|
||||
created_at: z.int().nullish(),
|
||||
@ -2048,12 +2048,11 @@ export const zMessageDetailResponse = z.object({
|
||||
inputs: z.record(z.string(), zJsonValue),
|
||||
message: zJsonValue.nullish(),
|
||||
message_files: z.array(zMessageFile).optional(),
|
||||
message_metadata_dict: zJsonValue.nullish(),
|
||||
message_tokens: z.int().nullish(),
|
||||
metadata: zJsonValue.nullish(),
|
||||
parent_message_id: z.string().nullish(),
|
||||
provider_response_latency: z.number().nullish(),
|
||||
query: z.string(),
|
||||
re_sign_file_url_answer: z.string(),
|
||||
status: z.string(),
|
||||
workflow_run_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
@ -472,6 +472,7 @@ export type MessageDetailResponse = {
|
||||
agent_thoughts?: Array<AgentThought>
|
||||
annotation?: ConversationAnnotation | null
|
||||
annotation_hit_history?: ConversationAnnotationHitHistory | null
|
||||
answer: string
|
||||
answer_tokens?: number | null
|
||||
conversation_id: string
|
||||
created_at?: number | null
|
||||
@ -487,12 +488,11 @@ export type MessageDetailResponse = {
|
||||
}
|
||||
message?: JsonValue | null
|
||||
message_files?: Array<MessageFile>
|
||||
message_metadata_dict?: JsonValue | null
|
||||
message_tokens?: number | null
|
||||
metadata?: JsonValue | null
|
||||
parent_message_id?: string | null
|
||||
provider_response_latency?: number | null
|
||||
query: string
|
||||
re_sign_file_url_answer: string
|
||||
status: string
|
||||
workflow_run_id?: string | null
|
||||
}
|
||||
@ -1498,7 +1498,6 @@ export type AgentThought = {
|
||||
created_at?: number | null
|
||||
files: Array<string>
|
||||
id: string
|
||||
message_chain_id?: string | null
|
||||
message_id: string
|
||||
observation?: string | null
|
||||
position: number
|
||||
@ -1518,8 +1517,8 @@ export type ConversationAnnotation = {
|
||||
|
||||
export type ConversationAnnotationHitHistory = {
|
||||
annotation_create_account?: SimpleAccount | null
|
||||
annotation_id: string
|
||||
created_at?: number | null
|
||||
id: string
|
||||
}
|
||||
|
||||
export type HumanInputContent = {
|
||||
|
||||
@ -1150,7 +1150,6 @@ export const zAgentThought = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
files: z.array(z.string()),
|
||||
id: z.string(),
|
||||
message_chain_id: z.string().nullish(),
|
||||
message_id: z.string(),
|
||||
observation: z.string().nullish(),
|
||||
position: z.int(),
|
||||
@ -1371,8 +1370,8 @@ export const zConversationAnnotation = z.object({
|
||||
*/
|
||||
export const zConversationAnnotationHitHistory = z.object({
|
||||
annotation_create_account: zSimpleAccount.nullish(),
|
||||
annotation_id: z.string(),
|
||||
created_at: z.int().nullish(),
|
||||
id: z.string(),
|
||||
})
|
||||
|
||||
/**
|
||||
@ -3455,6 +3454,7 @@ export const zMessageDetailResponse = z.object({
|
||||
agent_thoughts: z.array(zAgentThought).optional(),
|
||||
annotation: zConversationAnnotation.nullish(),
|
||||
annotation_hit_history: zConversationAnnotationHitHistory.nullish(),
|
||||
answer: z.string(),
|
||||
answer_tokens: z.int().nullish(),
|
||||
conversation_id: z.string(),
|
||||
created_at: z.int().nullish(),
|
||||
@ -3468,12 +3468,11 @@ export const zMessageDetailResponse = z.object({
|
||||
inputs: z.record(z.string(), zJsonValue),
|
||||
message: zJsonValue.nullish(),
|
||||
message_files: z.array(zMessageFile).optional(),
|
||||
message_metadata_dict: zJsonValue.nullish(),
|
||||
message_tokens: z.int().nullish(),
|
||||
metadata: zJsonValue.nullish(),
|
||||
parent_message_id: z.string().nullish(),
|
||||
provider_response_latency: z.number().nullish(),
|
||||
query: z.string(),
|
||||
re_sign_file_url_answer: z.string(),
|
||||
status: z.string(),
|
||||
workflow_run_id: z.string().nullish(),
|
||||
})
|
||||
|
||||
@ -246,7 +246,6 @@ export type AgentThought = {
|
||||
created_at?: number | null
|
||||
files: Array<string>
|
||||
id: string
|
||||
message_chain_id?: string | null
|
||||
message_id: string
|
||||
observation?: string | null
|
||||
position: number
|
||||
|
||||
@ -266,7 +266,6 @@ export const zAgentThought = z.object({
|
||||
created_at: z.int().nullish(),
|
||||
files: z.array(z.string()),
|
||||
id: z.string(),
|
||||
message_chain_id: z.string().nullish(),
|
||||
message_id: z.string(),
|
||||
observation: z.string().nullish(),
|
||||
position: z.int(),
|
||||
|
||||
@ -4,7 +4,15 @@ export type ClientOptions = {
|
||||
baseUrl: `${string}://${string}/console/api` | (string & {})
|
||||
}
|
||||
|
||||
export type SchemaDefinitionsResponse = unknown
|
||||
export type SchemaDefinitionsResponse = Array<SchemaDefinitionItemResponse>
|
||||
|
||||
export type SchemaDefinitionItemResponse = {
|
||||
label: string
|
||||
name: string
|
||||
schema: {
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export type GetSpecSchemaDefinitionsData = {
|
||||
body?: never
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,13 +10,21 @@ type SwaggerSchema = JsonObject & {
|
||||
$ref?: string
|
||||
}
|
||||
|
||||
type OpenApiMediaType = JsonObject & {
|
||||
schema?: SwaggerSchema
|
||||
}
|
||||
|
||||
type OpenApiResponse = JsonObject & {
|
||||
content?: Record<string, OpenApiMediaType>
|
||||
}
|
||||
|
||||
type OpenApiComponents = JsonObject & {
|
||||
schemas?: Record<string, SwaggerSchema>
|
||||
}
|
||||
|
||||
type SwaggerOperation = JsonObject & {
|
||||
operationId?: string
|
||||
responses?: Record<string, unknown>
|
||||
responses?: Record<string, OpenApiResponse>
|
||||
}
|
||||
|
||||
type SwaggerDocument = JsonObject & {
|
||||
@ -52,6 +60,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const apiOpenApiDir = path.resolve(currentDir, 'openapi')
|
||||
|
||||
const operationMethods = new Set(['delete', 'get', 'patch', 'post', 'put'])
|
||||
const pydanticDecimalStringPattern = '^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$'
|
||||
const codegenSafeDecimalStringPattern = '^(?![-+.]*$)[+-]?0*\\d*\\.?\\d*$'
|
||||
|
||||
const opaqueJsonContent = (): Record<string, OpenApiMediaType> => ({
|
||||
'application/json': {
|
||||
schema: {
|
||||
additionalProperties: true,
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const apiSpecs: ApiSpec[] = [
|
||||
{ filename: 'console-openapi.json', name: 'console' },
|
||||
@ -182,6 +201,46 @@ const addOperationIds = (document: SwaggerDocument) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isOpaqueContractResponse = (response: OpenApiResponse) => {
|
||||
const content = response.content
|
||||
if (!isObject(content))
|
||||
return false
|
||||
|
||||
return Object.entries(content).some(([mediaType, media]) => {
|
||||
if (!isObject(media))
|
||||
return false
|
||||
|
||||
return (mediaType === 'application/json' || mediaType === 'text/event-stream') && !('schema' in media)
|
||||
})
|
||||
}
|
||||
|
||||
const hasOpaqueContractSuccessResponse = (operation: SwaggerOperation) => {
|
||||
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
|
||||
return /^2\d\d$/.test(status) && isObject(response) && isOpaqueContractResponse(response)
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeOpaqueContractResponses = (document: SwaggerDocument) => {
|
||||
// Some backend endpoints has no schema (e.g. external) and will trap heyapi here
|
||||
// So we forge an opaque schema here
|
||||
for (const pathItem of Object.values(document.paths ?? {})) {
|
||||
for (const [method, operation] of Object.entries(pathItem)) {
|
||||
if (!operationMethods.has(method) || !isObject(operation))
|
||||
continue
|
||||
|
||||
const swaggerOperation = operation as SwaggerOperation
|
||||
if (!hasOpaqueContractSuccessResponse(swaggerOperation))
|
||||
continue
|
||||
|
||||
Object.values(swaggerOperation.responses ?? {})
|
||||
.filter(response => isObject(response) && isOpaqueContractResponse(response))
|
||||
.forEach((response) => {
|
||||
response.content = opaqueJsonContent()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasSuccessResponse = (operation: SwaggerOperation) => {
|
||||
return Object.entries(operation.responses ?? {}).some(([status, response]) => {
|
||||
if (!/^2\d\d$/.test(status))
|
||||
@ -215,6 +274,7 @@ const filterContractOperations = (document: SwaggerDocument) => {
|
||||
}
|
||||
|
||||
const normalizeApiSwagger = (document: SwaggerDocument) => {
|
||||
normalizeOpaqueContractResponses(document)
|
||||
filterContractOperations(document)
|
||||
addOperationIds(document)
|
||||
|
||||
@ -380,10 +440,20 @@ const createApiConfig = (job: ApiJob): UserConfig => ({
|
||||
'name': 'zod',
|
||||
'~resolvers': {
|
||||
string: (ctx) => {
|
||||
if (ctx.schema.format !== 'binary')
|
||||
return undefined
|
||||
if (ctx.schema.format === 'binary')
|
||||
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
|
||||
|
||||
return $(ctx.symbols.z).attr('custom').call().generic($.type.or($.type('Blob'), $.type('File')))
|
||||
if (ctx.schema.pattern === pydanticDecimalStringPattern) {
|
||||
// the pydantic generated regex will emit error like
|
||||
// regexp/no-useless-assertions, so patch the regex here
|
||||
return $(ctx.symbols.z)
|
||||
.attr('string')
|
||||
.call()
|
||||
.attr('regex')
|
||||
.call($.regexp(codegenSafeDecimalStringPattern))
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -217,14 +217,8 @@ const toFeedback = (feedback: NonNullable<MessageDetailResponse['feedbacks']>[nu
|
||||
}
|
||||
}
|
||||
|
||||
type AgentDebugMessageWithLegacyAnswer = MessageDetailResponse & {
|
||||
answer?: string | null
|
||||
}
|
||||
|
||||
const getAgentDebugMessageAnswer = (message: MessageDetailResponse) => {
|
||||
const legacyAnswer = (message as AgentDebugMessageWithLegacyAnswer).answer
|
||||
|
||||
return message.re_sign_file_url_answer ?? legacyAnswer ?? ''
|
||||
return message.answer ?? ''
|
||||
}
|
||||
|
||||
function getFormattedAgentDebugChatTree(messages: MessageDetailResponse[]): ChatItemInTree[] {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user