Compare commits

..

22 Commits

Author SHA1 Message Date
yyh
59912d10fc refactor(web): remove code generator React.FC 2026-08-09 17:46:48 +08:00
yyh
b975ed698e refactor(web): use css icon for code generator 2026-08-09 17:45:32 +08:00
yyh
03ca1730c7 fix(web): name code generator action 2026-08-09 17:44:35 +08:00
chelsealong
7522ae14b2 fix(agent): preserve plugin-declared model parameters through Agent Soul (#40163)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:48:01 +00:00
Likalikali
e09eee8fd1 refactor: replace manual model_validate with @model_validate in datasets controllers (#40236)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:25:27 +00:00
Likalikali
b692ddd80c refactor: replace manual model_validate with @model_validate in workspace remaining controllers (#40239)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:23:51 +00:00
Likalikali
5ebb389d26 refactor: replace manual model_validate with @model_validate in rag_pipeline controllers (#40237) 2026-08-09 08:22:50 +00:00
Likalikali
fff6f7cf2f refactor: replace manual model_validate with @model_validate in app workflow controllers (#40235) 2026-08-09 08:20:10 +00:00
Likalikali
516fd12b8d refactor: replace manual model_validate with @model_validate in app core controllers (#40234)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-09 08:18:02 +00:00
Likalikali
925ca49f21 refactor: replace manual model_validate with @model_validate in explore/snippets controllers (#40241) 2026-08-09 07:07:08 +00:00
Likalikali
a1577fc48c refactor: replace manual model_validate with @model_validate in auth/billing/tag controllers (#40240) 2026-08-09 06:14:07 +00:00
Likalikali
acda0341ed refactor: replace manual model_validate with @model_validate in workspace plugin/tool controllers (#40238) 2026-08-09 06:13:24 +00:00
Likalikali
98378cfe03 refactor: replace manual model_validate with @model_validate in agent controllers (#40233) 2026-08-09 06:11:33 +00:00
非法操作
3f89a3c742 fix(workflow): fall back when default socket is unavailable (#40191) 2026-08-09 04:13:41 +00:00
medimedi
e036075e22 chore: Remove redundant dataset context string conversions (#40189) 2026-08-08 18:26:52 +00:00
Asuka Minato
5a84cde198 test: move RAG pipeline workflow coverage to unit tests (#38937) 2026-08-08 12:00:32 +00:00
Byron.wang
4e18ab0ad4 refactor(api): decouple setup and spec endpoints (#40104) 2026-08-08 11:52:15 +00:00
chelsealong
2a742486c5 fix(agent-v2): accept composer UI ids on manual metadata filter conditions (#40171)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-08 06:55:27 +00:00
Likalikali
d362bdbb4e refactor: replace manual model_validate with @model_validate decorator in app controllers (#40170) 2026-08-08 05:47:11 +00:00
Likalikali
60d6be5b13 chore: dep inject for model in explore workflow run (#40158) 2026-08-08 02:50:27 +00:00
Likalikali
cc1b55aaf7 chore: dep inject for model in explore apis (#40160)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-08 02:23:17 +00:00
yyh
d1fa17032e revert(web): use suspense boundary in home page (#40167) 2026-08-07 18:50:33 +00:00
162 changed files with 5080 additions and 2351 deletions

View File

@@ -95,3 +95,33 @@ forbidden_modules =
services.workspace_member_role_resolver
sqlalchemy
werkzeug
[importlinter:contract:setup-service-boundary]
name = Setup application service is framework and persistence neutral
type = forbidden
source_modules =
services.setup_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug
[importlinter:contract:schema-definition-service-boundary]
name = Schema definition application service is framework and persistence neutral
type = forbidden
source_modules =
services.schema_definition_service
forbidden_modules =
configs
controllers
extensions
flask
models
repositories
sqlalchemy
werkzeug

View File

@@ -159,8 +159,12 @@ class AgentBackendModelConfig(BaseModel):
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
# only receives the keys the runtime contract accepts.
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
# config only receives the keys the runtime contract accepts directly; anything
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
# provider-specific parameters (see
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"temperature",
"top_p",
@@ -168,6 +172,7 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
"frequency_penalty",
"max_tokens",
)
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
@@ -177,6 +182,15 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
stop = settings.get("stop")
if isinstance(stop, list) and stop:
sanitized["stop_sequences"] = stop
extra_body: dict[str, JsonValue] = {
key: value
for key, value in settings.items()
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
}
if extra_body:
sanitized["extra_body"] = extra_body
return sanitized or None

View File

@@ -1,6 +1,5 @@
from uuid import UUID
from flask import request
from flask_restx import Resource
from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -64,8 +64,16 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def get(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -74,7 +82,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
snapshot_id=query.snapshot_id,
snapshot_id=req_data.snapshot_id,
),
)
@@ -91,8 +99,16 @@ class WorkflowAgentComposerApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def put(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -101,7 +117,7 @@ class WorkflowAgentComposerApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -123,8 +139,16 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -133,9 +157,9 @@ class WorkflowAgentComposerCopyFromRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
),
)
@@ -152,16 +176,16 @@ class WorkflowAgentComposerValidateApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session, tenant_id=tenant_id, app_id=app_model.id, node_id=node_id
),
@@ -204,9 +228,9 @@ class WorkflowAgentComposerImpactApi(Resource):
@with_current_tenant_id
@with_session(write=False)
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, app_model: App, node_id: str):
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -235,8 +259,16 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
@with_current_tenant_id
@with_session
@get_app_model(mode=[AppMode.WORKFLOW, AppMode.ADVANCED_CHAT])
def post(self, session: Session, tenant_id: str, account_id: str, app_model: App, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
app_model: App,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -245,7 +277,7 @@ class WorkflowAgentComposerSaveToRosterApi(Resource):
app_id=app_model.id,
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -270,8 +302,16 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def get(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
query = WorkflowAgentComposerQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowAgentComposerQuery)
def get(
self,
req_data: WorkflowAgentComposerQuery,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.load_workflow_composer(
@@ -280,7 +320,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
snapshot_id=query.snapshot_id,
snapshot_id=req_data.snapshot_id,
),
)
@@ -296,8 +336,16 @@ class SnippetAgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -306,7 +354,7 @@ class SnippetAgentComposerApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -327,8 +375,16 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = WorkflowComposerCopyFromRosterPayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowComposerCopyFromRosterPayload)
def post(
self,
req_data: WorkflowComposerCopyFromRosterPayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.copy_workflow_composer_from_roster(
@@ -337,9 +393,9 @@ class SnippetAgentComposerCopyFromRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
source_agent_id=payload.source_agent_id,
source_snapshot_id=payload.source_snapshot_id,
idempotency_key=payload.idempotency_key,
source_agent_id=req_data.source_agent_id,
source_snapshot_id=req_data.source_snapshot_id,
idempotency_key=req_data.idempotency_key,
),
)
@@ -355,17 +411,17 @@ class SnippetAgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
app_id = _require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=AgentComposerService.resolve_workflow_node_agent_id(
session=session,
tenant_id=tenant_id,
@@ -409,10 +465,10 @@ class SnippetAgentComposerImpactApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, snippet_id: UUID, node_id: str):
_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id)
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
current_snapshot_id = payload.binding.current_snapshot_id if payload.binding else None
current_snapshot_id = req_data.binding.current_snapshot_id if req_data.binding else None
if not current_snapshot_id:
return dump_response(
AgentComposerImpactResponse, {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -444,8 +500,16 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, account_id: str, snippet_id: UUID, node_id: str):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def post(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
account_id: str,
snippet_id: UUID,
node_id: str,
):
return dump_response(
WorkflowAgentComposerResponse,
AgentComposerService.save_workflow_composer(
@@ -454,7 +518,7 @@ class SnippetAgentComposerSaveToRosterApi(Resource):
app_id=_require_snippet_app_id(session=session, tenant_id=tenant_id, snippet_id=snippet_id),
node_id=node_id,
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -484,8 +548,8 @@ class AgentComposerApi(Resource):
@with_current_user_id
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, account_id: str, agent_id: UUID):
return dump_response(
AgentAppComposerResponse,
AgentComposerService.save_agent_composer(
@@ -493,7 +557,7 @@ class AgentComposerApi(Resource):
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=account_id,
payload=payload,
payload=req_data,
),
)
@@ -509,17 +573,17 @@ class AgentComposerValidateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, agent_id: UUID):
@model_validate(ComposerSavePayload)
def post(self, req_data: ComposerSavePayload, session: Session, tenant_id: str, agent_id: UUID):
AgentComposerService.load_agent_composer(session=session, tenant_id=tenant_id, agent_id=str(agent_id))
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
ComposerConfigValidator.validate_publish_payload(payload)
ComposerConfigValidator.validate_publish_payload(req_data)
AgentComposerService.validate_knowledge_datasets(
session=session, tenant_id=tenant_id, agent_soul=payload.agent_soul
session=session, tenant_id=tenant_id, agent_soul=req_data.agent_soul
)
findings = AgentComposerService.collect_validation_findings(
session=session,
tenant_id=tenant_id,
payload=payload,
payload=req_data,
agent_id=str(agent_id),
)
return dump_response(AgentComposerValidateResponse, {"result": "success", "errors": [], **findings})

View File

@@ -38,6 +38,7 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -607,16 +608,16 @@ class AgentAppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
args = AgentAppCreatePayload.model_validate(console_ns.payload)
@model_validate(AgentAppCreatePayload)
def post(self, req_data: AgentAppCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
params = CreateAppParams(
name=args.name,
description=args.description,
name=req_data.name,
description=req_data.description,
mode="agent",
agent_role=args.role or "",
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
agent_role=req_data.role or "",
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
app = AppService().create_app(current_tenant_id, params, current_user, session=session)
@@ -649,18 +650,25 @@ class AgentAppApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentAppUpdatePayload)
def put(
self,
req_data: AgentAppUpdatePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppUpdatePayload.model_validate(console_ns.payload)
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"role": args.role,
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
"role": req_data.role,
}
updated = AppService().update_app(app_model, args_dict, session=session)
return _serialize_agent_app_detail(session, updated, current_user=current_user)
@@ -721,14 +729,21 @@ class AgentPublishApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentPublishPayload.model_validate(console_ns.payload or {})
@model_validate(AgentPublishPayload)
def post(
self,
req_data: AgentPublishPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.publish_agent_app_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
version_note=args.version_note,
version_note=req_data.version_note,
)
@@ -744,14 +759,21 @@ class AgentBuildDraftCheckoutApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentBuildDraftCheckoutPayload.model_validate(console_ns.payload or {})
@model_validate(AgentBuildDraftCheckoutPayload)
def post(
self,
req_data: AgentBuildDraftCheckoutPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.checkout_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
force=args.force,
force=req_data.force,
)
@@ -783,14 +805,21 @@ class AgentBuildDraftApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def put(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = ComposerSavePayload.model_validate(console_ns.payload or {})
@model_validate(ComposerSavePayload)
def put(
self,
req_data: ComposerSavePayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return AgentComposerService.save_agent_app_build_draft(
session=session,
tenant_id=tenant_id,
agent_id=str(agent_id),
account_id=current_user.id,
payload=payload,
payload=req_data,
)
@console_ns.response(200, "Agent build draft discarded", console_ns.models[AgentSimpleResultResponse.__name__])
@@ -844,18 +873,25 @@ class AgentAppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
args = AgentAppCopyPayload.model_validate(console_ns.payload or {})
@model_validate(AgentAppCopyPayload)
def post(
self,
req_data: AgentAppCopyPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
copied_app = _agent_roster_service(session).duplicate_agent_app(
tenant_id=tenant_id,
agent_id=str(agent_id),
account=current_user,
name=args.name,
description=args.description,
role=args.role,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
role=req_data.role,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
return _serialize_agent_app_detail(session, copied_app, current_user=current_user), 201
@@ -887,10 +923,10 @@ class AgentApiStatusApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_RELEASE_AND_VERSION)
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, agent_id: UUID):
@model_validate(AgentApiStatusPayload)
def post(self, req_data: AgentApiStatusPayload, session: Session, tenant_id: str, agent_id: UUID):
app_model = _resolve_agent_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentApiStatusPayload.model_validate(console_ns.payload)
app_model = AppService().update_app_api_status(app_model, args.enable_api, session=session)
app_model = AppService().update_app_api_status(app_model, req_data.enable_api, session=session)
return _serialize_agent_api_access(session, app_model)
@@ -958,16 +994,16 @@ class AgentInviteOptionsApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, tenant_id: str):
query = AgentInviteOptionsQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(AgentInviteOptionsQuery)
def get(self, req_data: AgentInviteOptionsQuery, session: Session, tenant_id: str):
return dump_response(
AgentInviteOptionsResponse,
_agent_roster_service(session).list_invite_options(
tenant_id=tenant_id,
page=query.page,
limit=query.limit,
keyword=query.keyword,
app_id=query.app_id,
page=req_data.page,
limit=req_data.limit,
keyword=req_data.keyword,
app_id=req_data.app_id,
),
)
@@ -1082,16 +1118,23 @@ class AgentStatisticsSummaryApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentStatisticsQuery)
def get(
self,
req_data: AgentStatisticsQuery,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = _resolve_agent_runtime_app_model(session, tenant_id=tenant_id, agent_id=agent_id)
query = AgentStatisticsQuery.model_validate(request.args.to_dict(flat=True))
timezone = current_user.timezone or "UTC"
start, end = _parse_observability_time_range(query.start, query.end, current_user)
start, end = _parse_observability_time_range(req_data.start, req_data.end, current_user)
try:
payload = _agent_observability_service(session).get_statistics_summary(
app=app_model,
agent_id=str(agent_id),
params=AgentStatisticsQueryParams(source=query.source, start=start, end=end, timezone=timezone),
params=AgentStatisticsQueryParams(source=req_data.source, start=start, end=end, timezone=timezone),
)
except ValueError as exc:
abort(400, description=str(exc))

View File

@@ -1,6 +1,5 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -10,7 +9,7 @@ from controllers.common.schema import (
register_response_schema_models,
)
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, setup_required
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
from fields.base import ResponseModel
from libs.login import login_required
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
@@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource):
@setup_required
@login_required
@account_initialization_required
def get(self):
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(AdvancedPromptTemplateQuery)
def get(self, req_data: AdvancedPromptTemplateQuery):
prompt_args: AdvancedPromptTemplateArgs = {
"app_mode": args.app_mode,
"model_mode": args.model_mode,
"model_name": args.model_name,
"has_context": args.has_context,
"app_mode": req_data.app_mode,
"model_mode": req_data.model_mode,
"model_name": req_data.model_name,
"has_context": req_data.has_context,
}
return AdvancedPromptTemplateService.get_prompt(prompt_args)

View File

@@ -21,6 +21,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
query = query_params_from_request(AgentDriveMutationQuery)
node_id = query.node_id if allow_node_id else None
agent_id = _resolve_agent_id(session, app_model, node_id)
if not agent_id:
return _agent_not_bound()
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
upload_file = session.scalar(
select(UploadFile).where(
@@ -341,11 +342,11 @@ class AgentLogApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.AGENT_CHAT])
def get(self, session: Session, app_model: App):
@model_validate(AgentLogQuery)
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
"""Get agent logs"""
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")

View File

@@ -25,6 +25,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
@model_validate(AgentAppFeaturesPayload)
def post(
self,
req_data: AgentAppFeaturesPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
new_app_model_config = AgentAppFeatureConfigService.update_features(
app_model=app_model,
account=current_user,
config=args.model_dump(exclude_none=True),
config=req_data.model_dump(exclude_none=True),
session=session,
)

View File

@@ -30,6 +30,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -810,14 +811,21 @@ class AgentConfigFilesByAgentApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
@model_validate(AgentConfigFileUploadPayload)
def post(
self,
req_data: AgentConfigFileUploadPayload,
session: Session,
tenant_id: str,
current_user: Account,
agent_id: UUID,
):
return _with_agent_route_target(
session=session,
tenant_id=tenant_id,
agent_id=agent_id,
current_user=current_user,
action=lambda target: _file_upload_response(target, payload),
action=lambda target: _file_upload_response(target, req_data),
)
@@ -849,13 +857,13 @@ class AgentConfigFilesApi(Resource):
@with_current_user
@with_session
@get_app_model(mode=_WORKFLOW_APP_MODES)
def post(self, session: Session, current_user: Account, app_model: App):
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
@model_validate(AgentConfigFileUploadPayload)
def post(self, req_data: AgentConfigFileUploadPayload, session: Session, current_user: Account, app_model: App):
return _with_app_route_target(
session=session,
app_model=app_model,
current_user=current_user,
action=lambda target: _file_upload_response(target, payload),
action=lambda target: _file_upload_response(target, req_data),
)

View File

@@ -20,6 +20,7 @@ from controllers.console.wraps import (
annotation_import_rate_limit,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -180,14 +181,14 @@ class AnnotationReplyActionApi(Resource):
@cloud_edition_billing_resource_check("annotation")
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
args = AnnotationReplyPayload.model_validate(console_ns.payload)
@model_validate(AnnotationReplyPayload)
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
match action:
case "enable":
enable_args: EnableAnnotationArgs = {
"score_threshold": args.score_threshold,
"embedding_provider_name": args.embedding_provider_name,
"embedding_model_name": args.embedding_model_name,
"score_threshold": req_data.score_threshold,
"embedding_provider_name": req_data.embedding_provider_name,
"embedding_model_name": req_data.embedding_model_name,
}
result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id))
case "disable":
@@ -231,12 +232,17 @@ class AppAnnotationSettingUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID, annotation_setting_id: UUID):
@model_validate(AnnotationSettingUpdatePayload)
def post(
self,
req_data: AnnotationSettingUpdatePayload,
session: Session,
app_id: UUID,
annotation_setting_id: UUID,
):
annotation_setting_id_str = str(annotation_setting_id)
args = AnnotationSettingUpdatePayload.model_validate(console_ns.payload)
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": req_data.score_threshold}
result = AppAnnotationService.update_app_annotation_setting(
str(app_id), annotation_setting_id_str, setting_args, session
)
@@ -290,11 +296,11 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
def get(self, session: Session, app_id: UUID):
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
page = args.page
limit = args.limit
keyword = args.keyword
@model_validate(AnnotationListQuery)
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
page = req_data.page
limit = req_data.limit
keyword = req_data.keyword
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
str(app_id), page, limit, keyword, session
@@ -317,17 +323,17 @@ class AnnotationApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID):
args = CreateAnnotationPayload.model_validate(console_ns.payload)
@model_validate(CreateAnnotationPayload)
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
upsert_args: UpsertAnnotationArgs = {}
if args.answer is not None:
upsert_args["answer"] = args.answer
if args.content is not None:
upsert_args["content"] = args.content
if args.message_id is not None:
upsert_args["message_id"] = args.message_id
if args.question is not None:
upsert_args["question"] = args.question
if req_data.answer is not None:
upsert_args["answer"] = req_data.answer
if req_data.content is not None:
upsert_args["content"] = req_data.content
if req_data.message_id is not None:
upsert_args["message_id"] = req_data.message_id
if req_data.question is not None:
upsert_args["question"] = req_data.question
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id), session)
return dump_response(Annotation, annotation), 201
@@ -407,13 +413,13 @@ class AnnotationUpdateDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_session
def post(self, session: Session, app_id: UUID, annotation_id: UUID):
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
@model_validate(UpdateAnnotationPayload)
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
update_args: UpdateAnnotationArgs = {}
if args.answer is not None:
update_args["answer"] = args.answer
if args.question is not None:
update_args["question"] = args.question
if req_data.answer is not None:
update_args["answer"] = req_data.answer
if req_data.question is not None:
update_args["question"] = req_data.question
app_ref = _get_app_ref(session, str(app_id))
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, session)

View File

@@ -4,7 +4,6 @@ from collections.abc import Sequence
from datetime import datetime
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
from sqlalchemy import select
@@ -33,6 +32,7 @@ from controllers.console.wraps import (
edit_permission_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -697,16 +697,16 @@ class AppListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
@model_validate(CreateAppPayload)
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
"""Create app"""
args = CreateAppPayload.model_validate(console_ns.payload)
params = CreateAppParams(
name=args.name,
description=args.description,
mode=args.mode,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
mode=req_data.mode,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
app_service = AppService()
@@ -908,20 +908,20 @@ class AppApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def put(self, session: Session, app_model: App):
@model_validate(UpdateAppPayload)
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
"""Update app"""
args = UpdateAppPayload.model_validate(console_ns.payload)
app_service = AppService()
args_dict: AppService.ArgsDict = {
"name": args.name,
"description": args.description or "",
"icon_type": args.icon_type,
"icon": args.icon or "",
"icon_background": args.icon_background or "",
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
"max_active_requests": args.max_active_requests or 0,
"name": req_data.name,
"description": req_data.description or "",
"icon_type": req_data.icon_type,
"icon": req_data.icon or "",
"icon_background": req_data.icon_background or "",
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
"max_active_requests": req_data.max_active_requests or 0,
}
app_model = app_service.update_app(app_model, args_dict, session=session)
return AppDetailWithSite.model_validate(
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model(mode=None)
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(CopyAppPayload)
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
"""Copy app"""
# The role of the current user in the ta table must be admin, owner, or editor
args = CopyAppPayload.model_validate(console_ns.payload or {})
with Session(db.engine, expire_on_commit=False) as session:
import_service = AppDslService(session)
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
account=current_user,
import_mode=ImportMode.YAML_CONTENT,
yaml_content=yaml_content,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
@agent_manage_required_for_agent_app
@get_app_model
def get(self, app_model: App):
@model_validate(AppExportQuery)
def get(self, req_data: AppExportQuery, app_model: App):
"""Export app"""
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
response = AppExportResponse(
data=AppDslService.export_dsl(
app_model=app_model,
session=db.session(),
include_secret=args.include_secret,
workflow_id=args.workflow_id,
include_secret=req_data.include_secret,
workflow_id=req_data.workflow_id,
)
)
return response.model_dump(mode="json")
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppNamePayload.model_validate(console_ns.payload)
@model_validate(AppNamePayload)
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_name(app_model, args.name, session=session)
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppIconPayload.model_validate(console_ns.payload or {})
@model_validate(AppIconPayload)
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_icon(
app_model,
args.icon or "",
args.icon_background or "",
args.icon_type,
req_data.icon or "",
req_data.icon_background or "",
req_data.icon_type,
session=session,
)
return AppDetail.model_validate(
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppSiteStatusPayload.model_validate(console_ns.payload)
@model_validate(AppSiteStatusPayload)
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
@agent_manage_required_for_agent_app
@with_session
@get_app_model(mode=None)
def post(self, session: Session, app_model: App):
args = AppApiStatusPayload.model_validate(console_ns.payload)
@model_validate(AppApiStatusPayload)
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
app_service = AppService()
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
return AppDetail.model_validate(
app_model,
from_attributes=True,
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def post(self, app_model: App):
@model_validate(AppTracePayload)
def post(self, req_data: AppTracePayload, app_model: App):
# add app trace
args = AppTracePayload.model_validate(console_ns.payload)
OpsTraceManager.update_app_tracing_config(
app_id=app_model.id,
enabled=args.enabled,
tracing_provider=args.tracing_provider,
enabled=req_data.enabled,
tracing_provider=req_data.tracing_provider,
)
return SimpleResultResponse(result="success").model_dump(mode="json")

View File

@@ -12,6 +12,7 @@ from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -83,8 +84,8 @@ class AppImportApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
@with_current_user
def post(self, current_user: Account | None = None):
args = AppImportPayload.model_validate(console_ns.payload)
@model_validate(AppImportPayload)
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
# AppDslService performs internal commits for some creation paths, so use a plain
@@ -96,15 +97,15 @@ class AppImportApi(Resource):
try:
result = import_service.import_app(
account=account,
import_mode=args.mode,
yaml_content=args.yaml_content,
yaml_url=args.yaml_url,
name=args.name,
description=args.description,
icon_type=args.icon_type,
icon=args.icon,
icon_background=args.icon_background,
app_id=args.app_id,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
name=req_data.name,
description=req_data.description,
icon_type=req_data.icon_type,
icon=req_data.icon,
icon_background=req_data.icon_background,
app_id=req_data.app_id,
)
except NoPermissionError as e:
raise Forbidden(str(e))
@@ -113,7 +114,7 @@ class AppImportApi(Resource):
else:
session.commit()
is_created_app = args.app_id is None and result.status in {
is_created_app = req_data.app_id is None and result.status in {
ImportStatus.COMPLETED,
ImportStatus.COMPLETED_WITH_WARNINGS,
}

View File

@@ -31,6 +31,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -276,15 +277,15 @@ class ChatMessageTextApi(Resource):
@login_required
@account_initialization_required
@get_app_model
def post(self, app_model: App):
@model_validate(TextToSpeechPayload)
def post(self, req_data: TextToSpeechPayload, app_model: App):
try:
payload = TextToSpeechPayload.model_validate(console_ns.payload)
message_ref = None
if payload.message_id:
if req_data.message_id:
app_ref = AppRefService.create_app_ref(app_model)
message_ref = AppRefService.create_message_ref(
app_ref,
payload.message_id,
req_data.message_id,
account_id=current_user.id,
)
@@ -292,8 +293,8 @@ class ChatMessageTextApi(Resource):
return AudioService.transcript_tts(
app_model=app_model,
session=db.session(),
text=payload.text,
voice=payload.voice,
text=req_data.text,
voice=req_data.voice,
message_ref=message_ref,
is_draft=True,
)
@@ -339,13 +340,12 @@ class TextModesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
@model_validate(TextToSpeechVoiceQuery)
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
try:
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
response = AudioService.transcript_tts_voices(
tenant_id=app_model.tenant_id,
language=args.language,
language=req_data.language,
)
return dump_response(TextToSpeechVoiceListResponse, response)

View File

@@ -29,6 +29,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,11 +161,11 @@ class CompletionMessageApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
@with_session
@get_app_model(mode=AppMode.COMPLETION)
def post(self, session: Session, current_user: Account, app_model: App):
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
args = args_model.model_dump(exclude_none=True, by_alias=True)
@model_validate(CompletionMessagePayload)
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
args = req_data.model_dump(exclude_none=True, by_alias=True)
streaming = args_model.response_mode != "blocking"
streaming = req_data.response_mode != "blocking"
args["auto_generate_name"] = False
try:

View File

@@ -2,7 +2,7 @@ from typing import Literal
from uuid import UUID
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import func, or_
@@ -18,6 +18,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -108,17 +109,17 @@ class CompletionConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, session: Session, current_user: Account, app_model: App):
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(CompletionConversationQuery)
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
query = sa.select(Conversation).where(
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
)
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
query = query.join(Message, Message.conversation_id == Conversation.id).where(
or_(
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
@@ -130,7 +131,7 @@ class CompletionConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -142,7 +143,7 @@ class CompletionConversationApi(Resource):
query = query.where(Conversation.created_at < end_datetime_utc)
# FIXME, the type ignore in this file
if args.annotation_status == "annotated":
if req_data.annotation_status == "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
.join( # type: ignore
@@ -150,7 +151,7 @@ class CompletionConversationApi(Resource):
)
.group_by(Conversation.id)
)
elif args.annotation_status == "not_annotated":
elif req_data.annotation_status == "not_annotated":
query = (
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
.group_by(Conversation.id)
@@ -159,7 +160,7 @@ class CompletionConversationApi(Resource):
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationPaginationResponse,
@@ -238,8 +239,8 @@ class ChatConversationApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@with_session(write=False)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, session: Session, current_user: Account, app_model: App):
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ChatConversationQuery)
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
subquery = (
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
@@ -249,10 +250,10 @@ class ChatConversationApi(Resource):
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
if args.keyword:
if req_data.keyword:
from libs.helper import escape_like_pattern
escaped_keyword = escape_like_pattern(args.keyword)
escaped_keyword = escape_like_pattern(req_data.keyword)
keyword_filter = f"%{escaped_keyword}%"
query = (
query.join(
@@ -276,12 +277,12 @@ class ChatConversationApi(Resource):
assert account.timezone is not None
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
if start_datetime_utc:
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at >= start_datetime_utc)
case "created_at" | "-created_at" | _:
@@ -289,13 +290,13 @@ class ChatConversationApi(Resource):
if end_datetime_utc:
end_datetime_utc = end_datetime_utc.replace(second=59)
match args.sort_by:
match req_data.sort_by:
case "updated_at" | "-updated_at":
query = query.where(Conversation.updated_at <= end_datetime_utc)
case "created_at" | "-created_at" | _:
query = query.where(Conversation.created_at <= end_datetime_utc)
match args.annotation_status:
match req_data.annotation_status:
case "annotated":
query = (
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
@@ -316,7 +317,7 @@ class ChatConversationApi(Resource):
if app_model.mode == AppMode.ADVANCED_CHAT:
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
match args.sort_by:
match req_data.sort_by:
case "created_at":
query = query.order_by(Conversation.created_at.asc())
case "-created_at":
@@ -328,7 +329,7 @@ class ChatConversationApi(Resource):
case _:
query = query.order_by(Conversation.created_at.desc())
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
return dump_response(
ConversationWithSummaryPaginationResponse,

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
from datetime import datetime
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -16,6 +15,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def get(self, app_model: App):
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ConversationVariablesQuery)
def get(self, req_data: ConversationVariablesQuery, app_model: App):
stmt = (
select(ConversationVariable)
.where(ConversationVariable.app_id == app_model.id)
.order_by(ConversationVariable.created_at)
)
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
# NOTE: This is a temporary solution to avoid performance issues.
page = 1

View File

@@ -17,7 +17,12 @@ from controllers.console.app.error import (
ProviderQuotaExceededError,
)
from controllers.console.app.wraps import with_session
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
from controllers.console.wraps import (
account_initialization_required,
model_validate,
setup_required,
with_current_tenant_id,
)
from core.app.app_config.entities import ModelConfig
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.helper.code_executor.code_node_provider import CodeNodeProvider
@@ -238,11 +243,11 @@ class RuleGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleGeneratePayload)
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
try:
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
@@ -267,13 +272,13 @@ class RuleCodeGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
@model_validate(RuleCodeGeneratePayload)
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
try:
code_result = LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -299,13 +304,13 @@ class RuleStructuredOutputGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
@model_validate(RuleStructuredOutputPayload)
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
try:
structured_output = LLMGenerator.generate_structured_output(
tenant_id=current_tenant_id,
args=args,
args=req_data,
)
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
@@ -332,36 +337,36 @@ class InstructionGenerateApi(Resource):
@account_initialization_required
@with_current_tenant_id
@with_session(write=False)
def post(self, session: Session, current_tenant_id: str):
args = InstructionGeneratePayload.model_validate(console_ns.payload)
@model_validate(InstructionGeneratePayload)
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
code_provider: type[CodeNodeProvider] | None = next(
(p for p in providers if p.is_accept_language(args.language)), None
(p for p in providers if p.is_accept_language(req_data.language)), None
)
code_template = code_provider.get_default_code() if code_provider else ""
try:
# Generate from nothing for a workflow node
if (args.current in (code_template, "")) and args.node_id != "":
if (req_data.current in (code_template, "")) and req_data.node_id != "":
app = session.scalar(
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
)
if not app:
return {"error": f"app {args.flow_id} not found"}, 400
return {"error": f"app {req_data.flow_id} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
if not workflow:
return {"error": f"workflow {args.flow_id} not found"}, 400
return {"error": f"workflow {req_data.flow_id} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == args.node_id]
node = [node for node in nodes if node["id"] == req_data.node_id]
if len(node) == 0:
return {"error": f"node {args.node_id} not found"}, 400
return {"error": f"node {req_data.node_id} not found"}, 400
node_type = node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -369,8 +374,8 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_rule_config(
current_tenant_id,
args=RuleGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
no_variable=True,
),
)
@@ -378,31 +383,31 @@ class InstructionGenerateApi(Resource):
return LLMGenerator.generate_code(
tenant_id=current_tenant_id,
args=RuleCodeGeneratePayload(
instruction=args.instruction,
model_config=args.model_config_data,
code_language=args.language,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
code_language=req_data.language,
),
)
case _:
return {"error": f"invalid node type: {node_type}"}
if args.node_id == "" and args.current != "": # For legacy app without a workflow
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
)
if args.node_id != "" and args.current != "": # For workflow node
if req_data.node_id != "" and req_data.current != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_tenant_id,
flow_id=args.flow_id,
node_id=args.node_id,
current=args.current,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
flow_id=req_data.flow_id,
node_id=req_data.node_id,
current=req_data.current,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
workflow_service=WorkflowService(),
)
return {"error": "incompatible parameters"}, 400
@@ -426,9 +431,9 @@ class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
args = InstructionTemplatePayload.model_validate(console_ns.payload)
match args.type:
@model_validate(InstructionTemplatePayload)
def post(self, req_data: InstructionTemplatePayload):
match req_data.type:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
@@ -438,7 +443,7 @@ class InstructionGenerationTemplateApi(Resource):
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
case _:
raise ValueError(f"Invalid type: {args.type}")
raise ValueError(f"Invalid type: {req_data.type}")
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
@@ -492,24 +497,24 @@ class WorkflowGenerateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Reject empty / over-length instructions at the boundary (shared with
# the streaming endpoint) before spending a planner+builder roundtrip.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
try:
result = WorkflowGeneratorService.generate_workflow_graph(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
if args.current_graph
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None,
)
except ProviderTokenNotInitError as ex:
@@ -547,13 +552,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
@model_validate(WorkflowInstructionSuggestionsPayload)
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
tenant_id=current_tenant_id,
mode=args.mode,
language=args.language,
count=args.count,
mode=req_data.mode,
language=req_data.language,
count=req_data.count,
)
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
@@ -583,12 +588,12 @@ class WorkflowGenerateStreamApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
@model_validate(WorkflowGeneratePayload)
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
# Same boundary guards as the blocking endpoint — return a normal 400
# JSON for these BEFORE opening the stream.
guard = _workflow_instruction_guard(args)
guard = _workflow_instruction_guard(req_data)
if guard is not None:
return guard
@@ -596,12 +601,14 @@ class WorkflowGenerateStreamApi(Resource):
try:
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
tenant_id=current_tenant_id,
mode=args.mode,
instruction=args.instruction,
model_config=args.model_config_data,
ideal_output=args.ideal_output,
mode=req_data.mode,
instruction=req_data.instruction,
model_config=req_data.model_config_data,
ideal_output=req_data.ideal_output,
current_graph=(
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
if req_data.current_graph
else None
),
):
body = {"event": event_name, **payload}

View File

@@ -28,6 +28,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model
def get(self, app_model: App):
args = FeedbackExportQuery.model_validate(request.args.to_dict())
@model_validate(FeedbackExportQuery)
def get(self, req_data: FeedbackExportQuery, app_model: App):
# Import the service function
from services.feedback_service import FeedbackService
@@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource):
export_data = FeedbackService.export_feedbacks(
app_model.id,
session=db.session(),
from_source=args.from_source,
rating=args.rating,
has_comment=args.has_comment,
start_date=args.start_date,
end_date=args.end_date,
format_type=args.format,
from_source=req_data.from_source,
rating=req_data.rating,
has_comment=req_data.has_comment,
start_date=req_data.start_date,
end_date=req_data.end_date,
format_type=req_data.format,
)
return export_data

View File

@@ -1,6 +1,5 @@
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from werkzeug.exceptions import BadRequest
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -74,12 +74,11 @@ class TraceAppConfigApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def get(self, app_model: App):
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
@model_validate(TraceProviderQuery)
def get(self, req_data: TraceProviderQuery, app_model: App):
try:
trace_config = OpsService.get_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
)
if not trace_config:
return {"has_not_configured": True}
@@ -104,15 +103,14 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def post(self, app_model: App):
@model_validate(TraceConfigPayload)
def post(self, req_data: TraceConfigPayload, app_model: App):
"""Create a new trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.create_tracing_app_config(
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
session=db.session(),
)
if not result:
@@ -140,15 +138,14 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def patch(self, app_model: App):
@model_validate(TraceConfigPayload)
def patch(self, req_data: TraceConfigPayload, app_model: App):
"""Update an existing trace app configuration"""
args = TraceConfigPayload.model_validate(console_ns.payload)
try:
result = OpsService.update_tracing_app_config(
app_id=app_model.id,
tracing_provider=args.tracing_provider,
tracing_config=args.tracing_config,
tracing_provider=req_data.tracing_provider,
tracing_config=req_data.tracing_config,
session=db.session(),
)
if not result:
@@ -170,13 +167,12 @@ class TraceAppConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
@get_app_model
def delete(self, app_model: App):
@model_validate(TraceProviderQuery)
def delete(self, req_data: TraceProviderQuery, app_model: App):
"""Delete an existing trace app configuration"""
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
try:
result = OpsService.delete_tracing_app_config(
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
)
if not result:
raise TracingConfigNotExist()

View File

@@ -17,6 +17,7 @@ from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -98,8 +99,8 @@ class AppSite(Resource):
@with_current_user
@with_session
@get_app_model
def post(self, session: Session, current_user: Account, app_model: App):
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(AppSiteUpdatePayload)
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
if not site:
raise NotFound
@@ -123,7 +124,7 @@ class AppSite(Resource):
"show_workflow_steps",
"use_icon_as_answer_icon",
]:
value = getattr(args, attr_name)
value = getattr(req_data, attr_name)
if value is not None:
setattr(site, attr_name, value)

View File

@@ -1,7 +1,7 @@
from decimal import Decimal
import sqlalchemy as sa
from flask import abort, request
from flask import abort
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -177,7 +178,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -237,7 +238,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -296,7 +297,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -357,7 +358,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("c.created_at")
sql_query = f"""SELECT
@@ -427,7 +428,7 @@ FROM
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("m.created_at")
sql_query = f"""SELECT
@@ -502,7 +503,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=AppMode.COMPLETION)
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -567,7 +568,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(StatisticTimeRangeQuery)
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
converted_created_at = convert_datetime_to_date("created_at")
sql_query = f"""SELECT
@@ -630,7 +631,7 @@ WHERE
}
try:
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -2,7 +2,6 @@ from datetime import datetime
from typing import Any
from dateutil.parser import isoparse
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
)
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow app logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
# get paginate workflow app logs
workflow_app_service = WorkflowAppService()
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
session=session,
app_model=app_model,
keyword=args.keyword,
status=args.status,
created_at_before=args.created_at__before,
created_at_after=args.created_at__after,
page=args.page,
limit=args.limit,
detail=args.detail,
created_by_end_user_session_id=args.created_by_end_user_session_id,
created_by_account=args.created_by_account,
keyword=req_data.keyword,
status=req_data.status,
created_at_before=req_data.created_at__before,
created_at_after=req_data.created_at__after,
page=req_data.page,
limit=req_data.limit,
detail=req_data.detail,
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
created_by_account=req_data.created_by_account,
)
return WorkflowAppLogPaginationResponse.model_validate(
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowAppLogQuery)
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
"""
Get workflow archived logs
"""
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
workflow_app_service = WorkflowAppService()
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
session=session,
app_model=app_model,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
)
return WorkflowArchivedLogPaginationResponse.model_validate(

View File

@@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
@model_validate(WorkflowCommentCreatePayload)
def post(
self,
req_data: WorkflowCommentCreatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
):
"""Create a new workflow comment."""
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
created_by=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentCreate, result), 201
@@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentUpdatePayload)
def put(
self,
req_data: WorkflowCommentUpdatePayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Update a workflow comment."""
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.update_comment(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
user_id=current_user.id,
content=payload.content,
position_x=payload.position_x,
position_y=payload.position_y,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
position_x=req_data.position_x,
position_y=req_data.position_y,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentUpdate, result)
@@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
@model_validate(WorkflowCommentReplyPayload)
def post(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
):
"""Add a reply to a workflow comment."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
result = WorkflowCommentService.create_reply(
comment_id=comment_id,
content=payload.content,
content=req_data.content,
created_by=current_user.id,
mentioned_user_ids=payload.mentioned_user_ids,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyCreate, result), 201
@@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource):
@with_current_user
@with_current_tenant_id
@get_app_model()
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
@model_validate(WorkflowCommentReplyPayload)
def put(
self,
req_data: WorkflowCommentReplyPayload,
current_tenant_id: str,
current_user: Account,
app_model: App,
comment_id: str,
reply_id: str,
):
"""Update a comment reply."""
# Validate comment access first
WorkflowCommentService.validate_comment_access(
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
)
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
reply = WorkflowCommentService.update_reply(
tenant_id=current_tenant_id,
app_id=app_model.id,
comment_id=comment_id,
reply_id=reply_id,
user_id=current_user.id,
content=payload.content,
mentioned_user_ids=payload.mentioned_user_ids,
content=req_data.content,
mentioned_user_ids=req_data.mentioned_user_ids,
)
return dump_response(WorkflowCommentReplyUpdate, reply)

View File

@@ -4,7 +4,7 @@ from functools import wraps
from typing import Any, Concatenate, Self, TypedDict, override
from uuid import UUID
from flask import Response, request
from flask import Response
from flask_restx import Resource, fields, marshal, marshal_with
from pydantic import BaseModel, ConfigDict, Field, model_validator
from sqlalchemy.orm import sessionmaker
@@ -22,6 +22,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -359,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
def get(self, current_user: Account, app_model: App):
@model_validate(WorkflowDraftVariableListQuery)
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
"""
Get draft workflow
"""
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
# fetch draft workflow by app_model
workflow_service = WorkflowService()
@@ -378,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=app_model.id,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
user_id=current_user.id,
)
@@ -479,7 +480,14 @@ class VariableApi(Resource):
@console_ns.response(404, "Variable not found")
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
@model_validate(WorkflowDraftVariableUpdatePayload)
def patch(
self,
req_data: WorkflowDraftVariableUpdatePayload,
current_user: Account,
app_model: App,
variable_id: UUID,
):
# Request payload for file types:
#
# Local File:
@@ -504,7 +512,6 @@ class VariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
variable_id_str = str(variable_id)
variable = ensure_variable_access(
@@ -514,8 +521,8 @@ class VariableApi(Resource):
current_user_id=current_user.id,
)
new_name = args_model.name
raw_value = args_model.value
new_name = req_data.name
raw_value = req_data.value
if new_name is None and raw_value is None:
return variable
@@ -660,13 +667,13 @@ class ConversationVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=AppMode.ADVANCED_CHAT)
def post(self, current_user: Account, app_model: App):
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(ConversationVariableUpdatePayload)
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
conversation_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
]
conversation_variables = [
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
@@ -755,24 +762,24 @@ class EnvironmentVariableCollectionApi(Resource):
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
@with_current_user
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def post(self, current_user: Account, app_model: App):
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(EnvironmentVariableUpdatePayload)
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
workflow_service = WorkflowService()
environment_variables_list = [
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
]
environment_variables = [
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
]
if payload.patch:
if req_data.patch:
workflow_service.patch_draft_workflow_environment_variables(
app_model=app_model,
account=current_user,
environment_variables=environment_variables,
deleted_environment_variable_ids=payload.deleted_environment_variable_ids,
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
session=db.session(),
)
else:

View File

@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
from typing import Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get advanced chat app workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get advanced chat workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING if not specified
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunListQuery)
def get(self, req_data: WorkflowRunListQuery, app_model: App):
"""
Get workflow run list
"""
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
args: WorkflowRunListArgs = {"limit": args_model.limit}
if args_model.last_id is not None:
args["last_id"] = args_model.last_id
if args_model.status is not None:
args["status"] = args_model.status
args: WorkflowRunListArgs = {"limit": req_data.limit}
if req_data.last_id is not None:
args["last_id"] = req_data.last_id
if req_data.status is not None:
args["status"] = req_data.status
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
def get(self, app_model: App):
@model_validate(WorkflowRunCountQuery)
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
"""
Get workflow runs count statistics
"""
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
args = args_model.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
# Default to DEBUGGING for workflow if not specified (backward compatibility)
triggered_from = (
WorkflowRunTriggeredFrom(args_model.triggered_from)
if args_model.triggered_from
WorkflowRunTriggeredFrom(req_data.triggered_from)
if req_data.triggered_from
else WorkflowRunTriggeredFrom.DEBUGGING
)

View File

@@ -1,4 +1,4 @@
from flask import abort, jsonify, request
from flask import abort, jsonify
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
from sqlalchemy.orm import sessionmaker
@@ -10,6 +10,7 @@ from controllers.console.wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -104,13 +105,13 @@ class WorkflowDailyRunsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -148,13 +149,13 @@ class WorkflowDailyTerminalsStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -192,13 +193,13 @@ class WorkflowDailyTokenCostStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))
@@ -236,13 +237,13 @@ class WorkflowAverageAppInteractionStatistic(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
@get_app_model(mode=[AppMode.WORKFLOW])
def get(self, account: Account, app_model: App):
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(WorkflowStatisticQuery)
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
assert account.timezone is not None
try:
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
except ValueError as e:
abort(400, description=str(e))

View File

@@ -1,7 +1,6 @@
import logging
from datetime import datetime
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, field_validator
from sqlalchemy import select
@@ -25,6 +24,7 @@ from ..wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -102,11 +102,11 @@ class WebhookTriggerApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__])
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
@get_app_model(mode=AppMode.WORKFLOW)
def get(self, app_model: App):
@model_validate(Parser)
def get(self, req_data: Parser, app_model: App):
"""Get webhook trigger for a node"""
args = Parser.model_validate(request.args.to_dict(flat=True))
node_id = args.node_id
node_id = req_data.node_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Get webhook trigger for this app and node
@@ -175,11 +175,11 @@ class AppTriggerEnableApi(Resource):
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
@with_current_tenant_id
@get_app_model(mode=AppMode.WORKFLOW)
def post(self, current_tenant_id: str, app_model: App):
@model_validate(ParserEnable)
def post(self, req_data: ParserEnable, current_tenant_id: str, app_model: App):
"""Update app trigger (enable/disable)"""
args = ParserEnable.model_validate(console_ns.payload)
trigger_id = args.trigger_id
trigger_id = req_data.trigger_id
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
# Find the trigger using select
trigger = session.execute(
@@ -194,7 +194,7 @@ class AppTriggerEnableApi(Resource):
raise NotFound("Trigger not found")
# Update status based on enable_trigger boolean
trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED
trigger.status = AppTriggerStatus.ENABLED if req_data.enable_trigger else AppTriggerStatus.DISABLED
# Add computed icon field
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"

View File

@@ -9,6 +9,7 @@ from controllers.common.schema import query_params_from_model, register_schema_m
from controllers.console import console_ns
from controllers.console.auth.error import InvitationAccountMismatchError
from controllers.console.error import AccountInFreezeError, AlreadyActivateError
from controllers.console.wraps import model_validate
from extensions.ext_database import db
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, timezone
@@ -86,14 +87,14 @@ class ActivateCheckApi(Resource):
"Success",
console_ns.models[ActivationCheckResponse.__name__],
)
def get(self):
args = ActivateCheckQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ActivateCheckQuery)
def get(self, req_data: ActivateCheckQuery):
workspaceId = args.workspace_id
token = args.token
workspaceId = req_data.workspace_id
token = req_data.token
invitation = RegisterService.get_invitation_with_case_fallback(
workspaceId, args.email, token, session=db.session()
workspaceId, req_data.email, token, session=db.session()
)
if invitation:
data = invitation.get("data", {})
@@ -138,18 +139,18 @@ class ActivateApi(Resource):
console_ns.models[ActivationResponse.__name__],
)
@console_ns.response(400, "Already activated or invalid token")
def post(self):
@model_validate(ActivatePayload)
def post(self, req_data: ActivatePayload):
"""Accept an invitation without letting an existing session act for another account.
Token-only activation remains available for legacy clients. When the request already
carries a console session, that session must belong to the account encoded in the
invitation before the token is consumed or tenant membership is changed.
"""
args = ActivatePayload.model_validate(console_ns.payload)
normalized_request_email = args.email.lower() if args.email else None
normalized_request_email = req_data.email.lower() if req_data.email else None
invitation = RegisterService.get_invitation_with_case_fallback(
args.workspace_id, args.email, args.token, session=db.session()
req_data.workspace_id, req_data.email, req_data.token, session=db.session()
)
if invitation is None:
raise AlreadyActivateError()
@@ -185,11 +186,11 @@ class ActivateApi(Resource):
setup_fields: tuple[str, str, str] | None = None
if requires_setup:
if not args.name or not args.interface_language or not args.timezone:
if not req_data.name or not req_data.interface_language or not req_data.timezone:
raise AlreadyActivateError()
setup_fields = (args.name, args.interface_language, args.timezone)
setup_fields = (req_data.name, req_data.interface_language, req_data.timezone)
RegisterService.revoke_token(args.workspace_id, normalized_request_email, args.token)
RegisterService.revoke_token(req_data.workspace_id, normalized_request_email, req_data.token)
if membership_id is None:
TenantService.create_tenant_member(tenant, account, db.session(), role=role)

