Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
c4a6504
fix(api): reject duplicate OpenAPI schema names in Flask-RESTX specs
hyoban Oct 15, 2026
8f6d929
refactor(web): migrate log queries to the generated console contract
hyoban Oct 15, 2026
e592a2b
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
ce6c9d1
refactor(web): migrate annotation requests to the generated console c…
hyoban Oct 15, 2026
bfe2915
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
be52167
refactor(web): migrate app log messages and agent logs to the console…
hyoban Oct 15, 2026
b289d81
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
6691077
refactor(web): migrate model provider credentials to the console cont…
hyoban Oct 15, 2026
fdf6377
refactor(web): migrate publish, notion import, hit testing and worksp…
hyoban Oct 15, 2026
50e8aa8
refactor(web): migrate workspace members and RBAC roles to the consol…
hyoban Oct 15, 2026
7915c5e
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
bdf872c
refactor(web): migrate console auth, account and workspace calls in s…
hyoban Oct 15, 2026
c812d24
refactor(web): migrate service/use-common queries and mutations to th…
hyoban Oct 15, 2026
5c9f951
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
4d2c64d
fix: repair tests, unused exports and i18n keys after console contrac…
hyoban Oct 15, 2026
29c2f5e
refactor(web): migrate datasets service and knowledge hooks to the co…
hyoban Oct 15, 2026
d789287
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
64d03e7
refactor(web): migrate plugin console calls in use-plugins to the con…
hyoban Oct 15, 2026
42a3ab4
chore(web): prune oxlint suppressions for migrated service files
hyoban Oct 15, 2026
0f7e4fd
refactor(web): migrate tool services and hooks to the console contract
hyoban Oct 15, 2026
191d4d8
refactor(web): migrate pipeline and datasource hooks to the console c…
hyoban Oct 15, 2026
c9409f5
refactor(web): migrate workflow draft variables, debug and plugin ser…
hyoban Oct 15, 2026
03fa068
fix: CI failures after migrating knowledge, plugin and tool calls
hyoban Oct 15, 2026
68002ef
test(api): build the same-named schema model with explicit fields
hyoban Oct 15, 2026
3c64697
Merge remote-tracking branch 'origin/main' into refactor/web-orpc-con…
hyoban Oct 15, 2026
da0d1bb
refactor(web): use generated datasource auth options directly
hyoban Oct 15, 2026
edddfaf
test(web): allow the Builder model settings flow more time on loaded …
hyoban Oct 15, 2026
d553a13
test(web): wait longer for the Builder model settings button on loade…
hyoban Oct 15, 2026
7553fb3
chore(web): restore use-tools lint suppressions
hyoban Oct 15, 2026
6a889f4
fix(web): convert agent log thought files only once
hyoban Oct 15, 2026
791829f
refactor(web): use generated tool options directly
hyoban Oct 15, 2026
adb3945
fix(web): keep full schema definitions for workflow schema type matching
hyoban Oct 15, 2026
1322499
test(web): follow the generated tool list keys in plugin and agent specs
hyoban Oct 15, 2026
a695c88
refactor(web): narrow tool and datasource responses without casts
hyoban Oct 15, 2026
57de707
refactor(web): map model provider details from the contract
hyoban Oct 15, 2026
c2b7592
chore: keep oxlint suppressions without a final newline
hyoban Oct 15, 2026
c282d41
test(web): stub model provider details options in the provider card spec
hyoban Oct 15, 2026
f4978ea
refactor(web): use generated pipeline options directly
hyoban Oct 15, 2026
9038b78
refactor(web): narrow RAG plugin recommendations from the contract
hyoban Oct 15, 2026
cd6c270
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2026
ae6410f
fix(api): declare dataset metadata value types
hyoban Oct 16, 2026
d889f6a
docs(api): regenerate OpenAPI markdown for dataset metadata types
hyoban Oct 16, 2026
4ae641c
fix(web): skip the datasource run response mapping when the request f…
hyoban Oct 16, 2026
4337c24
fix(api): type document process rules and source details
hyoban Oct 16, 2026
98f57d8
fix(web): keep typed defaults when editing pipeline input fields
hyoban Oct 16, 2026
320c160
Merge remote-tracking branch 'origin/main' into refactor/web-orpc-con…
hyoban Oct 16, 2026
588a232
refactor(web): use generated document options directly
hyoban Oct 16, 2026
b232dc1
Merge branch 'refactor/web-orpc-console-calls' of github.com:langgeni…
hyoban Oct 16, 2026
3c2b31d
fix(web): keep the document list invalidation stable
hyoban Oct 16, 2026
432bd25
Merge remote-tracking branch 'origin/main' into refactor/web-orpc-con…
hyoban Oct 16, 2026
58cf9f4
refactor(web): use generated metadata options directly
hyoban Oct 16, 2026
7c5f211
Merge branch 'refactor/web-orpc-console-calls' of github.com:langgeni…
hyoban Oct 16, 2026
2290bc5
refactor(web): use generated segment options directly
hyoban Oct 16, 2026
5bacd67
fix(api): keep segment response schemas free of model imports
hyoban Oct 16, 2026
15c81f3
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 16, 2026
83d5a74
chore(web): drop the test-only DocumentListResponse alias
hyoban Oct 16, 2026
7fe5440
test(web): route the recipient spec members query through the console…
hyoban Oct 16, 2026
4784faf
refactor(web): use generated dataset options directly
hyoban Oct 16, 2026
a95584b
Merge branch 'refactor/web-orpc-console-calls' of github.com:langgeni…
hyoban Oct 16, 2026
1a42625
chore(web): drop the test-only ErrorDocsResponse alias
hyoban Oct 16, 2026
58f1605
refactor(web): type dataset service calls by the contract
hyoban Oct 16, 2026
43aabb9
Merge branch 'refactor/web-orpc-console-calls' of github.com:langgeni…
hyoban Oct 16, 2026
b55011a
refactor(web): map tool service responses from the contract
hyoban Oct 16, 2026
f531912
chore(web): drop test-only DocumentIndexingStatus and isLegacyDataSou…
hyoban Oct 16, 2026
804b71b
refactor(web): type plugin service calls by the contract
hyoban Oct 16, 2026
42fb846
Merge branch 'refactor/web-orpc-console-calls' of github.com:langgeni…
hyoban Oct 16, 2026
6ee8886
refactor(web): drop narrowResponse
hyoban Oct 16, 2026
c08553f
test: follow the typed generator and preview picker responses
hyoban Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 4 additions & 4 deletions api/controllers/common/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from graphon.file import helpers as file_helpers
from models.model import IconType

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


