Compare commits

...

14 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
110 changed files with 3149 additions and 1567 deletions

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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,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

@@ -538,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

@@ -14933,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

@@ -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

@@ -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

@@ -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:] == (
@@ -378,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)
@@ -445,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": []}
@@ -475,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}
@@ -528,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")]
@@ -574,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"}]}
@@ -604,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"}]}
@@ -634,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:

View File

@@ -8,12 +8,14 @@ from controllers.common.errors import InvalidArgumentError, NotFoundError
from controllers.console import console_ns
from controllers.console.app.error import DraftWorkflowNotExist
from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable import (
PaginationQuery,
RagPipelineEnvironmentVariableCollectionApi,
RagPipelineNodeVariableCollectionApi,
RagPipelineSystemVariableCollectionApi,
RagPipelineVariableApi,
RagPipelineVariableCollectionApi,
RagPipelineVariableResetApi,
WorkflowDraftVariablePatchPayload,
)
from core.workflow.llm_environment_variable import LLMEnvironmentVariable
from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID
@@ -72,7 +74,7 @@ class TestRagPipelineVariableCollectionApi:
return_value=draft_srv,
),
):
result = method(api, editor_user, pipeline)
result = method(api, PaginationQuery(page=1, limit=10), editor_user, pipeline)
assert result is var_list
draft_srv.list_variables_without_values.assert_called_once_with(
@@ -100,7 +102,7 @@ class TestRagPipelineVariableCollectionApi:
),
):
with pytest.raises(DraftWorkflowNotExist):
method(api, editor_user, pipeline)
method(api, PaginationQuery(), editor_user, pipeline)
def test_delete_variables_success(self, app: Flask, fake_db, editor_user):
api = RagPipelineVariableCollectionApi()
@@ -198,7 +200,7 @@ class TestRagPipelineVariableApi:
),
):
with pytest.raises(InvalidArgumentError):
method(api, editor_user, pipeline, "v1")
method(api, WorkflowDraftVariablePatchPayload.model_validate(payload), editor_user, pipeline, "v1")
def test_delete_variable_success(self, app: Flask, fake_db, editor_user):
api = RagPipelineVariableApi()

View File

@@ -11,10 +11,12 @@ from flask import Flask
from controllers.console import console_ns
from controllers.console.datasets.rag_pipeline.rag_pipeline_import import (
IncludeSecretQuery,
RagPipelineExportApi,
RagPipelineImportApi,
RagPipelineImportCheckDependenciesApi,
RagPipelineImportConfirmApi,
RagPipelineImportPayload,
)
from core.plugin.entities.plugin import PluginDependency, PluginDependencyType
from models.dataset import Pipeline
@@ -67,7 +69,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 200
assert response == {
@@ -105,7 +107,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 400
assert response["status"] == "failed"
@@ -139,7 +141,7 @@ class TestRagPipelineImportApi:
return_value=service,
),
):
response, status = method(api, user)
response, status = method(api, RagPipelineImportPayload(mode="create"), user)
assert status == 202
assert response["status"] == "pending"
@@ -287,7 +289,7 @@ class TestRagPipelineExportApi:
return_value=service,
),
):
response, status = method(api, pipeline)
response, status = method(api, IncludeSecretQuery(), pipeline)
assert status == 200
assert response == {"data": "yaml: data"}

View File

@@ -16,6 +16,14 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden
from controllers.console.datasets.rag_pipeline import rag_pipeline_workflow as module
from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
DraftWorkflowRunPayload,
NodeIdQuery,
PublishedWorkflowRunPayload,
RagPipelineRecommendedPluginQuery,
WorkflowListQuery,
WorkflowUpdatePayload,
)
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from models.account import Account, TenantAccountRole
from models.dataset import Pipeline
@@ -133,7 +141,9 @@ def test_published_rag_pipeline_workflows_serialize_items_before_session_closes(
method="GET",
query_string={"page": 1, "limit": 10, "user_id": "", "named_only": "false"},
):
response = handler(api, _account(), pipeline=pipeline)
response = handler(
api, WorkflowListQuery(page=1, limit=10, user_id="", named_only=False), _account(), pipeline=pipeline
)
assert response["items"][0]["id"] == DEFAULT_WORKFLOW_ID
assert response["page"] == 1
@@ -157,6 +167,7 @@ def test_rag_pipeline_workflow_patch_serializes_response_model(
):
response = handler(
api,
WorkflowUpdatePayload.model_validate(payload),
_account(),
pipeline=_pipeline(),
workflow_id=DEFAULT_WORKFLOW_ID,
@@ -198,7 +209,7 @@ def test_draft_rag_pipeline_second_step_parameters_serializes_variables(database
database_app.test_request_context("/?node_id=node-1"),
patch.object(RagPipelineService, "get_second_step_parameters", return_value=variables),
):
response = handler(api, _pipeline())
response = handler(api, NodeIdQuery(node_id="node-1"), _pipeline())
assert response["variables"] == variables
@@ -215,7 +226,7 @@ def test_rag_pipeline_recommended_plugins_serializes_known_envelope(database_app
database_app.test_request_context("/?type=tool"),
patch.object(RagPipelineService, "get_recommended_plugins", return_value=recommended_plugins),
):
response = handler(api, DEFAULT_WORKFLOW_TENANT_ID, _account())
response = handler(api, RagPipelineRecommendedPluginQuery(type="tool"), DEFAULT_WORKFLOW_TENANT_ID, _account())
assert response == recommended_plugins
@@ -270,7 +281,12 @@ def test_rag_pipeline_run_uses_sqlite_session(
patch.object(module.PipelineGenerateService, "generate", return_value=MagicMock()) as generate,
patch.object(module.helper, "compact_generate_response", return_value={"ok": True}),
):
response = handler(api, session, _account(), pipeline.id)
req_data = (
DraftWorkflowRunPayload.model_validate(payload)
if api_type is module.DraftRagPipelineRunApi
else PublishedWorkflowRunPayload.model_validate(payload)
)
response = handler(api, req_data, session, _account(), pipeline.id)
assert response == {"ok": True}
load_pipeline.assert_called_once_with(session, pipeline.id)
@@ -293,6 +309,11 @@ def test_rag_pipeline_run_translates_rate_limit(
api = api_type()
handler = unwrap_all(api.post)
pipeline = _pipeline()
req_data = (
DraftWorkflowRunPayload.model_validate(payload)
if api_type is module.DraftRagPipelineRunApi
else PublishedWorkflowRunPayload.model_validate(payload)
)
with (
Session(sqlite_engine) as session,
@@ -301,4 +322,4 @@ def test_rag_pipeline_run_translates_rate_limit(
patch.object(module.PipelineGenerateService, "generate", side_effect=InvokeRateLimitError("limit")),
pytest.raises(InvokeRateLimitHttpError),
):
handler(api, session, _account(), pipeline.id)
handler(api, req_data, session, _account(), pipeline.id)

View File

@@ -22,8 +22,12 @@ import services
from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync
from controllers.console.datasets.rag_pipeline import rag_pipeline_workflow as workflow_controller
from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
DatasourceVariablesPayload,
DefaultBlockConfigQuery,
DefaultRagPipelineBlockConfigApi,
DraftRagPipelineApi,
NodeRunPayload,
NodeRunRequiredPayload,
PublishedAllRagPipelineApi,
PublishedRagPipelineApi,
RagPipelineByIdApi,
@@ -33,9 +37,12 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_workflow import (
RagPipelineDraftRunLoopNodeApi,
RagPipelineDraftWorkflowRestoreApi,
RagPipelineRecommendedPluginApi,
RagPipelineRecommendedPluginQuery,
RagPipelineTaskStopApi,
RagPipelineWorkflowLastRunApi,
RagPipelineWorkflowRunNodeExecutionListApi,
WorkflowListQuery,
WorkflowUpdatePayload,
)
from graphon.enums import WorkflowNodeExecutionStatus
from libs.datetime_utils import naive_utc_now
@@ -418,7 +425,7 @@ class TestDraftRunNodes:
return_value={"ok": True},
),
):
result = method(api, user, pipeline, "node")
result = method(api, NodeRunPayload(), user, pipeline, "node")
assert result == {"ok": True}
def test_iteration_node_conversation_not_exists(self, app: Flask) -> None:
@@ -436,7 +443,7 @@ class TestDraftRunNodes:
),
):
with pytest.raises(NotFound):
method(api, user, pipeline, "node")
method(api, NodeRunPayload(), user, pipeline, "node")
def test_loop_node_success(self, app: Flask) -> None:
api = RagPipelineDraftRunLoopNodeApi()
@@ -456,7 +463,7 @@ class TestDraftRunNodes:
return_value={"ok": True},
),
):
assert method(api, user, pipeline, "node") == {"ok": True}
assert method(api, NodeRunPayload(), user, pipeline, "node") == {"ok": True}
class TestDraftNodeRun:
@@ -478,7 +485,7 @@ class TestDraftNodeRun:
),
):
with pytest.raises(ValueError):
method(api, user, pipeline, "node")
method(api, NodeRunRequiredPayload(inputs={}), user, pipeline, "node")
class TestPublishedPipelineApis:
@@ -551,7 +558,7 @@ class TestMiscApis:
return_value=service,
),
):
result = method(api, tenant_id, user)
result = method(api, RagPipelineRecommendedPluginQuery(type="all"), tenant_id, user)
assert result == recommended_plugins
service.get_recommended_plugins.assert_called_once_with("all", user, tenant_id)
@@ -573,7 +580,7 @@ class TestDefaultBlockConfigApi:
return_value=service,
),
):
result = method(api, pipeline, "llm")
result = method(api, DefaultBlockConfigQuery(q="{}"), pipeline, "llm")
assert result == {"k": "v"}
def test_get_block_config_invalid_json(self, app: Flask) -> None:
@@ -584,7 +591,7 @@ class TestDefaultBlockConfigApi:
with app.test_request_context("/?q=bad-json"):
with pytest.raises(ValueError):
method(api, pipeline, "llm")
method(api, DefaultBlockConfigQuery(q="bad-json"), pipeline, "llm")
class TestPublishedAllRagPipelineApi:
@@ -605,7 +612,7 @@ class TestPublishedAllRagPipelineApi:
return_value=service,
),
):
result = method(api, user, pipeline)
result = method(api, WorkflowListQuery(), user, pipeline)
assert result["items"][0]["id"] == "w1"
assert result["items"][0]["graph"] == {"nodes": [], "edges": []}
@@ -622,7 +629,7 @@ class TestPublishedAllRagPipelineApi:
app.test_request_context("/?user_id=u2"),
):
with pytest.raises(Forbidden):
method(api, user, pipeline)
method(api, WorkflowListQuery(user_id="u2"), user, pipeline)
class TestRagPipelineByIdApi:
@@ -647,7 +654,7 @@ class TestRagPipelineByIdApi:
return_value=service,
),
):
result = method(api, user, pipeline, "w1")
result = method(api, WorkflowUpdatePayload.model_validate(payload), user, pipeline, "w1")
assert result["id"] == "w1"
assert result["marked_name"] == "test"
@@ -661,7 +668,7 @@ class TestRagPipelineByIdApi:
user = make_account()
with app.test_request_context("/", json={}):
result, status = method(api, user, pipeline, "w1")
result, status = method(api, WorkflowUpdatePayload(), user, pipeline, "w1")
assert status == 400
def test_delete_success(self, app: Flask) -> None:
@@ -797,6 +804,6 @@ class TestRagPipelineDatasourceVariableApi:
return_value=service,
),
):
result = method(api, user, pipeline)
result = method(api, DatasourceVariablesPayload.model_validate(payload), user, pipeline)
assert result["node_id"] == "n1"
assert result["process_data"] == {}

