Skip to content

Commit 22d109c

Browse files
committed
Merge branch 'main' into feat/dify-builder
2 parents 3ce404d + 7cafd65 commit 22d109c

883 files changed

Lines changed: 18303 additions & 15855 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎api/.ruff.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ ignore = [
6363
"B901", # allow return in yield
6464
"B903", # class-as-data-structure
6565
"B904", # raise-without-from-inside-except
66-
"N806", # non-lowercase-variable-in-function
6766
"N815", # mixed-case-variable-in-class-scope
6867
"PT011", # pytest-raises-too-broad
6968
"SIM102", # collapsible-if

‎api/controllers/common/fields.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from graphon.file import helpers as file_helpers
99
from models.model import IconType
1010

11-
type JSONValue = str | int | float | bool | None | dict[str, Any] | list[Any]
11+
type ShallowJSONValue = str | int | float | bool | None | dict[str, Any] | list[Any]
1212
type JSONObject = dict[str, Any]
1313
URLString = Annotated[str, WithJsonSchema({"format": "url", "type": "string"})]
1414
UUIDString = Annotated[str, WithJsonSchema({"format": "uuid", "type": "string"})]
@@ -335,8 +335,8 @@ class SimpleResultResponse(ResponseModel):
335335
result: str = Field(description="Operation result.")
336336

337337

338-
class GeneratedAppResponse(RootModel[JSONValue]):
339-
root: JSONValue
338+
class GeneratedAppResponse(RootModel[ShallowJSONValue]):
339+
root: ShallowJSONValue
340340

341341

342342
class EventStreamResponse(RootModel[str]):
@@ -360,7 +360,7 @@ class AudioBinaryResponse(RootModel[bytes]):
360360

361361

362362
class AudioTranscriptResponse(ResponseModel):
363-
text: str
363+
text: str = Field(description="Transcribed text from audio")
364364

365365

366366
class ValidationResultResponse(ResponseModel):

‎api/controllers/common/schema.py‎

Lines changed: 122 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
`register_schema_models` emit resolvable OpenAPI 3 references.
77
"""
88

9+
import re
910
from collections.abc import Iterable, Mapping
1011
from enum import StrEnum
11-
from typing import Any, Literal, NotRequired, Protocol, TypedDict
12+
from typing import Any, Literal, NotRequired, Protocol, TypedDict, override
13+
from weakref import WeakKeyDictionary
1214

1315
from flask import request
1416
from flask_restx import Namespace
1517
from pydantic import BaseModel, TypeAdapter
18+
from pydantic.json_schema import CoreModeRef, DefsRef, GenerateJsonSchema
1619

1720
from libs.flask_restx_compat import TYPED_EVENT_STREAM_RESPONSE_VENDOR_KEY
1821

@@ -53,39 +56,146 @@ def to_dict(self, flat: bool = True) -> dict[str, str]: ...
5356
def getlist(self, key: str) -> list[str]: ...
5457

5558

56-
def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None:
57-
"""Register a JSON schema and promote any nested Pydantic `$defs`."""
59+
class SchemaNameCollisionError(ValueError):
60+
"""Raised when two different schemas would be published under the same OpenAPI component name."""
61+
62+
63+
# Pydantic names `$defs` after the bare class name and only qualifies them when two
64+
# same-named types meet inside one schema, so the same type can surface under different
65+
# names depending on which model pulled it in. Types that share a class name with
66+
# another documented type get a fixed, unique component name here instead.
67+
SCHEMA_NAME_OVERRIDES: Mapping[str, str] = {
68+
"graphon.model_runtime.entities.common_entities.I18nObject": "ModelRuntimeI18nObject",
69+
"services.knowledge.entities.knowledge_entities.FileInfo": "KnowledgeFileInfo",
70+
"core.plugin.entities.plugin.PluginDependency.Github": "PluginDependencyGithub",
71+
"core.plugin.entities.plugin.PluginDependency.Marketplace": "PluginDependencyMarketplace",
72+
"core.plugin.entities.plugin.PluginDependency.Package": "PluginDependencyPackage",
73+
"core.plugin.entities.bundle.PluginBundleDependency.Github": "PluginBundleDependencyGithub",
74+
"core.plugin.entities.bundle.PluginBundleDependency.Marketplace": "PluginBundleDependencyMarketplace",
75+
"core.plugin.entities.bundle.PluginBundleDependency.Package": "PluginBundleDependencyPackage",
76+
"core.app.entities.task_entities.AgentLogStreamResponse.Data": "AgentLogStreamResponseData",
77+
"core.app.entities.task_entities.HumanInputFormFilledResponse.Data": "HumanInputFormFilledResponseData",
78+
"core.app.entities.task_entities.HumanInputFormTimeoutResponse.Data": "HumanInputFormTimeoutResponseData",
79+
"core.app.entities.task_entities.HumanInputRequiredResponse.Data": "HumanInputRequiredResponseData",
80+
"core.app.entities.task_entities.IterationNodeCompletedStreamResponse.Data": (
81+
"IterationNodeCompletedStreamResponseData"
82+
),
83+
"core.app.entities.task_entities.IterationNodeNextStreamResponse.Data": "IterationNodeNextStreamResponseData",
84+
"core.app.entities.task_entities.IterationNodeStartStreamResponse.Data": "IterationNodeStartStreamResponseData",
85+
"core.app.entities.task_entities.LoopNodeCompletedStreamResponse.Data": "LoopNodeCompletedStreamResponseData",
86+
"core.app.entities.task_entities.LoopNodeNextStreamResponse.Data": "LoopNodeNextStreamResponseData",
87+
"core.app.entities.task_entities.LoopNodeStartStreamResponse.Data": "LoopNodeStartStreamResponseData",
88+
"core.app.entities.task_entities.NodeFinishStreamResponse.Data": "NodeFinishStreamResponseData",
89+
"core.app.entities.task_entities.NodeRetryStreamResponse.Data": "NodeRetryStreamResponseData",
90+
"core.app.entities.task_entities.NodeStartStreamResponse.Data": "NodeStartStreamResponseData",
91+
"core.app.entities.task_entities.ReasoningChunkStreamResponse.Data": "ReasoningChunkStreamResponseData",
92+
"core.app.entities.task_entities.TextChunkStreamResponse.Data": "TextChunkStreamResponseData",
93+
"core.app.entities.task_entities.TextReplaceStreamResponse.Data": "TextReplaceStreamResponseData",
94+
"core.app.entities.task_entities.WorkflowAppBlockingResponse.Data": "WorkflowAppBlockingResponseData",
95+
"core.app.entities.task_entities.WorkflowFinishStreamResponse.Data": "WorkflowFinishStreamResponseData",
96+
"core.app.entities.task_entities.WorkflowPauseStreamResponse.Data": "WorkflowPauseStreamResponseData",
97+
"core.app.entities.task_entities.WorkflowStartStreamResponse.Data": "WorkflowStartStreamResponseData",
98+
}
99+
100+
_REF_ID_SUFFIX = re.compile(r":\d+(?=$|[\[\],])")
101+
_MODULE_PREFIX = re.compile(r"(?:[^.[\],]+\.)+([^.[\],]+)")
102+
103+
# Normalized component name -> (component name, fully qualified source type, schema), per namespace.
104+
_registered_schemas: WeakKeyDictionary[Namespace, dict[str, tuple[str, str, dict]]] = WeakKeyDictionary()
105+
_NON_ALPHANUMERIC = re.compile(r"[^a-z0-9]")
106+
107+
108+
def _normalized_schema_name(name: str) -> str:
109+
"""Fold names that client code generators turn into the same identifier (`JSONValue` / `JsonValue`)."""
110+
111+
return _NON_ALPHANUMERIC.sub("", name.lower())
112+
113+
114+
class _OpenApiJsonSchemaGenerator(GenerateJsonSchema):
115+
"""Emit stable component names and remember which type produced each of them."""
116+
117+
def __init__(self, *args: Any, **kwargs: Any) -> None:
118+
super().__init__(*args, **kwargs)
119+
self.schema_sources: dict[str, str] = {}
120+
121+
@override
122+
def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
123+
core_ref, _mode = core_mode_ref
124+
source = _REF_ID_SUFFIX.sub("", core_ref)
125+
name = SCHEMA_NAME_OVERRIDES.get(source) or self.normalize_name(_MODULE_PREFIX.sub(r"\1", source))
126+
existing_source = self.schema_sources.setdefault(name, source)
127+
if existing_source != source:
128+
raise SchemaNameCollisionError(
129+
f"OpenAPI schema name {name!r} is produced by both {existing_source} and {source}; "
130+
"rename one of them or add it to SCHEMA_NAME_OVERRIDES."
131+
)
132+
defs_ref = DefsRef(name)
133+
self._prioritized_defsref_choices[defs_ref] = [defs_ref]
134+
return defs_ref
135+
136+
137+
def _register_json_schema(namespace: Namespace, name: str, schema: dict, sources: Mapping[str, str]) -> None:
138+
"""Register a JSON schema and promote any nested Pydantic `$defs`.
139+
140+
`sources` maps each component name to the type that produced it. Flask-RESTX keeps
141+
one flat model namespace, so a second registration under an existing name would
142+
silently replace the first. Re-registering the same type with the same schema is a
143+
no-op; a different type, or the same type with a different shape (for example a
144+
response model also registered in validation mode), raises instead.
145+
"""
58146

59147
nested_definitions = schema.get("$defs")
60148
schema_to_register = dict(schema)
61149
if isinstance(nested_definitions, dict):
62150
schema_to_register.pop("$defs")
63151

152+
source = sources.get(name, name)
153+
registered = _registered_schemas.setdefault(namespace, {})
154+
normalized_name = _normalized_schema_name(name)
155+
existing = registered.get(normalized_name)
156+
if existing is not None and existing != (name, source, schema_to_register):
157+
existing_name, existing_source, _ = existing
158+
if existing_name != name:
159+
reason = f"collides with {existing_name!r} from {existing_source} once normalized by client generators"
160+
elif existing_source != source:
161+
reason = f"is produced by both {existing_source} and {source}"
162+
else:
163+
reason = f"is registered by {source} with two different shapes (validation vs serialization mode?)"
164+
raise SchemaNameCollisionError(
165+
f"OpenAPI schema name {name!r} in namespace {namespace.name!r} {reason}; give each shape its own "
166+
"class name, add it to SCHEMA_NAME_OVERRIDES, or register response models only with "
167+
"register_response_schema_models."
168+
)
169+
registered[normalized_name] = (name, source, schema_to_register)
170+
64171
namespace.schema_model(name, schema_to_register)
65172

66173
if not isinstance(nested_definitions, dict):
67174
return
68175

69176
for nested_name, nested_schema in nested_definitions.items():
70177
if isinstance(nested_schema, dict):
71-
_register_json_schema(namespace, nested_name, nested_schema)
178+
_register_json_schema(namespace, nested_name, nested_schema, sources)
72179

73180

74181
JsonSchemaMode = Literal["validation", "serialization"]
75182

76183

77-
def _register_schema_model(namespace: Namespace, model: type[BaseModel], *, mode: JsonSchemaMode) -> None:
184+
def _register_type_schema(namespace: Namespace, source_type: type, *, mode: JsonSchemaMode) -> None:
185+
generator = _OpenApiJsonSchemaGenerator(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0)
186+
schema = generator.generate(TypeAdapter(source_type).core_schema, mode=mode)
78187
_register_json_schema(
79188
namespace,
80-
model.__name__,
81-
model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0, mode=mode),
189+
source_type.__name__,
190+
schema,
191+
{**generator.schema_sources, source_type.__name__: f"{source_type.__module__}.{source_type.__qualname__}"},
82192
)
83193

84194

85195
def register_schema_model(namespace: Namespace, model: type[BaseModel]) -> None:
86196
"""Register a BaseModel and its nested component schemas for OpenAPI documentation."""
87197

88-
_register_schema_model(namespace, model, mode="validation")
198+
_register_type_schema(namespace, model, mode="validation")
89199

90200

91201
def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None:
@@ -98,7 +208,7 @@ def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> No
98208
def register_response_schema_model(namespace: Namespace, model: type[BaseModel]) -> None:
99209
"""Register a BaseModel using its serialized response shape."""
100210

101-
_register_schema_model(namespace, model, mode="serialization")
211+
_register_type_schema(namespace, model, mode="serialization")
102212

103213

104214
def register_response_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None:
@@ -134,11 +244,7 @@ def get_or_create_model(model_name: str, field_def):
134244
def register_enum_models(namespace: Namespace, *models: type[StrEnum]) -> None:
135245
"""Register multiple StrEnum with a namespace."""
136246
for model in models:
137-
_register_json_schema(
138-
namespace,
139-
model.__name__,
140-
TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
141-
)
247+
_register_type_schema(namespace, model, mode="validation")
142248

143249

144250
def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]:
@@ -349,6 +455,8 @@ def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str
349455

350456
__all__ = [
351457
"DEFAULT_REF_TEMPLATE_OPENAPI_3_0",
458+
"SCHEMA_NAME_OVERRIDES",
459+
"SchemaNameCollisionError",
352460
"get_or_create_model",
353461
"query_params_from_model",
354462
"query_params_from_request",

‎api/controllers/console/app/advanced_prompt_template.py‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Literal
22

33
from flask_restx import Resource
44
from pydantic import BaseModel, Field
@@ -23,9 +23,33 @@ class AdvancedPromptTemplateQuery(BaseModel):
2323
model_name: str = Field(..., description="Model name")
2424

2525

26+
class PromptTemplateMessageResponse(ResponseModel):
27+
role: Literal["system", "user", "assistant"]
28+
text: str
29+
30+
31+
class ChatPromptTemplateResponse(ResponseModel):
32+
prompt: list[PromptTemplateMessageResponse]
33+
34+
35+
class CompletionPromptTemplateTextResponse(ResponseModel):
36+
text: str
37+
38+
39+
class ConversationHistoriesRoleResponse(ResponseModel):
40+
user_prefix: str
41+
assistant_prefix: str
42+
43+
44+
class CompletionPromptTemplateResponse(ResponseModel):
45+
prompt: CompletionPromptTemplateTextResponse
46+
conversation_histories_role: ConversationHistoriesRoleResponse | None = None
47+
48+
2649
class AdvancedPromptTemplateResponse(ResponseModel):
27-
chat_prompt_config: dict[str, Any] | None = Field(default=None)
28-
completion_prompt_config: dict[str, Any] | None = Field(default=None)
50+
chat_prompt_config: ChatPromptTemplateResponse | None = Field(default=None)
51+
completion_prompt_config: CompletionPromptTemplateResponse | None = Field(default=None)
52+
stop: list[str] | None = Field(default=None, description="Stop words for completion prompts.")
2953

3054

3155
console_ns.schema_model(

‎api/controllers/console/app/agent.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ class AgentToolCallResponse(ResponseModel):
4848
error: str | None = None
4949
time_cost: float | int
5050
tool_name: str
51-
tool_label: str
51+
tool_label: str | dict[str, str] = Field(description="Tool name or its i18n labels keyed by locale")
5252
tool_input: dict[str, Any]
5353
tool_output: dict[str, Any]
5454
tool_parameters: dict[str, Any]
55-
tool_icon: Any = Field(default=None)
55+
tool_icon: str | dict[str, str] | None = Field(default=None, description="Icon URL or emoji icon")
5656

5757

5858
class AgentIterationLogResponse(ResponseModel):

‎api/controllers/console/app/annotation.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ class AnnotationListQuery(BaseModel):
7676
keyword: str = Field(default="", description="Search keyword")
7777

7878

79+
class AnnotationBatchDeleteQuery(BaseModel):
80+
annotation_id: list[str] = Field(
81+
default_factory=list,
82+
description="Annotation IDs to delete; omitting it clears all annotations of the app",
83+
)
84+
85+
7986
class CreateAnnotationPayload(BaseModel):
8087
message_id: str | None = Field(default=None, description="Message ID")
8188
question: str | None = Field(default=None, description="Question text")
@@ -140,11 +147,6 @@ class AnnotationBatchImportResponse(ResponseModel):
140147

141148
register_schema_models(
142149
console_ns,
143-
Annotation,
144-
AnnotationList,
145-
AnnotationExportList,
146-
AnnotationHitHistory,
147-
AnnotationHitHistoryList,
148150
AnnotationReplyPayload,
149151
AnnotationSettingUpdatePayload,
150152
AnnotationListQuery,
@@ -349,6 +351,7 @@ def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID
349351
@account_initialization_required
350352
@edit_permission_required
351353
@rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp()))
354+
@console_ns.doc(params=query_params_from_model(AnnotationBatchDeleteQuery))
352355
@console_ns.response(204, "Annotations deleted successfully")
353356
@with_session
354357
def delete(self, session: Session, app_id: UUID):

‎api/controllers/console/app/app.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from extensions.ext_application_services import application_services
3333
from fields.app_fields import (
3434
AppDetail,
35-
AppDetailSiteResponse,
3635
AppDetailWithSite,
3736
AppExportResponse,
3837
AppImportResponse,
@@ -41,7 +40,6 @@
4140
AppPartial,
4241
AppTraceResponse,
4342
DeletedTool,
44-
ModelConfigPartial,
4543
RecentAppListResponse,
4644
RecentAppResponse,
4745
Tag,
@@ -275,8 +273,6 @@ def validate_tracing_provider(cls, value: str | None, info) -> str | None:
275273
AppTracePayload,
276274
Tag,
277275
WorkflowPartial,
278-
ModelConfigPartial,
279-
AppDetailSiteResponse,
280276
DeletedTool,
281277
AppExportResponse,
282278
Segmentation,

‎api/controllers/console/app/audio.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from werkzeug.exceptions import HTTPException, InternalServerError
1010

1111
import services
12+
from controllers.common.fields import AudioTranscriptResponse
1213
from controllers.common.rbac import AgentId, PlainApp, RBACCheck, enforce_rbac_checks
1314
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
1415
from controllers.console import console_ns
@@ -79,10 +80,6 @@ class TextToSpeechVoiceQuery(BaseModel):
7980
language: str = Field(..., description="Language code")
8081

8182

82-
class AudioTranscriptResponse(ResponseModel):
83-
text: str = Field(description="Transcribed text from audio")
84-
85-
8683
class AgentAudioTranscriptFormPayload(BaseModel):
8784
model_config = ConfigDict(extra="forbid")
8885

0 commit comments

Comments
 (0)