class GeneratedAppResponse(RootModel[JSONValue]):
root: JSONValue
class GeneratedAppResponse(RootModel[ShallowJSONValue]):
root: ShallowJSONValue


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


class AudioTranscriptResponse(ResponseModel):
text: str
text: str = Field(description="Transcribed text from audio")


class ValidationResultResponse(ResponseModel):
Expand Down
115 changes: 101 additions & 14 deletions api/controllers/common/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
`register_schema_models` emit resolvable OpenAPI 3 references.
"""

import re
from collections.abc import Iterable, Mapping
from enum import StrEnum
from typing import Any, Literal, NotRequired, Protocol, TypedDict
from typing import Any, Literal, NotRequired, Protocol, TypedDict, override
from weakref import WeakKeyDictionary

from flask import request
from flask_restx import Namespace
from pydantic import BaseModel, TypeAdapter
from pydantic.json_schema import CoreModeRef, DefsRef, GenerateJsonSchema

DEFAULT_REF_TEMPLATE_OPENAPI_3_0 = "#/components/schemas/{model}"

Expand Down Expand Up @@ -51,39 +54,125 @@ def to_dict(self, flat: bool = True) -> dict[str, str]: ...
def getlist(self, key: str) -> list[str]: ...


def _register_json_schema(namespace: Namespace, name: str, schema: dict) -> None:
"""Register a JSON schema and promote any nested Pydantic `$defs`."""
class SchemaNameCollisionError(ValueError):
"""Raised when two different schemas would be published under the same OpenAPI component name."""


# Pydantic names `$defs` after the bare class name and only qualifies them when two
# same-named types meet inside one schema, so the same type can surface under different
# names depending on which model pulled it in. Types that share a class name with
# another documented type get a fixed, unique component name here instead.
SCHEMA_NAME_OVERRIDES: Mapping[str, str] = {
"graphon.model_runtime.entities.common_entities.I18nObject": "ModelRuntimeI18nObject",
"services.knowledge.entities.knowledge_entities.FileInfo": "KnowledgeFileInfo",
"core.plugin.entities.plugin.PluginDependency.Github": "PluginDependencyGithub",
"core.plugin.entities.plugin.PluginDependency.Marketplace": "PluginDependencyMarketplace",
"core.plugin.entities.plugin.PluginDependency.Package": "PluginDependencyPackage",
"core.plugin.entities.bundle.PluginBundleDependency.Github": "PluginBundleDependencyGithub",
"core.plugin.entities.bundle.PluginBundleDependency.Marketplace": "PluginBundleDependencyMarketplace",
"core.plugin.entities.bundle.PluginBundleDependency.Package": "PluginBundleDependencyPackage",
"core.app.entities.task_entities.WorkflowAppBlockingResponse.Data": "WorkflowAppBlockingResponseData",
}

_REF_ID_SUFFIX = re.compile(r":\d+(?=$|[\[\],])")
_MODULE_PREFIX = re.compile(r"(?:[^.[\],]+\.)+([^.[\],]+)")

# Normalized component name -> (component name, fully qualified source type, schema), per namespace.
_registered_schemas: WeakKeyDictionary[Namespace, dict[str, tuple[str, str, dict]]] = WeakKeyDictionary()
_NON_ALPHANUMERIC = re.compile(r"[^a-z0-9]")


def _normalized_schema_name(name: str) -> str:
"""Fold names that client code generators turn into the same identifier (`JSONValue` / `JsonValue`)."""

return _NON_ALPHANUMERIC.sub("", name.lower())


class _OpenApiJsonSchemaGenerator(GenerateJsonSchema):
"""Emit stable component names and remember which type produced each of them."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.schema_sources: dict[str, str] = {}