View File

@@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
from werkzeug.exceptions import NotFound
from controllers.console.datasets import data_source as module
from controllers.console.datasets.data_source import DataSourceApi, DataSourceNotionListApi
from controllers.console.datasets.data_source import DataSourceApi, DataSourceNotionListApi, DataSourceNotionListQuery
from models import Account, DataSourceOauthBinding
from models.engine import db
@@ -217,7 +217,11 @@ def test_notion_pre_import_pages_serializes_frontend_list_shape(
patch("core.datasource.datasource_manager.DatasourceManager.get_datasource_runtime", return_value=runtime),
):
response, status = unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(), sqlite_session, "tenant-1", current_user
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1"),
sqlite_session,
"tenant-1",
current_user,
)
assert status == 200
@@ -255,7 +259,13 @@ def test_notion_pre_import_pages_rejects_missing_credential(
patch.object(module.DatasourceProviderService, "get_datasource_credentials", return_value=None),
pytest.raises(NotFound, match="Credential not found"),
):
unwrap(DataSourceNotionListApi().get)(DataSourceNotionListApi(), sqlite_session, TENANT_ID, current_user)
unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1"),
sqlite_session,
TENANT_ID,
current_user,
)
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
@@ -276,4 +286,10 @@ def test_notion_pre_import_pages_rejects_non_notion_dataset(
patch.object(module.DatasetService, "get_dataset", return_value=dataset),
pytest.raises(ValueError, match="Dataset is not notion type"),
):
unwrap(DataSourceNotionListApi().get)(DataSourceNotionListApi(), sqlite_session, TENANT_ID, current_user)
unwrap(DataSourceNotionListApi().get)(
DataSourceNotionListApi(),
DataSourceNotionListQuery(credential_id="credential-1", dataset_id="dataset-1"),
sqlite_session,
TENANT_ID,
current_user,
)

View File

@@ -15,6 +15,8 @@ from controllers.console.datasets.data_source import (
DataSourceNotionDocumentSyncApi,
DataSourceNotionIndexingEstimateApi,
DataSourceNotionPreviewApi,
DataSourceNotionPreviewQuery,
NotionEstimatePayload,
)
from core.rag.index_processor.constant.index_type import IndexStructureType
from models import Account
@@ -45,7 +47,7 @@ class TestDataSourceNotionPreviewApi:
return_value=extractor,
),
):
response, status = method(api, "tenant-1", "p1", "page")
response, status = method(api, DataSourceNotionPreviewQuery(credential_id="c1"), "tenant-1", "p1", "page")
assert status == 200
@@ -80,7 +82,7 @@ class TestDataSourceNotionIndexingEstimateApi:
return_value=MagicMock(model_dump=lambda: {"total_pages": 1}),
),
):
response, status = method(api, sqlite_session, "tenant-1")
response, status = method(api, NotionEstimatePayload.model_validate(payload), sqlite_session, "tenant-1")
assert status == 200

View File

@@ -18,6 +18,7 @@ from controllers.console.datasets.datasets import (
DatasetApiDeleteApi,
DatasetApiKeyApi,
DatasetAutoDisableLogApi,
DatasetCreatePayload,
DatasetEnableApiApi,
DatasetErrorDocs,
DatasetIndexingEstimateApi,
@@ -28,7 +29,9 @@ from controllers.console.datasets.datasets import (
DatasetRelatedAppListApi,
DatasetRetrievalSettingApi,
DatasetRetrievalSettingMockApi,
DatasetUpdatePayload,
DatasetUseCheckApi,
IndexingEstimatePayload,
_get_retrieval_methods_by_vector_type,
)
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
@@ -489,7 +492,7 @@ class TestDatasetListApiPost:
patch.object(type(console_ns), "payload", payload),
patch.object(DatasetService, "create_empty_dataset", return_value=dataset),
):
_, status = method(api, MagicMock(), "tenant-1", user)
_, status = method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
assert status == 201
def test_post_forbidden(self, app: Flask):
@@ -499,7 +502,7 @@ class TestDatasetListApiPost:
user = make_account(TenantAccountRole.NORMAL)
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", user)
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
def test_post_duplicate_name(self, app: Flask):
api = DatasetListApi()
@@ -514,14 +517,14 @@ class TestDatasetListApiPost:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", user)
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", user)
def test_post_invalid_payload_missing_name(self, app: Flask):
api = DatasetListApi()
method = unwrap(api.post)
with app.test_request_context("/datasets", json={}), patch.object(type(console_ns), "payload", {}):
with pytest.raises(ValueError):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(), MagicMock(), "tenant-1", make_account())
def test_post_invalid_indexing_technique(self, app: Flask):
api = DatasetListApi()
@@ -529,7 +532,7 @@ class TestDatasetListApiPost:
payload = {"name": "bad", "indexing_technique": "invalid-tech"}
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(ValueError, match="Invalid indexing technique"):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account())
def test_post_invalid_provider(self, app: Flask):
api = DatasetListApi()
@@ -537,7 +540,7 @@ class TestDatasetListApiPost:
payload = {"name": "bad", "provider": "unknown"}
with app.test_request_context("/datasets", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(ValueError, match="Invalid provider"):
method(api, MagicMock(), "tenant-1", make_account())
method(api, DatasetCreatePayload(**payload), MagicMock(), "tenant-1", make_account())
class TestDatasetApiGet:
@@ -692,7 +695,7 @@ class TestDatasetApiPatch:
patch.object(DatasetService, "update_dataset", return_value=dataset),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]),
):
result, status = method(api, MagicMock(), tenant_id, user, dataset_id)
result, status = method(api, DatasetUpdatePayload(), MagicMock(), tenant_id, user, dataset_id)
assert status == 200
assert result["partial_member_list"] == []
@@ -704,7 +707,7 @@ class TestDatasetApiPatch:
patch.object(DatasetService, "get_dataset", return_value=None),
):
with pytest.raises(NotFound, match="Dataset not found"):
method(api, MagicMock(), "tenant-1", make_account(), "missing")
method(api, DatasetUpdatePayload(), MagicMock(), "tenant-1", make_account(), "missing")
def test_patch_permission_denied(self, app: Flask):
api = DatasetApi()
@@ -719,7 +722,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "check_permission", side_effect=Forbidden("no permission")),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant", make_account(), dataset_id)
method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
def test_patch_partial_members_update(self, app: Flask):
api = DatasetApi()
@@ -736,7 +739,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "update_partial_member_list", return_value=None),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=["u1", "u2"]),
):
result, _ = method(api, MagicMock(), "tenant", make_account(), dataset_id)
result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
assert result["partial_member_list"] == ["u1", "u2"]
def test_patch_clear_partial_members(self, app: Flask):
@@ -754,7 +757,7 @@ class TestDatasetApiPatch:
patch.object(DatasetPermissionService, "clear_partial_member_list", return_value=None),
patch.object(DatasetPermissionService, "get_dataset_partial_member_list", return_value=[]),
):
result, _ = method(api, MagicMock(), "tenant", make_account(), dataset_id)
result, _ = method(api, DatasetUpdatePayload(), MagicMock(), "tenant", make_account(), dataset_id)
assert result["partial_member_list"] == []
@@ -1014,7 +1017,12 @@ class TestDatasetIndexingEstimateApi:
patch("controllers.console.datasets.datasets.DocumentService.estimate_args_validate", return_value=None),
patch("controllers.console.datasets.datasets.IndexingRunner.indexing_estimate", return_value=mock_response),
):
response, status = method(api, session, "tenant-1")
response, status = method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
assert status == 200
assert response == {
"tokens": 0,
@@ -1036,7 +1044,12 @@ class TestDatasetIndexingEstimateApi:
patch("controllers.console.datasets.datasets.DocumentService.estimate_args_validate", return_value=None),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_llm_bad_request_error(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1055,7 +1068,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_provider_token_not_init(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1074,7 +1092,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
def test_post_generic_exception(self, app: Flask):
api = DatasetIndexingEstimateApi()
@@ -1092,7 +1115,12 @@ class TestDatasetIndexingEstimateApi:
),
):
with pytest.raises(IndexingEstimateError):
method(api, session, "tenant-1")
method(
api,
IndexingEstimatePayload(**payload),
session,
"tenant-1",
)
class TestDatasetRelatedAppListApi:

View File

@@ -9,9 +9,11 @@ from flask import Flask
from werkzeug.exceptions import Forbidden, NotFound
import services
from controllers.common.controller_schemas import ChildChunkCreatePayload, ChildChunkUpdatePayload
from controllers.console import console_ns
from controllers.console.app.error import ProviderNotInitializeError
from controllers.console.datasets.datasets_segments import (
BatchImportPayload,
ChildChunkAddApi,
ChildChunkBatchUpdatePayload,
ChildChunkUpdateApi,
@@ -20,6 +22,8 @@ from controllers.console.datasets.datasets_segments import (
DatasetDocumentSegmentBatchImportApi,
DatasetDocumentSegmentListApi,
DatasetDocumentSegmentUpdateApi,
SegmentCreatePayload,
SegmentUpdatePayload,
)
from controllers.console.datasets.error import ChildChunkDeleteIndexError, ChildChunkIndexingError, InvalidActionError
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
@@ -50,6 +54,7 @@ def _segment():
status=SegmentStatus.COMPLETED,
updated_by="u1",
)
segment.id = "seg-1"
segment.created_at = naive_utc_now()
segment.updated_at = naive_utc_now()
@@ -354,7 +359,9 @@ class TestDatasetDocumentSegmentAddApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, SegmentCreatePayload(content="test content"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 200
assert response["data"]["id"] == "seg-1"
@@ -378,7 +385,9 @@ class TestDatasetDocumentSegmentAddApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
def test_post_provider_token_not_init(self, app: Flask):
api = DatasetDocumentSegmentAddApi()
@@ -400,7 +409,9 @@ class TestDatasetDocumentSegmentAddApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
class TestDatasetDocumentSegmentUpdateApi:
@@ -441,7 +452,9 @@ class TestDatasetDocumentSegmentUpdateApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1", "seg-1")
response, status = method(
api, SegmentUpdatePayload(content="test content"), session, "tenant-1", user, "ds-1", "doc-1", "seg-1"
)
assert status == 200
assert "data" in response
@@ -467,7 +480,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_patch_segment_not_found(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
@@ -495,7 +517,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_patch_llm_bad_request(self, app: Flask):
api = DatasetDocumentSegmentUpdateApi()
@@ -525,7 +556,16 @@ class TestDatasetDocumentSegmentUpdateApi:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
SegmentUpdatePayload(content="test content"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
class TestDatasetDocumentSegmentBatchImportApi:
@@ -564,7 +604,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
return_value=None,
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 200
assert response["job_status"] == "waiting"
@@ -581,7 +623,15 @@ class TestDatasetDocumentSegmentBatchImportApi:
patch("controllers.console.datasets.datasets_segments.DatasetService.get_dataset", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_post_document_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -599,7 +649,15 @@ class TestDatasetDocumentSegmentBatchImportApi:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_post_upload_file_not_found(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -619,7 +677,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_post_invalid_file_type(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -641,7 +701,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
),
):
with pytest.raises(ValueError):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_post_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -665,7 +727,9 @@ class TestDatasetDocumentSegmentBatchImportApi:
"controllers.console.datasets.datasets_segments.redis_client.setnx", side_effect=Exception("redis down")
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 500
assert "error" in response
@@ -747,7 +811,9 @@ class TestChildChunkAddApi:
return_value=child_chunk,
),
):
response, status = method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
response, status = method(
api, ChildChunkCreatePayload(content="child"), MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1"
)
assert status == 200
assert response["data"]["id"] == "cc-1"
@@ -778,7 +844,16 @@ class TestChildChunkAddApi:
),
):
with pytest.raises(ChildChunkIndexingError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
ChildChunkCreatePayload(content="child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
def test_post_permission_denied(self, app: Flask):
api = ChildChunkAddApi()
@@ -799,7 +874,16 @@ class TestChildChunkAddApi:
),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1")
method(
api,
ChildChunkCreatePayload(content="child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
)
class TestChildChunkUpdateApi:
@@ -922,7 +1006,17 @@ class TestChildChunkUpdateApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1", "seg-1", "cc-1")
method(
api,
ChildChunkUpdatePayload(content="updated child"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
"seg-1",
"cc-1",
)
class TestSegmentListAdvancedCases:
@@ -1047,7 +1141,9 @@ class TestSegmentOperationCases:
),
):
with pytest.raises(ProviderTokenNotInitError):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api, SegmentCreatePayload(content="test content"), MagicMock(), "tenant-1", user, "ds-1", "doc-1"
)
def test_batch_import_with_document_not_found(self, app: Flask):
"""Test batch import with document not found"""
@@ -1063,7 +1159,15 @@ class TestSegmentOperationCases:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=None),
):
with pytest.raises(NotFound):
method(api, MagicMock(), "tenant-1", user, "ds-1", "doc-1")
method(
api,
BatchImportPayload(upload_file_id="test-file-id"),
MagicMock(),
"tenant-1",
user,
"ds-1",
"doc-1",
)
def test_batch_import_with_invalid_file(self, app: Flask):
"""Test batch import with invalid file type"""
@@ -1083,7 +1187,9 @@ class TestSegmentOperationCases:
patch("controllers.console.datasets.datasets_segments.DocumentService.get_document", return_value=document),
):
with pytest.raises(NotFound):
method(api, session, "tenant-1", user, "ds-1", "doc-1")
method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
def test_batch_import_with_async_task_failure(self, app: Flask):
api = DatasetDocumentSegmentBatchImportApi()
@@ -1122,7 +1228,9 @@ class TestSegmentOperationCases:
side_effect=Exception("Task failed"),
),
):
response, status = method(api, session, "tenant-1", user, "ds-1", "doc-1")
response, status = method(
api, BatchImportPayload(upload_file_id="test-file-id"), session, "tenant-1", user, "ds-1", "doc-1"
)
assert status == 500
assert "error" in response