View File

@@ -26,7 +26,7 @@ from services.billing_service import BillingService
from services.errors.account import AccountRegisterError, SeatsLimitExceededError
from ..error import AccountInFreezeError, EmailSendIpLimitError, SeatsLimitExceeded
from ..wraps import email_password_login_enabled, email_register_enabled, setup_required
from ..wraps import email_password_login_enabled, email_register_enabled, model_validate, setup_required
class EmailRegisterSendPayload(BaseModel):
@@ -87,21 +87,21 @@ class EmailRegisterSendEmailApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterSendPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
def post(self):
args = EmailRegisterSendPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
@model_validate(EmailRegisterSendPayload)
def post(self, req_data: EmailRegisterSendPayload):
normalized_email = req_data.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
language = "en-US"
if args.language is not None and args.language in languages:
language = args.language
if req_data.language is not None and req_data.language in languages:
language = req_data.language
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
raise AccountInFreezeError()
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=db.session())
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
token = AccountService.send_email_register_email(email=normalized_email, account=account, language=language)
return {"result": "success", "data": token}
@@ -113,16 +113,16 @@ class EmailRegisterCheckApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterValidityPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[VerificationTokenResponse.__name__])
def post(self):
args = EmailRegisterValidityPayload.model_validate(console_ns.payload)
@model_validate(EmailRegisterValidityPayload)
def post(self, req_data: EmailRegisterValidityPayload):
user_email = args.email.lower()
user_email = req_data.email.lower()
is_email_register_error_rate_limit = AccountService.is_email_register_error_rate_limit(user_email)
if is_email_register_error_rate_limit:
raise EmailRegisterLimitError()
token_data = AccountService.get_email_register_data(args.token)
token_data = AccountService.get_email_register_data(req_data.token)
if token_data is None:
raise InvalidTokenError()
@@ -132,16 +132,16 @@ class EmailRegisterCheckApi(Resource):
if user_email != normalized_token_email:
raise InvalidEmailError()
if args.code != token_data.get("code"):
if req_data.code != token_data.get("code"):
AccountService.add_email_register_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_email_register_token(args.token)
AccountService.revoke_email_register_token(req_data.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_email_register_token(
user_email, code=args.code, additional_data={"phase": "register"}
user_email, code=req_data.code, additional_data={"phase": "register"}
)
AccountService.reset_email_register_error_rate_limit(user_email)
@@ -155,15 +155,15 @@ class EmailRegisterResetApi(Resource):
@email_register_enabled
@console_ns.expect(console_ns.models[EmailRegisterResetPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[EmailRegisterResetResponse.__name__])
def post(self):
args = EmailRegisterResetPayload.model_validate(console_ns.payload)
@model_validate(EmailRegisterResetPayload)
def post(self, req_data: EmailRegisterResetPayload):
# Validate passwords match
if args.new_password != args.password_confirm:
if req_data.new_password != req_data.password_confirm:
raise PasswordMismatchError()
# Validate token and get register data
register_data = AccountService.get_email_register_data(args.token)
register_data = AccountService.get_email_register_data(req_data.token)
if not register_data:
raise InvalidTokenError()
# Must use token in reset phase
@@ -171,7 +171,7 @@ class EmailRegisterResetApi(Resource):
raise InvalidTokenError()
# Revoke token to prevent reuse
AccountService.revoke_email_register_token(args.token)
AccountService.revoke_email_register_token(req_data.token)
email = register_data.get("email", "")
normalized_email = email.lower()
@@ -183,9 +183,9 @@ class EmailRegisterResetApi(Resource):
account = self._create_new_account(
email=normalized_email,
password=args.password_confirm,
timezone=args.timezone,
language=args.language,
password=req_data.password_confirm,
timezone=req_data.timezone,
language=req_data.language,
)
token_pair = AccountService.login(account=account, session=db.session(), ip_address=extract_remote_ip(request))
AccountService.reset_login_error_rate_limit(normalized_email)

View File

@@ -15,7 +15,7 @@ from controllers.console.auth.error import (
PasswordMismatchError,
)
from controllers.console.error import AccountNotFound, EmailSendIpLimitError
from controllers.console.wraps import email_password_login_enabled, setup_required
from controllers.console.wraps import email_password_login_enabled, model_validate, setup_required
from extensions.ext_database import db
from libs.helper import EmailStr, extract_remote_ip
from libs.password import hash_password
@@ -68,20 +68,20 @@ class ForgotPasswordSendEmailApi(Resource):
@console_ns.response(400, "Invalid email or rate limit exceeded")
@setup_required
@email_password_login_enabled
def post(self):
args = ForgotPasswordSendPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
@model_validate(ForgotPasswordSendPayload)
def post(self, req_data: ForgotPasswordSendPayload):
normalized_email = req_data.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
if args.language is not None and args.language == "zh-Hans":
if req_data.language is not None and req_data.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
account = AccountService.get_account_by_email_with_case_fallback(args.email, session=db.session())
account = AccountService.get_account_by_email_with_case_fallback(req_data.email, session=db.session())
token = AccountService.send_reset_password_email(
account=account,
@@ -106,16 +106,16 @@ class ForgotPasswordCheckApi(Resource):
@console_ns.response(400, "Invalid code or token")
@setup_required
@email_password_login_enabled
def post(self):
args = ForgotPasswordCheckPayload.model_validate(console_ns.payload)
@model_validate(ForgotPasswordCheckPayload)
def post(self, req_data: ForgotPasswordCheckPayload):
user_email = args.email.lower()
user_email = req_data.email.lower()
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(user_email)
if is_forgot_password_error_rate_limit:
raise EmailPasswordResetLimitError()
token_data = AccountService.get_reset_password_data(args.token)
token_data = AccountService.get_reset_password_data(req_data.token)
if token_data is None:
raise InvalidTokenError()
@@ -127,16 +127,16 @@ class ForgotPasswordCheckApi(Resource):
if user_email != normalized_token_email:
raise InvalidEmailError()
if args.code != token_data.get("code"):
if req_data.code != token_data.get("code"):
AccountService.add_forgot_password_error_rate_limit(user_email)
raise EmailCodeError()
# Verified, revoke the first token
AccountService.revoke_reset_password_token(args.token)
AccountService.revoke_reset_password_token(req_data.token)
# Refresh token data by generating a new token
_, new_token = AccountService.generate_reset_password_token(
token_email, code=args.code, additional_data={"phase": "reset"}
token_email, code=req_data.code, additional_data={"phase": "reset"}
)
AccountService.reset_forgot_password_error_rate_limit(user_email)
@@ -156,15 +156,15 @@ class ForgotPasswordResetApi(Resource):
@console_ns.response(400, "Invalid token or password mismatch")
@setup_required
@email_password_login_enabled
def post(self):
args = ForgotPasswordResetPayload.model_validate(console_ns.payload)
@model_validate(ForgotPasswordResetPayload)
def post(self, req_data: ForgotPasswordResetPayload):
# Validate passwords match
if args.new_password != args.password_confirm:
if req_data.new_password != req_data.password_confirm:
raise PasswordMismatchError()
# Validate token and get reset data
reset_data = AccountService.get_reset_password_data(args.token)
reset_data = AccountService.get_reset_password_data(req_data.token)
if not reset_data:
raise InvalidTokenError()
# Must use token in reset phase
@@ -172,11 +172,11 @@ class ForgotPasswordResetApi(Resource):
raise InvalidTokenError()
# Revoke token to prevent reuse
AccountService.revoke_reset_password_token(args.token)
AccountService.revoke_reset_password_token(req_data.token)
# Generate secure salt and hash password
salt = secrets.token_bytes(16)
password_hashed = hash_password(args.new_password, salt)
password_hashed = hash_password(req_data.new_password, salt)
email = reset_data.get("email", "")
account = AccountService.get_account_by_email_with_case_fallback(email, session=db.session())

View File

@@ -39,6 +39,7 @@ from controllers.console.wraps import (
decrypt_code_field,
decrypt_password_field,
email_password_login_enabled,
model_validate,
setup_required,
with_current_user,
)
@@ -114,10 +115,10 @@ class LoginApi(Resource):
@console_ns.expect(console_ns.models[LoginPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultOptionalDataResponse.__name__])
@decrypt_password_field
def post(self):
@model_validate(LoginPayload)
def post(self, req_data: LoginPayload):
"""Authenticate user and login."""
args = LoginPayload.model_validate(console_ns.payload)
request_email = args.email
request_email = req_data.email
normalized_email = request_email.lower()
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(normalized_email):
@@ -129,7 +130,7 @@ class LoginApi(Resource):
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.LOGIN_RATE_LIMITED)
raise EmailPasswordLoginLimitError()
invite_token = args.invite_token
invite_token = req_data.invite_token
invitation_data: InvitationDetailDict | None = None
if invite_token:
invitation_data = RegisterService.get_invitation_with_case_fallback(
@@ -150,7 +151,7 @@ class LoginApi(Resource):
)
raise InvalidEmailError()
account = _authenticate_account_with_case_fallback(
request_email, normalized_email, args.password, invite_token
request_email, normalized_email, req_data.password, invite_token
)
except services.errors.account.AccountLoginError:
_log_console_login_failure(email=normalized_email, reason=LoginFailureReason.ACCOUNT_BANNED)
@@ -215,16 +216,16 @@ class ResetPasswordSendEmailApi(Resource):
@email_password_login_enabled
@console_ns.expect(console_ns.models[EmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
def post(self):
args = EmailPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
@model_validate(EmailPayload)
def post(self, req_data: EmailPayload):
normalized_email = req_data.email.lower()
if args.language is not None and args.language == "zh-Hans":
if req_data.language is not None and req_data.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
try:
account = _get_account_with_case_fallback(args.email)
account = _get_account_with_case_fallback(req_data.email)
except AccountRegisterError:
raise AccountInFreezeError()
@@ -243,20 +244,20 @@ class EmailCodeLoginSendEmailApi(Resource):
@setup_required
@console_ns.expect(console_ns.models[EmailPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultDataResponse.__name__])
def post(self):
args = EmailPayload.model_validate(console_ns.payload)
normalized_email = args.email.lower()
@model_validate(EmailPayload)
def post(self, req_data: EmailPayload):
normalized_email = req_data.email.lower()
ip_address = extract_remote_ip(request)
if AccountService.is_email_send_ip_limit(ip_address):
raise EmailSendIpLimitError()
if args.language is not None and args.language == "zh-Hans":
if req_data.language is not None and req_data.language == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
try:
account = _get_account_with_case_fallback(args.email)
account = _get_account_with_case_fallback(req_data.email)
except AccountRegisterError:
raise AccountInFreezeError()
@@ -277,14 +278,14 @@ class EmailCodeLoginApi(Resource):
@console_ns.expect(console_ns.models[EmailCodeLoginPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
@decrypt_code_field
def post(self):
args = EmailCodeLoginPayload.model_validate(console_ns.payload)
@model_validate(EmailCodeLoginPayload)
def post(self, req_data: EmailCodeLoginPayload):
original_email = args.email
original_email = req_data.email
user_email = original_email.lower()
language = args.language
language = req_data.language
token_data = AccountService.get_email_code_login_data(args.token)
token_data = AccountService.get_email_code_login_data(req_data.token)
if token_data is None:
_log_console_login_failure(email=user_email, reason=LoginFailureReason.INVALID_EMAIL_CODE_TOKEN)
raise InvalidTokenError()
@@ -295,11 +296,11 @@ class EmailCodeLoginApi(Resource):
_log_console_login_failure(email=user_email, reason=LoginFailureReason.EMAIL_CODE_EMAIL_MISMATCH)
raise InvalidEmailError()
if token_data["code"] != args.code:
if token_data["code"] != req_data.code:
_log_console_login_failure(email=user_email, reason=LoginFailureReason.INVALID_EMAIL_CODE)
raise EmailCodeError()
AccountService.revoke_email_code_login_token(args.token)
AccountService.revoke_email_code_login_token(req_data.token)
try:
account = _get_account_with_case_fallback(original_email)
except Unauthorized as exc:
@@ -325,7 +326,7 @@ class EmailCodeLoginApi(Resource):
email=user_email,
name=user_email,
interface_language=get_valid_language(language),
timezone=args.timezone,
timezone=req_data.timezone,
session=db.session(),
)
except WorkSpaceNotAllowedCreateError:

View File

@@ -1,7 +1,6 @@
import base64
from typing import Any, Literal
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel
from werkzeug.exceptions import BadRequest
@@ -10,6 +9,7 @@ from controllers.common.schema import query_params_from_model, register_response
from controllers.console import console_ns
from controllers.console.wraps import (
account_initialization_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_tenant_id,
@@ -54,10 +54,10 @@ class Subscription(Resource):
@only_edition_cloud
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
args = SubscriptionQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(SubscriptionQuery)
def get(self, req_data: SubscriptionQuery, current_tenant_id: str, current_user: Account):
BillingService.is_tenant_owner_or_admin(current_user, session=db.session())
return BillingService.get_subscription(args.plan, args.interval, current_user.email, current_tenant_id)
return BillingService.get_subscription(req_data.plan, req_data.interval, current_user.email, current_tenant_id)
@console_ns.route("/billing/invoices")
@@ -87,10 +87,10 @@ class PartnerTenants(Resource):
@account_initialization_required
@only_edition_cloud
@with_current_user
def put(self, current_user: Account, partner_key: str):
@model_validate(PartnerTenantsPayload)
def put(self, req_data: PartnerTenantsPayload, current_user: Account, partner_key: str):
try:
args = PartnerTenantsPayload.model_validate(console_ns.payload or {})
click_id = args.click_id
click_id = req_data.click_id
decoded_partner_key = base64.b64decode(partner_key).decode("utf-8")
except Exception:
raise BadRequest("Invalid partner_key")

View File

@@ -14,6 +14,7 @@ from ...common.schema import DEFAULT_REF_TEMPLATE_OPENAPI_3_0
from .. import console_ns
from ..wraps import (
account_initialization_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_tenant_id,
@@ -48,13 +49,13 @@ class ComplianceApi(Resource):
@only_edition_cloud
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
args = ComplianceDownloadQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ComplianceDownloadQuery)
def get(self, req_data: ComplianceDownloadQuery, current_tenant_id: str, current_user: Account):
ip_address = extract_remote_ip(request)
device_info = request.headers.get("User-Agent", "Unknown device")
return BillingService.get_compliance_download_link(
doc_name=args.doc_name,
doc_name=req_data.doc_name,
account_id=current_user.id,
tenant_id=current_tenant_id,
ip=ip_address,

View File

@@ -36,6 +36,7 @@ from ..wraps import (
RBACPermission,
RBACResourceScope,
account_initialization_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -229,12 +230,18 @@ class DataSourceNotionListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session(write=False)
def get(self, session: Session, current_tenant_id: str, current_user: Account) -> tuple[dict[str, Any], int]:
query = DataSourceNotionListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(DataSourceNotionListQuery)
def get(
self,
req_data: DataSourceNotionListQuery,
session: Session,
current_tenant_id: str,
current_user: Account,
) -> tuple[dict[str, Any], int]:
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=query.credential_id,
credential_id=req_data.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -242,8 +249,8 @@ class DataSourceNotionListApi(Resource):
raise NotFound("Credential not found.")
exist_page_ids = []
# import notion in the exist dataset
if query.dataset_id:
dataset = DatasetService.get_dataset(query.dataset_id, session)
if req_data.dataset_id:
dataset = DatasetService.get_dataset(req_data.dataset_id, session)
if not dataset:
raise NotFound("Dataset not found.")
if dataset.data_source_type != "notion_import":
@@ -251,7 +258,7 @@ class DataSourceNotionListApi(Resource):
documents = session.scalars(
select(Document).where(
Document.dataset_id == query.dataset_id,
Document.dataset_id == req_data.dataset_id,
Document.tenant_id == current_tenant_id,
Document.data_source_type == "notion_import",
Document.enabled.is_(True),
@@ -318,13 +325,19 @@ class DataSourceNotionPreviewApi(Resource):
@console_ns.doc(params=query_params_from_model(DataSourceNotionPreviewQuery))
@console_ns.response(200, "Success", console_ns.models[TextContentResponse.__name__])
@with_current_tenant_id
def get(self, current_tenant_id: str, page_id: UUID, page_type: str) -> tuple[dict[str, str], int]:
query = DataSourceNotionPreviewQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(DataSourceNotionPreviewQuery)
def get(
self,
req_data: DataSourceNotionPreviewQuery,
current_tenant_id: str,
page_id: UUID,
page_type: str,
) -> tuple[dict[str, str], int]:
datasource_provider_service = DatasourceProviderService()
credential = datasource_provider_service.get_datasource_credentials(
tenant_id=current_tenant_id,
credential_id=query.credential_id,
credential_id=req_data.credential_id,
provider="notion_datasource",
plugin_id="langgenius/notion_datasource",
)
@@ -354,12 +367,17 @@ class DataSourceNotionIndexingEstimateApi(Resource):
@console_ns.response(200, "Success", console_ns.models[IndexingEstimate.__name__])
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str) -> tuple[dict[str, Any], int]:
payload = NotionEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
@model_validate(NotionEstimatePayload)
def post(
self,
req_data: NotionEstimatePayload,
session: Session,
current_tenant_id: str,
) -> tuple[dict[str, Any], int]:
args = req_data.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
notion_info_list = payload.notion_info_list
notion_info_list = req_data.notion_info_list
extract_settings = []
for notion_info in notion_info_list:
workspace_id = notion_info["workspace_id"]

View File

@@ -26,6 +26,7 @@ from controllers.console.wraps import (
cloud_edition_billing_rate_limit_check,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -572,9 +573,8 @@ class DatasetListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = DatasetCreatePayload.model_validate(console_ns.payload or {})
@model_validate(DatasetCreatePayload)
def post(self, req_data: DatasetCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
@@ -582,20 +582,20 @@ class DatasetListApi(Resource):
if dify_config.RBAC_ENABLED:
permission = DatasetPermissionEnum.ALL_TEAM
else:
permission = payload.permission or DatasetPermissionEnum.ONLY_ME
permission = req_data.permission or DatasetPermissionEnum.ONLY_ME
try:
dataset = DatasetService.create_empty_dataset(
session=session,
tenant_id=current_tenant_id,
name=payload.name,
description=payload.description,
indexing_technique=payload.indexing_technique,
name=req_data.name,
description=req_data.description,
indexing_technique=req_data.indexing_technique,
account=current_user,
permission=permission,
provider=payload.provider,
external_knowledge_api_id=payload.external_knowledge_api_id,
external_knowledge_id=payload.external_knowledge_id,
provider=req_data.provider,
external_knowledge_api_id=req_data.external_knowledge_api_id,
external_knowledge_id=req_data.external_knowledge_id,
)
except services.errors.dataset.DatasetNameDuplicateError:
raise DatasetNameDuplicateError()
@@ -715,28 +715,35 @@ class DatasetApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def patch(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
@model_validate(DatasetUpdatePayload)
def patch(
self,
req_data: DatasetUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
payload = DatasetUpdatePayload.model_validate(console_ns.payload or {})
# check embedding model setting
if (
payload.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and payload.embedding_model_provider is not None
and payload.embedding_model is not None
req_data.indexing_technique == IndexTechniqueType.HIGH_QUALITY
and req_data.embedding_model_provider is not None
and req_data.embedding_model is not None
):
is_multimodal = DatasetService.check_is_multimodal_model(
dataset.tenant_id, payload.embedding_model_provider, payload.embedding_model
dataset.tenant_id, req_data.embedding_model_provider, req_data.embedding_model
)
payload.is_multimodal = is_multimodal
payload_data = payload.model_dump(exclude_unset=True)
req_data.is_multimodal = is_multimodal
payload_data = req_data.model_dump(exclude_unset=True)
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
if not dify_config.RBAC_ENABLED:
DatasetPermissionService.check_permission(
current_user, dataset, payload.permission, payload.partial_member_list, session=session
current_user, dataset, req_data.permission, req_data.partial_member_list, session=session
)
dataset = DatasetService.update_dataset(dataset_id_str, payload_data, current_user, session=session)
@@ -754,12 +761,12 @@ class DatasetApi(Resource):
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
tenant_id = current_tenant_id
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
if req_data.partial_member_list is not None and req_data.permission == DatasetPermissionEnum.PARTIAL_TEAM:
DatasetPermissionService.update_partial_member_list(
tenant_id, dataset_id_str, payload.partial_member_list, session
tenant_id, dataset_id_str, req_data.partial_member_list, session
)
# clear partial member list when permission is only_me or all_team_members
elif payload.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
elif req_data.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
DatasetPermissionService.clear_partial_member_list(dataset_id_str, session)
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, session)
@@ -872,9 +879,9 @@ class DatasetIndexingEstimateApi(Resource):
@console_ns.expect(console_ns.models[IndexingEstimatePayload.__name__])
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str):
payload = IndexingEstimatePayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
@model_validate(IndexingEstimatePayload)
def post(self, req_data: IndexingEstimatePayload, session: Session, current_tenant_id: str):
args = req_data.model_dump()
# validate args
DocumentService.estimate_args_validate(args)
extract_settings = []

View File

@@ -36,6 +36,7 @@ from controllers.console.wraps import (
cloud_edition_billing_knowledge_limit_check,
cloud_edition_billing_rate_limit_check,
cloud_edition_billing_resource_check,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -414,8 +415,10 @@ class DatasetDocumentSegmentAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentCreatePayload)
def post(
self,
req_data: SegmentCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -455,8 +458,7 @@ class DatasetDocumentSegmentAddApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
# validate args
payload = SegmentCreatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
payload_dict = req_data.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
segment = type_cast(DocumentSegment, SegmentService.create_segment(payload_dict, document, dataset, session))
summary = SummaryIndexService.get_segment_summary(
@@ -485,8 +487,10 @@ class DatasetDocumentSegmentUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(SegmentUpdatePayload)
def patch(
self,
req_data: SegmentUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -532,13 +536,12 @@ class DatasetDocumentSegmentUpdateApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload = SegmentUpdatePayload.model_validate(console_ns.payload or {})
payload_dict = payload.model_dump(exclude_none=True)
payload_dict = req_data.model_dump(exclude_none=True)
SegmentService.segment_create_args_validate(payload_dict, document)
# Update segment (summary update with change detection is handled in SegmentService.update_segment)
segment = SegmentService.update_segment(
SegmentUpdateArgs.model_validate(payload.model_dump(exclude_none=True)),
SegmentUpdateArgs.model_validate(req_data.model_dump(exclude_none=True)),
segment,
document,
dataset,
@@ -616,8 +619,10 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(BatchImportPayload)
def post(
self,
req_data: BatchImportPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -635,8 +640,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
if not document:
raise NotFound("Document not found.")
payload = BatchImportPayload.model_validate(console_ns.payload or {})
upload_file_id = payload.upload_file_id
upload_file_id = req_data.upload_file_id
upload_file = session.scalar(select(UploadFile).where(UploadFile.id == upload_file_id).limit(1))
if not upload_file:
@@ -697,8 +701,10 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkCreatePayload)
def post(
self,
req_data: ChildChunkCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -742,8 +748,7 @@ class ChildChunkAddApi(Resource):
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
try:
payload = ChildChunkCreatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, session)
child_chunk = SegmentService.create_child_chunk(req_data.content, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200
@@ -812,8 +817,10 @@ class ChildChunkAddApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkBatchUpdatePayload)
def patch(
self,
req_data: ChildChunkBatchUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -843,9 +850,8 @@ class ChildChunkAddApi(Resource):
segment_id_str = str(segment_id)
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
# validate args
payload = ChildChunkBatchUpdatePayload.model_validate(console_ns.payload or {})
try:
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, session)
child_chunks = SegmentService.update_child_chunks(req_data.chunks, segment, document, dataset, session)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))
return dump_response(ChildChunkBatchUpdateResponse, {"data": child_chunks}), 200
@@ -918,8 +924,10 @@ class ChildChunkUpdateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(ChildChunkUpdatePayload)
def patch(
self,
req_data: ChildChunkUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
@@ -955,9 +963,8 @@ class ChildChunkUpdateApi(Resource):
raise NotFound("Child chunk not found.")
# validate args
try:
payload = ChildChunkUpdatePayload.model_validate(console_ns.payload or {})
child_chunk = SegmentService.update_child_chunk(
payload.content, child_chunk, segment, document, dataset, session
req_data.content, child_chunk, segment, document, dataset, session
)
except ChildChunkIndexingServiceError as e:
raise ChildChunkIndexingError(str(e))

View File

@@ -5,7 +5,6 @@ from datetime import datetime
from typing import Any
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import AliasChoices, BaseModel, Field, field_validator
from sqlalchemy.orm import Session
@@ -26,6 +25,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -186,18 +186,18 @@ class ExternalApiTemplateListApi(Resource):
@with_current_tenant_id
@account_initialization_required
@with_session(write=False)
def get(self, session: Session, current_tenant_id: str):
query = ExternalApiTemplateListQuery.model_validate(request.args.to_dict())
@model_validate(ExternalApiTemplateListQuery)
def get(self, req_data: ExternalApiTemplateListQuery, session: Session, current_tenant_id: str):
external_knowledge_apis, total = ExternalDatasetService.get_external_knowledge_apis(
query.page, query.limit, current_tenant_id, query.keyword, session=session
req_data.page, req_data.limit, current_tenant_id, req_data.keyword, session=session
)
return ExternalKnowledgeApiListResponse(
data=[external_knowledge_api_response(item, session=session) for item in external_knowledge_apis],
has_more=len(external_knowledge_apis) == query.limit,
limit=query.limit,
has_more=len(external_knowledge_apis) == req_data.limit,
limit=req_data.limit,
total=total,
page=query.page,
page=req_data.page,
).model_dump(mode="json"), 200
@console_ns.doc("create_external_api_template")
@@ -215,10 +215,16 @@ class ExternalApiTemplateListApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
@model_validate(ExternalKnowledgeApiPayload)
def post(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
ExternalDatasetService.validate_api_list(payload.settings)
ExternalDatasetService.validate_api_list(req_data.settings)
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -228,7 +234,7 @@ class ExternalApiTemplateListApi(Resource):
external_knowledge_api = ExternalDatasetService.create_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=payload.model_dump(),
args=req_data.model_dump(),
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -279,17 +285,24 @@ class ExternalApiTemplateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def patch(self, session: Session, current_tenant_id: str, current_user: Account, external_knowledge_api_id: UUID):
@model_validate(ExternalKnowledgeApiPayload)
def patch(
self,
req_data: ExternalKnowledgeApiPayload,
session: Session,
current_tenant_id: str,
current_user: Account,
external_knowledge_api_id: UUID,
):
external_knowledge_api_id_str = str(external_knowledge_api_id)
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
ExternalDatasetService.validate_api_list(payload.settings)
ExternalDatasetService.validate_api_list(req_data.settings)
external_knowledge_api = ExternalDatasetService.update_external_knowledge_api(
tenant_id=current_tenant_id,
user_id=current_user.id,
external_knowledge_api_id=external_knowledge_api_id_str,
args=payload.model_dump(),
args=req_data.model_dump(),
session=session,
)
@@ -354,9 +367,15 @@ class ExternalDatasetCreateApi(Resource):
@with_current_user
@with_current_tenant_id
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account):
@model_validate(ExternalDatasetCreatePayload)
def post(
self,
req_data: ExternalDatasetCreatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
):
# The role of the current user in the ta table must be admin, owner, or editor
payload = ExternalDatasetCreatePayload.model_validate(console_ns.payload or {})
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
@@ -366,7 +385,7 @@ class ExternalDatasetCreateApi(Resource):
dataset = ExternalDatasetService.create_external_dataset(
tenant_id=current_tenant_id,
user_id=current_user.id,
args=payload,
args=req_data,
session=session,
)
except services.errors.dataset.DatasetNameDuplicateError:
@@ -405,7 +424,8 @@ class ExternalKnowledgeHitTestingApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
@with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID):
@model_validate(ExternalHitTestingPayload)
def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -416,17 +436,16 @@ class ExternalKnowledgeHitTestingApi(Resource):
except services.errors.account.NoPermissionError as e:
raise Forbidden(str(e))
payload = ExternalHitTestingPayload.model_validate(console_ns.payload or {})
HitTestingService.hit_testing_args_check(payload.model_dump())
HitTestingService.hit_testing_args_check(req_data.model_dump())
try:
response = HitTestingService.external_retrieve(
session=session,
dataset=dataset,
query=payload.query,
query=req_data.query,
account=current_user,
external_retrieval_model=payload.external_retrieval_model,
metadata_filtering_conditions=payload.metadata_filtering_conditions,
external_retrieval_model=req_data.external_retrieval_model,
metadata_filtering_conditions=req_data.metadata_filtering_conditions,
)
return dump_response(ExternalHitTestingResponse, response)
@@ -441,11 +460,11 @@ class BedrockRetrievalApi(Resource):
@console_ns.doc(description="Bedrock retrieval test (internal use only)")
@console_ns.expect(console_ns.models[BedrockRetrievalPayload.__name__])
@console_ns.response(200, "Bedrock retrieval test completed", console_ns.models[BedrockRetrievalResponse.__name__])
def post(self):
payload = BedrockRetrievalPayload.model_validate(console_ns.payload or {})
@model_validate(BedrockRetrievalPayload)
def post(self, req_data: BedrockRetrievalPayload):
# Call the knowledge retrieval service
result = ExternalDatasetTestService.knowledge_retrieval(
payload.retrieval_setting, payload.query, payload.knowledge_id
req_data.retrieval_setting, req_data.query, req_data.knowledge_id
)
return dump_response(BedrockRetrievalResponse, result), 200

View File

@@ -14,6 +14,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
enterprise_license_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -59,9 +60,15 @@ class DatasetMetadataCreateApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
@model_validate(MetadataArgs)
def post(
self,
req_data: MetadataArgs,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
@@ -69,7 +76,7 @@ class DatasetMetadataCreateApi(Resource):
DatasetService.check_dataset_permission(dataset, current_user, session)
metadata = MetadataService.create_metadata(
dataset_id_str, metadata_args, current_user, current_tenant_id, session=session
dataset_id_str, req_data, current_user, current_tenant_id, session=session
)
return dump_response(DatasetMetadataResponse, metadata), 201
@@ -103,16 +110,17 @@ class DatasetMetadataApi(Resource):
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
@model_validate(MetadataUpdatePayload)
def patch(
self,
req_data: MetadataUpdatePayload,
session: Session,
current_tenant_id: str,
current_user: Account,
dataset_id: UUID,
metadata_id: UUID,
):
payload = MetadataUpdatePayload.model_validate(console_ns.payload or {})
name = payload.name
name = req_data.name
dataset_id_str = str(dataset_id)
metadata_id_str = str(metadata_id)
@@ -203,16 +211,15 @@ class DocumentMetadataEditApi(Resource):
@with_current_user
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_session
def post(self, session: Session, current_user: Account, dataset_id: UUID):
@model_validate(MetadataOperationData)
def post(self, req_data: MetadataOperationData, session: Session, current_user: Account, dataset_id: UUID):
dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str, session)
if dataset is None:
raise NotFound("Dataset not found.")
DatasetService.check_dataset_permission(dataset, current_user, session)
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
MetadataService.update_documents_metadata(dataset, metadata_args, current_user, session=session)
MetadataService.update_documents_metadata(dataset, req_data, current_user, session=session)
# Frontend callers only await success and invalidate caches; no response body is consumed.
return "", 204

View File

@@ -14,6 +14,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -275,8 +276,8 @@ class DatasourceAuth(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCredentialPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceCredentialPayload)
def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
@@ -284,8 +285,8 @@ class DatasourceAuth(Resource):
datasource_provider_service.add_datasource_api_key_provider(
tenant_id=current_tenant_id,
provider_id=datasource_provider_id,
credentials=payload.credentials,
name=payload.name,
credentials=req_data.credentials,
name=req_data.name,
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
@@ -325,16 +326,16 @@ class DatasourceAuthDeleteApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
@model_validate(DatasourceCredentialDeletePayload)
def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
plugin_id = datasource_provider_id.plugin_id
provider_name = datasource_provider_id.provider_name
payload = DatasourceCredentialDeletePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.remove_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=payload.credential_id,
auth_id=req_data.credential_id,
provider=provider_name,
plugin_id=plugin_id,
session=db.session(),
@@ -354,18 +355,18 @@ class DatasourceAuthUpdateApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
@model_validate(DatasourceCredentialUpdatePayload)
def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
payload = DatasourceCredentialUpdatePayload.model_validate(console_ns.payload or {})
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_credentials(
tenant_id=current_tenant_id,
auth_id=payload.credential_id,
auth_id=req_data.credential_id,
provider=datasource_provider_id.provider_name,
plugin_id=datasource_provider_id.plugin_id,
credentials=payload.credentials or {},
name=payload.name,
credentials=req_data.credentials or {},
name=req_data.name,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
@@ -420,15 +421,15 @@ class DatasourceAuthOauthCustomClient(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceCustomClientPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceCustomClientPayload)
def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.setup_oauth_custom_client_params(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
client_params=payload.client_params or {},
enabled=payload.enable_oauth_custom_client or False,
client_params=req_data.client_params or {},
enabled=req_data.enable_oauth_custom_client or False,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -457,14 +458,14 @@ class DatasourceAuthDefaultApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceDefaultPayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceDefaultPayload)
def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.set_default_datasource_provider(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
credential_id=payload.id,
credential_id=req_data.id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -479,14 +480,14 @@ class DatasourceUpdateProviderNameApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, current_tenant_id: str, provider_id: str):
payload = DatasourceUpdateNamePayload.model_validate(console_ns.payload or {})
@model_validate(DatasourceUpdateNamePayload)
def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str):
datasource_provider_id = DatasourceProviderID(provider_id)
datasource_provider_service = DatasourceProviderService()
datasource_provider_service.update_datasource_provider_name(
tenant_id=current_tenant_id,
datasource_provider_id=datasource_provider_id,
name=payload.name,
credential_id=payload.credential_id,
name=req_data.name,
credential_id=req_data.credential_id,
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200

View File

@@ -8,7 +8,7 @@ from pydantic import BaseModel
from controllers.common.schema import register_schema_models
from controllers.console import console_ns
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
from extensions.ext_database import db
from libs.login import login_required
from models import Account
@@ -34,14 +34,14 @@ class DataSourceContentPreviewApi(Resource):
@account_initialization_required
@get_rag_pipeline
@with_current_user
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(Parser)
def post(self, req_data: Parser, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run datasource content preview
"""
args = Parser.model_validate(console_ns.payload)
inputs = args.inputs
datasource_type = args.datasource_type
inputs = req_data.inputs
datasource_type = req_data.datasource_type
rag_pipeline_service = RagPipelineService(db.session())
preview_content = rag_pipeline_service.run_datasource_node_preview(
pipeline=pipeline,
@@ -50,6 +50,6 @@ class DataSourceContentPreviewApi(Resource):
account=current_user,
datasource_type=datasource_type,
is_published=True,
credential_id=args.credential_id,
credential_id=req_data.credential_id,
)
return preview_content, 200

View File

@@ -1,7 +1,6 @@
import logging
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy import select
@@ -21,6 +20,7 @@ from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
knowledge_pipeline_publish_enabled,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -104,12 +104,17 @@ class PipelineTemplateListApi(Resource):
@enterprise_license_required
@with_current_tenant_id
@with_session
def get(self, session: Session, current_tenant_id: str) -> JsonResponseWithStatus:
query = PipelineTemplateListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(PipelineTemplateListQuery)
def get(
self,
req_data: PipelineTemplateListQuery,
session: Session,
current_tenant_id: str,
) -> JsonResponseWithStatus:
# get pipeline templates
pipeline_templates = RagPipelineService.get_pipeline_templates(
type=query.type,
language=query.language,
type=req_data.type,
language=req_data.language,
current_tenant_id=current_tenant_id,
session=session,
)
@@ -125,11 +130,11 @@ class PipelineTemplateDetailApi(Resource):
@account_initialization_required
@enterprise_license_required
@with_session
def get(self, session: Session, template_id: str) -> JsonResponseWithStatus:
query = PipelineTemplateDetailQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(PipelineTemplateDetailQuery)
def get(self, req_data: PipelineTemplateDetailQuery, session: Session, template_id: str) -> JsonResponseWithStatus:
pipeline_template = RagPipelineService.get_pipeline_template_detail(
template_id,
type=query.type,
type=req_data.type,
session=session,
)
if pipeline_template is None:
@@ -147,9 +152,15 @@ class CustomizedPipelineTemplateApi(Resource):
@enterprise_license_required
@with_current_user
@with_current_tenant_id
def patch(self, current_tenant_id: str, current_user: Account, template_id: str) -> tuple[str, int]:
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(payload.model_dump())
@model_validate(CustomizedPipelineTemplatePayload)
def patch(
self,
req_data: CustomizedPipelineTemplatePayload,
current_tenant_id: str,
current_user: Account,
template_id: str,
) -> tuple[str, int]:
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(req_data.model_dump())
RagPipelineService.update_customized_pipeline_template(
template_id, pipeline_template_info, current_user, current_tenant_id, session=db.session()
)
@@ -192,10 +203,16 @@ class PublishCustomizedPipelineTemplateApi(Resource):
@knowledge_pipeline_publish_enabled
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account, pipeline_id: str) -> tuple[str, int]:
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
@model_validate(CustomizedPipelineTemplatePayload)
def post(
self,
req_data: CustomizedPipelineTemplatePayload,
current_tenant_id: str,
current_user: Account,
pipeline_id: str,
) -> tuple[str, int]:
rag_pipeline_service = RagPipelineService(db.session())
rag_pipeline_service.publish_customized_pipeline_template(
pipeline_id, payload.model_dump(), current_user, current_tenant_id, session=db.session()
pipeline_id, req_data.model_dump(), current_user, current_tenant_id, session=db.session()
)
return "", 204

View File

@@ -10,6 +10,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_import import RagPip
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_rate_limit_check,
model_validate,
setup_required,
with_current_tenant_id,
with_current_user,
@@ -47,8 +48,13 @@ class CreateRagPipelineDatasetApi(Resource):
@cloud_edition_billing_rate_limit_check("knowledge")
@with_current_user
@with_current_tenant_id
def post(self, current_tenant_id: str, current_user: Account) -> JsonResponseWithStatus:
payload = RagPipelineDatasetImportPayload.model_validate(console_ns.payload or {})
@model_validate(RagPipelineDatasetImportPayload)
def post(
self,
req_data: RagPipelineDatasetImportPayload,
current_tenant_id: str,
current_user: Account,
) -> JsonResponseWithStatus:
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
if not current_user.is_dataset_editor:
raise Forbidden()
@@ -62,7 +68,7 @@ class CreateRagPipelineDatasetApi(Resource):
),
permission=DatasetPermissionEnum.ONLY_ME,
partial_member_list=None,
yaml_content=payload.yaml_content,
yaml_content=req_data.yaml_content,
)
try:
rag_pipeline_dsl_service = RagPipelineDslService(db.session())

View File

@@ -4,7 +4,7 @@ from functools import wraps
from typing import Any, Concatenate, NoReturn
from uuid import UUID
from flask import Response, request
from flask import Response
from flask_restx import Resource, marshal, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy.orm import sessionmaker
@@ -24,7 +24,7 @@ from controllers.console.app.workflow_draft_variable import (
workflow_draft_variable_model,
)
from controllers.console.datasets.wraps import get_rag_pipeline
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
from core.app.file_access import DatabaseFileAccessController
from core.workflow.llm_environment_variable import LLMEnvironmentVariable, environment_variable_value_type
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
@@ -92,11 +92,11 @@ class RagPipelineVariableCollectionApi(Resource):
)
@_api_prerequisite
@marshal_with(workflow_draft_variable_list_without_value_model)
def get(self, current_user: Account, pipeline: Pipeline):
@model_validate(PaginationQuery)
def get(self, req_data: PaginationQuery, current_user: Account, pipeline: Pipeline):
"""
Get draft workflow
"""
query = PaginationQuery.model_validate(request.args.to_dict())
# fetch draft workflow by app_model
rag_pipeline_service = RagPipelineService(db.session())
@@ -111,8 +111,8 @@ class RagPipelineVariableCollectionApi(Resource):
)
workflow_vars = draft_var_srv.list_variables_without_values(
app_id=pipeline.id,
page=query.page,
limit=query.limit,
page=req_data.page,
limit=req_data.limit,
user_id=current_user.id,
)
@@ -196,7 +196,14 @@ class RagPipelineVariableApi(Resource):
@_api_prerequisite
@marshal_with(workflow_draft_variable_model)
@console_ns.expect(console_ns.models[WorkflowDraftVariablePatchPayload.__name__])
def patch(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
@model_validate(WorkflowDraftVariablePatchPayload)
def patch(
self,
req_data: WorkflowDraftVariablePatchPayload,
_current_user: Account,
pipeline: Pipeline,
variable_id: UUID,
):
# Request payload for file types:
#
# Local File:
@@ -221,8 +228,7 @@ class RagPipelineVariableApi(Resource):
draft_var_srv = WorkflowDraftVariableService(
session=db.session(),
)
payload = WorkflowDraftVariablePatchPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
variable_id_str = str(variable_id)
variable = draft_var_srv.get_variable(variable_id=variable_id_str)

View File

@@ -1,4 +1,3 @@
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -85,9 +85,9 @@ class RagPipelineImportApi(Resource):
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
)
@with_current_user
def post(self, current_user: Account) -> JsonResponseWithStatus:
@model_validate(RagPipelineImportPayload)
def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus:
# Check user role first
payload = RagPipelineImportPayload.model_validate(console_ns.payload or {})
# Use a plain Session so that caught exceptions inside the service
# (which return FAILED status instead of re-raising) do not leave the
@@ -98,11 +98,11 @@ class RagPipelineImportApi(Resource):
account = current_user
result = import_service.import_rag_pipeline(
account=account,
import_mode=payload.mode,
yaml_content=payload.yaml_content,
yaml_url=payload.yaml_url,
pipeline_id=payload.pipeline_id,
dataset_name=payload.name,
import_mode=req_data.mode,
yaml_content=req_data.yaml_content,
yaml_url=req_data.yaml_url,
pipeline_id=req_data.pipeline_id,
dataset_name=req_data.name,
)
if result.status == ImportStatus.FAILED:
session.rollback()
@@ -179,14 +179,14 @@ class RagPipelineExportApi(Resource):
@account_initialization_required
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_IMPORT_EXPORT_DSL)
def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
@model_validate(IncludeSecretQuery)
def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus:
# Add include_secret params
query = IncludeSecretQuery.model_validate(request.args.to_dict())
with Session(db.engine, expire_on_commit=False) as session:
export_service = RagPipelineDslService(session)
result = export_service.export_rag_pipeline_dsl(
pipeline=pipeline, include_secret=query.include_secret == "true"
pipeline=pipeline, include_secret=req_data.include_secret == "true"
)
return dump_response(SimpleDataResponse, {"data": result}), 200

View File

@@ -33,6 +33,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -274,12 +275,12 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunPayload)
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow iteration node
"""
payload = NodeRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = PipelineGenerateService.generate_single_iteration(
@@ -309,12 +310,12 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunPayload)
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow loop node
"""
payload = NodeRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = PipelineGenerateService.generate_single_loop(
@@ -344,13 +345,13 @@ class DraftRagPipelineRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
@model_validate(DraftWorkflowRunPayload)
def post(self, req_data: DraftWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
"""
Run draft workflow
"""
pipeline = load_rag_pipeline(session, str(pipeline_id))
payload = DraftWorkflowRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump()
args = req_data.model_dump()
try:
response = PipelineGenerateService.generate(
@@ -378,14 +379,14 @@ class PublishedRagPipelineRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
@model_validate(PublishedWorkflowRunPayload)
def post(self, req_data: PublishedWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
"""
Run published workflow
"""
pipeline = load_rag_pipeline(session, str(pipeline_id))
payload = PublishedWorkflowRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
streaming = payload.response_mode == "streaming"
args = req_data.model_dump(exclude_none=True)
streaming = req_data.response_mode == "streaming"
try:
response = PipelineGenerateService.generate(
@@ -393,7 +394,7 @@ class PublishedRagPipelineRunApi(Resource):
pipeline=pipeline,
user=current_user,
args=args,
invoke_from=InvokeFrom.DEBUGGER if payload.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
invoke_from=InvokeFrom.DEBUGGER if req_data.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
streaming=streaming,
)
@@ -413,11 +414,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(DatasourceNodeRunPayload)
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run rag pipeline datasource
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
@@ -425,11 +426,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
rag_pipeline_service.run_datasource_workflow_node(
pipeline=pipeline,
node_id=node_id,
user_inputs=payload.inputs,
user_inputs=req_data.inputs,
account=current_user,
datasource_type=payload.datasource_type,
datasource_type=req_data.datasource_type,
is_published=False,
credential_id=payload.credential_id,
credential_id=req_data.credential_id,
)
)
)
@@ -446,11 +447,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
@account_initialization_required
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(DatasourceNodeRunPayload)
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run rag pipeline datasource
"""
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
rag_pipeline_service = RagPipelineService(db.session())
return helper.compact_generate_response(
@@ -458,11 +459,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
rag_pipeline_service.run_datasource_workflow_node(
pipeline=pipeline,
node_id=node_id,
user_inputs=payload.inputs,
user_inputs=req_data.inputs,
account=current_user,
datasource_type=payload.datasource_type,
datasource_type=req_data.datasource_type,
is_published=False,
credential_id=payload.credential_id,
credential_id=req_data.credential_id,
)
)
)
@@ -483,12 +484,12 @@ class RagPipelineDraftNodeRunApi(Resource):
@account_initialization_required
@with_current_user
@get_rag_pipeline
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
@model_validate(NodeRunRequiredPayload)
def post(self, req_data: NodeRunRequiredPayload, current_user: Account, pipeline: Pipeline, node_id: str):
"""
Run draft workflow node
"""
payload = NodeRunRequiredPayload.model_validate(console_ns.payload or {})
inputs = payload.inputs
inputs = req_data.inputs
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.run_draft_workflow_node(
@@ -617,16 +618,16 @@ class DefaultRagPipelineBlockConfigApi(Resource):
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@get_rag_pipeline
def get(self, pipeline: Pipeline, block_type: str):
@model_validate(DefaultBlockConfigQuery)
def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str):
"""
Get default block config
"""
query = DefaultBlockConfigQuery.model_validate(request.args.to_dict())
filters = None
if query.q:
if req_data.q:
try:
filters = json.loads(query.q)
filters = json.loads(req_data.q)
except json.JSONDecodeError:
raise ValueError("Invalid filters")
@@ -651,16 +652,16 @@ class PublishedAllRagPipelineApi(Resource):
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
@with_current_user
@get_rag_pipeline
def get(self, current_user: Account, pipeline: Pipeline):
@model_validate(WorkflowListQuery)
def get(self, req_data: WorkflowListQuery, current_user: Account, pipeline: Pipeline):
"""
Get published workflows
"""
query = WorkflowListQuery.model_validate(request.args.to_dict())
page = query.page
limit = query.limit
user_id = query.user_id
named_only = query.named_only
page = req_data.page
limit = req_data.limit
user_id = req_data.user_id
named_only = req_data.named_only
if user_id:
if user_id != current_user.id:
@@ -733,12 +734,12 @@ class RagPipelineByIdApi(Resource):
@with_current_user
@get_rag_pipeline
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
def patch(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
@model_validate(WorkflowUpdatePayload)
def patch(self, req_data: WorkflowUpdatePayload, current_user: Account, pipeline: Pipeline, workflow_id: str):
"""
Update workflow attributes
"""
payload = WorkflowUpdatePayload.model_validate(console_ns.payload or {})
update_data = payload.model_dump(exclude_unset=True)
update_data = req_data.model_dump(exclude_unset=True)
if not update_data:
return {"message": "No valid fields to update"}, 400
@@ -803,12 +804,12 @@ class PublishedRagPipelineSecondStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get second step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
@@ -826,12 +827,12 @@ class PublishedRagPipelineFirstStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get first step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
return {
@@ -849,12 +850,12 @@ class DraftRagPipelineFirstStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get first step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
return {
@@ -872,12 +873,12 @@ class DraftRagPipelineSecondStepApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def get(self, pipeline: Pipeline):
@model_validate(NodeIdQuery)
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
"""
Get second step parameters of rag pipeline
"""
query = NodeIdQuery.model_validate(request.args.to_dict())
node_id = query.node_id
node_id = req_data.node_id
rag_pipeline_service = RagPipelineService(db.session())
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
@@ -1045,11 +1046,12 @@ class RagPipelineDatasourceVariableApi(Resource):
@get_rag_pipeline
@edit_permission_required
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
def post(self, current_user: Account, pipeline: Pipeline):
@model_validate(DatasourceVariablesPayload)
def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline):
"""
Set datasource variables
"""
args = DatasourceVariablesPayload.model_validate(console_ns.payload or {}).model_dump()
args = req_data.model_dump()
rag_pipeline_service = RagPipelineService(db.session())
workflow_node_execution = rag_pipeline_service.set_datasource_variables(
@@ -1071,9 +1073,11 @@ class RagPipelineRecommendedPluginApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, current_tenant_id: str, current_user: Account):
query = RagPipelineRecommendedPluginQuery.model_validate(request.args.to_dict())
@model_validate(RagPipelineRecommendedPluginQuery)
def get(self, req_data: RagPipelineRecommendedPluginQuery, current_tenant_id: str, current_user: Account):
rag_pipeline_service = RagPipelineService(db.session())
recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type, current_user, current_tenant_id)
recommended_plugins = rag_pipeline_service.get_recommended_plugins(
req_data.type, current_user, current_tenant_id
)
return recommended_plugins

View File

@@ -20,6 +20,7 @@ from controllers.console.app.error import (
UnsupportedAudioTypeError,
)
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import model_validate
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from extensions.ext_database import db
from graphon.model_runtime.errors.invoke import InvokeError
@@ -100,16 +101,15 @@ class ChatAudioApi(InstalledAppResource):
class ChatTextApi(InstalledAppResource):
@console_ns.expect(console_ns.models[TextToAudioPayload.__name__])
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
def post(self, installed_app: InstalledApp):
@model_validate(TextToAudioPayload)
def post(self, req_data: TextToAudioPayload, installed_app: InstalledApp):
app_model = installed_app.app_with_session(session=db.session())
if app_model is None:
raise AppUnavailableError()
try:
payload = TextToAudioPayload.model_validate(console_ns.payload or {})
message_id = payload.message_id
text = payload.text
voice = payload.voice
message_id = req_data.message_id
text = req_data.text
voice = req_data.voice
message_ref = None
if message_id:
current_user, _ = current_account_with_tenant()

View File

@@ -1,13 +1,13 @@
from datetime import datetime
from typing import cast
from flask import request
from flask_restx import Namespace, Resource
from pydantic import BaseModel, Field, RootModel, field_validator
from sqlalchemy import select
from controllers.common.schema import query_params_from_model, register_response_schema_models
from controllers.console import api
from controllers.console.wraps import model_validate
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import dump_response
@@ -64,23 +64,22 @@ class BannerApi(Resource):
@api.doc(params=query_params_from_model(BannerListQuery))
@api.response(200, "Success", api.models[BannerListResponse.__name__])
def get(self):
@model_validate(BannerListQuery)
def get(self, req_data: BannerListQuery):
"""Get banner list."""
if not FeatureService.is_explore_banner_enabled():
return dump_response(BannerListResponse, [])
query = BannerListQuery.model_validate(request.args.to_dict(flat=True))
# Build base query for enabled banners
base_query = select(ExporleBanner).where(ExporleBanner.status == BannerStatus.ENABLED)
# Try to get banners in the requested language
banners = db.session.scalars(
base_query.where(ExporleBanner.language == query.language).order_by(ExporleBanner.sort)
base_query.where(ExporleBanner.language == req_data.language).order_by(ExporleBanner.sort)
).all()
# Fallback to en-US if no banners found and language is not en-US
if not banners and query.language != "en-US":
if not banners and req_data.language != "en-US":
banners = db.session.scalars(
base_query.where(ExporleBanner.language == "en-US").order_by(ExporleBanner.sort)
).all()

View File

@@ -20,7 +20,7 @@ from controllers.console.app.error import (
from controllers.console.app.wraps import with_session
from controllers.console.explore.error import NotChatAppError, NotCompletionAppError
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user, with_current_user_id
from controllers.console.wraps import model_validate, with_current_user, with_current_user_id
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import (
@@ -89,17 +89,23 @@ class CompletionApi(InstalledAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
@model_validate(CompletionMessageExplorePayload)
def post(
self,
req_data: CompletionMessageExplorePayload,
session: Session,
current_user: Account,
installed_app: InstalledApp,
):
app_model = installed_app.app_with_session(session=session)
if app_model is None:
raise AppUnavailableError()
if app_model.mode != AppMode.COMPLETION:
raise NotCompletionAppError()
payload = CompletionMessageExplorePayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
streaming = payload.response_mode == "streaming"
streaming = req_data.response_mode == "streaming"
args["auto_generate_name"] = False
installed_app.last_used_at = naive_utc_now()
@@ -173,7 +179,8 @@ class ChatApi(InstalledAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
@model_validate(ChatMessagePayload)
def post(self, req_data: ChatMessagePayload, session: Session, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app_with_session(session=session)
if app_model is None:
raise AppUnavailableError()
@@ -181,8 +188,7 @@ class ChatApi(InstalledAppResource):
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
payload = ChatMessagePayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
args["auto_generate_name"] = False
@@ -191,10 +197,10 @@ class ChatApi(InstalledAppResource):
try:
# Eagerly validate conversation to avoid hanging on invalid conversation_id
if payload.conversation_id:
if req_data.conversation_id:
ConversationService.get_conversation(
app_model=app_model,
conversation_id=payload.conversation_id,
conversation_id=req_data.conversation_id,
user=current_user,
session=session,
)

View File

@@ -11,7 +11,7 @@ from controllers.common.schema import query_params_from_model, register_response
from controllers.console.app.error import AppUnavailableError
from controllers.console.explore.error import NotChatAppError
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user
from controllers.console.wraps import model_validate, with_current_user
from core.app.entities.app_invoke_entities import InvokeFrom
from extensions.ext_database import db
from fields.conversation_fields import (
@@ -133,7 +133,8 @@ class ConversationRenameApi(InstalledAppResource):
@console_ns.expect(console_ns.models[ConversationRenamePayload.__name__])
@console_ns.response(200, "Conversation renamed successfully", console_ns.models[SimpleConversation.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp, c_id: UUID):
@model_validate(ConversationRenamePayload)
def post(self, req_data: ConversationRenamePayload, current_user: Account, installed_app: InstalledApp, c_id: UUID):
app_model = installed_app.app_with_session(session=db.session())
if app_model is None:
raise AppUnavailableError()
@@ -143,12 +144,10 @@ class ConversationRenameApi(InstalledAppResource):
conversation_id = str(c_id)
payload = ConversationRenamePayload.model_validate(console_ns.payload or {})
try:
session = db.session()
conversation = ConversationService.rename(
app_model, conversation_id, current_user, payload.name, payload.auto_generate, session=session
app_model, conversation_id, current_user, req_data.name, req_data.auto_generate, session=session
)
return (
TypeAdapter(SimpleConversation)

View File

@@ -15,6 +15,7 @@ from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import (
account_initialization_required,
cloud_edition_billing_resource_check,
model_validate,
with_current_tenant_id,
with_current_user,
)
@@ -195,16 +196,15 @@ class InstalledAppsListApi(Resource):
@console_ns.expect(console_ns.models[InstalledAppCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[SimpleMessageResponse.__name__])
@with_current_tenant_id
def post(self, current_tenant_id: str):
payload = InstalledAppCreatePayload.model_validate(console_ns.payload or {})
@model_validate(InstalledAppCreatePayload)
def post(self, req_data: InstalledAppCreatePayload, current_tenant_id: str):
recommended_app = db.session.scalar(
select(RecommendedApp).where(RecommendedApp.app_id == payload.app_id).limit(1)
select(RecommendedApp).where(RecommendedApp.app_id == req_data.app_id).limit(1)
)
if recommended_app is None:
raise NotFound("Recommended app not found")
app = db.session.get(App, payload.app_id)
app = db.session.get(App, req_data.app_id)
if app is None:
raise NotFound("App entity not found")
@@ -214,7 +214,7 @@ class InstalledAppsListApi(Resource):
installed_app = db.session.scalar(
select(InstalledApp)
.where(and_(InstalledApp.app_id == payload.app_id, InstalledApp.tenant_id == current_tenant_id))
.where(and_(InstalledApp.app_id == req_data.app_id, InstalledApp.tenant_id == current_tenant_id))
.limit(1)
)
@@ -223,7 +223,7 @@ class InstalledAppsListApi(Resource):
recommended_app.install_count += 1
new_installed_app = InstalledApp(
app_id=payload.app_id,
app_id=req_data.app_id,
tenant_id=current_tenant_id,
app_owner_tenant_id=app.tenant_id,
is_pinned=False,
@@ -281,12 +281,11 @@ class InstalledAppApi(InstalledAppResource):
@console_ns.response(200, "Success", console_ns.models[SimpleResultMessageResponse.__name__])
@console_ns.expect(console_ns.models[InstalledAppUpdatePayload.__name__])
def patch(self, installed_app: InstalledApp):
payload = InstalledAppUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(InstalledAppUpdatePayload)
def patch(self, req_data: InstalledAppUpdatePayload, installed_app: InstalledApp):
commit_args = False
if payload.is_pinned is not None:
installed_app.is_pinned = payload.is_pinned
if req_data.is_pinned is not None:
installed_app.is_pinned = req_data.is_pinned
commit_args = True
if commit_args:

View File

@@ -24,7 +24,7 @@ from controllers.console.explore.error import (
NotCompletionAppError,
)
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user
from controllers.console.wraps import model_validate, with_current_user
from core.app.entities.app_invoke_entities import InvokeFrom
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from extensions.ext_database import db
@@ -119,22 +119,23 @@ class MessageFeedbackApi(InstalledAppResource):
@console_ns.expect(console_ns.models[MessageFeedbackPayload.__name__])
@console_ns.response(200, "Feedback submitted successfully", console_ns.models[ResultResponse.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp, message_id: UUID):
@model_validate(MessageFeedbackPayload)
def post(
self, req_data: MessageFeedbackPayload, current_user: Account, installed_app: InstalledApp, message_id: UUID
):
app_model = installed_app.app_with_session(session=db.session())
if app_model is None:
raise AppUnavailableError()
message_id_str = str(message_id)
payload = MessageFeedbackPayload.model_validate(console_ns.payload or {})
try:
MessageService.create_feedback(
app_model=app_model,
message_id=message_id_str,
user=current_user,
rating=FeedbackRating(payload.rating) if payload.rating else None,
content=payload.content,
rating=FeedbackRating(req_data.rating) if req_data.rating else None,
content=req_data.content,
session=db.session(),
)
except MessageNotExistsError:

View File

@@ -1,14 +1,13 @@
from typing import Any
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel, computed_field, field_validator
from constants.languages import languages
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import account_initialization_required, with_current_user
from controllers.console.wraps import account_initialization_required, model_validate, with_current_user
from extensions.ext_database import db
from fields.base import ResponseModel
from libs.helper import build_icon_url, dump_response
@@ -114,10 +113,10 @@ class RecommendedAppListApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def get(self, current_user: Account):
@model_validate(RecommendedAppsQuery)
def get(self, req_data: RecommendedAppsQuery, current_user: Account):
# language args
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language_prefix = _resolve_language(args.language, current_user)
language_prefix = _resolve_language(req_data.language, current_user)
return dump_response(
RecommendedAppListResponse,
@@ -132,9 +131,9 @@ class LearnDifyAppListApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def get(self, current_user: Account):
args = RecommendedAppsQuery.model_validate(request.args.to_dict(flat=True))
language_prefix = _resolve_language(args.language, current_user)
@model_validate(RecommendedAppsQuery)
def get(self, req_data: RecommendedAppsQuery, current_user: Account):
language_prefix = _resolve_language(req_data.language, current_user)
return dump_response(
LearnDifyAppListResponse,

View File

@@ -10,7 +10,7 @@ from controllers.console import console_ns
from controllers.console.app.error import AppUnavailableError
from controllers.console.explore.error import NotCompletionAppError
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user
from controllers.console.wraps import model_validate, with_current_user
from extensions.ext_database import db
from fields.conversation_fields import MessageResponseSource, ResultResponse
from fields.message_fields import SavedMessageInfiniteScrollPagination, SavedMessageItem
@@ -55,17 +55,16 @@ class SavedMessageListApi(InstalledAppResource):
@console_ns.expect(console_ns.models[SavedMessageCreatePayload.__name__])
@console_ns.response(200, "Success", console_ns.models[ResultResponse.__name__])
@with_current_user
def post(self, current_user: Account, installed_app: InstalledApp):
@model_validate(SavedMessageCreatePayload)
def post(self, req_data: SavedMessageCreatePayload, current_user: Account, installed_app: InstalledApp):
app_model = installed_app.app_with_session(session=db.session())
if app_model is None:
raise AppUnavailableError()
if app_model.mode != "completion":
raise NotCompletionAppError()
payload = SavedMessageCreatePayload.model_validate(console_ns.payload or {})
try:
SavedMessageService.save(app_model, current_user, str(payload.message_id), session=db.session())
SavedMessageService.save(app_model, current_user, str(req_data.message_id), session=db.session())
except MessageNotExistsError:
raise NotFound("Message Not Exists.")

View File

@@ -49,7 +49,7 @@ from controllers.console.explore.error import (
from controllers.console.explore.wraps import TrialAppResource
from controllers.console.files import FILE_UPLOAD_PARAMS, upload_file_from_request
from controllers.console.remote_files import RemoteFileUploadPayload, upload_remote_file_from_request
from controllers.console.wraps import cloud_edition_billing_resource_check, with_current_user
from controllers.console.wraps import cloud_edition_billing_resource_check, model_validate, with_current_user
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict
from core.app.apps.base_app_queue_manager import AppQueueManager
@@ -487,7 +487,8 @@ class TrialAppWorkflowRunApi(TrialAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, trial_app):
@model_validate(WorkflowRunRequest)
def post(self, req_data: WorkflowRunRequest, session: Session, current_user: Account, trial_app):
"""
Run workflow
"""
@@ -498,8 +499,7 @@ class TrialAppWorkflowRunApi(TrialAppResource):
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
request_data = WorkflowRunRequest.model_validate(console_ns.payload)
args = request_data.model_dump()
args = req_data.model_dump()
try:
app_id = app_model.id
user_id = current_user.id
@@ -559,14 +559,14 @@ class TrialChatApi(TrialAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, trial_app):
@model_validate(ChatRequest)
def post(self, req_data: ChatRequest, session: Session, current_user: Account, trial_app):
app_model = trial_app
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
raise NotChatAppError()
request_data = ChatRequest.model_validate(console_ns.payload)
args = request_data.model_dump()
args = req_data.model_dump()
# Validate UUID values if provided
if args.get("conversation_id"):
@@ -709,14 +709,13 @@ class TrialChatTextApi(TrialAppResource):
@console_ns.expect(console_ns.models[TextToSpeechRequest.__name__])
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
@with_current_user
def post(self, current_user: Account, trial_app):
@model_validate(TextToSpeechRequest)
def post(self, req_data: TextToSpeechRequest, current_user: Account, trial_app):
app_model = trial_app
try:
request_data = TextToSpeechRequest.model_validate(console_ns.payload)
message_id = request_data.message_id
text = request_data.text
voice = request_data.voice
message_id = req_data.message_id
text = req_data.text
voice = req_data.voice
message_ref = None
if message_id:
app_ref = AppRefService.create_app_ref(app_model)
@@ -770,13 +769,13 @@ class TrialCompletionApi(TrialAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, trial_app):
@model_validate(CompletionRequest)
def post(self, req_data: CompletionRequest, session: Session, current_user: Account, trial_app):
app_model = trial_app
if app_model.mode != "completion":
raise NotCompletionAppError()
request_data = CompletionRequest.model_validate(console_ns.payload)
args = request_data.model_dump()
args = req_data.model_dump()
streaming = args["response_mode"] == "streaming"
args["auto_generate_name"] = False

View File

@@ -15,7 +15,7 @@ from controllers.console.app.error import (
from controllers.console.app.wraps import with_session
from controllers.console.explore.error import NotWorkflowAppError
from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import with_current_user
from controllers.console.wraps import model_validate, with_current_user
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from core.app.apps.base_app_queue_manager import AppQueueManager
from core.app.entities.app_invoke_entities import InvokeFrom
@@ -47,7 +47,14 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
@console_ns.response(200, "Success")
@with_current_user
@with_session
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
@model_validate(WorkflowRunPayload)
def post(
self,
req_data: WorkflowRunPayload,
session: Session,
current_user: Account,
installed_app: InstalledApp,
):
"""
Run workflow
"""
@@ -58,8 +65,7 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
if app_mode != AppMode.WORKFLOW:
raise NotWorkflowAppError()
payload = WorkflowRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = AppGenerateService.generate(
session=session,

View File

@@ -51,8 +51,8 @@ def upload_remote_file_from_request(
current_user: Account,
resource_tenant_id: str | None = None,
) -> FileWithSignedUrl:
"""Validate the JSON request, fetch its remote file, and persist it under the requested tenant."""
payload = RemoteFileUploadPayload.model_validate(console_ns.payload)
"""Validate the JSON request, fetch its remote file, and persist it under the requested tenant."""
url = payload.url
# Try to fetch remote file metadata/content first

View File

@@ -2,15 +2,16 @@ from typing import Literal
from flask import request
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from configs import dify_config
from controllers.fastopenapi import console_router
from enums.deployment_edition import DeploymentEdition
from extensions.ext_application_services import application_services
from libs.helper import EmailStr, extract_remote_ip
from libs.password import valid_password
from models.model import DifySetup, db
from services.account_service import RegisterService, TenantService
from services.setup_service import (
InitializationValidationRequiredError,
SetupAlreadyCompletedError,
SetupInput,
)
from .error import AlreadySetupError, NotInitValidateError
from .init_validate import get_init_validate_status
@@ -53,14 +54,12 @@ def get_setup_status_api() -> SetupStatusResponse:
Only bootstrap-safe status information should be returned by this endpoint.
"""
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
setup_status = get_setup_status()
if setup_status and not isinstance(setup_status, bool):
return SetupStatusResponse(step="finished", setup_at=setup_status.setup_at.isoformat())
if setup_status:
return SetupStatusResponse(step="finished")
setup_status = application_services().setup.get_status()
if not setup_status.completed:
return SetupStatusResponse(step="not_started")
return SetupStatusResponse(step="finished")
setup_at = setup_status.setup_at.isoformat() if setup_status.setup_at is not None else None
return SetupStatusResponse(step="finished", setup_at=setup_at)
@console_router.post(
@@ -77,33 +76,22 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
and init-password validation rather than user session authentication.
"""
if get_setup_status():
raise AlreadySetupError()
try:
application_services().setup.initialize(
SetupInput(
email=payload.email,
name=payload.name,
password=payload.password,
ip_address=extract_remote_ip(request),
language=payload.language,
),
initialization_validated=get_init_validate_status(),
)
except SetupAlreadyCompletedError:
raise AlreadySetupError() from None
except InitializationValidationRequiredError:
raise NotInitValidateError() from None
tenant_count = TenantService.get_tenant_count(session=db.session())
if tenant_count > 0:
raise AlreadySetupError()
if not get_init_validate_status():
raise NotInitValidateError()
normalized_email = payload.email.lower()
RegisterService.setup(
email=normalized_email,
name=payload.name,
password=payload.password,
ip_address=extract_remote_ip(request),
language=payload.language,
session=db.session(),
)
mark_setup_completed()
return SetupResponse(result="success")
def get_setup_status() -> DifySetup | bool | None:
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
return db.session.scalar(select(DifySetup).limit(1))
return True

View File

@@ -36,6 +36,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
edit_permission_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_user,
@@ -204,18 +205,18 @@ class SnippetDraftWorkflowApi(Resource):
@rbac_permission_required(
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def post(self, current_user: Account, snippet: CustomizedSnippet):
@model_validate(SnippetDraftSyncPayload)
def post(self, req_data: SnippetDraftSyncPayload, current_user: Account, snippet: CustomizedSnippet):
"""Sync draft workflow for snippet."""
payload = SnippetDraftSyncPayload.model_validate(console_ns.payload or {})
try:
snippet_service = _snippet_service()
workflow = snippet_service.sync_draft_workflow(
snippet=snippet,
graph=payload.graph,
unique_hash=payload.hash,
graph=req_data.graph,
unique_hash=req_data.hash,
account=current_user,
input_fields=payload.input_fields,
input_fields=req_data.input_fields,
)
except WorkflowHashNotEqualError:
raise DraftWorkflowNotSync()
@@ -363,24 +364,24 @@ class SnippetPublishedAllWorkflowApi(Resource):
@rbac_permission_required(
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def get(self, snippet: CustomizedSnippet):
@model_validate(SnippetWorkflowListQuery)
def get(self, req_data: SnippetWorkflowListQuery, snippet: CustomizedSnippet):
"""Get all published workflow versions for snippet."""
args = SnippetWorkflowListQuery.model_validate(request.args.to_dict(flat=True))
snippet_service = _snippet_service()
with Session(db.engine) as session:
workflows, has_more = snippet_service.get_all_published_workflows(
session=session,
snippet=snippet,
page=args.page,
limit=args.limit,
page=req_data.page,
limit=req_data.limit,
)
response = SnippetWorkflowPaginationResponse.model_validate(
{
"items": workflows,
"page": args.page,
"limit": args.limit,
"page": req_data.page,
"limit": req_data.limit,
"has_more": has_more,
},
from_attributes=True,
@@ -449,10 +450,16 @@ class SnippetWorkflowByIdApi(Resource):
@rbac_permission_required(
RBACResourceScope.WORKSPACE, RBACPermission.SNIPPETS_CREATE_AND_MODIFY, resource_required=False
)
def patch(self, current_user: Account, snippet: CustomizedSnippet, workflow_id: str):
@model_validate(WorkflowUpdatePayload)
def patch(
self,
req_data: WorkflowUpdatePayload,
current_user: Account,
snippet: CustomizedSnippet,
workflow_id: str,
):
"""Update a published snippet workflow version's display metadata."""
payload = WorkflowUpdatePayload.model_validate(console_ns.payload or {})
update_data = payload.model_dump(exclude_unset=True)
update_data = req_data.model_dump(exclude_unset=True)
if not update_data:
return {"message": "No valid fields to update"}, 400
@@ -575,16 +582,22 @@ class SnippetDraftNodeRunApi(Resource):
@with_current_user
@get_snippet
@edit_permission_required
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
@model_validate(SnippetDraftNodeRunPayload)
def post(
self,
req_data: SnippetDraftNodeRunPayload,
current_user: Account,
snippet: CustomizedSnippet,
node_id: str,
):
"""
Run a single node in snippet draft workflow.
Executes a specific node with provided inputs for single-step debugging.
Returns the node execution result including status, outputs, and timing.
"""
payload = SnippetDraftNodeRunPayload.model_validate(console_ns.payload or {})
user_inputs = payload.inputs
user_inputs = req_data.inputs
# Get draft workflow for file parsing
snippet_service = _snippet_service()
@@ -592,14 +605,14 @@ class SnippetDraftNodeRunApi(Resource):
if not draft_workflow:
raise NotFound("Draft workflow not found")
files = SnippetGenerateService.parse_files(draft_workflow, payload.files)
files = SnippetGenerateService.parse_files(draft_workflow, req_data.files)
workflow_node_execution = SnippetGenerateService.run_draft_node(
snippet=snippet,
node_id=node_id,
user_inputs=user_inputs,
account=current_user,
query=payload.query,
query=req_data.query,
files=files,
session_maker=_snippet_session_maker(),
)
@@ -663,14 +676,21 @@ class SnippetDraftRunIterationNodeApi(Resource):
@with_current_user
@get_snippet
@edit_permission_required
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
@model_validate(SnippetIterationNodeRunPayload)
def post(
self,
req_data: SnippetIterationNodeRunPayload,
current_user: Account,
snippet: CustomizedSnippet,
node_id: str,
):
"""
Run a draft workflow iteration node for snippet.
Iteration nodes execute their internal sub-graph multiple times over an input list.
Returns an SSE event stream with iteration progress and results.
"""
args = SnippetIterationNodeRunPayload.model_validate(console_ns.payload or {}).model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = SnippetGenerateService.generate_single_iteration(
@@ -708,21 +728,27 @@ class SnippetDraftRunLoopNodeApi(Resource):
@with_current_user
@get_snippet
@edit_permission_required
def post(self, current_user: Account, snippet: CustomizedSnippet, node_id: str):
@model_validate(SnippetLoopNodeRunPayload)
def post(
self,
req_data: SnippetLoopNodeRunPayload,
current_user: Account,
snippet: CustomizedSnippet,
node_id: str,
):
"""
Run a draft workflow loop node for snippet.
Loop nodes execute their internal sub-graph repeatedly until a condition is met.
Returns an SSE event stream with loop progress and results.
"""
args = SnippetLoopNodeRunPayload.model_validate(console_ns.payload or {})
try:
response = SnippetGenerateService.generate_single_loop(
snippet=snippet,
user=current_user,
node_id=node_id,
args=args,
args=req_data,
streaming=True,
session_maker=_snippet_session_maker(),
)
@@ -751,15 +777,15 @@ class SnippetDraftWorkflowRunApi(Resource):
@with_current_user
@get_snippet
@edit_permission_required
def post(self, current_user: Account, snippet: CustomizedSnippet):
@model_validate(SnippetDraftRunPayload)
def post(self, req_data: SnippetDraftRunPayload, current_user: Account, snippet: CustomizedSnippet):
"""
Run draft workflow for snippet.
Executes the snippet's draft workflow with the provided inputs
and returns an SSE event stream with execution progress and results.
"""
payload = SnippetDraftRunPayload.model_validate(console_ns.payload or {})
args = payload.model_dump(exclude_none=True)
args = req_data.model_dump(exclude_none=True)
try:
response = SnippetGenerateService.generate(

View File

@@ -36,6 +36,7 @@ from controllers.console.snippets.snippet_workflow import get_snippet
from controllers.console.wraps import (
account_initialization_required,
edit_permission_required,
model_validate,
setup_required,
with_current_user,
)
@@ -186,9 +187,15 @@ class SnippetVariableApi(Resource):
@console_ns.response(404, "Variable not found")
@_snippet_draft_var_prerequisite
@marshal_with(workflow_draft_variable_model)
def patch(self, current_user: Account, snippet: CustomizedSnippet, variable_id: str) -> WorkflowDraftVariable:
@model_validate(WorkflowDraftVariableUpdatePayload)
def patch(
self,
req_data: WorkflowDraftVariableUpdatePayload,
current_user: Account,
snippet: CustomizedSnippet,
variable_id: str,
) -> WorkflowDraftVariable:
draft_var_srv = WorkflowDraftVariableService(session=db.session())
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
variable = ensure_variable_access(
variable=draft_var_srv.get_variable(variable_id=variable_id),
@@ -198,8 +205,8 @@ class SnippetVariableApi(Resource):
)
_ensure_snippet_draft_variable_row_allowed(variable=variable, variable_id=variable_id)
new_name = args_model.name
raw_value = args_model.value
new_name = req_data.name
raw_value = req_data.value
if new_name is None and raw_value is None:
return variable

View File

@@ -1,23 +1,18 @@
import logging
from collections.abc import Mapping
from http import HTTPStatus
from typing import Any
from flask_restx import Resource
from pydantic import Field, RootModel
from controllers.common.schema import register_response_schema_models
from controllers.console.wraps import (
account_initialization_required,
setup_required,
)
from core.schemas.schema_manager import SchemaManager
from controllers.console.flask_admission import console_account_admission
from extensions.ext_application_services import application_services
from fields.base import ResponseModel
from libs.login import login_required
from machinery.context import RequestContext
from . import console_ns
logger = logging.getLogger(__name__)
class SchemaDefinitionItemResponse(ResponseModel):
name: str
@@ -34,20 +29,13 @@ register_response_schema_models(console_ns, SchemaDefinitionItemResponse, Schema
@console_ns.route("/spec/schema-definitions")
class SpecSchemaDefinitionsApi(Resource):
@console_ns.response(200, "Success", console_ns.models[SchemaDefinitionsResponse.__name__])
@setup_required
@login_required
@account_initialization_required
def get(self):
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SchemaDefinitionsResponse.__name__])
@console_account_admission()
def get(self, _request_context: RequestContext):
"""
Get system JSON Schema definitions specification
Used for frontend component type mapping
"""
try:
schema_manager = SchemaManager()
schema_definitions = schema_manager.get_all_schema_definitions()
return schema_definitions, 200
except Exception:
logger.exception("Failed to get schema definitions from local registry")
# Return empty array as fallback
return [], 200
schema_definitions = application_services().schema_definitions.list()
response = SchemaDefinitionsResponse.model_validate(schema_definitions).model_dump(mode="json")
return response, HTTPStatus.OK

View File

@@ -1,7 +1,6 @@
from typing import Literal
from uuid import UUID
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, RootModel, field_validator
from sqlalchemy import select
@@ -135,10 +134,9 @@ class TagListApi(Resource):
@console_ns.doc(params=query_params_from_model(TagListQueryParam))
@console_ns.response(200, "Success", console_ns.models[TagListResponse.__name__])
@with_current_tenant_id
def get(self, current_tenant_id: str):
raw_args = request.args.to_dict()
param = TagListQueryParam.model_validate(raw_args)
tags = TagService.get_tags(param.type, current_tenant_id, param.keyword, session=db.session())
@model_validate(TagListQueryParam)
def get(self, req_data: TagListQueryParam, current_tenant_id: str):
tags = TagService.get_tags(req_data.type, current_tenant_id, req_data.keyword, session=db.session())
return dump_response(TagListResponse, tags), 200
@@ -213,10 +211,9 @@ def _require_tag_binding_edit_permission(current_user: Account) -> None:
raise Forbidden()
def _create_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]:
def _create_tag_bindings(current_user: Account, payload: TagBindingPayload) -> tuple[dict[str, str], int]:
_require_tag_binding_edit_permission(current_user)
payload = TagBindingPayload.model_validate(console_ns.payload or {})
_enforce_snippet_tag_rbac_if_needed(payload.type)
TagService.save_tag_binding(
TagBindingCreatePayload(
@@ -229,10 +226,9 @@ def _create_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]:
return {"result": "success"}, 200
def _remove_tag_bindings(current_user: Account) -> tuple[dict[str, str], int]:
def _remove_tag_bindings(current_user: Account, payload: TagBindingRemovePayload) -> tuple[dict[str, str], int]:
_require_tag_binding_edit_permission(current_user)
payload = TagBindingRemovePayload.model_validate(console_ns.payload or {})
_enforce_snippet_tag_rbac_if_needed(payload.type)
TagService.delete_tag_binding(
TagBindingDeletePayload(
@@ -256,8 +252,9 @@ class TagBindingCollectionApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def post(self, current_user: Account):
return _create_tag_bindings(current_user)
@model_validate(TagBindingPayload)
def post(self, req_data: TagBindingPayload, current_user: Account):
return _create_tag_bindings(current_user, req_data)
@console_ns.route("/tag-bindings/remove")
@@ -272,5 +269,6 @@ class TagBindingRemoveApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def post(self, current_user: Account):
return _remove_tag_bindings(current_user)
@model_validate(TagBindingRemovePayload)
def post(self, req_data: TagBindingRemovePayload, current_user: Account):
return _remove_tag_bindings(current_user, req_data)

View File

@@ -41,6 +41,7 @@ from controllers.console.wraps import (
cloud_edition_billing_enabled,
enable_change_email,
enterprise_license_required,
model_validate,
only_edition_cloud,
setup_required,
with_current_user,
@@ -332,9 +333,9 @@ class AccountAvatarApi(Resource):
@login_required
@account_initialization_required
@with_current_user
def get(self, current_user: Account):
args = AccountAvatarQuery.model_validate(request.args.to_dict(flat=True))
avatar = args.avatar
@model_validate(AccountAvatarQuery)
def get(self, req_data: AccountAvatarQuery, current_user: Account):
avatar = req_data.avatar
if avatar.startswith(("http://", "https://")):
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")

View File

@@ -11,7 +11,6 @@ from enum import StrEnum
from http import HTTPStatus
from typing import Any
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field
@@ -294,14 +293,14 @@ class EndpointListApi(Resource):
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def get(self, tenant_id: str, user_id: str):
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(EndpointListQuery)
def get(self, req_data: EndpointListQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints(
tenant_id=tenant_id,
user_id=user_id,
page=args.page,
page_size=args.page_size,
page=req_data.page,
page_size=req_data.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
@@ -322,15 +321,15 @@ class EndpointListForSinglePluginApi(Resource):
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def get(self, tenant_id: str, user_id: str):
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(EndpointListForPluginQuery)
def get(self, req_data: EndpointListForPluginQuery, tenant_id: str, user_id: str):
endpoints = EndpointService.list_endpoints_for_single_plugin(
tenant_id=tenant_id,
user_id=user_id,
plugin_id=args.plugin_id,
page=args.page,
page_size=args.page_size,
plugin_id=req_data.plugin_id,
page=req_data.page,
page_size=req_data.page_size,
)
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")

View File

@@ -1,7 +1,6 @@
import logging
from typing import Any, cast
from flask import request
from flask_restx import Resource
from pydantic import BaseModel, Field, field_validator
@@ -18,6 +17,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -212,12 +212,12 @@ class DefaultModelApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserGetDefault.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserGetDefault)
def get(self, req_data: ParserGetDefault, tenant_id: str):
model_provider_service = ModelProviderService()
default_model_entity = model_provider_service.get_default_model_of_model_type(
tenant_id=tenant_id, model_type=args.model_type
tenant_id=tenant_id, model_type=req_data.model_type
)
return DefaultModelDataResponse(data=default_model_entity).model_dump(mode="json")
@@ -230,10 +230,10 @@ class DefaultModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserPostDefault.model_validate(console_ns.payload)
@model_validate(ParserPostDefault)
def post(self, req_data: ParserPostDefault, tenant_id: str):
model_provider_service = ModelProviderService()
model_settings = args.model_settings
model_settings = req_data.model_settings
for model_setting in model_settings:
if model_setting.provider is None:
continue
@@ -279,43 +279,43 @@ class ModelProviderModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
@model_validate(ParserPostModels)
def post(self, req_data: ParserPostModels, tenant_id: str, provider: str):
# To save the model's load balance configs
args = ParserPostModels.model_validate(console_ns.payload)
if args.config_from == "custom-model":
if not args.credential_id:
if req_data.config_from == "custom-model":
if not req_data.credential_id:
raise ValueError("credential_id is required when configuring a custom-model")
service = ModelProviderService()
service.switch_active_custom_model_credential(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
model_load_balancing_service = ModelLoadBalancingService()
if args.load_balancing and args.load_balancing.configs:
if req_data.load_balancing and req_data.load_balancing.configs:
# save load balancing configs
model_load_balancing_service.update_load_balancing_configs(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
configs=args.load_balancing.configs,
config_from=args.config_from or "",
model=req_data.model,
model_type=req_data.model_type,
configs=req_data.load_balancing.configs,
config_from=req_data.config_from or "",
session=db.session(),
)
if args.load_balancing.enabled:
if req_data.load_balancing.enabled:
model_load_balancing_service.enable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
else:
model_load_balancing_service.disable_model_load_balancing(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
@@ -328,12 +328,12 @@ class ModelProviderModelApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def delete(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def delete(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return "", 204
@@ -352,29 +352,29 @@ class ModelProviderModelCredentialApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, user: Account, provider: str):
args = ParserGetCredentials.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserGetCredentials)
def get(self, req_data: ParserGetCredentials, tenant_id: str, user: Account, provider: str):
model_provider_service = ModelProviderService()
current_credential = model_provider_service.get_model_credential(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
model_load_balancing_service = ModelLoadBalancingService()
is_load_balancing_enabled, load_balancing_configs = model_load_balancing_service.get_load_balancing_configs(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
model=req_data.model,
model_type=req_data.model_type,
session=db.session(),
config_from=args.config_from or "",
config_from=req_data.config_from or "",
)
if args.config_from == "predefined-model":
if req_data.config_from == "predefined-model":
# Only the predefined-model branch needs visibility filtering by user.
# The account is injected once by the handler and only passed into the
# service branch that needs user-scoped credential visibility.
@@ -387,8 +387,8 @@ class ModelProviderModelCredentialApi(Resource):
available_credentials = model_provider_service.get_provider_model_available_credentials(
tenant_id=tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
model_type=req_data.model_type,
model=req_data.model,
)
credentials: dict[str, Any] = {}
@@ -414,8 +414,8 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
args = ParserCreateCredential.model_validate(console_ns.payload)
@model_validate(ParserCreateCredential)
def post(self, req_data: ParserCreateCredential, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -423,17 +423,17 @@ class ModelProviderModelCredentialApi(Resource):
model_provider_service.create_model_credential(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
credentials=args.credentials,
credential_name=args.name,
model=req_data.model,
model_type=req_data.model_type,
credentials=req_data.credentials,
credential_name=req_data.name,
)
except CredentialsValidateFailedError as ex:
logger.exception(
"Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s",
tenant_id,
args.model,
args.model_type,
req_data.model,
req_data.model_type,
)
raise ValueError(str(ex))
@@ -447,8 +447,8 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def put(self, current_tenant_id: str, provider: str):
args = ParserUpdateCredential.model_validate(console_ns.payload)
@model_validate(ParserUpdateCredential)
def put(self, req_data: ParserUpdateCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -456,11 +456,11 @@ class ModelProviderModelCredentialApi(Resource):
model_provider_service.update_model_credential(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credentials=args.credentials,
credential_id=args.credential_id,
credential_name=args.name,
model_type=req_data.model_type,
model=req_data.model,
credentials=req_data.credentials,
credential_id=req_data.credential_id,
credential_name=req_data.name,
)
except CredentialsValidateFailedError as ex:
raise ValueError(str(ex))
@@ -475,16 +475,16 @@ class ModelProviderModelCredentialApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def delete(self, current_tenant_id: str, provider: str):
args = ParserDeleteCredential.model_validate(console_ns.payload)
@model_validate(ParserDeleteCredential)
def delete(self, req_data: ParserDeleteCredential, current_tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.remove_model_credential(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
return "", 204
@@ -500,16 +500,16 @@ class ModelProviderModelCredentialSwitchApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str, provider: str):
args = ParserSwitch.model_validate(console_ns.payload)
@model_validate(ParserSwitch)
def post(self, req_data: ParserSwitch, current_tenant_id: str, provider: str):
service = ModelProviderService()
service.add_model_credential_to_model_list(
tenant_id=current_tenant_id,
provider=provider,
model_type=args.model_type,
model=args.model,
credential_id=args.credential_id,
model_type=req_data.model_type,
model=req_data.model,
credential_id=req_data.credential_id,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -525,12 +525,12 @@ class ModelProviderModelEnableApi(Resource):
@account_initialization_required
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
def patch(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.enable_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -547,12 +547,12 @@ class ModelProviderModelDisableApi(Resource):
@account_initialization_required
@with_current_tenant_id
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
def patch(self, tenant_id: str, provider: str):
args = ParserDeleteModels.model_validate(console_ns.payload)
@model_validate(ParserDeleteModels)
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
model_provider_service.disable_model(
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -579,8 +579,8 @@ class ModelProviderModelValidateApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
args = ParserValidate.model_validate(console_ns.payload)
@model_validate(ParserValidate)
def post(self, req_data: ParserValidate, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
@@ -591,9 +591,9 @@ class ModelProviderModelValidateApi(Resource):
model_provider_service.validate_model_credentials(
tenant_id=tenant_id,
provider=provider,
model=args.model,
model_type=args.model_type,
credentials=args.credentials,
model=req_data.model,
model_type=req_data.model_type,
credentials=req_data.credentials,
)
except CredentialsValidateFailedError as ex:
result = False
@@ -617,12 +617,12 @@ class ModelProviderModelParameterRuleApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, provider: str):
args = ParserParameter.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserParameter)
def get(self, req_data: ParserParameter, tenant_id: str, provider: str):
model_provider_service = ModelProviderService()
parameter_rules = model_provider_service.get_model_parameter_rules(
tenant_id=tenant_id, provider=provider, model=args.model
tenant_id=tenant_id, provider=provider, model=req_data.model
)
return ModelParameterRuleListResponse(data=parameter_rules).model_dump(mode="json")

View File

@@ -24,6 +24,7 @@ from controllers.console.wraps import (
RBACResourceScope,
account_initialization_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -592,10 +593,10 @@ class PluginListApi(Resource):
@account_initialization_required
@with_current_user_id
@with_current_tenant_id
def get(self, tenant_id: str, user_id: str):
args = ParserList.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserList)
def get(self, req_data: ParserList, tenant_id: str, user_id: str):
try:
plugins_with_total = PluginService.list_with_total(tenant_id, user_id, args.page, args.page_size)
plugins_with_total = PluginService.list_with_total(tenant_id, user_id, req_data.page, req_data.page_size)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -660,10 +661,10 @@ class PluginInstalledIdsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = PluginInstalledIdsQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(PluginInstalledIdsQuery)
def get(self, req_data: PluginInstalledIdsQuery, tenant_id: str):
try:
plugin_ids = PluginService.list_installed_plugin_ids(tenant_id, args.category)
plugin_ids = PluginService.list_installed_plugin_ids(tenant_id, req_data.category)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -677,11 +678,11 @@ class PluginListLatestVersionsApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
args = ParserLatest.model_validate(console_ns.payload)
@model_validate(ParserLatest)
def post(self, req_data: ParserLatest):
try:
versions = PluginService.list_latest_versions(args.plugin_ids)
versions = PluginService.list_latest_versions(req_data.plugin_ids)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -696,11 +697,11 @@ class PluginListInstallationsFromIdsApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserLatest.model_validate(console_ns.payload)
@model_validate(ParserLatest)
def post(self, req_data: ParserLatest, tenant_id: str):
try:
plugins = PluginService.list_installations_from_ids(tenant_id, args.plugin_ids)
plugins = PluginService.list_installations_from_ids(tenant_id, req_data.plugin_ids)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -712,11 +713,11 @@ class PluginIconApi(Resource):
@console_ns.doc(params=query_params_from_model(ParserIcon))
@console_ns.response(200, "Success", console_ns.models[BinaryFileResponse.__name__])
@setup_required
def get(self):
args = ParserIcon.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserIcon)
def get(self, req_data: ParserIcon):
try:
icon_bytes, mimetype = PluginService.get_asset(args.tenant_id, args.filename)
icon_bytes, mimetype = PluginService.get_asset(req_data.tenant_id, req_data.filename)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -732,11 +733,11 @@ class PluginAssetApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserAsset.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserAsset)
def get(self, req_data: ParserAsset, tenant_id: str):
try:
binary = PluginService.extract_asset(tenant_id, args.plugin_unique_identifier, args.file_name)
binary = PluginService.extract_asset(tenant_id, req_data.plugin_unique_identifier, req_data.file_name)
return send_file(io.BytesIO(binary), mimetype="application/octet-stream")
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -773,11 +774,13 @@ class PluginUploadFromGithubApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserGithubUpload.model_validate(console_ns.payload)
@model_validate(ParserGithubUpload)
def post(self, req_data: ParserGithubUpload, tenant_id: str):
try:
response = PluginService.upload_pkg_from_github(tenant_id, args.repo, args.version, args.package)
response = PluginService.upload_pkg_from_github(
tenant_id, req_data.repo, req_data.version, req_data.package
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -814,11 +817,11 @@ class PluginInstallFromPkgApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserPluginIdentifiers.model_validate(console_ns.payload)
@model_validate(ParserPluginIdentifiers)
def post(self, req_data: ParserPluginIdentifiers, tenant_id: str):
try:
response = PluginService.install_from_local_pkg(tenant_id, args.plugin_unique_identifiers)
response = PluginService.install_from_local_pkg(tenant_id, req_data.plugin_unique_identifiers)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -835,16 +838,16 @@ class PluginInstallFromGithubApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserGithubInstall.model_validate(console_ns.payload)
@model_validate(ParserGithubInstall)
def post(self, req_data: ParserGithubInstall, tenant_id: str):
try:
response = PluginService.install_from_github(
tenant_id,
args.plugin_unique_identifier,
args.repo,
args.version,
args.package,
req_data.plugin_unique_identifier,
req_data.repo,
req_data.version,
req_data.package,
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -862,11 +865,11 @@ class PluginInstallFromMarketplaceApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserPluginIdentifiers.model_validate(console_ns.payload)
@model_validate(ParserPluginIdentifiers)
def post(self, req_data: ParserPluginIdentifiers, tenant_id: str):
try:
response = PluginService.install_from_marketplace_pkg(tenant_id, args.plugin_unique_identifiers)
response = PluginService.install_from_marketplace_pkg(tenant_id, req_data.plugin_unique_identifiers)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -883,15 +886,15 @@ class PluginFetchMarketplacePkgApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserPluginIdentifierQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserPluginIdentifierQuery)
def get(self, req_data: ParserPluginIdentifierQuery, tenant_id: str):
try:
return jsonable_encoder(
{
"manifest": PluginService.fetch_marketplace_pkg(
tenant_id,
args.plugin_unique_identifier,
req_data.plugin_unique_identifier,
)
}
)
@@ -909,12 +912,16 @@ class PluginFetchManifestApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_INSTALL, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserPluginIdentifierQuery.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserPluginIdentifierQuery)
def get(self, req_data: ParserPluginIdentifierQuery, tenant_id: str):
try:
return jsonable_encoder(
{"manifest": PluginService.fetch_plugin_manifest(tenant_id, args.plugin_unique_identifier).model_dump()}
{
"manifest": PluginService.fetch_plugin_manifest(
tenant_id, req_data.plugin_unique_identifier
).model_dump()
},
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -929,11 +936,13 @@ class PluginFetchInstallTasksApi(Resource):
@account_initialization_required
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserTasks.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserTasks)
def get(self, req_data: ParserTasks, tenant_id: str):
try:
return jsonable_encoder({"tasks": PluginService.fetch_install_tasks(tenant_id, args.page, args.page_size)})
return jsonable_encoder(
{"tasks": PluginService.fetch_install_tasks(tenant_id, req_data.page, req_data.page_size)}
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -1008,13 +1017,13 @@ class PluginUpgradeFromMarketplaceApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserMarketplaceUpgrade.model_validate(console_ns.payload)
@model_validate(ParserMarketplaceUpgrade)
def post(self, req_data: ParserMarketplaceUpgrade, tenant_id: str):
try:
return jsonable_encoder(
PluginService.upgrade_plugin_with_marketplace(
tenant_id, args.original_plugin_unique_identifier, args.new_plugin_unique_identifier
tenant_id, req_data.original_plugin_unique_identifier, req_data.new_plugin_unique_identifier
)
)
except PluginDaemonClientSideError as e:
@@ -1031,18 +1040,18 @@ class PluginUpgradeFromGithubApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_MODEL_CONFIG, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserGithubUpgrade.model_validate(console_ns.payload)
@model_validate(ParserGithubUpgrade)
def post(self, req_data: ParserGithubUpgrade, tenant_id: str):
try:
return jsonable_encoder(
PluginService.upgrade_plugin_with_github(
tenant_id,
args.original_plugin_unique_identifier,
args.new_plugin_unique_identifier,
args.repo,
args.version,
args.package,
req_data.original_plugin_unique_identifier,
req_data.new_plugin_unique_identifier,
req_data.repo,
req_data.version,
req_data.package,
)
)
except PluginDaemonClientSideError as e:
@@ -1059,15 +1068,15 @@ class PluginUninstallApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_DELETE, resource_required=False)
@plugin_permission_required(install_required=True)
@with_current_tenant_id
def post(self, tenant_id: str):
args = ParserUninstall.model_validate(console_ns.payload)
@model_validate(ParserUninstall)
def post(self, req_data: ParserUninstall, tenant_id: str):
try:
return {
"success": PluginService.uninstall(
tenant_id,
args.plugin_installation_id,
preserve_credentials=args.preserve_credentials,
req_data.plugin_installation_id,
preserve_credentials=req_data.preserve_credentials,
)
}
except PluginDaemonClientSideError as e:
@@ -1083,14 +1092,13 @@ class PluginChangePermissionApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
@model_validate(ParserPermissionChange)
def post(self, req_data: ParserPermissionChange, tenant_id: str, user: Account):
if not user.is_admin_or_owner:
raise Forbidden()
args = ParserPermissionChange.model_validate(console_ns.payload)
set_permission_result = PluginPermissionService.change_permission(
tenant_id, args.install_permission, args.debug_permission, session=db.session()
tenant_id, req_data.install_permission, req_data.debug_permission, session=db.session()
)
if not set_permission_result:
return jsonable_encoder({"success": False, "message": "Failed to set permission"})
@@ -1134,19 +1142,19 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def get(self, tenant_id: str, current_user: Account):
args = ParserDynamicOptions.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserDynamicOptions)
def get(self, req_data: ParserDynamicOptions, tenant_id: str, current_user: Account):
try:
options = PluginParameterService.get_dynamic_select_options(
tenant_id=tenant_id,
user_id=current_user.id,
plugin_id=args.plugin_id,
provider=args.provider,
action=args.action,
parameter=args.parameter,
credential_id=args.credential_id,
provider_type=args.provider_type,
plugin_id=req_data.plugin_id,
provider=req_data.provider,
action=req_data.action,
parameter=req_data.parameter,
credential_id=req_data.credential_id,
provider_type=req_data.provider_type,
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -1165,20 +1173,20 @@ class PluginFetchDynamicSelectOptionsWithCredentialsApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account):
@model_validate(ParserDynamicOptionsWithCredentials)
def post(self, req_data: ParserDynamicOptionsWithCredentials, tenant_id: str, current_user: Account):
"""Fetch dynamic options using credentials directly (for edit mode)."""
args = ParserDynamicOptionsWithCredentials.model_validate(console_ns.payload)
try:
options = PluginParameterService.get_dynamic_select_options_with_credentials(
tenant_id=tenant_id,
user_id=current_user.id,
plugin_id=args.plugin_id,
provider=args.provider,
action=args.action,
parameter=args.parameter,
credential_id=args.credential_id,
credentials=args.credentials,
plugin_id=req_data.plugin_id,
provider=req_data.provider,
action=req_data.action,
parameter=req_data.parameter,
credential_id=req_data.credential_id,
credentials=req_data.credentials,
)
except PluginDaemonClientSideError as e:
return {"code": "plugin_error", "message": e.description}, 400
@@ -1196,13 +1204,12 @@ class PluginChangeAutoUpgradeApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
@model_validate(ParserAutoUpgradeChange)
def post(self, req_data: ParserAutoUpgradeChange, tenant_id: str, user: Account):
if not dify_config.RBAC_ENABLED and not user.is_admin_or_owner:
raise Forbidden()
args = ParserAutoUpgradeChange.model_validate(console_ns.payload)
auto_upgrade = args.auto_upgrade
auto_upgrade = req_data.auto_upgrade
set_auto_upgrade_strategy_result = PluginAutoUpgradeService.change_strategy(
tenant_id,
auto_upgrade.strategy_setting,
@@ -1210,7 +1217,7 @@ class PluginChangeAutoUpgradeApi(Resource):
auto_upgrade.upgrade_mode,
auto_upgrade.exclude_plugins,
auto_upgrade.include_plugins,
category=args.category,
category=req_data.category,
session=db.session(),
)
if not set_auto_upgrade_strategy_result:
@@ -1227,16 +1234,16 @@ class PluginFetchAutoUpgradeApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserAutoUpgradeFetch.model_validate(request.args.to_dict(flat=True))
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, args.category, session=db.session())
@model_validate(ParserAutoUpgradeFetch)
def get(self, req_data: ParserAutoUpgradeFetch, tenant_id: str):
auto_upgrade = PluginAutoUpgradeService.get_strategy(tenant_id, req_data.category, session=db.session())
auto_upgrade_dict = (
_auto_upgrade_settings_to_dict(auto_upgrade) if auto_upgrade else _missing_auto_upgrade_settings(tenant_id)
)
return jsonable_encoder(
{
"category": args.category,
"category": req_data.category,
"auto_upgrade": auto_upgrade_dict,
}
)
@@ -1251,14 +1258,14 @@ class PluginAutoUpgradeExcludePluginApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@with_current_tenant_id
def post(self, tenant_id: str):
@model_validate(ParserExcludePlugin)
def post(self, req_data: ParserExcludePlugin, tenant_id: str):
# exclude one single plugin
args = ParserExcludePlugin.model_validate(console_ns.payload)
return jsonable_encoder(
{
"success": PluginAutoUpgradeService.exclude_plugin(
tenant_id, args.plugin_id, args.category, session=db.session()
tenant_id, req_data.plugin_id, req_data.category, session=db.session()
)
}
)
@@ -1272,8 +1279,12 @@ class PluginReadmeApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str):
args = ParserReadme.model_validate(request.args.to_dict(flat=True))
@model_validate(ParserReadme)
def get(self, req_data: ParserReadme, tenant_id: str):
return jsonable_encoder(
{"readme": PluginService.fetch_plugin_readme(tenant_id, args.plugin_unique_identifier, args.language)}
{
"readme": PluginService.fetch_plugin_readme(
tenant_id, req_data.plugin_unique_identifier, req_data.language
)
},
)

View File

@@ -11,7 +11,7 @@ from werkzeug.exceptions import NotFound
from configs import dify_config
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
from core.db.session_factory import session_factory
from core.rbac import RBACResourceWhitelistScope
from extensions.ext_database import db
@@ -310,19 +310,19 @@ class RBACRolesApi(Resource):
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__])
def get(self):
@model_validate(_RolesListQuery)
def get(self, req_data: _RolesListQuery):
tenant_id, account_id = _current_ids()
query = _RolesListQuery.model_validate(request.args.to_dict(flat=True))
options = query.to_inner_options()
options = req_data.to_inner_options()
if not dify_config.RBAC_ENABLED:
result = _legacy_workspace_roles(
options, include_owner=query.include_owner, billing_enabled=dify_config.BILLING_ENABLED
options, include_owner=req_data.include_owner, billing_enabled=dify_config.BILLING_ENABLED
)
else:
result = svc.RBACService.Roles.list(
tenant_id,
account_id,
include_owner=query.include_owner,
include_owner=req_data.include_owner,
biiling_enabled=dify_config.BILLING_ENABLED,
options=options,
)

View File

@@ -33,6 +33,7 @@ from controllers.console.wraps import (
account_initialization_required,
enterprise_license_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -555,16 +556,15 @@ class ToolBuiltinProviderDeleteApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
payload = BuiltinToolCredentialDeletePayload.model_validate(console_ns.payload or {})
@model_validate(BuiltinToolCredentialDeletePayload)
def post(self, req_data: BuiltinToolCredentialDeletePayload, tenant_id: str, provider: str):
return dump_response(
SimpleResultResponse,
BuiltinToolManageService.delete_builtin_tool_provider(
tenant_id,
provider,
payload.credential_id,
req_data.credential_id,
),
)
@@ -582,19 +582,18 @@ class ToolBuiltinProviderAddApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str):
payload = BuiltinToolAddPayload.model_validate(console_ns.payload or {})
@model_validate(BuiltinToolAddPayload)
def post(self, req_data: BuiltinToolAddPayload, tenant_id: str, user: Account, provider: str):
return dump_response(
SimpleResultResponse,
BuiltinToolManageService.add_builtin_tool_provider(
user_id=user.id,
tenant_id=tenant_id,
provider=provider,
credentials=payload.credentials,
name=payload.name,
api_type=CredentialType.of(payload.type),
visibility=payload.visibility,
credentials=req_data.credentials,
name=req_data.name,
api_type=CredentialType.of(req_data.type),
visibility=req_data.visibility,
),
)
@@ -614,16 +613,15 @@ class ToolBuiltinProviderUpdateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str):
payload = BuiltinToolUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(BuiltinToolUpdatePayload)
def post(self, req_data: BuiltinToolUpdatePayload, tenant_id: str, user: Account, provider: str):
result = BuiltinToolManageService.update_builtin_tool_provider(
user_id=user.id,
tenant_id=tenant_id,
provider=provider,
credential_id=payload.credential_id,
credentials=payload.credentials,
name=payload.name or "",
credential_id=req_data.credential_id,
credentials=req_data.credentials,
name=req_data.name or "",
)
return dump_response(SimpleResultResponse, result)
@@ -683,22 +681,21 @@ class ToolApiProviderAddApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = ApiToolProviderAddPayload.model_validate(console_ns.payload or {})
@model_validate(ApiToolProviderAddPayload)
def post(self, req_data: ApiToolProviderAddPayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
ApiToolManageService.create_api_tool_provider(
user.id,
tenant_id,
payload.provider,
payload.icon.model_dump(mode="json"),
payload.credentials,
payload.schema_type,
payload.schema_,
payload.privacy_policy or "",
payload.custom_disclaimer or "",
payload.labels or [],
req_data.provider,
req_data.icon.model_dump(mode="json"),
req_data.credentials,
req_data.schema_type,
req_data.schema_,
req_data.privacy_policy or "",
req_data.custom_disclaimer or "",
req_data.labels or [],
),
)
@@ -764,23 +761,22 @@ class ToolApiProviderUpdateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = ApiToolProviderUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(ApiToolProviderUpdatePayload)
def post(self, req_data: ApiToolProviderUpdatePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
ApiToolManageService.update_api_tool_provider(
user.id,
tenant_id,
payload.provider,
payload.original_provider,
payload.icon.model_dump(mode="json"),
payload.credentials,
payload.schema_type,
payload.schema_,
payload.privacy_policy,
payload.custom_disclaimer,
payload.labels or [],
req_data.provider,
req_data.original_provider,
req_data.icon.model_dump(mode="json"),
req_data.credentials,
req_data.schema_type,
req_data.schema_,
req_data.privacy_policy,
req_data.custom_disclaimer,
req_data.labels or [],
),
)
@@ -796,15 +792,14 @@ class ToolApiProviderDeleteApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = ApiToolProviderDeletePayload.model_validate(console_ns.payload or {})
@model_validate(ApiToolProviderDeletePayload)
def post(self, req_data: ApiToolProviderDeletePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
ApiToolManageService.delete_api_tool_provider(
user.id,
tenant_id,
payload.provider,
req_data.provider,
),
)
@@ -861,10 +856,9 @@ class ToolApiProviderSchemaApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
payload = ApiToolSchemaPayload.model_validate(console_ns.payload or {})
return dump_response(ApiSchemaParseResponse, ApiToolManageService.parser_api_schema(schema=payload.schema_))
@model_validate(ApiToolSchemaPayload)
def post(self, req_data: ApiToolSchemaPayload):
return dump_response(ApiSchemaParseResponse, ApiToolManageService.parser_api_schema(schema=req_data.schema_))
@console_ns.route("/workspaces/current/tool-provider/api/test/pre")
@@ -879,18 +873,18 @@ class ToolApiProviderPreviousTestApi(Resource):
@login_required
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str):
payload = ApiToolTestPayload.model_validate(console_ns.payload or {})
@model_validate(ApiToolTestPayload)
def post(self, req_data: ApiToolTestPayload, current_tenant_id: str):
return dump_response(
ApiToolPreviewResponse,
ApiToolManageService.test_api_tool_preview(
current_tenant_id,
payload.provider_name or "",
payload.tool_name,
payload.credentials,
payload.parameters,
payload.schema_type,
payload.schema_,
req_data.provider_name or "",
req_data.tool_name,
req_data.credentials,
req_data.parameters,
req_data.schema_type,
req_data.schema_,
),
)
@@ -906,22 +900,21 @@ class ToolWorkflowProviderCreateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = WorkflowToolCreatePayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowToolCreatePayload)
def post(self, req_data: WorkflowToolCreatePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.create_workflow_tool(
user_id=user.id,
tenant_id=tenant_id,
workflow_app_id=payload.workflow_app_id,
name=payload.name,
label=payload.label,
icon=payload.icon.model_dump(mode="json"),
description=payload.description,
parameters=payload.parameters,
privacy_policy=payload.privacy_policy or "",
labels=payload.labels or [],
workflow_app_id=req_data.workflow_app_id,
name=req_data.name,
label=req_data.label,
icon=req_data.icon.model_dump(mode="json"),
description=req_data.description,
parameters=req_data.parameters,
privacy_policy=req_data.privacy_policy or "",
labels=req_data.labels or [],
),
)
@@ -937,22 +930,21 @@ class ToolWorkflowProviderUpdateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = WorkflowToolUpdatePayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowToolUpdatePayload)
def post(self, req_data: WorkflowToolUpdatePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.update_workflow_tool(
user.id,
tenant_id,
payload.workflow_tool_id,
payload.name,
payload.label,
payload.icon.model_dump(mode="json"),
payload.description,
payload.parameters,
payload.privacy_policy or "",
payload.labels or [],
req_data.workflow_tool_id,
req_data.name,
req_data.label,
req_data.icon.model_dump(mode="json"),
req_data.description,
req_data.parameters,
req_data.privacy_policy or "",
req_data.labels or [],
),
)
@@ -968,15 +960,14 @@ class ToolWorkflowProviderDeleteApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = WorkflowToolDeletePayload.model_validate(console_ns.payload or {})
@model_validate(WorkflowToolDeletePayload)
def post(self, req_data: WorkflowToolDeletePayload, tenant_id: str, user: Account):
return dump_response(
SimpleResultResponse,
WorkflowToolManageService.delete_workflow_tool(
user.id,
tenant_id,
payload.workflow_tool_id,
req_data.workflow_tool_id,
),
)
@@ -1224,12 +1215,12 @@ class ToolBuiltinProviderSetDefaultApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, current_tenant_id: str, provider: str):
payload = BuiltinProviderDefaultCredentialPayload.model_validate(console_ns.payload or {})
@model_validate(BuiltinProviderDefaultCredentialPayload)
def post(self, req_data: BuiltinProviderDefaultCredentialPayload, current_tenant_id: str, provider: str):
return dump_response(
SimpleResultResponse,
BuiltinToolManageService.set_default_provider(
tenant_id=current_tenant_id, provider=provider, id=payload.id
tenant_id=current_tenant_id, provider=provider, id=req_data.id
),
)
@@ -1246,17 +1237,16 @@ class ToolOAuthCustomClient(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
payload = ToolOAuthCustomClientPayload.model_validate(console_ns.payload or {})
@model_validate(ToolOAuthCustomClientPayload)
def post(self, req_data: ToolOAuthCustomClientPayload, tenant_id: str, provider: str):
return dump_response(
SimpleResultResponse,
BuiltinToolManageService.save_custom_oauth_client_params(
tenant_id=tenant_id,
provider=provider,
client_params=payload.client_params or {},
enable_oauth_custom_client=payload.enable_oauth_custom_client
if payload.enable_oauth_custom_client is not None
client_params=req_data.client_params or {},
enable_oauth_custom_client=req_data.enable_oauth_custom_client
if req_data.enable_oauth_custom_client is not None
else True,
),
)
@@ -1349,11 +1339,10 @@ class ToolProviderMCPApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False)
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account):
payload = MCPProviderCreatePayload.model_validate(console_ns.payload or {})
configuration = payload.configuration or MCPConfiguration()
authentication = payload.authentication
@model_validate(MCPProviderCreatePayload)
def post(self, req_data: MCPProviderCreatePayload, tenant_id: str, user: Account):
configuration = req_data.configuration or MCPConfiguration()
authentication = req_data.authentication
# 1) Create provider in a short transaction (no network I/O inside)
with session_factory.create_session() as session, session.begin():
@@ -1361,24 +1350,24 @@ class ToolProviderMCPApi(Resource):
result = service.create_provider(
tenant_id=tenant_id,
user_id=user.id,
server_url=payload.server_url,
name=payload.name,
icon=payload.icon,
icon_type=payload.icon_type,
icon_background=payload.icon_background,
server_identifier=payload.server_identifier,
headers=payload.headers or {},
server_url=req_data.server_url,
name=req_data.name,
icon=req_data.icon,
icon_type=req_data.icon_type,
icon_background=req_data.icon_background,
server_identifier=req_data.server_identifier,
headers=req_data.headers or {},
configuration=configuration,
authentication=authentication,
identity_mode=_resolve_identity_mode(payload.identity_mode, current=IdentityMode.OFF),
identity_mode=_resolve_identity_mode(req_data.identity_mode, current=IdentityMode.OFF),
)
# 2) Try to fetch tools immediately after creation so they appear without a second save.
# Perform network I/O outside any DB session to avoid holding locks.
try:
reconnect = MCPToolManageService.reconnect_with_url(
server_url=payload.server_url,
headers=payload.headers or {},
server_url=req_data.server_url,
headers=req_data.headers or {},
timeout=configuration.timeout,
sse_read_timeout=configuration.sse_read_timeout,
)
@@ -1403,24 +1392,24 @@ class ToolProviderMCPApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False)
@with_current_tenant_id
def put(self, current_tenant_id: str):
payload = MCPProviderUpdatePayload.model_validate(console_ns.payload or {})
configuration = payload.configuration or MCPConfiguration()
authentication = payload.authentication
@model_validate(MCPProviderUpdatePayload)
def put(self, req_data: MCPProviderUpdatePayload, current_tenant_id: str):
configuration = req_data.configuration or MCPConfiguration()
authentication = req_data.authentication
# Step 1: Get provider data for URL validation (short-lived session, no network I/O)
validation_data = None
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
validation_data = service.get_provider_for_url_validation(
tenant_id=current_tenant_id, provider_id=payload.provider_id
tenant_id=current_tenant_id, provider_id=req_data.provider_id
)
# Step 2: Perform URL validation with network I/O OUTSIDE of any database session
# This prevents holding database locks during potentially slow network operations
validation_result = MCPToolManageService.validate_server_url_standalone(
tenant_id=current_tenant_id,
new_server_url=payload.server_url,
new_server_url=req_data.server_url,
validation_data=validation_data,
)
@@ -1430,18 +1419,18 @@ class ToolProviderMCPApi(Resource):
# Resolve "leave unchanged" (None) against the stored value, and gate
# the result on ENTERPRISE_ENABLED — both are API-layer concerns, so
# the service receives a concrete IdentityMode.
existing = service.get_provider(provider_id=payload.provider_id, tenant_id=current_tenant_id)
identity_mode = _resolve_identity_mode(payload.identity_mode, current=IdentityMode(existing.identity_mode))
existing = service.get_provider(provider_id=req_data.provider_id, tenant_id=current_tenant_id)
identity_mode = _resolve_identity_mode(req_data.identity_mode, current=IdentityMode(existing.identity_mode))
service.update_provider(
tenant_id=current_tenant_id,
provider_id=payload.provider_id,
server_url=payload.server_url,
name=payload.name,
icon=payload.icon,
icon_type=payload.icon_type,
icon_background=payload.icon_background,
server_identifier=payload.server_identifier,
headers=payload.headers or {},
provider_id=req_data.provider_id,
server_url=req_data.server_url,
name=req_data.name,
icon=req_data.icon,
icon_type=req_data.icon_type,
icon_background=req_data.icon_background,
server_identifier=req_data.server_identifier,
headers=req_data.headers or {},
configuration=configuration,
authentication=authentication,
validation_result=validation_result,
@@ -1457,12 +1446,11 @@ class ToolProviderMCPApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False)
@with_current_tenant_id
def delete(self, current_tenant_id: str):
payload = MCPProviderDeletePayload.model_validate(console_ns.payload or {})
@model_validate(MCPProviderDeletePayload)
def delete(self, req_data: MCPProviderDeletePayload, current_tenant_id: str):
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
service.delete_provider(tenant_id=current_tenant_id, provider_id=payload.provider_id)
service.delete_provider(tenant_id=current_tenant_id, provider_id=req_data.provider_id)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -1476,9 +1464,9 @@ class ToolMCPAuthApi(Resource):
@account_initialization_required
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.MCP_MANAGE, resource_required=False)
@with_current_tenant_id
def post(self, tenant_id: str):
payload = MCPAuthPayload.model_validate(console_ns.payload or {})
provider_id = payload.provider_id
@model_validate(MCPAuthPayload)
def post(self, req_data: MCPAuthPayload, tenant_id: str):
provider_id = req_data.provider_id
with sessionmaker(db.engine).begin() as session:
service = MCPToolManageService(session=session)
@@ -1515,7 +1503,7 @@ class ToolMCPAuthApi(Resource):
# Pass the extracted OAuth metadata hints to auth()
auth_result = auth(
provider_entity,
payload.authorization_code,
req_data.authorization_code,
resource_metadata_url=e.resource_metadata_url,
scope_hint=e.scope_hint,
)

View File

@@ -39,6 +39,7 @@ from ..wraps import (
account_initialization_required,
edit_permission_required,
is_admin_or_owner_required,
model_validate,
rbac_permission_required,
setup_required,
with_current_tenant_id,
@@ -233,13 +234,12 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str):
@model_validate(TriggerSubscriptionBuilderCreatePayload)
def post(self, req_data: TriggerSubscriptionBuilderCreatePayload, tenant_id: str, user: Account, provider: str):
"""Add a new subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {})
try:
credential_type = CredentialType.of(payload.credential_type)
credential_type = CredentialType.of(req_data.credential_type)
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
tenant_id=tenant_id,
user_id=user.id,
@@ -298,11 +298,17 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionBuilderVerifyPayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Verify and update a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
try:
# Use atomic update_and_verify to prevent race conditions
result = TriggerSubscriptionBuilderService.update_and_verify_builder(
@@ -311,7 +317,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
credentials=payload.credentials,
credentials=req_data.credentials,
),
)
return dump_response(TriggerVerificationResponse, result)
@@ -337,10 +343,17 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Update a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
try:
return TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
tenant_id=tenant_id,
@@ -348,10 +361,10 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=payload.name,
parameters=payload.parameters,
properties=payload.properties,
credentials=payload.credentials,
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
credentials=req_data.credentials,
),
).model_dump(mode="json")
except Exception as e:
@@ -406,9 +419,16 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(
self,
req_data: TriggerSubscriptionBuilderUpdatePayload,
tenant_id: str,
user: Account,
provider: str,
subscription_builder_id: str,
):
"""Build a subscription instance for a trigger provider"""
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
try:
# Use atomic update_and_build to prevent race conditions
TriggerSubscriptionBuilderService.update_and_build_builder(
@@ -417,9 +437,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
provider_id=TriggerProviderID(provider),
subscription_builder_id=subscription_builder_id,
subscription_builder_updater=SubscriptionBuilderUpdater(
name=payload.name,
parameters=payload.parameters,
properties=payload.properties,
name=req_data.name,
parameters=req_data.parameters,
properties=req_data.properties,
),
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -442,11 +462,10 @@ class TriggerSubscriptionUpdateApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, subscription_id: str):
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
def post(self, req_data: TriggerSubscriptionBuilderUpdatePayload, tenant_id: str, subscription_id: str):
"""Update a subscription instance"""
request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
subscription = TriggerProviderService.get_subscription_by_id(
tenant_id=tenant_id,
subscription_id=subscription_id,
@@ -458,7 +477,9 @@ class TriggerSubscriptionUpdateApi(Resource):
try:
# For rename only, just update the name
rename = request.name is not None and not any((request.credentials, request.parameters, request.properties))
rename = req_data.name is not None and not any(
(req_data.credentials, req_data.parameters, req_data.properties)
)
# When credential type is UNAUTHORIZED, it indicates the subscription was manually created
# For Manually created subscription, they dont have credentials, parameters
# They only have name and properties(which is input by user)
@@ -467,8 +488,8 @@ class TriggerSubscriptionUpdateApi(Resource):
TriggerProviderService.update_trigger_subscription(
tenant_id=tenant_id,
subscription_id=subscription_id,
name=request.name,
properties=request.properties,
name=req_data.name,
properties=req_data.properties,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
@@ -476,11 +497,11 @@ class TriggerSubscriptionUpdateApi(Resource):
# we need to call third party provider(e.g. GitHub) to rebuild the subscription
TriggerProviderService.rebuild_trigger_subscription(
tenant_id=tenant_id,
name=request.name,
name=req_data.name,
provider_id=provider_id,
subscription_id=subscription_id,
credentials=request.credentials or subscription.credentials,
parameters=request.parameters or subscription.parameters,
credentials=req_data.credentials or subscription.credentials,
parameters=req_data.parameters or subscription.parameters,
)
return SimpleResultResponse(result="success").model_dump(mode="json")
except ValueError as e:
@@ -740,18 +761,17 @@ class TriggerOAuthClientManageApi(Resource):
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
@account_initialization_required
@with_current_tenant_id
def post(self, tenant_id: str, provider: str):
@model_validate(TriggerOAuthClientPayload)
def post(self, req_data: TriggerOAuthClientPayload, tenant_id: str, provider: str):
"""Configure custom OAuth client for a provider"""
payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {})
try:
provider_id = TriggerProviderID(provider)
result = TriggerProviderService.save_custom_oauth_client_params(
tenant_id=tenant_id,
provider_id=provider_id,
client_params=payload.client_params,
enabled=payload.enabled,
client_params=req_data.client_params,
enabled=req_data.enabled,
)
return dump_response(SimpleResultResponse, result)
@@ -805,18 +825,24 @@ class TriggerSubscriptionVerifyApi(Resource):
@account_initialization_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, user: Account, provider: str, subscription_id: str):
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
def post(
self,
req_data: TriggerSubscriptionBuilderVerifyPayload,
tenant_id: str,
user: Account,
provider: str,
subscription_id: str,
):
"""Verify credentials for an existing subscription (edit mode only)"""
verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
try:
result = TriggerProviderService.verify_subscription_credentials(
tenant_id=tenant_id,
user_id=user.id,
provider_id=TriggerProviderID(provider),
subscription_id=subscription_id,
credentials=verify_request.credentials,
credentials=req_data.credentials,
)
return dump_response(TriggerVerificationResponse, result)
except ValueError as e:

View File

@@ -123,7 +123,7 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
for hit_callback in self.hit_callbacks:
hit_callback.return_retriever_resource_info(context_list)
return str("\n".join([item.page_content for item in results]))
return "\n".join([item.page_content for item in results])
else:
if metadata_condition and not document_ids_filter:
return ""
@@ -139,7 +139,7 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
top_k=self.top_k,
document_ids_filter=document_ids_filter,
)
return str("\n".join([document.page_content for document in documents]))
return "\n".join([document.page_content for document in documents])
else:
if self.top_k > 0:
# retrieval source
@@ -241,5 +241,5 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
hit_callback.return_retriever_resource_info(retrieval_resource_list)
if document_context_list:
document_context_list = sorted(document_context_list, key=lambda x: x.score or 0.0, reverse=True)
return str("\n".join([document_context.content for document_context in document_context_list]))
return "\n".join([document_context.content for document_context in document_context_list])
return ""

View File

@@ -836,7 +836,12 @@ def _knowledge_metadata_filtering_config(
return DifyKnowledgeMetadataFilteringConfig(
mode=metadata_filtering.mode,
model_config=_knowledge_model_config(metadata_filtering.metadata_model_config),
conditions=cast(Any, metadata_filtering.conditions.model_dump(mode="json"))
conditions=cast(
Any,
metadata_filtering.conditions.model_dump(
mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}}
),
)
if metadata_filtering.conditions is not None
else None,
)

View File

@@ -6,13 +6,21 @@ from typing import cast
from flask import Flask, current_app
from sqlalchemy.orm import Session, sessionmaker
from configs import dify_config
from constants.dsl_version import CURRENT_APP_DSL_VERSION
from core.db.session_factory import get_session_maker
from core.schemas.schema_manager import SchemaManager
from enums.deployment_edition import DeploymentEdition
from extensions.ext_redis import RedisClientWrapper, redis_client
from repositories.installation_state_repository import InstallationStateRepository
from repositories.workspace_member_query_repository import WorkspaceMemberQueryRepository
from repositories.workspace_query_repository import WorkspaceQueryRepository
from services.feature_query_service import FeatureQueryService
from services.feature_service import FeatureService
from services.feature_service_gateway import FeatureServiceGateway
from services.schema_definition_service import SchemaDefinitionService
from services.setup_adapters import RedisSetupLock, RegisterServiceAccountProvisioner
from services.setup_service import SetupService
from services.workspace_member_query_service import WorkspaceMemberQueryService
from services.workspace_member_role_resolver import DeploymentWorkspaceMemberRoleResolver
from services.workspace_plan_gateway import DeploymentWorkspacePlanGateway
@@ -23,6 +31,8 @@ _EXTENSION_KEY = "application_services"
@dataclass(frozen=True, slots=True)
class ApplicationServices:
schema_definitions: SchemaDefinitionService
setup: SetupService
feature_queries: FeatureQueryService
workspace_queries: WorkspaceQueryService
workspace_member_queries: WorkspaceMemberQueryService
@@ -31,8 +41,18 @@ class ApplicationServices:
def build_application_services(
*,
database_client: sessionmaker[Session],
deployment_edition: DeploymentEdition,
redis: RedisClientWrapper,
) -> ApplicationServices:
installation_state = InstallationStateRepository(client=database_client)
return ApplicationServices(
schema_definitions=SchemaDefinitionService(source_factory=SchemaManager),
setup=SetupService(
state=installation_state,
accounts=RegisterServiceAccountProvisioner(client=database_client),
lock=RedisSetupLock(client=redis),
setup_required=deployment_edition != DeploymentEdition.CLOUD,
),
feature_queries=FeatureQueryService(
features=FeatureServiceGateway(),
trial_models=FeatureService.get_trial_models(),
@@ -56,6 +76,8 @@ def build_application_services(
def init_app(app: Flask) -> None:
app.extensions[_EXTENSION_KEY] = build_application_services(
database_client=get_session_maker(),
deployment_edition=dify_config.DEPLOYMENT_EDITION,
redis=redis_client,
)

View File

@@ -420,8 +420,19 @@ class AgentKnowledgeRetrievalConfig(BaseModel):
class AgentKnowledgeMetadataCondition(BaseModel):
"""One manual metadata filter clause.
``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
every save (a stable row key and a reference to the selected metadata
field). They are persisted here for round-tripping the composer's draft
state but are stripped before building the Agent runtime request, whose
DTO only accepts ``name``/``comparison_operator``/``value``.
"""
model_config = ConfigDict(extra="forbid")
id: str | None = None
metadata_id: str | None = None
name: str = Field(min_length=1, max_length=255)
comparison_operator: SupportedComparisonOperator
value: ConditionValue = None
@@ -527,8 +538,14 @@ class AgentModelResponseFormatConfig(AgentFlexibleConfig):
type: str | None = Field(default=None, max_length=64)
class AgentSoulModelSettings(BaseModel):
model_config = ConfigDict(extra="ignore")
class AgentSoulModelSettings(AgentFlexibleConfig):
"""Model parameters for the Agent Soul model.
Model plugins can declare arbitrary parameters via ``parameter_rules``
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
fields typed below, so extra keys must round-trip through persistence
rather than being dropped.
"""
temperature: float | None = None
top_p: float | None = None

View File

@@ -14302,9 +14302,19 @@ the current roster/workflow APIs scoped to Dify Agent.
#### AgentKnowledgeMetadataCondition
One manual metadata filter clause.
``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
every save (a stable row key and a reference to the selected metadata
field). They are persisted here for round-tripping the composer's draft
state but are stripped before building the Agent runtime request, whose
DTO only accepts ``name``/``comparison_operator``/``value``.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| comparison_operator | string, <br>**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes |
| id | string | | No |
| metadata_id | string | | No |
| name | string | | Yes |
| value | string<br>[ string ]<br>number | | No |
@@ -14923,6 +14933,13 @@ Reference to model credentials resolved only at runtime.
#### AgentSoulModelSettings
Model parameters for the Agent Soul model.
Model plugins can declare arbitrary parameters via ``parameter_rules``
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
fields typed below, so extra keys must round-trip through persistence
rather than being dropped.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| frequency_penalty | number | | No |

View File

@@ -0,0 +1,27 @@
"""Persistence adapter for installation setup and tenant-existence state."""
from datetime import datetime
from sqlalchemy import exists, select
from sqlalchemy.orm import Session, sessionmaker
from models.account import Tenant
from models.model import DifySetup
class InstallationStateRepository:
"""Read persistent state shared by installation bootstrap use cases."""
def __init__(self, client: sessionmaker[Session]) -> None:
self._client = client
def get_setup_at(self) -> datetime | None:
with self._client() as session:
return session.scalar(select(DifySetup.setup_at).limit(1))
def is_setup(self) -> bool:
return self.get_setup_at() is not None
def has_tenants(self) -> bool:
with self._client() as session:
return session.scalar(select(exists().select_from(Tenant))) is True

View File

@@ -0,0 +1,24 @@
"""Application service for querying Console schema definitions."""
import logging
from collections.abc import Callable, Mapping
from typing import Any, Protocol
logger = logging.getLogger(__name__)
class SchemaDefinitionSource(Protocol):
def get_all_schema_definitions(self, version: str = "v1") -> list[Mapping[str, Any]]: ...
class SchemaDefinitionService:
def __init__(self, *, source_factory: Callable[[], SchemaDefinitionSource]) -> None:
self._source_factory = source_factory
def list(self) -> tuple[Mapping[str, Any], ...]:
try:
source = self._source_factory()
return tuple(source.get_all_schema_definitions())
except Exception:
logger.exception("Failed to get schema definitions from local registry")
return ()

View File

@@ -0,0 +1,43 @@
"""Infrastructure adapters for the first-time setup application service."""
from contextlib import AbstractContextManager
from typing import override
from sqlalchemy.orm import Session, sessionmaker
from extensions.ext_redis import RedisClientWrapper
from services.account_service import RegisterService
from services.setup_service import SetupAccountProvisioner, SetupInput, SetupLock
_SETUP_LOCK_KEY = "setup:initialize"
_SETUP_LOCK_TIMEOUT_SECONDS = 300
class RegisterServiceAccountProvisioner(SetupAccountProvisioner):
def __init__(self, client: sessionmaker[Session]) -> None:
self._client = client
@override
def provision(self, setup: SetupInput) -> None:
with self._client() as session:
RegisterService.setup(
email=setup.email,
name=setup.name,
password=setup.password,
ip_address=setup.ip_address,
language=setup.language,
session=session,
)
class RedisSetupLock(SetupLock):
def __init__(self, *, client: RedisClientWrapper) -> None:
self._client = client
@override
def acquire(self) -> AbstractContextManager[None]:
return self._client.lock(
_SETUP_LOCK_KEY,
timeout=_SETUP_LOCK_TIMEOUT_SECONDS,
blocking_timeout=_SETUP_LOCK_TIMEOUT_SECONDS,
)

View File

@@ -0,0 +1,83 @@
"""Application service for first-time Dify setup."""
from contextlib import AbstractContextManager
from dataclasses import dataclass
from datetime import datetime
from typing import Protocol
@dataclass(frozen=True, slots=True)
class SetupInput:
email: str
name: str
password: str
ip_address: str
language: str | None
@dataclass(frozen=True, slots=True)
class SetupStatus:
completed: bool
setup_at: datetime | None = None
class SetupState(Protocol):
def get_setup_at(self) -> datetime | None: ...
def has_tenants(self) -> bool: ...
class SetupAccountProvisioner(Protocol):
def provision(self, setup: SetupInput) -> None: ...
class SetupLock(Protocol):
def acquire(self) -> AbstractContextManager[None]: ...
class SetupAlreadyCompletedError(Exception):
"""Raised when setup has already created persistent installation state."""
class InitializationValidationRequiredError(Exception):
"""Raised when initialization-password validation has not completed."""
class SetupService:
def __init__(
self,
*,
state: SetupState,
accounts: SetupAccountProvisioner,
lock: SetupLock,
setup_required: bool,
) -> None:
self._state = state
self._accounts = accounts
self._lock = lock
self._setup_required = setup_required
def get_status(self) -> SetupStatus:
if not self._setup_required:
return SetupStatus(completed=True)
setup_at = self._state.get_setup_at()
return SetupStatus(completed=setup_at is not None, setup_at=setup_at)
def initialize(self, setup: SetupInput, *, initialization_validated: bool) -> None:
with self._lock.acquire():
if self._state.get_setup_at() is not None or self._state.has_tenants():
raise SetupAlreadyCompletedError
if not initialization_validated:
raise InitializationValidationRequiredError
self._accounts.provision(
SetupInput(
email=setup.email.lower(),
name=setup.name,
password=setup.password,
ip_address=setup.ip_address,
language=setup.language,
)
)

View File

@@ -7,7 +7,7 @@ from uuid import uuid4
from flask import Flask
from sqlalchemy.orm import Session
from controllers.console.datasets.data_source import DataSourceNotionListApi
from controllers.console.datasets.data_source import DataSourceNotionListApi, DataSourceNotionListQuery
from models import Account
from models.dataset import Document
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus
@@ -78,7 +78,11 @@ def test_notion_page_is_marked_bound_from_persisted_document(
),
):
response, status = unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(), db_session_with_containers, tenant_id, account
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="c1", dataset_id=dataset_id),
db_session_with_containers,
tenant_id,
account,
)
assert status == 200

View File

@@ -198,7 +198,13 @@ class TestConversationRenameApi:
return_value=conversation,
),
):
result = method(api, user, chat_app, "cid")
result = method(
api,
conversation_module.ConversationRenamePayload.model_validate({"name": "new"}),
user,
chat_app,
"cid",
)
assert result["id"] == "cid"
@@ -215,7 +221,13 @@ class TestConversationRenameApi:
),
):
with pytest.raises(NotFound):
method(api, user, chat_app, "cid")
method(
api,
conversation_module.ConversationRenamePayload.model_validate({"name": "new"}),
user,
chat_app,
"cid",
)
class TestConversationPinApi:

View File

@@ -0,0 +1,159 @@
from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor
from contextlib import AbstractContextManager
from threading import Event, Lock
from unittest.mock import MagicMock, patch
import pytest
from faker import Faker
from flask import Flask
from flask.testing import FlaskClient
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from models.account import Account, Tenant, TenantAccountJoin
from models.model import DifySetup
from services.account_service import RegisterService
from services.setup_adapters import RedisSetupLock
from tests.test_containers_integration_tests.helpers import generate_valid_password
@pytest.fixture
def setup_dependencies() -> Iterator[MagicMock]:
with (
patch("services.account_service.FeatureService") as feature_service,
patch("services.account_service.BillingService") as billing_service,
patch("services.account_service.CommunityTelemetryService.report_install") as report_install,
):
feature_service.get_system_features.return_value.is_allow_register = True
feature_service.get_license.return_value.seats.is_available.return_value = True
feature_service.get_license.return_value.workspaces.is_available.return_value = True
feature_service.is_workspace_creation_allowed.return_value = True
billing_service.is_email_in_freeze.return_value = False
yield report_install
def _setup_payload(*, email: str, password: str) -> dict[str, str]:
return {
"email": email,
"name": "Admin",
"password": password,
"language": "en-US",
}
def test_setup_endpoint_persists_bootstrap_state_and_rejects_repeat(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
monkeypatch: pytest.MonkeyPatch,
setup_dependencies: MagicMock,
) -> None:
monkeypatch.delenv("INIT_PASSWORD", raising=False)
password = generate_valid_password(Faker())
response = test_client_with_containers.post(
"/console/api/setup",
json=_setup_payload(email="admin@example.com", password=password),
headers={"CF-Connecting-IP": "203.0.113.7"},
)
assert response.status_code == 201
assert response.get_json() == {"result": "success"}
setup_dependencies.assert_called_once()
repeated_response = test_client_with_containers.post(
"/console/api/setup",
json=_setup_payload(email="other@example.com", password=password),
)
assert repeated_response.status_code == 403
db_session_with_containers.expire_all()
assert db_session_with_containers.scalar(select(func.count()).select_from(DifySetup)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(Account)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(Tenant)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(TenantAccountJoin)) == 1
account = db_session_with_containers.scalar(select(Account))
assert account is not None
assert account.email == "admin@example.com"
assert account.last_login_ip == "203.0.113.7"
def test_concurrent_setup_requests_create_only_one_bootstrap_identity(
flask_app_with_containers: Flask,
db_session_with_containers: Session,
monkeypatch: pytest.MonkeyPatch,
setup_dependencies: MagicMock,
) -> None:
monkeypatch.delenv("INIT_PASSWORD", raising=False)
password = generate_valid_password(Faker())
provision_started = Event()
allow_provision_to_finish = Event()
second_lock_attempted = Event()
attempt_guard = Lock()
lock_attempts = 0
original_setup = RegisterService.setup
original_acquire = RedisSetupLock.acquire
def blocking_setup(
email: str,
name: str,
password: str,
ip_address: str,
language: str | None,
*,
session: Session,
) -> None:
if not provision_started.is_set():
provision_started.set()
assert allow_provision_to_finish.wait(timeout=10)
original_setup(
email=email,
name=name,
password=password,
ip_address=ip_address,
language=language,
session=session,
)
def tracked_acquire(self: RedisSetupLock) -> AbstractContextManager[None]:
nonlocal lock_attempts
with attempt_guard:
lock_attempts += 1
if lock_attempts == 2:
second_lock_attempted.set()
return original_acquire(self)
def post_setup(email: str) -> int:
with flask_app_with_containers.test_client() as client:
response = client.post(
"/console/api/setup",
json=_setup_payload(email=email, password=password),
)
return response.status_code
with (
patch.object(RegisterService, "setup", side_effect=blocking_setup),
patch.object(RedisSetupLock, "acquire", tracked_acquire),
ThreadPoolExecutor(max_workers=2) as executor,
):
first_request = executor.submit(post_setup, "admin-1@example.com")
try:
assert provision_started.wait(timeout=10)
second_request = executor.submit(post_setup, "admin-2@example.com")
assert second_lock_attempted.wait(timeout=10)
assert not second_request.done()
finally:
allow_provision_to_finish.set()
results = [first_request.result(timeout=30), second_request.result(timeout=30)]
assert sorted(results) == [201, 403]
setup_dependencies.assert_called_once()
db_session_with_containers.expire_all()
assert db_session_with_containers.scalar(select(func.count()).select_from(DifySetup)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(Account)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(Tenant)) == 1
assert db_session_with_containers.scalar(select(func.count()).select_from(TenantAccountJoin)) == 1

View File

@@ -0,0 +1,37 @@
from flask.testing import FlaskClient
from sqlalchemy.orm import Session
from tests.test_containers_integration_tests.controllers.console.helpers import (
authenticate_console_client,
create_console_account_and_tenant,
)
def test_schema_definitions_endpoint_uses_admission_and_builtin_registry(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
) -> None:
account, _ = create_console_account_and_tenant(db_session_with_containers)
headers = authenticate_console_client(test_client_with_containers, account)
response = test_client_with_containers.get(
"/console/api/spec/schema-definitions",
headers=headers,
)
assert response.status_code == 200
definitions = response.get_json()
assert isinstance(definitions, list)
assert definitions
assert all({"name", "label", "schema"} <= definition.keys() for definition in definitions)
def test_schema_definitions_endpoint_rejects_unauthenticated_request(
db_session_with_containers: Session,
test_client_with_containers: FlaskClient,
) -> None:
create_console_account_and_tenant(db_session_with_containers)
response = test_client_with_containers.get("/console/api/spec/schema-definitions")
assert response.status_code == 401

View File

@@ -119,6 +119,29 @@ def test_request_builder_separates_agent_soul_and_workflow_job_prompt():
assert dumped["composition"]["layers"][2]["config"]["user"] == "Summarize the report."
def test_request_builder_forwards_plugin_specific_model_settings_via_extra_body():
run_input = _run_input().model_copy(
update={
"model": AgentBackendModelConfig(
plugin_id="langgenius/tongyi",
model_provider="tongyi",
model="qwen-plus-latest",
credentials={"api_key": "secret-key"},
model_settings={"temperature": 0.7, "enable_thinking": True, "thinking_budget": 4096},
)
}
)
request = AgentBackendRunRequestBuilder().build_for_workflow_node(run_input)
layers = {layer.name: layer for layer in request.composition.layers}
model_config = cast(DifyPluginLLMLayerConfig, layers[DIFY_AGENT_MODEL_LAYER_ID].config)
assert model_config.model_settings == {
"temperature": 0.7,
"extra_body": {"enable_thinking": True, "thinking_budget": 4096},
}
@pytest.mark.parametrize("agent_config_version_kind", ["snapshot", "draft"])
def test_agent_app_request_builder_keeps_agent_soul_prompt_for_snapshot_and_draft(
agent_config_version_kind: str,

View File

@@ -28,21 +28,29 @@ from controllers.console.agent.roster import (
AgentApiKeyApi,
AgentApiKeyListApi,
AgentApiStatusApi,
AgentApiStatusPayload,
AgentAppApi,
AgentAppCopyApi,
AgentAppCopyPayload,
AgentAppCreatePayload,
AgentAppListApi,
AgentAppUpdatePayload,
AgentBuildDraftApi,
AgentBuildDraftApplyApi,
AgentBuildDraftCheckoutApi,
AgentBuildDraftCheckoutPayload,
AgentDebugConversationRefreshApi,
AgentInviteOptionsApi,
AgentInviteOptionsQuery,
AgentLogMessagesApi,
AgentLogsApi,
AgentLogSourcesApi,
AgentPublishApi,
AgentPublishPayload,
AgentRosterVersionDetailApi,
AgentRosterVersionRestoreApi,
AgentRosterVersionsApi,
AgentStatisticsQuery,
AgentStatisticsSummaryApi,
)
from controllers.console.app import completion as completion_controller
@@ -59,7 +67,13 @@ from core.app.entities.app_invoke_entities import InvokeFrom
from models.agent import Agent, AgentConfigDraftType, AgentScope, AgentSource, AgentStatus
from models.enums import ConversationFromSource
from models.model import AppMode, Conversation, Message
from services.entities.agent_entities import ComposerSaveStrategy, ComposerVariant
from services.entities.agent_entities import (
ComposerSavePayload,
ComposerSaveStrategy,
ComposerVariant,
WorkflowAgentComposerQuery,
WorkflowComposerCopyFromRosterPayload,
)
def _persist_conversation_message(
@@ -430,7 +444,13 @@ def test_agent_app_list_and_create_use_agent_route(
json={"name": "Iris", "description": "Agent app", "role": "Coordinator", "icon_type": "emoji", "icon": "robot"},
):
created, status = unwrap(AgentAppListApi.post)(
AgentAppListApi(), MagicMock(), "tenant-1", SimpleNamespace(id=account_id)
AgentAppListApi(),
AgentAppCreatePayload(
name="Iris", description="Agent app", role="Coordinator", icon_type="emoji", icon="robot"
),
MagicMock(),
"tenant-1",
SimpleNamespace(id=account_id),
)
assert status == 201
assert created["id"] == "agent-created"
@@ -484,7 +504,13 @@ def test_agent_app_create_omits_optional_role_as_empty_string(
"/console/api/agent",
json={"name": "No-role Iris", "description": "Agent app", "icon_type": "emoji", "icon": "robot"},
):
created, status = unwrap(AgentAppListApi.post)(AgentAppListApi(), MagicMock(), "tenant-1", current_user)
created, status = unwrap(AgentAppListApi.post)(
AgentAppListApi(),
AgentAppCreatePayload(name="No-role Iris", description="Agent app", icon_type="emoji", icon="robot"),
MagicMock(),
"tenant-1",
current_user,
)
assert status == 201
assert created == {"id": "agent-created", "app_id": "app-created"}
create_call = cast(dict[str, object], captured["create"])
@@ -566,7 +592,14 @@ def test_agent_app_detail_update_delete_resolve_app_from_agent_id(
"/console/api/agent/00000000-0000-0000-0000-000000000001",
json={"name": "Renamed", "description": "", "role": "Reviewer", "icon_type": "emoji", "icon": "R"},
):
updated = unwrap(AgentAppApi.put)(AgentAppApi(), session, tenant_id, SimpleNamespace(id=account_id), agent_id)
updated = unwrap(AgentAppApi.put)(
AgentAppApi(),
AgentAppUpdatePayload(name="Renamed", description="", role="Reviewer", icon_type="emoji", icon="R"),
session,
tenant_id,
SimpleNamespace(id=account_id),
agent_id,
)
assert updated["name"] == "Renamed"
assert updated["id"] == agent_id
assert updated["app_id"] == app_id
@@ -615,7 +648,19 @@ def test_agent_app_copy_uses_agent_id_and_returns_agent_detail(
},
):
copied, status = unwrap(AgentAppCopyApi.post)(
AgentAppCopyApi(), MagicMock(), "tenant-1", current_user, agent_id
AgentAppCopyApi(),
AgentAppCopyPayload(
name="Iris copy",
description="Copied",
role="Copied role",
icon_type="emoji",
icon="sparkles",
icon_background="#fff",
),
MagicMock(),
"tenant-1",
current_user,
agent_id,
)
assert status == 201
assert copied == {"id": "copied-agent", "app_id": "copied-app", "name": "Iris"}
@@ -715,7 +760,14 @@ def test_agent_publish_and_build_draft_routes_call_composer_service(
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001/publish", json={"version_note": "publish v1"}
):
published = unwrap(AgentPublishApi.post)(AgentPublishApi(), MagicMock(), "tenant-1", current_user, agent_id)
published = unwrap(AgentPublishApi.post)(
AgentPublishApi(),
AgentPublishPayload(version_note="publish v1"),
MagicMock(),
"tenant-1",
current_user,
agent_id,
)
assert published["active_config_snapshot_id"] == "version-1"
captured["publish"].pop("session", None)
assert captured["publish"] == {
@@ -728,7 +780,12 @@ def test_agent_publish_and_build_draft_routes_call_composer_service(
"/console/api/agent/00000000-0000-0000-0000-000000000001/build-draft/checkout", json={"force": True}
):
checked_out = unwrap(AgentBuildDraftCheckoutApi.post)(
AgentBuildDraftCheckoutApi(), MagicMock(), "tenant-1", current_user, agent_id
AgentBuildDraftCheckoutApi(),
AgentBuildDraftCheckoutPayload(force=True),
MagicMock(),
"tenant-1",
current_user,
agent_id,
)
assert checked_out["draft"]["id"] == "build-draft-1"
captured["checkout"].pop("session", None)
@@ -747,7 +804,17 @@ def test_agent_publish_and_build_draft_routes_call_composer_service(
"/console/api/agent/00000000-0000-0000-0000-000000000001/build-draft",
json={"variant": "agent_app", "save_strategy": "save_to_current_version", "agent_soul": {}},
):
saved = unwrap(AgentBuildDraftApi.put)(AgentBuildDraftApi(), MagicMock(), "tenant-1", current_user, agent_id)
saved = unwrap(AgentBuildDraftApi.put)(
AgentBuildDraftApi(),
ComposerSavePayload(
variant=ComposerVariant.AGENT_APP,
save_strategy=ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
),
MagicMock(),
"tenant-1",
current_user,
agent_id,
)
assert saved["draft"]["id"] == "build-draft-1"
assert captured["save"]["tenant_id"] == "tenant-1"
assert captured["save"]["agent_id"] == agent_id
@@ -864,7 +931,9 @@ def test_agent_api_status_and_key_routes_resolve_backing_app(
with app.test_request_context(
"/console/api/agent/00000000-0000-0000-0000-000000000001/api-enable", json={"enable_api": True}
):
enabled = unwrap(AgentApiStatusApi.post)(AgentApiStatusApi(), unbound_session, "tenant-1", agent_id)
enabled = unwrap(AgentApiStatusApi.post)(
AgentApiStatusApi(), AgentApiStatusPayload(enable_api=True), unbound_session, "tenant-1", agent_id
)
assert enabled["enabled"] is True
assert captured["enable"] == {"app": app_model, "enable_api": True}
keys = unwrap(AgentApiKeyListApi.get)(AgentApiKeyListApi(), unbound_session, "tenant-1", agent_id)
@@ -948,7 +1017,12 @@ def test_agent_app_update_allows_empty_role(app: Flask, monkeypatch: pytest.Monk
json={"name": "Renamed", "description": "", "role": "", "icon_type": "emoji", "icon": "R"},
):
updated = unwrap(AgentAppApi.put)(
AgentAppApi(), MagicMock(), "tenant-1", SimpleNamespace(id="account-1"), agent_id
AgentAppApi(),
AgentAppUpdatePayload(name="Renamed", description="", role="", icon_type="emoji", icon="R"),
MagicMock(),
"tenant-1",
SimpleNamespace(id="account-1"),
agent_id,
)
assert updated["role"] == ""
update_call = cast(dict[str, object], captured["update"])
@@ -964,7 +1038,9 @@ def test_invite_options_get_parses_app_id(app: Flask, monkeypatch: pytest.Monkey
monkeypatch.setattr(roster_controller.AgentRosterService, "list_invite_options", list_invite_options)
with app.test_request_context("/console/api/agent/invite-options?page=1&limit=10&app_id=app-1"):
result = unwrap(AgentInviteOptionsApi.get)(AgentInviteOptionsApi(), MagicMock(), "tenant-1")
result = unwrap(AgentInviteOptionsApi.get)(
AgentInviteOptionsApi(), AgentInviteOptionsQuery(page=1, limit=10, app_id="app-1"), MagicMock(), "tenant-1"
)
assert result == {"data": [], "page": 1, "limit": 10, "total": 0, "has_more": False}
assert captured == {"tenant_id": "tenant-1", "page": 1, "limit": 10, "keyword": None, "app_id": "app-1"}
@@ -1201,7 +1277,7 @@ def test_agent_observability_routes_resolve_app_from_agent_id(
"/console/api/agent/00000000-0000-0000-0000-000000000001/statistics/summary?source=api"
):
statistics = unwrap(AgentStatisticsSummaryApi.get)(
AgentStatisticsSummaryApi(), MagicMock(), "tenant-1", account, agent_id
AgentStatisticsSummaryApi(), AgentStatisticsQuery(source="api"), MagicMock(), "tenant-1", account, agent_id
)
assert statistics["summary"]["total_messages"] == 1
stats_call = cast(dict[str, object], captured["statistics"])
@@ -1253,18 +1329,40 @@ def test_workflow_composer_get_put_validate_candidates_impact_and_save(
)
with app.test_request_context("?snapshot_id=preview-version"):
workflow_state = unwrap(WorkflowAgentComposerApi.get)(
WorkflowAgentComposerApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1"
WorkflowAgentComposerApi(),
WorkflowAgentComposerQuery(snapshot_id="preview-version"),
MagicMock(),
"tenant-1",
account_id,
app_model,
"node-1",
)
assert workflow_state["node_id"] == "node-1"
assert captured_load["account_id"] == account_id
assert captured_load["snapshot_id"] == "preview-version"
composer_save_payload = ComposerSavePayload(
variant=ComposerVariant.WORKFLOW,
save_strategy=ComposerSaveStrategy.NODE_JOB_ONLY,
binding={"binding_type": "roster_agent", "current_snapshot_id": "version-1"},
)
with app.test_request_context(json=payload):
saved_state = unwrap(WorkflowAgentComposerApi.put)(
WorkflowAgentComposerApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1"
WorkflowAgentComposerApi(),
composer_save_payload,
MagicMock(),
"tenant-1",
account_id,
app_model,
"node-1",
)
assert saved_state["save_options"] == ["node_job_only"]
assert unwrap(WorkflowAgentComposerValidateApi.post)(
WorkflowAgentComposerValidateApi(), MagicMock(), "tenant-1", app_model, "node-1"
WorkflowAgentComposerValidateApi(),
composer_save_payload,
MagicMock(),
"tenant-1",
app_model,
"node-1",
) == {"result": "success", "errors": [], "warnings": [], "knowledge_retrieval_placeholder": []}
assert (
unwrap(WorkflowAgentComposerCandidatesApi.get)(
@@ -1274,10 +1372,21 @@ def test_workflow_composer_get_put_validate_candidates_impact_and_save(
)
with app.test_request_context(json=payload):
assert unwrap(WorkflowAgentComposerImpactApi.post)(
WorkflowAgentComposerImpactApi(), MagicMock(), "tenant-1", app_model, "node-1"
WorkflowAgentComposerImpactApi(),
composer_save_payload,
MagicMock(),
"tenant-1",
app_model,
"node-1",
) == {"current_snapshot_id": "version-1", "workflow_node_count": 1, "bindings": []}
assert unwrap(WorkflowAgentComposerSaveToRosterApi.post)(
WorkflowAgentComposerSaveToRosterApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1"
WorkflowAgentComposerSaveToRosterApi(),
composer_save_payload,
MagicMock(),
"tenant-1",
account_id,
app_model,
"node-1",
)["save_options"] == ["node_job_only"]
@@ -1325,7 +1434,17 @@ def test_workflow_composer_copy_from_roster(app: Flask, monkeypatch: pytest.Monk
}
):
result = unwrap(WorkflowAgentComposerCopyFromRosterApi.post)(
WorkflowAgentComposerCopyFromRosterApi(), MagicMock(), "tenant-1", account_id, app_model, "node-1"
WorkflowAgentComposerCopyFromRosterApi(),
WorkflowComposerCopyFromRosterPayload(
source_agent_id="roster-agent-1",
source_snapshot_id="roster-version-1",
idempotency_key="copy-1",
),
MagicMock(),
"tenant-1",
account_id,
app_model,
"node-1",
)
assert result["binding"]["binding_type"] == "inline_agent"
captured.pop("session", None)
@@ -1344,7 +1463,15 @@ def test_workflow_impact_returns_empty_without_version(app: Flask) -> None:
payload = {"variant": ComposerVariant.WORKFLOW.value, "save_strategy": ComposerSaveStrategy.NODE_JOB_ONLY.value}
with app.test_request_context(json=payload):
result = unwrap(WorkflowAgentComposerImpactApi.post)(
WorkflowAgentComposerImpactApi(), MagicMock(), "tenant-1", SimpleNamespace(id="app-1"), "node-1"
WorkflowAgentComposerImpactApi(),
ComposerSavePayload(
variant=ComposerVariant.WORKFLOW,
save_strategy=ComposerSaveStrategy.NODE_JOB_ONLY,
),
MagicMock(),
"tenant-1",
SimpleNamespace(id="app-1"),
"node-1",
)
assert result == {"current_snapshot_id": None, "workflow_node_count": 0, "bindings": []}
@@ -1387,12 +1514,21 @@ def test_agent_composer_routes_resolve_app_from_agent_id(
assert composer["variant"] == "agent_app"
assert composer["active_config_is_published"] is True
assert cast(dict[str, object], captured["load"])["agent_id"] == agent_id
composer_save_payload = ComposerSavePayload(
variant=ComposerVariant.AGENT_APP,
save_strategy=ComposerSaveStrategy.SAVE_TO_CURRENT_VERSION,
agent_soul={"prompt": {"system_prompt": "x"}},
)
with app.test_request_context(json=payload):
saved_composer = unwrap(AgentComposerApi.put)(AgentComposerApi(), MagicMock(), "tenant-1", account_id, agent_id)
saved_composer = unwrap(AgentComposerApi.put)(
AgentComposerApi(), composer_save_payload, MagicMock(), "tenant-1", account_id, agent_id
)
assert saved_composer["variant"] == "agent_app"
assert saved_composer["active_config_is_published"] is True
assert cast(dict[str, object], captured["save"])["agent_id"] == agent_id
assert unwrap(AgentComposerValidateApi.post)(AgentComposerValidateApi(), MagicMock(), "tenant-1", agent_id) == {
assert unwrap(AgentComposerValidateApi.post)(
AgentComposerValidateApi(), composer_save_payload, MagicMock(), "tenant-1", agent_id
) == {
"result": "success",
"errors": [],
"warnings": [],

View File

@@ -8,7 +8,7 @@ from __future__ import annotations
from inspect import unwrap
from types import SimpleNamespace
from unittest.mock import MagicMock, PropertyMock, patch
from unittest.mock import MagicMock, patch
from flask import Flask
from sqlalchemy import event
@@ -28,7 +28,6 @@ from controllers.console.app.agent_config_inspector import (
AgentConfigSkillInspectByAgentApi,
AgentConfigSkillsApi,
AgentConfigSkillUploadByAgentApi,
console_ns,
)
from services.agent_config_service import AgentConfigServiceError
@@ -212,13 +211,10 @@ def test_skill_upload_by_agent_delegates_after_version_resolution():
def test_file_upload_by_agent_delegates_to_service_owned_upload_lookup():
raw = _raw(AgentConfigFilesByAgentApi.post)
with app.test_request_context("/?draft_type=debug_build"):
with app.test_request_context("/?draft_type=debug_build", json={"upload_file_id": "upload-1"}):
with (
patch(f"{_MOD}.resolve_agent_runtime_app_model", return_value=_APP),
patch(f"{_MOD}.AgentComposerService") as composer,
patch.object(
type(console_ns), "payload", new_callable=PropertyMock, return_value={"upload_file_id": "upload-1"}
),
patch(f"{_MOD}.AgentConfigService") as config_service,
):
composer.load_agent_app_build_draft.return_value = {"draft": {"id": "build-draft-1"}}
@@ -226,7 +222,14 @@ def test_file_upload_by_agent_delegates_to_service_owned_upload_lookup():
"file": {"id": "guide.txt", "name": "guide.txt", "file_id": "upload-1"},
"config_version": {"id": "build-draft-1", "kind": "build_draft", "writable": True},
}
body, status = raw(AgentConfigFilesByAgentApi(), MagicMock(), "tenant-1", _USER, "agent-1")
body, status = raw(
AgentConfigFilesByAgentApi(),
inspector.AgentConfigFileUploadPayload(upload_file_id="upload-1"),
MagicMock(),
"tenant-1",
_USER,
"agent-1",
)
assert status == 201
assert body["file"]["name"] == "guide.txt"
assert config_service.return_value.push_file_for_console.call_args.kwargs["upload_file_id"] == "upload-1"

View File

@@ -163,12 +163,10 @@ class TestConsoleAnnotationRefBoundaries:
api = annotation_module.AnnotationUpdateDeleteApi()
handler = unwrap(api.post)
update_mock = Mock(return_value=_annotation_model())
payload = {"question": "updated"}
_persist_app(sqlite_session)
with (
app.test_request_context("/annotations/ann-1", method="POST", json=payload),
patch.object(type(annotation_module.console_ns), "payload", payload),
app.test_request_context("/annotations/ann-1", method="POST", json={"question": "updated"}),
patch.object(
annotation_module,
"current_account_with_tenant",
@@ -176,7 +174,13 @@ class TestConsoleAnnotationRefBoundaries:
),
patch.object(annotation_module.AppAnnotationService, "update_app_annotation_directly", update_mock),
):
response = handler(api, sqlite_session, "app-1", "ann-1")
response = handler(
api,
annotation_module.UpdateAnnotationPayload(question="updated"),
sqlite_session,
"app-1",
"ann-1",
)
assert response["question"] == "q"
update_mock.assert_called_once()

View File

@@ -59,6 +59,7 @@ from controllers.console.app.workflow import AdvancedChatWorkflowRunPayload, Syn
from controllers.console.app.workflow_app_log import WorkflowAppLogQuery
from controllers.console.app.workflow_draft_variable import (
EnvironmentVariableUpdatePayload,
WorkflowDraftVariableListQuery,
WorkflowDraftVariableUpdatePayload,
)
from controllers.console.app.workflow_statistic import WorkflowStatisticQuery
@@ -144,7 +145,13 @@ class TestCompletionEndpoints:
Session(sqlite_engine) as session,
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
):
resp = method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
resp = method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
assert resp == {"result": {"text": "ok"}}
@@ -167,7 +174,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(NotFound),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
def test_completion_api_provider_not_initialized(
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine
@@ -186,7 +199,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(completion_module.ProviderNotInitializeError),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
def test_completion_api_quota_exceeded(
self, app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_engine: Engine
@@ -205,7 +224,13 @@ class TestCompletionEndpoints:
app.test_request_context("/", json={"inputs": {}, "model_config": {}, "query": "hi"}),
pytest.raises(completion_module.ProviderQuotaExceededError),
):
method(api, session, _make_account(), app_model=MagicMock(id=APP_ID))
method(
api,
CompletionMessagePayload(inputs={}, model_config={}, query="hi"),
session,
_make_account(),
app_model=MagicMock(id=APP_ID),
)
class TestAppEndpoints:
@@ -232,7 +257,17 @@ class TestAppEndpoints:
app.test_request_context("/console/api/apps/app-1", method="PUT", json=payload),
patch.object(type(console_ns), "payload", payload),
):
response = method(api, unbound_session, app_model=_make_app(icon_type=app_module.IconType.EMOJI))
response = method(
api,
app_module.UpdateAppPayload(
name="Updated App",
description="Updated description",
icon="🤖",
icon_background="#FFFFFF",
),
unbound_session,
app_model=_make_app(icon_type=app_module.IconType.EMOJI),
)
assert response == {"id": "app-1"}
assert app_service.update_app.call_args.args[1]["icon_type"] is None
@@ -271,7 +306,16 @@ class TestAppEndpoints:
app.test_request_context("/console/api/apps/app-1/icon", method="POST", json=payload),
patch.object(type(console_ns), "payload", payload),
):
response = method(api, unbound_session, app_model=_make_app())
response = method(
api,
app_module.AppIconPayload(
icon="https://example.com/icon.png",
icon_type=app_module.IconType.IMAGE,
icon_background="#FFFFFF",
),
unbound_session,
app_model=_make_app(),
)
assert response == {"id": "app-1"}
assert app_service.update_app_icon.call_args.args[1:] == (
@@ -301,7 +345,7 @@ class TestOpsTraceEndpoints:
)
with app.test_request_context("/?tracing_provider=langfuse"):
result = method(api, app_model=MagicMock(id="app-1"))
result = method(api, TraceProviderQuery(tracing_provider="langfuse"), MagicMock(id="app-1"))
assert result == {"has_not_configured": True}
@@ -320,7 +364,11 @@ class TestOpsTraceEndpoints:
json={"tracing_provider": "langfuse", "tracing_config": {"api_key": "k"}},
):
with pytest.raises(BadRequest):
method(api, app_model=MagicMock(id="app-1"))
method(
api,
TraceConfigPayload(tracing_provider="langfuse", tracing_config={"api_key": "k"}),
MagicMock(id="app-1"),
)
def test_trace_app_config_delete_not_found(self, app: Flask, monkeypatch: pytest.MonkeyPatch):
api = ops_trace_module.TraceAppConfigApi()
@@ -334,7 +382,7 @@ class TestOpsTraceEndpoints:
with app.test_request_context("/?tracing_provider=langfuse"):
with pytest.raises(BadRequest):
method(api, app_model=MagicMock(id="app-1"))
method(api, TraceProviderQuery(tracing_provider="langfuse"), MagicMock(id="app-1"))
class TestSiteEndpoints:
@@ -374,7 +422,13 @@ class TestSiteEndpoints:
site = self._add_site(db.session)
with database_app.test_request_context("/", json={"title": "My Site", "input_placeholder": "Ask me anything"}):
result = method(api, db.session, _make_account(), app_model=_make_app())
result = method(
api,
AppSiteUpdatePayload(title="My Site", input_placeholder="Ask me anything"),
db.session,
_make_account(),
app_model=_make_app(),
)
db.session.refresh(site)
assert isinstance(result, dict)
@@ -441,7 +495,7 @@ class TestWorkflowAppLogEndpoints:
)
with database_app.test_request_context("/?page=1&limit=20"):
result = method(api, app_model=_make_app("app-1"))
result = method(api, WorkflowAppLogQuery(page=1, limit=20), app_model=_make_app("app-1"))
assert result == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}
@@ -471,7 +525,12 @@ class TestWorkflowDraftVariableEndpoints:
monkeypatch.setattr(workflow_draft_variable_module, "WorkflowService", DummyWorkflowService)
with database_app.test_request_context("/?page=1&limit=20"):
result = method(api, _make_account(), app_model=_make_app("app-1"))
result = method(
api,
WorkflowDraftVariableListQuery(page=1, limit=20),
_make_account(),
app_model=_make_app("app-1"),
)
assert result == {"items": [], "total": 0}
@@ -524,7 +583,16 @@ class TestWorkflowDraftVariableEndpoints:
"deleted_environment_variable_ids": ["env-b"],
},
):
result = method(api, _make_account(), app_model=_make_app())
result = method(
api,
EnvironmentVariableUpdatePayload(
environment_variables=[{"id": "env-a", "name": "a", "value_type": "string", "value": "new-a"}],
patch=True,
deleted_environment_variable_ids=["env-b"],
),
_make_account(),
app_model=_make_app(),
)
assert result == {"result": "success"}
assert [(variable.id, variable.value) for variable in captured["environment_variables"]] == [("env-a", "new-a")]
@@ -570,7 +638,7 @@ class TestWorkflowStatisticEndpoints:
with database_app.test_request_context("/"):
account = _make_account()
account.timezone = "UTC"
response = method(api, account, app_model=_make_app("app-1", tenant_id="t1"))
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
assert response.get_json() == {"data": [{"date": "2024-01-01"}]}
@@ -600,7 +668,7 @@ class TestWorkflowStatisticEndpoints:
with database_app.test_request_context("/"):
account = _make_account()
account.timezone = "UTC"
response = method(api, account, app_model=_make_app("app-1", tenant_id="t1"))
response = method(api, WorkflowStatisticQuery(), account, app_model=_make_app("app-1", tenant_id="t1"))
assert response.get_json() == {"data": [{"date": "2024-01-02"}]}
@@ -630,7 +698,7 @@ class TestWorkflowTriggerEndpoints:
db.session.commit()
with database_app.test_request_context("/?node_id=node-1"):
result = method(api, app_model=_make_app())
result = method(api, Parser(node_id="node-1"), app_model=_make_app())
assert isinstance(result, dict)
assert {"id", "webhook_id", "webhook_url", "webhook_debug_url", "node_id", "created_at"} <= set(result.keys())

View File

@@ -159,7 +159,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.rollbacks == 1
assert transaction_events.commits == 0
@@ -185,7 +185,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.commits == 1
assert transaction_events.rollbacks == 0
@@ -213,7 +213,7 @@ class TestAppImportApi:
monkeypatch.setattr(app_import_module.EnterpriseService.WebAppAuth, "update_app_access_mode", update_access)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method(api, _make_account())
response, status = method(api, app_import_module.AppImportPayload(mode="yaml-content"), _make_account())
assert transaction_events.commits == 1
assert transaction_events.rollbacks == 0
@@ -251,7 +251,7 @@ class TestAppImportApi:
)
with app.test_request_context("/console/api/apps/imports", method="POST", json={"mode": "yaml-content"}):
response, status = method()
response, status = method(app_import_module.AppImportPayload(mode="yaml-content"))
assert transaction_events.commits == 1
_assert_app_persistence(sqlite_app_engine, app_id, persisted=True)
@@ -291,7 +291,7 @@ class TestAppImportApi:
method="POST",
json={"mode": "yaml-content", "app_id": "existing-app"},
):
response, status = method()
response, status = method(app_import_module.AppImportPayload(mode="yaml-content", app_id="existing-app"))
assert transaction_events.commits == 1
_assert_app_persistence(sqlite_app_engine, app_id, persisted=True)

View File

@@ -331,19 +331,9 @@ def test_app_list_query_accepts_single_repeated_tag_id(app_module):
assert query.tag_ids == [tag_id]
def test_create_app_endpoint_rejects_agent_mode(app_module, monkeypatch: pytest.MonkeyPatch, unbound_session: Session):
payload = {"name": "Iris", "mode": "agent", "description": "Agent app"}
app_service = MagicMock()
monkeypatch.setattr(app_module, "AppService", lambda: app_service)
app_module.console_ns.payload = payload
try:
with pytest.raises(ValidationError):
_unwrap(app_module.AppListApi().post)(unbound_session, "tenant-1", SimpleNamespace(id="account-1"))
finally:
app_module.console_ns.payload = None
app_service.create_app.assert_not_called()
def test_create_app_endpoint_rejects_agent_mode(app_module):
with pytest.raises(ValidationError):
app_module.CreateAppPayload.model_validate({"name": "Iris", "mode": "agent", "description": "Agent app"})
def test_app_partial_serialization_uses_aliases(app_models):
@@ -650,7 +640,17 @@ def test_app_create_api_attaches_permission_keys(app, app_module, unbound_sessio
replace_whitelist,
)
resp, status = method(app_module.AppListApi(), unbound_session, "tenant-1", SimpleNamespace(id="acct-1"))
resp, status = method(
app_module.AppListApi(),
app_module.CreateAppPayload(
name="Created App",
description="Summary",
mode="advanced-chat",
),
unbound_session,
"tenant-1",
SimpleNamespace(id="acct-1"),
)
assert status == 201
assert resp["permission_keys"] == ["app.acl.view_layout", "app.acl.edit"]
@@ -1076,6 +1076,7 @@ def test_app_copy_api_attaches_permission_keys(app, app_module, sqlite_session:
resp, status = method(
app_module.AppCopyApi(),
app_module.CopyAppPayload(),
"tenant-1",
SimpleNamespace(id="acct-1"),
app_model=SimpleNamespace(id="app-original"),

View File

@@ -17,6 +17,8 @@ from controllers.console.app.audio import (
ChatMessageAudioApi,
ChatMessageTextApi,
TextModesApi,
TextToSpeechPayload,
TextToSpeechVoiceQuery,
)
from controllers.console.app.error import (
AppUnavailableError,
@@ -290,7 +292,7 @@ def test_console_text_api_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -
method="POST",
json={"text": "hello", "voice": "v"},
):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "ok"}
@@ -315,7 +317,7 @@ def test_console_text_api_builds_message_ref(app: Flask, monkeypatch: pytest.Mon
),
patch("controllers.console.app.audio.current_user", SimpleNamespace(id="account-1")),
):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechPayload(text="hello", message_id="message-1"), app_model=app_model)
assert response == {"audio": "ok"}
assert calls["message_ref"] == MessageRef(AppRef("tenant-1", "app-1"), "message-1", account_id="account-1")
@@ -334,7 +336,7 @@ def test_console_text_api_error_mapping(app: Flask, monkeypatch: pytest.MonkeyPa
json={"text": "hello"},
):
with pytest.raises(ProviderQuotaExceededError):
handler(api, app_model=app_model)
handler(api, TextToSpeechPayload(text="hello"), app_model=app_model)
def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -346,7 +348,7 @@ def test_console_text_modes_success(app: Flask, monkeypatch: pytest.MonkeyPatch)
app_model = SimpleNamespace(tenant_id="t1")
with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"):
response = handler(api, app_model=app_model)
response = handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert response == expected_voices
@@ -364,7 +366,7 @@ def test_console_text_modes_language_error(app: Flask, monkeypatch: pytest.Monke
with app.test_request_context("/console/api/apps/app/text-to-audio/voices?language=en", method="GET"):
with pytest.raises(AppUnavailableError):
handler(api, app_model=app_model)
handler(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
def test_audio_to_text_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -424,7 +426,7 @@ def test_text_to_audio_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N
method="POST",
json={"text": "hello"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "ok"}
@@ -443,7 +445,7 @@ def test_text_to_audio_voices_success(app: Flask, monkeypatch: pytest.MonkeyPatc
method="GET",
query_string={"language": "en-US"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert response == expected_voices
@@ -481,7 +483,7 @@ def test_text_to_audio_with_language_param(app: Flask, monkeypatch: pytest.Monke
method="POST",
json={"text": "hello", "language": "en-US"},
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechPayload(text="hello"), app_model=app_model)
assert response == {"audio": "test"}
@@ -501,5 +503,5 @@ def test_text_to_audio_voices_with_language_filter(app: Flask, monkeypatch: pyte
"/console/api/apps/app-1/text-to-audio/voices?language=en-US",
method="GET",
):
response = method(api, app_model=app_model)
response = method(api, TextToSpeechVoiceQuery(language="en-US"), app_model=app_model)
assert isinstance(response, list)

View File

@@ -58,7 +58,13 @@ def test_completion_conversation_list_returns_paginated_result(
paginate_result.items = []
monkeypatch.setattr(conversation_module, "paginate_query", lambda *_args, **_kwargs: paginate_result)
with app.test_request_context("/console/api/apps/app-1/completion-conversations", method="GET"):
response = method(api, unbound_session, account, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_module.CompletionConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1"),
)
assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}
@@ -77,7 +83,13 @@ def test_completion_conversation_list_invalid_time_range(
"/console/api/apps/app-1/completion-conversations", method="GET", query_string={"start": "bad"}
):
with pytest.raises(BadRequest):
method(api, unbound_session, account, app_model=SimpleNamespace(id="app-1"))
method(
api,
conversation_module.CompletionConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1"),
)
def test_chat_conversation_list_advanced_chat_calls_paginate(
@@ -96,7 +108,11 @@ def test_chat_conversation_list_advanced_chat_calls_paginate(
monkeypatch.setattr(conversation_module, "paginate_query", lambda *_args, **_kwargs: paginate_result)
with app.test_request_context("/console/api/apps/app-1/chat-conversations", method="GET"):
response = method(
api, unbound_session, account, app_model=SimpleNamespace(id="app-1", mode=AppMode.ADVANCED_CHAT)
api,
conversation_module.ChatConversationQuery(),
unbound_session,
account,
app_model=SimpleNamespace(id="app-1", mode=AppMode.ADVANCED_CHAT),
)
assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []}

View File

@@ -51,7 +51,11 @@ def test_get_conversation_variables_returns_paginated_response(
method="GET",
query_string={"conversation_id": "conv-1"},
):
response = method(api, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"),
app_model=SimpleNamespace(id="app-1"),
)
assert response["page"] == 1
assert response["limit"] == 100
@@ -90,7 +94,11 @@ def test_get_conversation_variables_normalizes_value_type_and_value(
method="GET",
query_string={"conversation_id": "conv-1"},
):
response = method(api, app_model=SimpleNamespace(id="app-1"))
response = method(
api,
conversation_variables_module.ConversationVariablesQuery(conversation_id="conv-1"),
app_model=SimpleNamespace(id="app-1"),
)
assert response["data"][0]["value_type"] == "number"
assert response["data"][0]["value"] == "42"
@@ -102,4 +110,4 @@ def test_get_conversation_variables_requires_conversation_id(app) -> None:
with app.test_request_context("/console/api/apps/app-1/conversation-variables", method="GET"):
with pytest.raises(ValidationError):
method(api, app_model=SimpleNamespace(id="app-1"))
conversation_variables_module.ConversationVariablesQuery.model_validate({})

View File

@@ -6,11 +6,19 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from flask import Flask
from flask import Flask, request
from sqlalchemy.orm import Session
from controllers.console.app import generator as generator_module
from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.app.generator import (
InstructionGeneratePayload,
InstructionTemplatePayload,
RuleCodeGeneratePayload,
RuleGeneratePayload,
WorkflowGeneratePayload,
WorkflowInstructionSuggestionsPayload,
)
from core.errors.error import ProviderTokenNotInitError
from models.model import App, AppMode
@@ -61,7 +69,7 @@ def test_rule_generate_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> N
method="POST",
json={"instruction": "do it", "model_config": _model_config_payload()},
):
response = method(api, "t1")
response = method(api, RuleGeneratePayload.model_validate(request.get_json()), "t1")
assert response == {"rules": []}
@@ -81,7 +89,7 @@ def test_rule_code_generate_maps_token_error(app: Flask, monkeypatch: pytest.Mon
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(ProviderNotInitializeError):
method(api, "t1")
method(api, RuleCodeGeneratePayload.model_validate(request.get_json()), "t1")
@pytest.mark.parametrize("sqlite_session", [(App,)], indirect=True)
@@ -100,7 +108,9 @@ def test_instruction_generate_app_not_found(app: Flask, sqlite_session: Session)
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "app app-1 not found"
@@ -127,7 +137,9 @@ def test_instruction_generate_workflow_not_found(
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "workflow app-1 not found"
@@ -155,7 +167,9 @@ def test_instruction_generate_node_missing(
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "node node-1 not found"
@@ -188,7 +202,7 @@ def test_instruction_generate_code_node(app: Flask, monkeypatch: pytest.MonkeyPa
"model_config": _model_config_payload(),
},
):
response = method(api, sqlite_session, "t1")
response = method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")
assert response == {"code": "x"}
assert workflow_service.app_model is app_model
@@ -218,7 +232,7 @@ def test_instruction_generate_legacy_modify(
"model_config": _model_config_payload(),
},
):
response = method(api, sqlite_session, "t1")
response = method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")
assert response == {"instruction": "ok"}
@@ -238,7 +252,9 @@ def test_instruction_generate_incompatible_params(app: Flask, sqlite_session: Se
"model_config": _model_config_payload(),
},
):
response, status = method(api, sqlite_session, "t1")
response, status = method(
api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1"
)
assert status == 400
assert response["error"] == "incompatible parameters"
@@ -253,7 +269,7 @@ def test_instruction_template_prompt(app: Flask) -> None:
method="POST",
json={"type": "prompt"},
):
response = method(api)
response = method(api, InstructionTemplatePayload.model_validate(request.get_json()))
assert "data" in response
@@ -268,7 +284,7 @@ def test_instruction_template_invalid_type(app: Flask) -> None:
json={"type": "unknown"},
):
with pytest.raises(ValueError):
method(api)
method(api, InstructionTemplatePayload.model_validate(request.get_json()))
# ─ /workflow-generate ─────────────────────────────────────────────────────────
@@ -331,7 +347,7 @@ def test_workflow_generate_returns_service_result(app: Flask, monkeypatch: pytes
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert response == expected
@@ -361,7 +377,7 @@ def test_workflow_generate_maps_provider_token_error(app: Flask, monkeypatch: py
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderNotInitializeError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_quota_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -379,7 +395,7 @@ def test_workflow_generate_maps_quota_error(app: Flask, monkeypatch: pytest.Monk
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_model_not_support_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -397,7 +413,7 @@ def test_workflow_generate_maps_model_not_support_error(app: Flask, monkeypatch:
json=_workflow_generate_payload(),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_maps_invoke_error(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -415,7 +431,7 @@ def test_workflow_generate_maps_invoke_error(app: Flask, monkeypatch: pytest.Mon
json=_workflow_generate_payload(),
):
with pytest.raises(CompletionRequestError):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
def test_workflow_generate_accepts_advanced_chat_mode(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -442,7 +458,7 @@ def test_workflow_generate_accepts_advanced_chat_mode(app: Flask, monkeypatch: p
method="POST",
json=payload,
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["mode"] == "advanced-chat"
assert captured["instruction"] == "Summarize a URL"
@@ -474,7 +490,7 @@ def test_workflow_generate_forwards_current_graph_for_refine(app: Flask, monkeyp
method="POST",
json=payload,
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["current_graph"] == graph
@@ -501,7 +517,7 @@ def test_workflow_generate_current_graph_defaults_to_none(app: Flask, monkeypatc
method="POST",
json=_workflow_generate_payload(),
):
method(api, "t1")
method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["current_graph"] is None
@@ -528,7 +544,7 @@ def test_workflow_generate_accepts_auto_mode(app: Flask, monkeypatch: pytest.Mon
payload = _workflow_generate_payload()
payload["mode"] = "auto"
with app.test_request_context("/console/api/workflow-generate", method="POST", json=payload):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert captured["mode"] == "auto"
assert response["mode"] == "advanced-chat"
@@ -608,7 +624,7 @@ def test_workflow_instruction_suggestions_route_returns_list(app: Flask, monkeyp
method="POST",
json={"mode": "workflow", "language": "French", "count": 3},
):
response = method(api, "t1")
response = method(api, WorkflowInstructionSuggestionsPayload.model_validate(request.get_json()), "t1")
assert response == {"suggestions": ["Summarize a URL", "Translate text"]}
assert captured["mode"] == "workflow"
@@ -632,7 +648,7 @@ def test_workflow_instruction_suggestions_route_empty_is_valid_200(app: Flask, m
method="POST",
json={"mode": "advanced-chat"},
):
response = method(api, "t1")
response = method(api, WorkflowInstructionSuggestionsPayload.model_validate(request.get_json()), "t1")
assert response == {"suggestions": []}
@@ -667,7 +683,7 @@ def test_workflow_generate_stream_emits_plan_then_result(app: Flask, monkeypatch
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert response.mimetype == "text/event-stream"
frames = _read_sse_frames(response)
@@ -694,7 +710,7 @@ def test_workflow_generate_stream_provider_error_emits_result_event(
method="POST",
json=_workflow_generate_payload(),
):
response = method(api, "t1")
response = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
frames = _read_sse_frames(response)
assert len(frames) == 1
@@ -711,7 +727,7 @@ def test_workflow_generate_stream_rejects_empty_instruction(app: Flask, monkeypa
payload = _workflow_generate_payload()
payload["instruction"] = " "
with app.test_request_context("/console/api/workflow-generate/stream", method="POST", json=payload):
response, status = method(api, "t1")
response, status = method(api, WorkflowGeneratePayload.model_validate(request.get_json()), "t1")
assert status == 400
assert response["errors"][0]["code"] == "EMPTY_INSTRUCTION"

View File

@@ -1,8 +1,14 @@
import pytest
from flask import Flask
from flask import Flask, request
from sqlalchemy.orm import Session
from controllers.console.app import generator as generator_module
from controllers.console.app.generator import (
InstructionGeneratePayload,
RuleCodeGeneratePayload,
RuleGeneratePayload,
RuleStructuredOutputPayload,
)
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from graphon.model_runtime.errors.invoke import InvokeError
@@ -47,7 +53,7 @@ def test_rule_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleGeneratePayload.model_validate(request.get_json()), "t1")
def test_rule_code_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -73,7 +79,7 @@ def test_rule_code_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPat
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleCodeGeneratePayload.model_validate(request.get_json()), "t1")
def test_structured_output_generate_exceptions(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -100,7 +106,7 @@ def test_structured_output_generate_exceptions(app: Flask, monkeypatch: pytest.M
json={"instruction": "do it", "model_config": _model_config_payload()},
):
with pytest.raises(expected_exception):
method(api, "t1")
method(api, RuleStructuredOutputPayload.model_validate(request.get_json()), "t1")
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
@@ -138,4 +144,4 @@ def test_instruction_generate_exceptions(
},
):
with pytest.raises(expected_exception):
method(api, sqlite_session, "t1")
method(api, InstructionGeneratePayload.model_validate(request.get_json()), sqlite_session, "t1")

View File

@@ -53,7 +53,12 @@ def test_daily_message_statistic_returns_rows(app: Flask, monkeypatch: pytest.Mo
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-01", "message_count": 3}]}
@@ -67,7 +72,12 @@ def test_daily_conversation_statistic_returns_rows(app: Flask, monkeypatch: pyte
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]}
@@ -81,7 +91,12 @@ def test_daily_token_cost_statistic_returns_rows(app: Flask, monkeypatch: pytest
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 1
@@ -99,7 +114,12 @@ def test_daily_terminals_statistic_returns_rows(app: Flask, monkeypatch: pytest.
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-end-users", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-04", "terminal_count": 7}]}
@@ -126,7 +146,12 @@ def test_daily_message_statistic_with_invalid_time_range(app: Flask, monkeypatch
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
with pytest.raises(BadRequest):
method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -142,7 +167,12 @@ def test_daily_message_statistic_multiple_rows(app: Flask, monkeypatch: pytest.M
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 3
@@ -156,7 +186,12 @@ def test_daily_message_statistic_empty_result(app: Flask, monkeypatch: pytest.Mo
_install_db(monkeypatch, [])
with app.test_request_context("/console/api/apps/app-1/statistics/daily-messages", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": []}
@@ -175,7 +210,12 @@ def test_daily_conversation_statistic_with_time_range(app: Flask, monkeypatch: p
monkeypatch.setattr(statistic_module, "convert_datetime_to_date", lambda field: field)
with app.test_request_context("/console/api/apps/app-1/statistics/daily-conversations", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
assert _json_payload(response) == {"data": [{"date": "2024-01-02", "conversation_count": 5}]}
@@ -192,7 +232,12 @@ def test_daily_token_cost_with_multiple_currencies(app: Flask, monkeypatch: pyte
_install_db(monkeypatch, rows)
with app.test_request_context("/console/api/apps/app-1/statistics/token-costs", method="GET"):
response = method(api, SimpleNamespace(timezone="UTC"), app_model=SimpleNamespace(id="app-1"))
response = method(
api,
SimpleNamespace(start=None, end=None),
SimpleNamespace(timezone="UTC"),
app_model=SimpleNamespace(id="app-1"),
)
data = _json_payload(response)
assert len(data["data"]) == 2

View File

@@ -98,7 +98,11 @@ def test_workflow_run_list_returns_frontend_history_contract(app: Flask, monkeyp
handler = unwrap(api.get)
with app.test_request_context("/apps/app-1/workflow-runs?limit=10", method="GET"):
payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"))
payload = handler(
api,
workflow_run_module.WorkflowRunListQuery(limit=10),
app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"),
)
response = _serialize_200_response(api.get, payload)
@@ -139,7 +143,11 @@ def test_advanced_chat_workflow_run_list_keeps_message_fields(app: Flask, monkey
handler = unwrap(api.get)
with app.test_request_context("/apps/app-1/advanced-chat/workflow-runs?limit=1", method="GET"):
payload = handler(api, app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"))
payload = handler(
api,
workflow_run_module.WorkflowRunListQuery(limit=1),
app_model=SimpleNamespace(id="app-1", tenant_id="tenant-1"),
)
response = _serialize_200_response(api.get, payload)

View File

@@ -142,7 +142,12 @@ def test_app_trigger_enable_uses_injected_tenant_id(app: Flask, database_session
app.test_request_context("/", json=payload),
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
):
response = method(api, app_model.tenant_id, app_model)
response = method(
api,
workflow_trigger_module.ParserEnable(trigger_id=trigger.id, enable_trigger=True),
app_model.tenant_id,
app_model,
)
assert response["id"] == trigger.id
assert response["status"] == "enabled"

View File

@@ -5,7 +5,7 @@ from unittest.mock import patch
import pytest
from flask import Flask
from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import BadRequest, UnprocessableEntity
from controllers.console import wraps as console_wraps
from controllers.console.billing.billing import PartnerTenants
@@ -129,7 +129,7 @@ class TestPartnerTenants:
assert "Invalid partner_key" in str(exc_info.value)
def test_put_missing_click_id(self, app: Flask, mock_account, mock_billing_service, mock_decorators):
"""Test that missing click_id raises BadRequest."""
"""Test that missing click_id raises UnprocessableEntity (422)."""
# Arrange
partner_key_encoded = base64.b64encode(b"partner-key-123").decode("utf-8")
@@ -148,8 +148,8 @@ class TestPartnerTenants:
resource = PartnerTenants()
# Act & Assert
# Validation should raise BadRequest for missing required field
with pytest.raises(BadRequest):
# Validation should raise UnprocessableEntity (422) for missing required field
with pytest.raises(UnprocessableEntity):
resource.put(partner_key_encoded)
def test_put_billing_service_json_decode_error(

View File

@@ -14,9 +14,15 @@ from controllers.console.datasets.rag_pipeline.datasource_auth import (
DatasourceAuthListApi,
DatasourceAuthOauthCustomClient,
DatasourceAuthUpdateApi,
DatasourceCredentialDeletePayload,
DatasourceCredentialPayload,
DatasourceCredentialUpdatePayload,
DatasourceCustomClientPayload,
DatasourceDefaultPayload,
DatasourceHardCodeAuthListApi,
DatasourceOAuthCallback,
DatasourcePluginOAuthAuthorizationUrl,
DatasourceUpdateNamePayload,
DatasourceUpdateProviderNameApi,
)
from core.plugin.impl.oauth import OAuthHandler
@@ -405,7 +411,8 @@ class TestDatasourceAuth:
return_value=None,
) as add_api_key_provider,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -431,7 +438,7 @@ class TestDatasourceAuth:
),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialPayload.model_validate(payload), "tenant-1", "notion")
def test_get_success(self, app: Flask):
api = DatasourceAuth()
@@ -462,7 +469,7 @@ class TestDatasourceAuth:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialPayload.model_validate(payload), "tenant-1", "notion")
def test_get_empty_list(self, app: Flask):
api = DatasourceAuth()
@@ -499,7 +506,8 @@ class TestDatasourceAuthDeleteApi:
return_value=None,
) as remove_datasource_credentials,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialDeletePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -522,7 +530,7 @@ class TestDatasourceAuthDeleteApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceCredentialDeletePayload.model_validate(payload), "tenant-1", "notion")
class TestDatasourceAuthUpdateApi:
@@ -545,7 +553,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_datasource_credentials,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 201
@@ -573,7 +582,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
@@ -595,7 +605,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
),
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
assert status == 201
@@ -615,7 +626,8 @@ class TestDatasourceAuthUpdateApi:
return_value=None,
) as update_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCredentialUpdatePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
update_mock.assert_called_once()
@@ -716,7 +728,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
) as setup_custom_client,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -760,7 +773,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
),
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
assert status == 200
@@ -783,7 +797,8 @@ class TestDatasourceAuthOauthCustomClient:
return_value=None,
) as setup_mock,
):
response, status = method(api, "tenant-1", "notion")
req_data = DatasourceCustomClientPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", "notion")
assert response == _success_response()
setup_mock.assert_called_once()
@@ -808,7 +823,8 @@ class TestDatasourceAuthDefaultApi:
return_value=None,
) as set_default_datasource_provider,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceDefaultPayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -828,7 +844,7 @@ class TestDatasourceAuthDefaultApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceDefaultPayload.model_validate(payload), "tenant-1", "notion")
class TestDatasourceUpdateProviderNameApi:
@@ -847,7 +863,8 @@ class TestDatasourceUpdateProviderNameApi:
return_value=None,
) as update_datasource_provider_name,
):
response, status = method(api, "tenant-1", _PROVIDER_ID)
req_data = DatasourceUpdateNamePayload.model_validate(payload)
response, status = method(api, req_data, "tenant-1", _PROVIDER_ID)
assert response == _success_response()
assert status == 200
@@ -871,7 +888,7 @@ class TestDatasourceUpdateProviderNameApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceUpdateNamePayload.model_validate(payload), "tenant-1", "notion")
def test_update_name_missing_credential_id(self, app: Flask):
api = DatasourceUpdateProviderNameApi()
@@ -884,4 +901,4 @@ class TestDatasourceUpdateProviderNameApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", "notion")
method(api, DatasourceUpdateNamePayload.model_validate(payload), "tenant-1", "notion")

View File

@@ -7,6 +7,7 @@ from flask import Flask
from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline.datasource_content_preview import (
DataSourceContentPreviewApi,
Parser,
)
from models import Account
from models.dataset import Pipeline
@@ -49,7 +50,8 @@ class TestDataSourceContentPreviewApi:
return_value=service_instance,
),
):
response, status = method(api, account, pipeline, node_id)
req_data = Parser.model_validate(payload)
response, status = method(api, req_data, account, pipeline, node_id)
service_instance.run_datasource_node_preview.assert_called_once_with(
pipeline=pipeline,
@@ -80,7 +82,7 @@ class TestDataSourceContentPreviewApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, account, pipeline, "node-1")
method(api, Parser.model_validate(payload), account, pipeline, "node-1")
def test_post_without_credential_id(self, app: Flask):
api = DataSourceContentPreviewApi()
@@ -106,7 +108,8 @@ class TestDataSourceContentPreviewApi:
return_value=service_instance,
),
):
response, status = method(api, account, pipeline, "node-1")
req_data = Parser.model_validate(payload)
response, status = method(api, req_data, account, pipeline, "node-1")
service_instance.run_datasource_node_preview.assert_called_once()
assert status == 200

View File

@@ -15,8 +15,11 @@ from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline import rag_pipeline as module
from controllers.console.datasets.rag_pipeline.rag_pipeline import (
CustomizedPipelineTemplateApi,
CustomizedPipelineTemplatePayload,
PipelineTemplateDetailApi,
PipelineTemplateDetailQuery,
PipelineTemplateListApi,
PipelineTemplateListQuery,
PublishCustomizedPipelineTemplateApi,
)
from models.account import Account
@@ -90,7 +93,7 @@ class TestPipelineTemplateListApi:
app.test_request_context("/rag/pipeline/templates"),
patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates),
):
response, status = method(api, session, tenant_id)
response, status = method(api, PipelineTemplateListQuery(), session, tenant_id)
assert status == 200
assert service_calls == [("built-in", "en-US", tenant_id)]
@@ -120,7 +123,9 @@ class TestPipelineTemplateListApi:
app.test_request_context("/rag/pipeline/templates?type=customized&language=ja-JP"),
patch.object(module.RagPipelineService, "get_pipeline_templates", side_effect=get_pipeline_templates),
):
response, status = method(api, session, tenant_id)
response, status = method(
api, PipelineTemplateListQuery(type="customized", language="ja-JP"), session, tenant_id
)
assert status == 200
assert response == {"pipeline_templates": []}
@@ -147,7 +152,7 @@ class TestPipelineTemplateDetailApi:
side_effect=get_pipeline_template_detail,
),
):
response, status = method(api, session, "template-1")
response, status = method(api, PipelineTemplateDetailQuery(type="customized"), session, "template-1")
assert status == 200
assert response == {**_template_detail(), "created_by": None}
@@ -170,7 +175,7 @@ class TestPipelineTemplateDetailApi:
),
pytest.raises(NotFound),
):
method(api, session, "missing")
method(api, PipelineTemplateDetailQuery(), session, "missing")
class TestCustomizedPipelineTemplateApi:
@@ -198,7 +203,9 @@ class TestCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template),
):
response, status = method(api, tenant_id, account, "template-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "template-1"
)
assert (response, status) == ("", 204)
assert len(service_calls) == 1
@@ -242,7 +249,9 @@ class TestCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module.RagPipelineService, "update_customized_pipeline_template", side_effect=update_template),
):
response, status = method(api, tenant_id, account, "template-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "template-1"
)
assert (response, status) == ("", 204)
assert len(service_calls) == 1
@@ -349,7 +358,9 @@ class TestPublishCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module, "RagPipelineService", Service),
):
response, status = method(api, tenant_id, account, "pipeline-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "pipeline-1"
)
assert (response, status) == ("", 204)
assert service_calls == [("pipeline-1", payload, account, tenant_id)]
@@ -386,7 +397,9 @@ class TestPublishCustomizedPipelineTemplateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(module, "RagPipelineService", Service),
):
response, status = method(api, tenant_id, account, "pipeline-1")
response, status = method(
api, CustomizedPipelineTemplatePayload.model_validate(payload), tenant_id, account, "pipeline-1"
)
assert (response, status) == ("", 204)
assert service_calls == [

View File

@@ -15,6 +15,7 @@ from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.datasets.rag_pipeline.rag_pipeline_datasets import (
CreateEmptyRagPipelineDatasetApi,
CreateRagPipelineDatasetApi,
RagPipelineDatasetImportPayload,
)
from services.entities.dsl_entities import ImportStatus
@@ -50,7 +51,7 @@ class TestCreateRagPipelineDatasetApi:
return_value=mock_service,
),
):
response, status = method(api, "tenant-1", user)
response, status = method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
assert status == 201
assert response == {
@@ -75,7 +76,7 @@ class TestCreateRagPipelineDatasetApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(Forbidden):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
def test_post_dataset_name_duplicate(self, app: Flask) -> None:
api = CreateRagPipelineDatasetApi()
@@ -96,7 +97,7 @@ class TestCreateRagPipelineDatasetApi:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
def test_post_invalid_payload(self, app: Flask) -> None:
api = CreateRagPipelineDatasetApi()
@@ -110,7 +111,7 @@ class TestCreateRagPipelineDatasetApi:
patch.object(type(console_ns), "payload", payload),
):
with pytest.raises(ValueError):
method(api, "tenant-1", user)
method(api, RagPipelineDatasetImportPayload.model_validate(payload), "tenant-1", user)
class TestCreateEmptyRagPipelineDatasetApi:

Some files were not shown because too many files have changed in this diff Show More