@override
def get_defs_ref(self, core_mode_ref: CoreModeRef) -> DefsRef:
core_ref, _mode = core_mode_ref
source = _REF_ID_SUFFIX.sub("", core_ref)
name = SCHEMA_NAME_OVERRIDES.get(source) or self.normalize_name(_MODULE_PREFIX.sub(r"\1", source))
existing_source = self.schema_sources.setdefault(name, source)
if existing_source != source:
raise SchemaNameCollisionError(
f"OpenAPI schema name {name!r} is produced by both {existing_source} and {source}; "
"rename one of them or add it to SCHEMA_NAME_OVERRIDES."
)
defs_ref = DefsRef(name)
self._prioritized_defsref_choices[defs_ref] = [defs_ref]
return defs_ref


def _register_json_schema(namespace: Namespace, name: str, schema: dict, sources: Mapping[str, str]) -> None:
"""Register a JSON schema and promote any nested Pydantic `$defs`.
`sources` maps each component name to the type that produced it. Flask-RESTX keeps
one flat model namespace, so a second registration under an existing name would
silently replace the first. Re-registering the same type with the same schema is a
no-op; a different type, or the same type with a different shape (for example a
response model also registered in validation mode), raises instead.
"""

nested_definitions = schema.get("$defs")
schema_to_register = dict(schema)
if isinstance(nested_definitions, dict):
schema_to_register.pop("$defs")