View File

@@ -12,10 +12,14 @@ from controllers.console import console_ns
from controllers.console.datasets.error import DatasetNameDuplicateError
from controllers.console.datasets.external import (
BedrockRetrievalApi,
BedrockRetrievalPayload,
ExternalApiTemplateApi,
ExternalApiTemplateListApi,
ExternalApiTemplateListQuery,
ExternalApiUseCheckApi,
ExternalDatasetCreateApi,
ExternalHitTestingPayload,
ExternalKnowledgeApiPayload,
ExternalKnowledgeHitTestingApi,
)
from models.account import Account, TenantAccountRole
@@ -174,7 +178,9 @@ class TestExternalApiTemplateListApi:
return_value=([api_item], 3),
) as get_external_knowledge_apis,
):
resp, status = method(api, session, "tenant-1")
resp, status = method(
api, ExternalApiTemplateListQuery(page=2, limit=1, keyword="vector"), session, "tenant-1"
)
assert status == 200
assert resp == {
@@ -213,7 +219,9 @@ class TestExternalApiTemplateListApi:
return_value=created,
) as create_external_knowledge_api,
):
resp, status = method(api, session, "tenant-1", current_user)
resp, status = method(
api, ExternalKnowledgeApiPayload.model_validate(payload), session, "tenant-1", current_user
)
assert status == 201
assert resp == _external_api_dict("api-created")
@@ -239,7 +247,7 @@ class TestExternalApiTemplateListApi:
patch.object(ExternalDatasetService, "validate_api_list"),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
def test_post_duplicate_name(self, app: Flask, current_user: Account):
api = ExternalApiTemplateListApi()
@@ -258,7 +266,7 @@ class TestExternalApiTemplateListApi:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalApiTemplateApi:
@@ -325,7 +333,14 @@ class TestExternalApiTemplateApi:
return_value=updated,
) as update_external_knowledge_api,
):
resp, status = method(api, session, "tenant-1", current_user, "api-updated")
resp, status = method(
api,
ExternalKnowledgeApiPayload.model_validate(payload),
session,
"tenant-1",
current_user,
"api-updated",
)
assert status == 200
assert resp == _external_api_dict("api-updated")
@@ -405,7 +420,9 @@ class TestExternalDatasetCreateApi:
) as dataset_response_source,
):
session = MagicMock()
resp, status = method(api, session, "tenant-1", current_user)
resp, status = method(
api, ExternalDatasetCreatePayload.model_validate(payload), session, "tenant-1", current_user
)
assert status == 201
assert resp == _expected_dataset_detail_payload()
@@ -433,7 +450,7 @@ class TestExternalDatasetCreateApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalDatasetCreatePayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalKnowledgeHitTestingApi:
@@ -450,7 +467,7 @@ class TestExternalKnowledgeHitTestingApi:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), current_user, "dataset-id")
method(api, ExternalHitTestingPayload(query="test"), MagicMock(), current_user, "dataset-id")
def test_hit_testing_success(self, app: Flask, current_user: Account):
api = ExternalKnowledgeHitTestingApi()
@@ -496,7 +513,7 @@ class TestExternalKnowledgeHitTestingApi:
) as external_retrieve,
patch("controllers.console.datasets.external.dump_response", side_effect=lambda _model, value: value),
):
resp = method(api, session, current_user, "dataset-id")
resp = method(api, ExternalHitTestingPayload.model_validate(payload), session, current_user, "dataset-id")
assert resp == retrieve_response
check_dataset_permission.assert_called_once_with(dataset, current_user, session)
@@ -549,7 +566,7 @@ class TestBedrockRetrievalApi:
return_value=retrieval_response,
) as knowledge_retrieval,
):
resp, status = method()
resp, status = method(api, BedrockRetrievalPayload.model_validate(payload))
assert status == 200
assert resp == retrieval_response
@@ -576,7 +593,7 @@ class TestExternalApiTemplateListApiAdvanced:
),
):
with pytest.raises(DatasetNameDuplicateError):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalKnowledgeApiPayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
def test_get_with_pagination(self, app: Flask):
api = ExternalApiTemplateListApi()
@@ -591,7 +608,7 @@ class TestExternalApiTemplateListApiAdvanced:
return_value=(templates, 25),
) as get_external_knowledge_apis,
):
resp, status = method(api, MagicMock(), "tenant-1")
resp, status = method(api, ExternalApiTemplateListQuery(page=2, limit=3), MagicMock(), "tenant-1")
assert status == 200
assert resp == {
@@ -621,7 +638,7 @@ class TestExternalDatasetCreateApiAdvanced:
with app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload):
with pytest.raises(Forbidden):
method(api, MagicMock(), "tenant-1", current_user)
method(api, ExternalDatasetCreatePayload.model_validate(payload), MagicMock(), "tenant-1", current_user)
class TestExternalKnowledgeHitTestingApiAdvanced:
@@ -644,7 +661,7 @@ class TestExternalKnowledgeHitTestingApiAdvanced:
),
):
with pytest.raises(NotFound):
method(api, MagicMock(), current_user, "ds-1")
method(api, ExternalHitTestingPayload.model_validate(payload), MagicMock(), current_user, "ds-1")
def test_hit_testing_with_custom_retrieval_model(self, app: Flask, current_user: Account):
api = ExternalKnowledgeHitTestingApi()
@@ -682,7 +699,7 @@ class TestExternalKnowledgeHitTestingApiAdvanced:
},
) as external_retrieve,
):
resp = method(api, session, current_user, "ds-1")
resp = method(api, ExternalHitTestingPayload.model_validate(payload), session, current_user, "ds-1")
assert resp == {
"query": {"content": "test query"},
@@ -727,4 +744,4 @@ class TestBedrockRetrievalApiAdvanced:
),
):
with pytest.raises(ValueError):
method()
method(api, BedrockRetrievalPayload.model_validate(payload))

View File