source = sources.get(name, name)
registered = _registered_schemas.setdefault(namespace, {})
normalized_name = _normalized_schema_name(name)
existing = registered.get(normalized_name)
if existing is not None and existing != (name, source, schema_to_register):
existing_name, existing_source, _ = existing
if existing_name != name:
reason = f"collides with {existing_name!r} from {existing_source} once normalized by client generators"
elif existing_source != source:
reason = f"is produced by both {existing_source} and {source}"
else:
reason = f"is registered by {source} with two different shapes (validation vs serialization mode?)"
raise SchemaNameCollisionError(
f"OpenAPI schema name {name!r} in namespace {namespace.name!r} {reason}; give each shape its own "
"class name, add it to SCHEMA_NAME_OVERRIDES, or register response models only with "
"register_response_schema_models."
)
registered[normalized_name] = (name, source, schema_to_register)

namespace.schema_model(name, schema_to_register)

if not isinstance(nested_definitions, dict):
return

for nested_name, nested_schema in nested_definitions.items():
if isinstance(nested_schema, dict):
_register_json_schema(namespace, nested_name, nested_schema)
_register_json_schema(namespace, nested_name, nested_schema, sources)


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


def _register_schema_model(namespace: Namespace, model: type[BaseModel], *, mode: JsonSchemaMode) -> None:
def _register_type_schema(namespace: Namespace, source_type: type, *, mode: JsonSchemaMode) -> None:
generator = _OpenApiJsonSchemaGenerator(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0)
schema = generator.generate(TypeAdapter(source_type).core_schema, mode=mode)
_register_json_schema(
namespace,
model.__name__,
model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0, mode=mode),
source_type.__name__,
schema,
{**generator.schema_sources, source_type.__name__: f"{source_type.__module__}.{source_type.__qualname__}"},
)


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

_register_schema_model(namespace, model, mode="validation")
_register_type_schema(namespace, model, mode="validation")


def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None:
Expand All @@ -96,7 +185,7 @@ def register_schema_models(namespace: Namespace, *models: type[BaseModel]) -> No
def register_response_schema_model(namespace: Namespace, model: type[BaseModel]) -> None:
"""Register a BaseModel using its serialized response shape."""

_register_schema_model(namespace, model, mode="serialization")
_register_type_schema(namespace, model, mode="serialization")


def register_response_schema_models(namespace: Namespace, *models: type[BaseModel]) -> None:
Expand All @@ -119,11 +208,7 @@ def get_or_create_model(model_name: str, field_def):
def register_enum_models(namespace: Namespace, *models: type[StrEnum]) -> None:
"""Register multiple StrEnum with a namespace."""
for model in models:
_register_json_schema(
namespace,
model.__name__,
TypeAdapter(model).json_schema(ref_template=DEFAULT_REF_TEMPLATE_OPENAPI_3_0),
)
_register_type_schema(namespace, model, mode="validation")


def query_params_from_model(model: type[BaseModel]) -> dict[str, QueryParamDoc]:
Expand Down Expand Up @@ -334,6 +419,8 @@ def _nullable_property_schema(property_schema: Mapping[str, Any]) -> Mapping[str

__all__ = [
"DEFAULT_REF_TEMPLATE_OPENAPI_3_0",
"SCHEMA_NAME_OVERRIDES",
"SchemaNameCollisionError",
"get_or_create_model",
"query_params_from_model",
"query_params_from_request",
Expand Down
30 changes: 27 additions & 3 deletions api/controllers/console/app/advanced_prompt_template.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any
from typing import Literal

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


class PromptTemplateMessageResponse(ResponseModel):
role: Literal["system", "user", "assistant"]
text: str


class ChatPromptTemplateResponse(ResponseModel):
prompt: list[PromptTemplateMessageResponse]


class CompletionPromptTemplateTextResponse(ResponseModel):
text: str


class ConversationHistoriesRoleResponse(ResponseModel):
user_prefix: str
assistant_prefix: str


class CompletionPromptTemplateResponse(ResponseModel):
prompt: CompletionPromptTemplateTextResponse
conversation_histories_role: ConversationHistoriesRoleResponse | None = None


class AdvancedPromptTemplateResponse(ResponseModel):
chat_prompt_config: dict[str, Any] | None = Field(default=None)
completion_prompt_config: dict[str, Any] | None = Field(default=None)
chat_prompt_config: ChatPromptTemplateResponse | None = Field(default=None)
completion_prompt_config: CompletionPromptTemplateResponse | None = Field(default=None)
stop: list[str] | None = Field(default=None, description="Stop words for completion prompts.")


console_ns.schema_model(
Expand Down
4 changes: 2 additions & 2 deletions api/controllers/console/app/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ class AgentToolCallResponse(ResponseModel):
error: str | None = None
time_cost: float | int
tool_name: str
tool_label: str
tool_label: str | dict[str, str] = Field(description="Tool name or its i18n labels keyed by locale")
tool_input: dict[str, Any]
tool_output: dict[str, Any]
tool_parameters: dict[str, Any]
tool_icon: Any = Field(default=None)
tool_icon: str | dict[str, str] | None = Field(default=None, description="Icon URL or emoji icon")


class AgentIterationLogResponse(ResponseModel):
Expand Down
13 changes: 8 additions & 5 deletions api/controllers/console/app/annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ class AnnotationListQuery(BaseModel):
keyword: str = Field(default="", description="Search keyword")


class AnnotationBatchDeleteQuery(BaseModel):
annotation_id: list[str] = Field(
default_factory=list,
description="Annotation IDs to delete; omitting it clears all annotations of the app",
)


class CreateAnnotationPayload(BaseModel):
message_id: str | None = Field(default=None, description="Message ID")
question: str | None = Field(default=None, description="Question text")
Expand Down Expand Up @@ -140,11 +147,6 @@ class AnnotationBatchImportResponse(ResponseModel):

register_schema_models(
console_ns,
Annotation,
AnnotationList,
AnnotationExportList,
AnnotationHitHistory,
AnnotationHitHistoryList,
AnnotationReplyPayload,
AnnotationSettingUpdatePayload,
AnnotationListQuery,
Expand Down Expand Up @@ -349,6 +351,7 @@ def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACCheck(RBACPermission.APP_CREATE_AND_MANAGEMENT, PlainApp()))
@console_ns.doc(params=query_params_from_model(AnnotationBatchDeleteQuery))
@console_ns.response(204, "Annotations deleted successfully")
@with_session
def delete(self, session: Session, app_id: UUID):
Expand Down
4 changes: 0 additions & 4 deletions api/controllers/console/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from extensions.ext_application_services import application_services
from fields.app_fields import (
AppDetail,
AppDetailSiteResponse,
AppDetailWithSite,
AppExportResponse,
AppImportResponse,
Expand All @@ -41,7 +40,6 @@
AppPartial,
AppTraceResponse,
DeletedTool,
ModelConfigPartial,
RecentAppListResponse,
RecentAppResponse,
Tag,
Expand Down Expand Up @@ -269,8 +267,6 @@ def validate_tracing_provider(cls, value: str | None, info) -> str | None:
AppTracePayload,
Tag,
WorkflowPartial,
ModelConfigPartial,
AppDetailSiteResponse,
DeletedTool,
AppExportResponse,
Segmentation,
Expand Down
5 changes: 1 addition & 4 deletions api/controllers/console/app/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from werkzeug.exceptions import HTTPException, InternalServerError

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


class AudioTranscriptResponse(ResponseModel):
text: str = Field(description="Transcribed text from audio")


class AgentAudioTranscriptFormPayload(BaseModel):
model_config = ConfigDict(extra="forbid")

Expand Down
Loading
Loading