@@ -7,6 +7,7 @@ from flask import Flask
from pytest_mock import MockerFixture
from werkzeug.exceptions import NotFound
from controllers.common.controller_schemas import MetadataUpdatePayload
from controllers.console import console_ns
from controllers.console.datasets.metadata import (
DatasetMetadataApi,
@@ -24,6 +25,7 @@ from services.metadata_service import MetadataService
@pytest.fixture
def app():
app = Flask("test_dataset_metadata")
app.config["TESTING"] = True
return app
@@ -76,7 +78,9 @@ class TestDatasetMetadataCreateApi:
MetadataService, "create_metadata", return_value={"id": "m1", "type": "string", "name": "author"}
),
):
result, status = method(api, MagicMock(), "tenant-1", current_user, dataset_id)
result, status = method(
api, MetadataArgs(type="string", name="author"), MagicMock(), "tenant-1", current_user, dataset_id
)
assert status == 201
assert result["type"] == "string"
assert result["name"] == "author"
@@ -92,7 +96,9 @@ class TestDatasetMetadataCreateApi:
patch.object(DatasetService, "get_dataset", return_value=None),
):
with pytest.raises(NotFound, match="Dataset not found"):
method(api, MagicMock(), "tenant-1", current_user, dataset_id)
method(
api, MetadataArgs(type="string", name="author"), MagicMock(), "tenant-1", current_user, dataset_id
)
class TestDatasetMetadataGetApi:
@@ -140,7 +146,15 @@ class TestDatasetMetadataApi:
return_value={"id": "m1", "type": "string", "name": "updated-name"},
),
):
result, status = method(api, MagicMock(), "tenant-1", current_user, dataset_id, metadata_id)
result, status = method(
api,
MetadataUpdatePayload(name="updated-name"),
MagicMock(),
"tenant-1",
current_user,
dataset_id,
metadata_id,
)
assert status == 200
assert result["type"] == "string"
assert result["name"] == "updated-name"
@@ -201,9 +215,14 @@ class TestDocumentMetadataEditApi:
patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload),
patch.object(DatasetService, "get_dataset", return_value=dataset),
patch.object(DatasetService, "check_dataset_permission"),
patch.object(MetadataOperationData, "model_validate", return_value=MagicMock()),
patch.object(MetadataService, "update_documents_metadata"),
):
result, status = method(api, MagicMock(), current_user, dataset_id)
result, status = method(
api,
MetadataOperationData(operation_data=[{"document_id": "doc-1", "metadata_list": []}]),
MagicMock(),
current_user,
dataset_id,
)
assert status == 204
assert result == ""

View File

@@ -6,6 +6,7 @@ from flask import Flask
from pydantic import ValidationError
import controllers.console.explore.recommended_app as module
from controllers.console.explore.recommended_app import RecommendedAppsQuery
from models import Account
from models.model import AppMode, IconType
@@ -32,7 +33,7 @@ class TestRecommendedAppListApi:
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
result = method(api, RecommendedAppsQuery(language="en-US"), make_account("fr-FR"))
service_mock.assert_called_once_with("en-US", session=ANY)
assert result == result_data
@@ -51,7 +52,7 @@ class TestRecommendedAppListApi:
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
result = method(api, RecommendedAppsQuery(), make_account("fr-FR"))
service_mock.assert_called_once_with("fr-FR", session=ANY)
assert result == result_data
@@ -70,7 +71,7 @@ class TestRecommendedAppListApi:
return_value=result_data,
) as service_mock,
):
result = method(api, make_account(None))
result = method(api, RecommendedAppsQuery(), make_account(None))
service_mock.assert_called_once_with(module.languages[0], session=ANY)
assert result == result_data
@@ -91,7 +92,7 @@ class TestLearnDifyAppListApi:
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
result = method(api, RecommendedAppsQuery(language="en-US"), make_account("fr-FR"))
service_mock.assert_called_once_with("en-US", session=ANY)
assert result == result_data
@@ -110,7 +111,7 @@ class TestLearnDifyAppListApi:
return_value=result_data,
) as service_mock,
):
result = method(api, make_account("fr-FR"))
result = method(api, RecommendedAppsQuery(), make_account("fr-FR"))
service_mock.assert_called_once_with("fr-FR", session=ANY)
assert result == result_data

View File

@@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
from uuid import uuid4
import pytest
from flask import Flask
from flask import Flask, request
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
@@ -28,6 +28,7 @@ from controllers.console.explore.error import (
NotCompletionAppError,
NotWorkflowAppError,
)
from controllers.console.explore.trial import ChatRequest, CompletionRequest, TextToSpeechRequest, WorkflowRunRequest
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
from core.errors.error import (
ModelCurrentlyNotSupportError,
@@ -258,9 +259,15 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
api = module.TrialAppWorkflowRunApi()
method = unwrap(api.post)
with app.test_request_context("/"):
with app.test_request_context("/", json={"inputs": {}}):
with pytest.raises(NotWorkflowAppError):
method(api, self.sqlite_session, account, MagicMock(mode=AppMode.CHAT))
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
MagicMock(mode=AppMode.CHAT),
)
def test_success(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -271,7 +278,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
patch.object(module.AppGenerateService, "generate", return_value=MagicMock()),
patch.object(module.RecommendedAppService, "add_trial_app_record"),
):
result = method(api, self.sqlite_session, account, trial_app_workflow)
result = method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
assert result is not None
@@ -288,7 +301,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_quota_exceeded(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -303,7 +322,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_model_not_support(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -318,7 +343,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_invoke_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -333,7 +364,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(CompletionRequestError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_rate_limit_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -348,7 +385,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(InvokeRateLimitHttpError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_value_error(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -363,7 +406,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(ValueError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
def test_workflow_generic_exception(self, app: Flask, trial_app_workflow: MagicMock, account: Account) -> None:
api = module.TrialAppWorkflowRunApi()
@@ -378,7 +427,13 @@ class TestTrialAppWorkflowRunApi(_UsesSQLiteSession):
),
):
with pytest.raises(InternalServerError):
method(api, self.sqlite_session, account, trial_app_workflow)
method(
api,
WorkflowRunRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_workflow,
)
class TestTrialChatApi(_UsesSQLiteSession):
@@ -388,7 +443,13 @@ class TestTrialChatApi(_UsesSQLiteSession):
with app.test_request_context("/", json={"inputs": {}, "query": "hi"}):
with pytest.raises(NotChatAppError):
method(api, self.sqlite_session, account, MagicMock(mode="completion"))
method(
api,
ChatRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
MagicMock(mode="completion"),
)
def test_success(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -399,7 +460,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
patch.object(module.AppGenerateService, "generate", return_value=MagicMock()),
patch.object(module.RecommendedAppService, "add_trial_app_record"),
):
result = method(api, self.sqlite_session, account, trial_app_chat)
result = method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
assert result is not None
@@ -416,7 +479,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(NotFound):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_conversation_completed(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -431,7 +496,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(ConversationCompletedError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_app_config_broken(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -446,7 +513,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(AppUnavailableError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -461,7 +530,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -476,7 +547,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_model_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -491,7 +564,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -506,7 +581,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(CompletionRequestError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_rate_limit_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -521,7 +598,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(InvokeRateLimitHttpError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_value_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -536,7 +615,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(ValueError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
def test_chat_generic_exception(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatApi()
@@ -551,7 +632,9 @@ class TestTrialChatApi(_UsesSQLiteSession):
),
):
with pytest.raises(InternalServerError):
method(api, self.sqlite_session, account, trial_app_chat)
method(
api, ChatRequest.model_validate(request.get_json()), self.sqlite_session, account, trial_app_chat
)
class TestTrialCompletionApi(_UsesSQLiteSession):
@@ -561,7 +644,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
with app.test_request_context("/", json={"inputs": {}, "query": ""}):
with pytest.raises(NotCompletionAppError):
method(api, self.sqlite_session, account, MagicMock(mode=AppMode.CHAT))
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
MagicMock(mode=AppMode.CHAT),
)
def test_success(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -572,7 +661,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
patch.object(module.AppGenerateService, "generate", return_value=MagicMock()),
patch.object(module.RecommendedAppService, "add_trial_app_record"),
):
result = method(api, self.sqlite_session, account, trial_app_completion)
result = method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
assert result is not None
@@ -589,7 +684,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(AppUnavailableError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_provider_not_init(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -604,7 +705,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_quota_exceeded(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -619,7 +726,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_model_not_support(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -634,7 +747,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_invoke_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -649,7 +768,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(CompletionRequestError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_rate_limit_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -664,7 +789,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(InternalServerError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_value_error(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -679,7 +810,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(ValueError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
def test_completion_generic_exception(self, app: Flask, trial_app_completion: MagicMock, account: Account) -> None:
api = module.TrialCompletionApi()
@@ -694,7 +831,13 @@ class TestTrialCompletionApi(_UsesSQLiteSession):
),
):
with pytest.raises(InternalServerError):
method(api, self.sqlite_session, account, trial_app_completion)
method(
api,
CompletionRequest.model_validate(request.get_json()),
self.sqlite_session,
account,
trial_app_completion,
)
class TestTrialMessageSuggestedQuestionApi:
@@ -843,7 +986,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(module.AppUnavailableError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_no_audio_uploaded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -862,7 +1009,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(module.NoAudioUploadedError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_missing_file_field_returns_400(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
"""A multipart POST with no `file` field must surface as 400, not 500.
@@ -883,7 +1034,11 @@ class TestTrialChatAudioApi:
patch.object(module.AudioService, "transcript_asr", side_effect=fake_asr),
):
with pytest.raises(module.NoAudioUploadedError) as exc_info:
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
assert exc_info.value.code == 400
@@ -904,7 +1059,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(module.AudioTooLargeError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_unsupported_audio_type(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -923,7 +1082,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(module.UnsupportedAudioTypeError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_provider_not_support_tts(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -942,7 +1105,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(module.ProviderNotSupportSpeechToTextError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_speech_to_text_disabled(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -960,7 +1127,11 @@ class TestTrialChatAudioApi:
),
):
with pytest.raises(SpeechToTextDisabledError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -975,7 +1146,11 @@ class TestTrialChatAudioApi:
patch.object(module.AudioService, "transcript_asr", side_effect=ProviderTokenNotInitError("test")),
):
with pytest.raises(ProviderNotInitializeError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -990,7 +1165,11 @@ class TestTrialChatAudioApi:
patch.object(module.AudioService, "transcript_asr", side_effect=QuotaExceededError()),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
class TestTrialChatTextApi:
@@ -1003,7 +1182,9 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", return_value={"audio": "base64_data"}),
patch.object(module.RecommendedAppService, "add_trial_app_record"),
):
result = method(api, account, trial_app_chat)
result = method(
api, TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}), account, trial_app_chat
)
assert result == {"audio": "base64_data"}
@@ -1018,7 +1199,9 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", transcript_tts),
patch.object(module.RecommendedAppService, "add_trial_app_record"),
):
result = method(api, account, trial_app_chat)
result = method(
api, TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}), account, trial_app_chat
)
assert result == {"audio": "base64_data"}
assert transcript_tts.call_args.kwargs["message_ref"] == MessageRef(
@@ -1040,7 +1223,12 @@ class TestTrialChatTextApi:
),
):
with pytest.raises(module.AppUnavailableError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_provider_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1055,7 +1243,12 @@ class TestTrialChatTextApi:
),
):
with pytest.raises(module.ProviderNotSupportSpeechToTextError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_audio_too_large(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1070,7 +1263,12 @@ class TestTrialChatTextApi:
),
):
with pytest.raises(module.AudioTooLargeError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_no_audio_uploaded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1085,7 +1283,12 @@ class TestTrialChatTextApi:
),
):
with pytest.raises(module.NoAudioUploadedError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_provider_not_init(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1096,7 +1299,12 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", side_effect=ProviderTokenNotInitError("test")),
):
with pytest.raises(ProviderNotInitializeError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1107,7 +1315,12 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", side_effect=QuotaExceededError()),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_model_not_support(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1118,7 +1331,12 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", side_effect=ModelCurrentlyNotSupportError()),
):
with pytest.raises(ProviderModelCurrentlyNotSupportError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1129,14 +1347,19 @@ class TestTrialChatTextApi:
patch.object(module.AudioService, "transcript_tts", side_effect=InvokeError("test error")),
):
with pytest.raises(CompletionRequestError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
class TestTrialAppWorkflowTaskStopApi:
def test_not_workflow_app(self, app: Flask, trial_app_chat: MagicMock) -> None:
api = module.TrialAppWorkflowTaskStopApi()
with app.test_request_context("/"):
with app.test_request_context("/", json={"inputs": {}}):
with pytest.raises(NotWorkflowAppError):
api.post(trial_app_chat, str(uuid4()))
@@ -1336,7 +1559,11 @@ class TestTrialChatAudioApiExceptionHandlers:
),
):
with pytest.raises(ProviderNotInitializeError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_quota_exceeded(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -1355,7 +1582,11 @@ class TestTrialChatAudioApiExceptionHandlers:
),
):
with pytest.raises(ProviderQuotaExceededError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
def test_invoke_error(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatAudioApi()
@@ -1374,7 +1605,11 @@ class TestTrialChatAudioApiExceptionHandlers:
),
):
with pytest.raises(CompletionRequestError):
method(api, account, trial_app_chat)
method(
api,
account,
trial_app_chat,
)
class TestTrialChatTextApiExceptionHandlers:
@@ -1391,7 +1626,12 @@ class TestTrialChatTextApiExceptionHandlers:
),
):
with pytest.raises(module.AppUnavailableError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)
def test_unsupported_audio_type(self, app: Flask, trial_app_chat: MagicMock, account: Account) -> None:
api = module.TrialChatTextApi()
@@ -1406,4 +1646,9 @@ class TestTrialChatTextApiExceptionHandlers:
),
):
with pytest.raises(module.UnsupportedAudioTypeError):
method(api, account, trial_app_chat)
method(
api,
TextToSpeechRequest.model_validate(request.get_json(silent=True) or {}),
account,
trial_app_chat,
)

View File

@@ -132,7 +132,14 @@ def test_draft_workflow_post_returns_400_for_invalid_graph(app: Flask, monkeypat
method="POST",
json={"graph": {"nodes": [], "edges": []}, "hash": "hash-1"},
):
response, status_code = handler(api, user, snippet)
response, status_code = handler(
api,
snippet_workflow_module.SnippetDraftSyncPayload.model_validate(
{"graph": {"nodes": [], "edges": []}, "hash": "hash-1"}
),
user,
snippet,
)
assert status_code == 400
assert response == {"message": "invalid graph"}
@@ -244,7 +251,11 @@ def test_list_published_snippet_workflows_includes_input_fields(
handler = unwrap(api.get)
with app.test_request_context("/snippets/snippet-1/workflows?page=1&limit=20"):
response = handler(api, snippet=snippet)
response = handler(
api,
snippet_workflow_module.SnippetWorkflowListQuery.model_validate({"page": 1, "limit": 20}),
snippet=snippet,
)
assert response["items"][0]["input_fields"] == input_fields
@@ -411,7 +422,15 @@ def test_update_published_snippet_workflow_returns_updated_workflow(
method="PATCH",
json={"marked_name": "v1", "marked_comment": "first version"},
):
response = handler(api, user, snippet, workflow_id="workflow-1")
response = handler(
api,
snippet_workflow_module.WorkflowUpdatePayload.model_validate(
{"marked_name": "v1", "marked_comment": "first version"}
),
user,
snippet,
workflow_id="workflow-1",
)
update_workflow.assert_called_once()
update_call = update_workflow.call_args.kwargs
@@ -432,7 +451,13 @@ def test_update_published_snippet_workflow_returns_400_when_no_fields(app: Flask
handler = unwrap(api.patch)
with app.test_request_context("/snippets/snippet-1/workflows/workflow-1", method="PATCH", json={}):
response, status_code = handler(api, _account("account-1"), _snippet(), workflow_id="workflow-1")
response, status_code = handler(
api,
snippet_workflow_module.WorkflowUpdatePayload(),
_account("account-1"),
_snippet(),
workflow_id="workflow-1",
)
assert status_code == 400
assert response == {"message": "No valid fields to update"}
@@ -468,7 +493,13 @@ def test_update_published_snippet_workflow_raises_not_found(
json={"marked_name": "v1"},
):
with pytest.raises(NotFound, match="Workflow not found"):
handler(api, user, snippet, workflow_id="missing-workflow")
handler(
api,
snippet_workflow_module.WorkflowUpdatePayload.model_validate({"marked_name": "v1"}),
user,
snippet,
workflow_id="missing-workflow",
)
sqlite_session.refresh(snippet)
assert snippet.name == "Snippet"

View File

@@ -255,6 +255,7 @@ def test_variable_patch_returns_persisted_variable_without_committing_when_no_ch
with app.test_request_context("/", method="PATCH", json={}):
result = handler(
api,
module.WorkflowDraftVariableUpdatePayload(),
_make_account(),
snippet=SimpleNamespace(id="snippet-1", tenant_id="tenant-1"),
variable_id="var-1",

View File

@@ -13,8 +13,11 @@ from controllers.console import console_ns
from controllers.console.tag.tags import (
TagBasePayload,
TagBindingCollectionApi,
TagBindingPayload,
TagBindingRemoveApi,
TagBindingRemovePayload,
TagListApi,
TagListQueryParam,
TagUpdateDeleteApi,
TagUpdateRequestPayload,
)
@@ -132,7 +135,7 @@ class TestTagListApi:
],
),
):
result, status = method(api, "tenant-1")
result, status = method(api, TagListQueryParam(type="knowledge"), "tenant-1")
assert status == 200
assert result == [{"id": "1", "name": "tag", "type": "knowledge", "binding_count": "1"}]
@@ -155,7 +158,7 @@ class TestTagListApi:
],
) as get_tags_mock,
):
result, status = method(api, "tenant-1")
result, status = method(api, TagListQueryParam(type="snippet"), "tenant-1")
get_tags_mock.assert_called_once()
assert get_tags_mock.call_args.args == ("snippet", "tenant-1", None)
@@ -219,7 +222,7 @@ class TestTagListApi:
with app.test_request_context("/"):
with pytest.raises(Forbidden):
method(api, None, readonly_user)
method(api, TagBasePayload(name="test", type=TagType.KNOWLEDGE), readonly_user)
class TestTagUpdateDeleteApi:
@@ -256,7 +259,7 @@ class TestTagUpdateDeleteApi:
with app.test_request_context("/"):
with pytest.raises(Forbidden):
method(api, None, readonly_user, "tag-1")
method(api, TagUpdateRequestPayload(name="test"), readonly_user, "tag-1")
def test_delete_success(self, app: Flask, admin_user, sqlite_engine: Engine):
api = TagUpdateDeleteApi()
@@ -375,7 +378,7 @@ class TestTagBindingCollectionApi:
payload_patch(payload),
patch("controllers.console.tag.tags.TagService.save_tag_binding") as save_mock,
):
result, status = method(api, admin_user)
result, status = method(api, TagBindingPayload.model_validate(payload), admin_user)
save_mock.assert_called_once()
assert status == 200
@@ -396,7 +399,7 @@ class TestTagBindingCollectionApi:
payload_patch(payload),
patch("controllers.console.tag.tags.TagService.save_tag_binding") as save_mock,
):
result, status = method(api, admin_user)
result, status = method(api, TagBindingPayload.model_validate(payload), admin_user)
save_mock.assert_called_once()
binding_payload = save_mock.call_args.args[0]
@@ -414,7 +417,11 @@ class TestTagBindingCollectionApi:
payload_patch({}),
):
with pytest.raises(Forbidden):
method(api, readonly_user)
method(
api,
TagBindingPayload(tag_ids=["tag-1"], target_id="target-1", type=TagType.KNOWLEDGE),
readonly_user,
)
class TestTagBindingRemoveApi:
@@ -433,7 +440,7 @@ class TestTagBindingRemoveApi:
payload_patch(payload),
patch("controllers.console.tag.tags.TagService.delete_tag_binding") as delete_mock,
):
result, status = method(api, admin_user)
result, status = method(api, TagBindingRemovePayload.model_validate(payload), admin_user)
delete_mock.assert_called_once()
delete_payload = delete_mock.call_args.args[0]
@@ -450,7 +457,11 @@ class TestTagBindingRemoveApi:
payload_patch({}),
):
with pytest.raises(Forbidden):
method(api, readonly_user)
method(
api,
TagBindingRemovePayload(tag_ids=["tag-1"], target_id="target-1", type=TagType.KNOWLEDGE),
readonly_user,
)
class TestTagResponseModel:

View File

@@ -16,6 +16,7 @@ from controllers.console.auth.error import (
from controllers.console.error import AccountInFreezeError
from controllers.console.workspace.account import (
AccountAvatarApi,
AccountAvatarQuery,
AccountDeleteApi,
AccountDeleteVerifyApi,
AccountInitApi,
@@ -216,7 +217,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/example",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=file_id), user)
assert result == {"avatar_url": "https://signed/example"}
sign_mock.assert_called_once_with(upload_file_id=file_id)
@@ -256,7 +257,7 @@ class TestAccountAvatarApiGet:
) as sign_mock,
):
with pytest.raises(NotFound):
method(api, user)
method(api, AccountAvatarQuery(avatar=file_id), user)
sign_mock.assert_not_called()
@@ -289,7 +290,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/example",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=file_id), user)
assert result == {"avatar_url": "https://signed/example"}
sign_mock.assert_called_once_with(upload_file_id=file_id)
@@ -308,7 +309,7 @@ class TestAccountAvatarApiGet:
return_value="https://signed/should-not-use",
) as sign_mock,
):
result = method(api, user)
result = method(api, AccountAvatarQuery(avatar=external), user)
assert result == {"avatar_url": external}
sign_mock.assert_not_called()

View File

@@ -17,7 +17,9 @@ from controllers.console.workspace.endpoint import (
EndpointIdPayload,
EndpointItemApi,
EndpointListApi,
EndpointListForPluginQuery,
EndpointListForSinglePluginApi,
EndpointListQuery,
EndpointUpdatePayload,
LegacyEndpointUpdatePayload,
)
@@ -146,7 +148,7 @@ class TestEndpointListApi:
return_value=[endpoint_entity],
),
):
result = method(api, "t1", "u1")
result = method(api, EndpointListQuery(page=1, page_size=10), "t1", "u1")
endpoint = result["endpoints"][0]
assert endpoint["id"] == "e1"
@@ -180,7 +182,7 @@ class TestEndpointListApi:
app.test_request_context("/?page=0&page_size=10"),
):
with pytest.raises(ValueError):
method(api, "t1", "u1")
method(api, EndpointListQuery(page=0, page_size=10), "t1", "u1")
class TestEndpointListForSinglePluginApi:
@@ -195,7 +197,7 @@ class TestEndpointListForSinglePluginApi:
return_value=[_endpoint_entity()],
),
):
result = method(api, "t1", "u1")
result = method(api, EndpointListForPluginQuery(page=1, page_size=10, plugin_id="p1"), "t1", "u1")
assert result["endpoints"][0]["id"] == "e1"
assert result["endpoints"][0]["settings"]["api_key"] == "pl********et"
@@ -209,7 +211,7 @@ class TestEndpointListForSinglePluginApi:
app.test_request_context("/?page=1&page_size=10"),
):
with pytest.raises(ValueError):
method(api, "t1", "u1")
method(api, EndpointListForPluginQuery(page=1, page_size=10), "t1", "u1")
class TestEndpointItemApi:

View File

@@ -15,6 +15,16 @@ from controllers.console.workspace.models import (
ModelProviderModelEnableApi,
ModelProviderModelParameterRuleApi,
ModelProviderModelValidateApi,
ParserCreateCredential,
ParserDeleteCredential,
ParserDeleteModels,
ParserGetCredentials,
ParserGetDefault,
ParserParameter,
ParserPostDefault,
ParserPostModels,
ParserSwitch,
ParserValidate,
)
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.errors.validate import CredentialsValidateFailedError
@@ -43,7 +53,7 @@ class TestDefaultModelApi:
},
}
result = method(api, "tenant1")
result = method(api, ParserGetDefault(model_type=ModelType.LLM), "tenant1")
assert "data" in result
@@ -65,7 +75,7 @@ class TestDefaultModelApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1")
result = method(api, ParserPostDefault.model_validate(payload), "tenant1")
assert result["result"] == "success"
@@ -79,7 +89,7 @@ class TestDefaultModelApi:
):
service.return_value.get_default_model_of_model_type.return_value = None
result = method(api, "t1")
result = method(api, ParserGetDefault(model_type=ModelType.LLM), "t1")
assert "data" in result
@@ -117,7 +127,7 @@ class TestModelProviderModelApi:
patch("controllers.console.workspace.models.ModelProviderService"),
patch("controllers.console.workspace.models.ModelLoadBalancingService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserPostModels.model_validate(payload), "tenant1", "openai")
assert status == 200
@@ -134,7 +144,7 @@ class TestModelProviderModelApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert status == 204
@@ -177,7 +187,13 @@ class TestModelProviderModelCredentialApi:
provider_service.return_value.provider_manager.get_provider_model_available_credentials.return_value = []
lb_service.return_value.get_load_balancing_configs.return_value = (False, [])
result = method(api, "tenant1", SimpleNamespace(id="u1"), "openai")
result = method(
api,
ParserGetCredentials(model="gpt-4", model_type=ModelType.LLM),
"tenant1",
SimpleNamespace(id="u1"),
"openai",
)
assert "credentials" in result
@@ -195,7 +211,7 @@ class TestModelProviderModelCredentialApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "tenant1", "openai")
result, status = method(api, ParserCreateCredential.model_validate(payload), "tenant1", "openai")
assert status == 201
@@ -212,7 +228,13 @@ class TestModelProviderModelCredentialApi:
service.return_value.provider_manager.get_provider_model_available_credentials.return_value = []
lb.return_value.get_load_balancing_configs.return_value = (False, [])
result = method(api, "t1", SimpleNamespace(id="u1"), "openai")
result = method(
api,
ParserGetCredentials(model="gpt", model_type=ModelType.LLM),
"t1",
SimpleNamespace(id="u1"),
"openai",
)
assert result["credentials"] == {}
@@ -230,7 +252,7 @@ class TestModelProviderModelCredentialApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result, status = method(api, "t1", "openai")
result, status = method(api, ParserDeleteCredential.model_validate(payload), "t1", "openai")
assert status == 204
@@ -250,7 +272,7 @@ class TestModelProviderModelCredentialSwitchApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserSwitch.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -269,7 +291,7 @@ class TestModelEnableDisableApis:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -286,7 +308,7 @@ class TestModelEnableDisableApis:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserDeleteModels.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -306,7 +328,7 @@ class TestModelProviderModelValidateApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.models.ModelProviderService"),
):
result = method(api, "tenant1", "openai")
result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai")
assert result["result"] == "success"
@@ -327,7 +349,7 @@ class TestModelProviderModelValidateApi:
):
service_mock.return_value.validate_model_credentials.side_effect = CredentialsValidateFailedError("invalid")
result = method(api, "tenant1", "openai")
result = method(api, ParserValidate.model_validate(payload), "tenant1", "openai")
assert result["result"] == "error"
@@ -343,7 +365,7 @@ class TestParameterAndAvailableModels:
):
service_mock.return_value.get_model_parameter_rules.return_value = []
result = method(api, "tenant1", "openai")
result = method(api, ParserParameter(model="gpt-4"), "tenant1", "openai")
assert "data" in result
@@ -371,7 +393,7 @@ class TestParameterAndAvailableModels:
):
service.return_value.get_model_parameter_rules.return_value = []
result = method(api, "t1", "openai")
result = method(api, ParserParameter(model="gpt"), "t1", "openai")
assert result["data"] == []

View File

@@ -10,6 +10,25 @@ from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import Forbidden
from controllers.console.workspace.plugin import (
ParserAsset,
ParserAutoUpgradeChange,
ParserAutoUpgradeFetch,
ParserDynamicOptions,
ParserDynamicOptionsWithCredentials,
ParserExcludePlugin,
ParserGithubInstall,
ParserGithubUpgrade,
ParserGithubUpload,
ParserIcon,
ParserLatest,
ParserList,
ParserMarketplaceUpgrade,
ParserPermissionChange,
ParserPluginIdentifierQuery,
ParserPluginIdentifiers,
ParserReadme,
ParserTasks,
ParserUninstall,
PluginAssetApi,
PluginAutoUpgradeExcludePluginApi,
PluginCategoryListApi,
@@ -29,6 +48,7 @@ from controllers.console.workspace.plugin import (
PluginFetchPermissionApi,
PluginIconApi,
PluginInstalledIdsApi,
PluginInstalledIdsQuery,
PluginInstallFromGithubApi,
PluginInstallFromMarketplaceApi,
PluginInstallFromPkgApi,
@@ -367,7 +387,7 @@ class TestPluginListLatestVersionsApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginService.list_latest_versions", return_value=versions),
):
result = method(api)
result = method(api, ParserLatest.model_validate(payload))
assert result == {
"versions": {
@@ -389,7 +409,7 @@ class TestPluginListLatestVersionsApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api)
result = method(api, ParserLatest.model_validate(payload))
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -435,7 +455,7 @@ class TestPluginListApi:
return_value=plugins_with_total,
) as mock_list_with_total,
):
result = method(api, "t1", "u1")
result = method(api, ParserList(page=1, page_size=10), "t1", "u1")
assert result == {"plugins": [_expected_plugin_entity_dump()], "total": 1}
mock_list_with_total.assert_called_once_with("t1", "u1", 1, 10)
@@ -584,7 +604,7 @@ class TestPluginIconApi:
app.test_request_context("/?tenant_id=t1&filename=a.png"),
patch("controllers.console.workspace.plugin.PluginService.get_asset", return_value=(b"x", "image/png")),
):
response = method(api)
response = method(api, ParserIcon.model_validate({"tenant_id": "t1", "filename": "a.png"}))
assert response.mimetype == "image/png"
@@ -598,7 +618,9 @@ class TestPluginAssetApi:
app.test_request_context("/?plugin_unique_identifier=p&file_name=a.bin"),
patch("controllers.console.workspace.plugin.PluginService.extract_asset", return_value=b"x"),
):
response = method(api, "t1")
response = method(
api, ParserAsset.model_validate({"plugin_unique_identifier": "p", "file_name": "a.bin"}), "t1"
)
assert response.mimetype == "application/octet-stream"
@@ -653,7 +675,7 @@ class TestPluginInstallFromPkgApi:
"controllers.console.workspace.plugin.PluginService.install_from_local_pkg", return_value={"ok": True}
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifiers.model_validate(payload), "t1")
assert result["ok"] is True
@@ -673,7 +695,7 @@ class TestPluginUninstallApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginService.uninstall", return_value=True) as uninstall_mock,
):
result = method(api, "t1")
result = method(api, ParserUninstall.model_validate(payload), "t1")
assert result["success"] is True
uninstall_mock.assert_called_once_with(
@@ -699,7 +721,7 @@ class TestPluginChangePermissionApi:
app.test_request_context("/", json=payload),
):
with pytest.raises(Forbidden):
method(api, "t1", user)
method(api, ParserPermissionChange(), "t1", user)
def test_change_permission_success(self, app: Flask):
api = PluginChangePermissionApi()
@@ -716,7 +738,7 @@ class TestPluginChangePermissionApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginPermissionService.change_permission", return_value=True),
):
result = method(api, "t1", user)
result = method(api, ParserPermissionChange(), "t1", user)
assert result["success"] is True
@@ -747,7 +769,14 @@ class TestPluginFetchDynamicSelectOptionsApi:
return_value=[_dynamic_option()],
),
):
result = method(api, "t1", user)
result = method(
api,
ParserDynamicOptions.model_validate(
{"plugin_id": "p", "provider": "x", "action": "y", "parameter": "z", "provider_type": "tool"}
),
"t1",
user,
)
assert result == {"options": [_expected_dynamic_option_dump()]}
@@ -761,7 +790,7 @@ class TestPluginReadmeApi:
app.test_request_context("/?plugin_unique_identifier=p"),
patch("controllers.console.workspace.plugin.PluginService.fetch_plugin_readme", return_value="readme"),
):
result = method(api, "t1")
result = method(api, ParserReadme.model_validate({"plugin_unique_identifier": "p"}), "t1")
assert result["readme"] == "readme"
@@ -780,7 +809,7 @@ class TestPluginListInstallationsFromIdsApi:
return_value=[_plugin_installation()],
),
):
result = method(api, "t1")
result = method(api, ParserLatest.model_validate(payload), "t1")
assert result == {"plugins": [_expected_plugin_installation_dump()]}
@@ -797,7 +826,7 @@ class TestPluginListInstallationsFromIdsApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserLatest.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -813,7 +842,7 @@ class TestPluginInstalledIdsApi:
return_value=["langgenius/openai", "langgenius/anthropic"],
) as list_installed_plugin_ids,
):
result = method(api, "t1")
result = method(api, PluginInstalledIdsQuery.model_validate({"category": "tool"}), "t1")
assert result == {"plugin_ids": ["langgenius/openai", "langgenius/anthropic"]}
list_installed_plugin_ids.assert_called_once_with("t1", PluginCategory.Tool)
@@ -829,7 +858,7 @@ class TestPluginInstalledIdsApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, PluginInstalledIdsQuery.model_validate({"category": "tool"}), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -847,7 +876,7 @@ class TestPluginUploadFromGithubApi:
"controllers.console.workspace.plugin.PluginService.upload_pkg_from_github", return_value={"ok": True}
),
):
result = method(api, "t1")
result = method(api, ParserGithubUpload.model_validate(payload), "t1")
assert result["ok"] is True
@@ -864,7 +893,7 @@ class TestPluginUploadFromGithubApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserGithubUpload.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -933,7 +962,7 @@ class TestPluginInstallFromGithubApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginService.install_from_github", return_value={"ok": True}),
):
result = method(api, "t1")
result = method(api, ParserGithubInstall.model_validate(payload), "t1")
assert result["ok"] is True
@@ -955,7 +984,7 @@ class TestPluginInstallFromGithubApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserGithubInstall.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -973,7 +1002,7 @@ class TestPluginInstallFromMarketplaceApi:
return_value={"ok": True},
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifiers.model_validate(payload), "t1")
assert result["ok"] is True
@@ -990,7 +1019,7 @@ class TestPluginInstallFromMarketplaceApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifiers.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1006,7 +1035,7 @@ class TestPluginFetchMarketplacePkgApi:
return_value=_plugin_declaration(),
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifierQuery.model_validate({"plugin_unique_identifier": "p"}), "t1")
assert result == {"manifest": _expected_plugin_declaration_dump()}
@@ -1021,7 +1050,7 @@ class TestPluginFetchMarketplacePkgApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifierQuery.model_validate({"plugin_unique_identifier": "p"}), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1036,7 +1065,7 @@ class TestPluginFetchManifestApi:
app.test_request_context("/?plugin_unique_identifier=p"),
patch("controllers.console.workspace.plugin.PluginService.fetch_plugin_manifest", return_value=manifest),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifierQuery.model_validate({"plugin_unique_identifier": "p"}), "t1")
assert result == {"manifest": _expected_plugin_declaration_dump()}
@@ -1051,7 +1080,7 @@ class TestPluginFetchManifestApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserPluginIdentifierQuery.model_validate({"plugin_unique_identifier": "p"}), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1067,7 +1096,7 @@ class TestPluginFetchInstallTasksApi:
return_value=[_plugin_task()],
),
):
result = method(api, "t1")
result = method(api, ParserTasks(), "t1")
assert result == {"tasks": [_expected_plugin_task_dump()]}
@@ -1082,7 +1111,7 @@ class TestPluginFetchInstallTasksApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserTasks(), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1217,7 +1246,7 @@ class TestPluginUpgradeFromMarketplaceApi:
return_value={"ok": True},
),
):
result = method(api, "t1")
result = method(api, ParserMarketplaceUpgrade.model_validate(payload), "t1")
assert result["ok"] is True
@@ -1237,7 +1266,7 @@ class TestPluginUpgradeFromMarketplaceApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserMarketplaceUpgrade.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1261,7 +1290,7 @@ class TestPluginUpgradeFromGithubApi:
return_value={"ok": True},
),
):
result = method(api, "t1")
result = method(api, ParserGithubUpgrade.model_validate(payload), "t1")
assert result["ok"] is True
@@ -1284,7 +1313,7 @@ class TestPluginUpgradeFromGithubApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1")
result = method(api, ParserGithubUpgrade.model_validate(payload), "t1")
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1309,7 +1338,7 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
return_value=[_dynamic_option()],
),
):
result = method(api, "t1", user)
result = method(api, ParserDynamicOptionsWithCredentials.model_validate(payload), "t1", user)
assert result == {"options": [_expected_dynamic_option_dump()]}
@@ -1333,7 +1362,7 @@ class TestPluginFetchDynamicSelectOptionsWithCredentialsApi:
side_effect=PluginDaemonClientSideError("error"),
),
):
result = method(api, "t1", user)
result = method(api, ParserDynamicOptionsWithCredentials.model_validate(payload), "t1", user)
assert result == ({"code": "plugin_error", "message": "error"}, 400)
@@ -1361,7 +1390,7 @@ class TestPluginChangeAutoUpgradeApi:
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
) as change,
):
result = method(api, "t1", user)
result = method(api, ParserAutoUpgradeChange.model_validate(payload), "t1", user)
assert result["success"] is True
change.assert_called_once()
@@ -1389,7 +1418,7 @@ class TestPluginChangeAutoUpgradeApi:
"controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=True
) as change,
):
result = method(api, "t1", user)
result = method(api, ParserAutoUpgradeChange.model_validate(payload), "t1", user)
assert result["success"] is True
change.assert_called_once()
@@ -1416,7 +1445,7 @@ class TestPluginChangeAutoUpgradeApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.change_strategy", return_value=False),
):
result = method(api, "t1", user)
result = method(api, ParserAutoUpgradeChange.model_validate(payload), "t1", user)
assert result["success"] is False
@@ -1442,7 +1471,11 @@ class TestPluginFetchAutoUpgradeApi:
return_value=auto_upgrade,
),
):
result = method(api, "t1")
result = method(
api,
ParserAutoUpgradeFetch.model_validate({"category": TenantPluginAutoUpgradeCategory.TOOL.value}),
"t1",
)
assert result["category"] == TenantPluginAutoUpgradeCategory.TOOL
assert result["auto_upgrade"]["upgrade_time_of_day"] == 1
@@ -1462,7 +1495,11 @@ class TestPluginFetchAutoUpgradeApi:
return_value=78300,
),
):
result = method(api, "t1")
result = method(
api,
ParserAutoUpgradeFetch.model_validate({"category": TenantPluginAutoUpgradeCategory.MODEL.value}),
"t1",
)
assert result == {
"category": TenantPluginAutoUpgradeCategory.MODEL,
@@ -1487,7 +1524,7 @@ class TestPluginAutoUpgradeExcludePluginApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.exclude_plugin", return_value=True),
):
result = method(api, "t1")
result = method(api, ParserExcludePlugin.model_validate(payload), "t1")
assert result["success"] is True
@@ -1501,6 +1538,6 @@ class TestPluginAutoUpgradeExcludePluginApi:
app.test_request_context("/", json=payload),
patch("controllers.console.workspace.plugin.PluginAutoUpgradeService.exclude_plugin", return_value=False),
):
result = method(api, "t1")
result = method(api, ParserExcludePlugin.model_validate(payload), "t1")
assert result["success"] is False

View File

@@ -26,6 +26,7 @@ from werkzeug.exceptions import Forbidden, NotFound
from configs import dify_config
from controllers.console.workspace import rbac as rbac_mod
from controllers.console.workspace.rbac import _RolesListQuery
@pytest.fixture
@@ -175,7 +176,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"page": 1, "limit": 2, "include_owner": 1}),
)
owner_permission_keys = rbac_mod._LEGACY_ROLE_PERMISSION_KEYS["owner"]
valid_owner_permission_keys = []
@@ -230,7 +234,7 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery())
names = [r["name"] for r in response["data"]]
assert "owner" not in names
@@ -242,7 +246,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"include_owner": 1}),
)
names = [r["name"] for r in response["data"]]
assert "owner" in names
@@ -254,7 +261,7 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac._current_ids", return_value=("tenant-1", "acct-1")),
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list"),
):
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
response = inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi(), _RolesListQuery())
names = [r["name"] for r in response["data"]]
assert "owner" not in names
@@ -267,7 +274,10 @@ class TestPaginationMapping:
patch("controllers.console.workspace.rbac.svc.RBACService.Roles.list") as mock_list,
patch("controllers.console.workspace.rbac._dump", return_value={}),
):
inspect.unwrap(rbac_mod.RBACRolesApi.get)(rbac_mod.RBACRolesApi())
inspect.unwrap(rbac_mod.RBACRolesApi.get)(
rbac_mod.RBACRolesApi(),
_RolesListQuery.model_validate({"page": 2, "limit": 50, "reverse": True, "include_owner": 1}),
)
_, kwargs = mock_list.call_args
options = kwargs["options"]

View File

@@ -10,6 +10,13 @@ from flask import Flask
from werkzeug.exceptions import Forbidden
from controllers.console.workspace.tool_providers import (
ApiToolProviderAddPayload,
ApiToolProviderDeletePayload,
ApiToolProviderUpdatePayload,
BuiltinProviderDefaultCredentialPayload,
BuiltinToolAddPayload,
BuiltinToolCredentialDeletePayload,
BuiltinToolUpdatePayload,
ToolApiListApi,
ToolApiProviderAddApi,
ToolApiProviderDeleteApi,
@@ -32,6 +39,7 @@ from controllers.console.workspace.tool_providers import (
ToolLabelsApi,
ToolOAuthCallback,
ToolOAuthCustomClient,
ToolOAuthCustomClientPayload,
ToolPluginOAuthApi,
ToolProviderListApi,
ToolWorkflowListApi,
@@ -39,6 +47,9 @@ from controllers.console.workspace.tool_providers import (
ToolWorkflowProviderDeleteApi,
ToolWorkflowProviderGetApi,
ToolWorkflowProviderUpdateApi,
WorkflowToolCreatePayload,
WorkflowToolDeletePayload,
WorkflowToolUpdatePayload,
is_valid_url,
)
from core.tools.entities.api_entities import ToolProviderApiEntity as CoreToolProviderApiEntity
@@ -276,7 +287,8 @@ class TestBuiltinProviderApis:
return_value={"result": "success"},
),
):
assert method(api, "t1", "provider")["result"] == "success"
req = BuiltinToolCredentialDeletePayload(credential_id="cid")
assert method(api, req, "t1", "provider")["result"] == "success"
def test_add_invalid_type(self, app: Flask) -> None:
api = ToolBuiltinProviderAddApi()
@@ -286,7 +298,13 @@ class TestBuiltinProviderApis:
app.test_request_context("/", json={"credentials": empty_mapping(), "type": "invalid"}),
):
with pytest.raises(ValueError):
method(api, "t", make_account(), "provider")
method(
api,
BuiltinToolAddPayload(credentials=empty_mapping(), type="invalid"),
"t",
make_account(),
"provider",
)
def test_add_success(self, app: Flask) -> None:
api = ToolBuiltinProviderAddApi()
@@ -301,7 +319,7 @@ class TestBuiltinProviderApis:
return_value={"result": "success"},
),
):
assert method(api, "t", make_account(), "provider")["result"] == "success"
assert method(api, BuiltinToolAddPayload(**payload), "t", make_account(), "provider")["result"] == "success"
def test_update(self, app: Flask) -> None:
api = ToolBuiltinProviderUpdateApi()
@@ -316,7 +334,8 @@ class TestBuiltinProviderApis:
return_value={"result": "success"},
),
):
assert method(api, "t", make_account(), "provider")["result"] == "success"
req = BuiltinToolUpdatePayload(**payload)
assert method(api, req, "t", make_account(), "provider")["result"] == "success"
def test_get_credentials(self, app: Flask) -> None:
api = ToolBuiltinProviderGetCredentialsApi()
@@ -369,7 +388,8 @@ class TestBuiltinProviderApis:
return_value={"result": "success"},
),
):
assert method(api, "t", "provider")["result"] == "success"
req = BuiltinProviderDefaultCredentialPayload(id="c1")
assert method(api, req, "t", "provider")["result"] == "success"
def test_get_credential_info(self, app: Flask) -> None:
api = ToolBuiltinProviderGetCredentialInfoApi()
@@ -418,7 +438,7 @@ class TestApiProviderApis:
return_value={"result": "success"},
) as create_api_tool_provider,
):
assert method(api, "t", make_account()) == {"result": "success"}
assert method(api, ApiToolProviderAddPayload(**payload), "t", make_account()) == {"result": "success"}
create_api_tool_provider.assert_called_once()
assert create_api_tool_provider.call_args.args[3] == emoji_icon()
@@ -472,7 +492,7 @@ class TestApiProviderApis:
return_value={"result": "success"},
) as update_api_tool_provider,
):
assert method(api, "t", make_account()) == {"result": "success"}
assert method(api, ApiToolProviderUpdatePayload(**payload), "t", make_account()) == {"result": "success"}
update_api_tool_provider.assert_called_once()
assert update_api_tool_provider.call_args.args[4] == emoji_icon()
@@ -488,7 +508,8 @@ class TestApiProviderApis:
return_value={"result": "success"},
),
):
assert method(api, "t", make_account())["result"] == "success"
req = ApiToolProviderDeletePayload(provider="p")
assert method(api, req, "t", make_account())["result"] == "success"
def test_get(self, app: Flask) -> None:
api = ToolApiProviderGetApi()
@@ -525,7 +546,7 @@ class TestWorkflowApis:
return_value={"result": "success"},
) as create_workflow_tool,
):
assert method(api, "t", make_account()) == {"result": "success"}
assert method(api, WorkflowToolCreatePayload(**payload), "t", make_account()) == {"result": "success"}
create_workflow_tool.assert_called_once()
assert create_workflow_tool.call_args.kwargs["icon"] == emoji_icon()
@@ -549,7 +570,7 @@ class TestWorkflowApis:
return_value={"result": "success"},
) as update_workflow_tool,
):
result = method(api, "t", make_account())
result = method(api, WorkflowToolUpdatePayload(**payload), "t", make_account())
assert result == {"result": "success"}
update_workflow_tool.assert_called_once()
@@ -566,7 +587,8 @@ class TestWorkflowApis:
return_value={"result": "success"},
),
):
assert method(api, "t", make_account())["result"] == "success"
req = WorkflowToolDeletePayload(workflow_tool_id="123e4567-e89b-12d3-a456-426614174000")
assert method(api, req, "t", make_account())["result"] == "success"
def test_get_error(self, app: Flask) -> None:
api = ToolWorkflowProviderGetApi()
@@ -671,7 +693,8 @@ class TestOAuthCustomClient:
return_value={"result": "success"},
),
):
assert method(api, "t", "provider") == {"result": "success"}
req = ToolOAuthCustomClientPayload(client_params={"a": 1})
assert method(api, req, "t", "provider") == {"result": "success"}
def test_get_custom_client(self, app: Flask) -> None:
api = ToolOAuthCustomClient()

View File

@@ -15,15 +15,19 @@ from controllers.console.workspace.trigger_providers import (
TriggerOAuthAuthorizeApi,
TriggerOAuthCallbackApi,
TriggerOAuthClientManageApi,
TriggerOAuthClientPayload,
TriggerProviderIconApi,
TriggerProviderInfoApi,
TriggerProviderListApi,
TriggerSubscriptionBuilderBuildApi,
TriggerSubscriptionBuilderCreateApi,
TriggerSubscriptionBuilderCreatePayload,
TriggerSubscriptionBuilderGetApi,
TriggerSubscriptionBuilderLogsApi,
TriggerSubscriptionBuilderUpdateApi,
TriggerSubscriptionBuilderUpdatePayload,
TriggerSubscriptionBuilderVerifyApi,
TriggerSubscriptionBuilderVerifyPayload,
TriggerSubscriptionListApi,
TriggerSubscriptionUpdateApi,
TriggerSubscriptionVerifyApi,
@@ -163,7 +167,13 @@ class TestTriggerSubscriptionBuilderApis:
return_value=subscription_builder(),
),
):
result = method(api, "t1", mock_user(), "github")
result = method(
api,
TriggerSubscriptionBuilderCreatePayload(credential_type="UNAUTHORIZED"),
"t1",
mock_user(),
"github",
)
assert result["subscription_builder"]["id"] == "b1"
def test_get_builder(self, app: Flask) -> None:
@@ -196,7 +206,14 @@ class TestTriggerSubscriptionBuilderApis:
return_value={"verified": True},
),
):
assert method(api, "t1", mock_user(), "github", "b1") == {"verified": True}
assert method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={"a": 1}),
"t1",
mock_user(),
"github",
"b1",
) == {"verified": True}
def test_verify_builder_error(self, app: Flask) -> None:
api = TriggerSubscriptionBuilderVerifyApi()
@@ -210,7 +227,14 @@ class TestTriggerSubscriptionBuilderApis:
),
):
with pytest.raises(ValueError):
method(api, "t1", mock_user(), "github", "b1")
method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"b1",
)
def test_update_builder(self, app: Flask) -> None:
api = TriggerSubscriptionBuilderUpdateApi()
@@ -223,7 +247,17 @@ class TestTriggerSubscriptionBuilderApis:
return_value=subscription_builder(),
) as mock_update_builder,
):
assert method(api, "t1", mock_user(), "github", "b1")["id"] == "b1"
assert (
method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="n"),
"t1",
mock_user(),
"github",
"b1",
)["id"]
== "b1"
)
mock_update_builder.assert_called_once_with(
tenant_id="t1",
user_id="u1",
@@ -263,7 +297,14 @@ class TestTriggerSubscriptionBuilderApis:
return_value=None,
),
):
assert method(api, "t1", mock_user(), "github", "b1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="x"),
"t1",
mock_user(),
"github",
"b1",
) == {"result": "success"}
class TestTriggerSubscriptionCrud:
@@ -283,7 +324,12 @@ class TestTriggerSubscriptionCrud:
),
patch("controllers.console.workspace.trigger_providers.TriggerProviderService.update_trigger_subscription"),
):
assert method(api, "t1", "s1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(name="x"),
"t1",
"s1",
) == {"result": "success"}
def test_update_not_found(self, app: Flask) -> None:
api = TriggerSubscriptionUpdateApi()
@@ -297,7 +343,7 @@ class TestTriggerSubscriptionCrud:
),
):
with pytest.raises(NotFoundError):
method(api, "t1", "x")
method(api, TriggerSubscriptionBuilderUpdatePayload(name="x"), "t1", "x")
def test_update_rebuild(self, app: Flask) -> None:
api = TriggerSubscriptionUpdateApi()
@@ -319,7 +365,12 @@ class TestTriggerSubscriptionCrud:
"controllers.console.workspace.trigger_providers.TriggerProviderService.rebuild_trigger_subscription"
),
):
assert method(api, "t1", "s1") == {"result": "success"}
assert method(
api,
TriggerSubscriptionBuilderUpdatePayload(credentials={}),
"t1",
"s1",
) == {"result": "success"}
class TestTriggerOAuthApis:
@@ -499,7 +550,12 @@ class TestTriggerOAuthClientManageApi:
return_value={"result": "success"},
),
):
assert method(api, "t1", "github") == {"result": "success"}
assert method(
api,
TriggerOAuthClientPayload(enabled=True),
"t1",
"github",
) == {"result": "success"}
def test_delete_client(self, app: Flask) -> None:
api = TriggerOAuthClientManageApi()
@@ -526,7 +582,7 @@ class TestTriggerOAuthClientManageApi:
),
):
with pytest.raises(BadRequest):
method(api, "t1", "github")
method(api, TriggerOAuthClientPayload(enabled=True), "t1", "github")
class TestTriggerSubscriptionVerifyApi:
@@ -541,7 +597,14 @@ class TestTriggerSubscriptionVerifyApi:
return_value={"verified": True},
),
):
assert method(api, "t1", mock_user(), "github", "s1") == {"verified": True}
assert method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"s1",
) == {"verified": True}
@pytest.mark.parametrize("raised_exception", [ValueError("bad"), Exception("boom")])
def test_verify_errors(self, app: Flask, raised_exception: Exception) -> None:
@@ -556,4 +619,11 @@ class TestTriggerSubscriptionVerifyApi:
),
):
with pytest.raises(BadRequest):
method(api, "t1", mock_user(), "github", "s1")
method(
api,
TriggerSubscriptionBuilderVerifyPayload(credentials={}),
"t1",
mock_user(),
"github",
"s1",
)

View File

@@ -2,6 +2,7 @@ import pytest
from core.workflow.file_reference import build_file_reference
from models.agent_config_entities import (
AgentSoulModelSettings,
DeclaredArrayItem,
DeclaredOutputChildConfig,
DeclaredOutputConfig,
@@ -9,6 +10,22 @@ from models.agent_config_entities import (
)
def test_agent_soul_model_settings_preserves_plugin_declared_parameters() -> None:
settings = AgentSoulModelSettings.model_validate(
{
"temperature": 0.7,
"enable_thinking": True,
"thinking_budget": 4096,
}
)
dumped = settings.model_dump(mode="json", exclude_none=True)
assert dumped["temperature"] == 0.7
assert dumped["enable_thinking"] is True
assert dumped["thinking_budget"] == 4096
def test_file_default_value_accepts_canonical_reference_mapping() -> None:
reference = build_file_reference(record_id="tool-file-1")

View File

@@ -1586,6 +1586,7 @@ export type AgentSoulModelSettings = {
stop?: Array<string> | null
temperature?: number | null
top_p?: number | null
[key: string]: unknown
}
export type AgentSandboxProviderConfig = {

View File

@@ -2037,6 +2037,13 @@ export const zAgentModelResponseFormatConfig = z.object({
/**
* 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.
*/
export const zAgentSoulModelSettings = z.object({
frequency_penalty: z.number().nullish(),

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