Compare commits
8 Commits
codex/refa
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4db77fb0ac | ||
|
|
8913fd4609 | ||
|
|
2886a37305 | ||
|
|
925ca49f21 | ||
|
|
a1577fc48c | ||
|
|
acda0341ed | ||
|
|
98378cfe03 | ||
|
|
3f89a3c742 |
@@ -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})
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -12,9 +12,11 @@ import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import { LoroDoc, LoroMap } from 'loro-crdt'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { CollaborationManager } from '../collaboration-manager'
|
||||
import { webSocketClient } from '../websocket-manager'
|
||||
import * as websocketManager from '../websocket-manager'
|
||||
import { attachCrdtRuntime } from './test-crdt-runtime'
|
||||
|
||||
const { webSocketClient } = websocketManager
|
||||
|
||||
type ReactFlowStore = {
|
||||
getState: () => {
|
||||
getNodes: () => Node[]
|
||||
@@ -180,6 +182,108 @@ describe('CollaborationManager socket and subscription behavior', () => {
|
||||
expect(manager.canUseLocalDraftFallback()).toBe(false)
|
||||
})
|
||||
|
||||
it('switches to local editing and stops reconnecting when the initial connection fails', async () => {
|
||||
vi.spyOn(websocketManager, 'isDefaultSocketUrl').mockReturnValueOnce(true)
|
||||
const manager = new CollaborationManager()
|
||||
attachCrdtRuntime(manager)
|
||||
const socket = createMockSocket('socket-initial-failure')
|
||||
socket.connected = false
|
||||
const reactFlowStore: ReactFlowStore = {
|
||||
getState: () => ({
|
||||
getNodes: () => [],
|
||||
setNodes: vi.fn(),
|
||||
getEdges: () => [],
|
||||
setEdges: vi.fn(),
|
||||
}),
|
||||
}
|
||||
const connectSpy = vi
|
||||
.spyOn(webSocketClient, 'connect')
|
||||
.mockReturnValue(socket as unknown as Socket)
|
||||
vi.spyOn(webSocketClient, 'getSocket').mockReturnValue(socket as unknown as Socket)
|
||||
vi.spyOn(webSocketClient, 'isConnected').mockReturnValue(false)
|
||||
const disconnectSpy = vi
|
||||
.spyOn(webSocketClient, 'disconnect')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const connectionId = await manager.connect('app-initial-failure', reactFlowStore)
|
||||
const graphReadyStates: boolean[] = []
|
||||
manager.onGraphReadyChange((isReady) => graphReadyStates.push(isReady))
|
||||
|
||||
expect(manager.canPersistLocalGraph()).toBe(false)
|
||||
expect(graphReadyStates.at(-1)).toBe(false)
|
||||
|
||||
socket.trigger('connect_error', new Error('connect failed'))
|
||||
|
||||
expect(manager.canApplyLocalGraphMutation()).toBe(true)
|
||||
expect(manager.canPersistLocalGraph()).toBe(true)
|
||||
expect(disconnectSpy).toHaveBeenCalledWith('app-initial-failure')
|
||||
expect(graphReadyStates.at(-1)).toBe(true)
|
||||
|
||||
const secondConnectionId = await manager.connect('app-initial-failure', reactFlowStore)
|
||||
expect(connectSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
manager.disconnect(connectionId)
|
||||
manager.disconnect(secondConnectionId)
|
||||
expect(manager.canPersistLocalGraph()).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps editing blocked when a configured socket URL fails to connect', () => {
|
||||
vi.spyOn(websocketManager, 'isDefaultSocketUrl')
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(false)
|
||||
const manager = new CollaborationManager()
|
||||
attachCrdtRuntime(manager)
|
||||
const internals = getManagerInternals(manager)
|
||||
const socket = createMockSocket('socket-configured-failure')
|
||||
socket.connected = false
|
||||
internals.currentAppId = 'app-configured-failure'
|
||||
vi.spyOn(webSocketClient, 'getSocket').mockReturnValue(socket as unknown as Socket)
|
||||
vi.spyOn(webSocketClient, 'isConnected').mockReturnValue(false)
|
||||
const disconnectSpy = vi.spyOn(webSocketClient, 'disconnect')
|
||||
internals.setupSocketEventListeners(socket as unknown as Socket)
|
||||
|
||||
socket.trigger('connect_error', new Error('connect failed'))
|
||||
|
||||
expect(manager.canUseLocalDraftFallback()).toBe(false)
|
||||
expect(manager.canApplyLocalGraphMutation()).toBe(false)
|
||||
expect(manager.canPersistLocalGraph()).toBe(false)
|
||||
expect(disconnectSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps editing blocked when a previously established connection fails', async () => {
|
||||
const manager = new CollaborationManager()
|
||||
attachCrdtRuntime(manager)
|
||||
const socket = createMockSocket('socket-established-failure')
|
||||
const reactFlowStore: ReactFlowStore = {
|
||||
getState: () => ({
|
||||
getNodes: () => [],
|
||||
setNodes: vi.fn(),
|
||||
getEdges: () => [],
|
||||
setEdges: vi.fn(),
|
||||
}),
|
||||
}
|
||||
vi.spyOn(webSocketClient, 'connect').mockReturnValue(socket as unknown as Socket)
|
||||
vi.spyOn(webSocketClient, 'getSocket').mockReturnValue(socket as unknown as Socket)
|
||||
vi.spyOn(webSocketClient, 'isConnected').mockImplementation(() => socket.connected)
|
||||
const disconnectSpy = vi
|
||||
.spyOn(webSocketClient, 'disconnect')
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const connectionId = await manager.connect('app-established-failure', reactFlowStore)
|
||||
socket.trigger('status', { isLeader: true })
|
||||
expect(manager.canApplyLocalGraphMutation()).toBe(true)
|
||||
|
||||
socket.connected = false
|
||||
socket.trigger('disconnect', 'transport close')
|
||||
socket.trigger('connect_error', new Error('reconnect failed'))
|
||||
|
||||
expect(manager.canApplyLocalGraphMutation()).toBe(false)
|
||||
expect(manager.canPersistLocalGraph()).toBe(false)
|
||||
expect(disconnectSpy).not.toHaveBeenCalled()
|
||||
|
||||
manager.disconnect(connectionId)
|
||||
})
|
||||
|
||||
it('emits cursor/sync/workflow events via collaboration_event when connected', async () => {
|
||||
const { manager, internals } = setupManagerWithDoc()
|
||||
const socket = createMockSocket('socket-connected')
|
||||
|
||||
@@ -47,6 +47,14 @@ describe('WebSocketClient', () => {
|
||||
ioMock.mockReset()
|
||||
})
|
||||
|
||||
it('identifies the built-in socket URLs', async () => {
|
||||
const { isDefaultSocketUrl } = await import('../websocket-manager')
|
||||
|
||||
expect(isDefaultSocketUrl('ws://localhost')).toBe(true)
|
||||
expect(isDefaultSocketUrl('ws://localhost:5001')).toBe(true)
|
||||
expect(isDefaultSocketUrl('wss://collaboration.example.com')).toBe(false)
|
||||
})
|
||||
|
||||
it('connects with default url and registers base listeners', async () => {
|
||||
const mockSocket = createMockSocket('socket-fallback')
|
||||
ioMock.mockImplementation(() => mockSocket)
|
||||
|
||||
@@ -21,7 +21,7 @@ import type { CRDTProvider } from './crdt-provider'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { EventEmitter } from './event-emitter'
|
||||
import { emitWithAuthGuard, webSocketClient } from './websocket-manager'
|
||||
import { emitWithAuthGuard, isDefaultSocketUrl, webSocketClient } from './websocket-manager'
|
||||
|
||||
type CrdtRuntime = (typeof import('./crdt-runtime'))['crdtRuntime']
|
||||
|
||||
@@ -185,6 +185,7 @@ export class CollaborationManager {
|
||||
private visibilityListenerAttached = false
|
||||
private crdtTrusted = false
|
||||
private hasEstablishedConnection = false
|
||||
private localDraftFallbackActive = false
|
||||
private rebuildCrdtOnNextConnect = false
|
||||
private reconnectedWithFreshDoc = false
|
||||
private awaitingSnapshotImport = false
|
||||
@@ -729,6 +730,7 @@ export class CollaborationManager {
|
||||
private forceDisconnect = ({
|
||||
preserveConnectIntent = false,
|
||||
}: { preserveConnectIntent?: boolean } = {}): void => {
|
||||
this.localDraftFallbackActive = false
|
||||
if (this.currentAppId) webSocketClient.disconnect(this.currentAppId)
|
||||
|
||||
this.clearInitialSyncRetry()
|
||||
@@ -782,7 +784,10 @@ export class CollaborationManager {
|
||||
|
||||
canUseLocalDraftFallback(): boolean {
|
||||
// A graph from a previously connected session must recover through collaboration before saving.
|
||||
return !this.isConnected() && !this.hasEstablishedConnection
|
||||
return (
|
||||
this.localDraftFallbackActive ||
|
||||
(isDefaultSocketUrl() && !this.isConnected() && !this.hasEstablishedConnection)
|
||||
)
|
||||
}
|
||||
|
||||
getNodes(): Node[] {
|
||||
@@ -799,14 +804,30 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
canPersistLocalGraph(): boolean {
|
||||
if (this.localDraftFallbackActive) return true
|
||||
return this.crdtTrusted && !this.graphReloadRequired && this.graphViewActive !== false
|
||||
}
|
||||
|
||||
canApplyLocalGraphMutation(): boolean {
|
||||
if (!this.currentAppId) return true
|
||||
if (this.localDraftFallbackActive || !this.currentAppId) return true
|
||||
return this.canPersistLocalGraph() && this.getActiveSocket()?.connected === true
|
||||
}
|
||||
|
||||
private activateLocalDraftFallback(socket: Socket): void {
|
||||
// Before the first connection, ReactFlow still holds the HTTP draft baseline. Once a
|
||||
// collaboration session has existed, that graph may be stale and must recover through CRDT.
|
||||
if (!isDefaultSocketUrl() || this.localDraftFallbackActive || this.hasEstablishedConnection)
|
||||
return
|
||||
if (this.getActiveSocket() !== socket || !this.currentAppId) return
|
||||
|
||||
const appId = this.currentAppId
|
||||
this.localDraftFallbackActive = true
|
||||
this.clearInitialSyncRetry()
|
||||
this.pendingInitialSync = false
|
||||
webSocketClient.disconnect(appId)
|
||||
this.emitGraphReadyState()
|
||||
}
|
||||
|
||||
private emitGraphReadyState(): void {
|
||||
this.eventEmitter.emit('graphReadyChange', this.canApplyLocalGraphMutation())
|
||||
}
|
||||
@@ -1932,6 +1953,7 @@ export class CollaborationManager {
|
||||
|
||||
socket.on('connect_error', (error: Error) => {
|
||||
console.error('WebSocket connection error:', error)
|
||||
this.activateLocalDraftFallback(socket)
|
||||
this.eventEmitter.emit('stateChange', { isConnected: false, error: error.message })
|
||||
})
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import { SOCKET_URL } from '@/config'
|
||||
|
||||
type AckArgs = unknown[]
|
||||
|
||||
export const isDefaultSocketUrl = (socketUrl = SOCKET_URL) =>
|
||||
socketUrl === 'ws://localhost' || socketUrl === 'ws://localhost:5001'
|
||||
|
||||
const isUnauthorizedAck = (...ackArgs: AckArgs): boolean => {
|
||||
const [first, second] = ackArgs
|
||||
|
||||
|
||||
@@ -131,9 +131,10 @@ describe('VariableInspect Panel', () => {
|
||||
})
|
||||
|
||||
it('should render the listening state and stop the workflow on demand', () => {
|
||||
renderPanel({
|
||||
const { store } = renderPanel({
|
||||
isListening: true,
|
||||
listeningTriggerType: BlockEnum.TriggerWebhook,
|
||||
showVariableInspectPanel: true,
|
||||
})
|
||||
|
||||
fireEvent.click(
|
||||
@@ -144,6 +145,9 @@ describe('VariableInspect Panel', () => {
|
||||
expect(mockEmit).toHaveBeenCalledWith({
|
||||
type: EVENT_WORKFLOW_STOP,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
expect(store.getState().showVariableInspectPanel).toBe(false)
|
||||
})
|
||||
|
||||
it('should render the empty state and close the panel from the header action', () => {
|
||||
@@ -151,7 +155,7 @@ describe('VariableInspect Panel', () => {
|
||||
showVariableInspectPanel: true,
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button')[0]!)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(screen.getByText('workflow.debug.variableInspect.emptyTip'))!.toBeInTheDocument()
|
||||
expect(store.getState().showVariableInspectPanel).toBe(false)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import type { NodeProps } from '../types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
@@ -26,7 +24,7 @@ export type currentVarType = {
|
||||
nodeData: NodeProps['data']
|
||||
}
|
||||
|
||||
const Panel: FC = () => {
|
||||
const Panel = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const bottomPanelWidth = useStore((s) => s.bottomPanelWidth)
|
||||
@@ -175,8 +173,11 @@ const Panel: FC = () => {
|
||||
<div className="system-sm-semibold-uppercase text-text-primary">
|
||||
{t(($) => $['debug.variableInspect.title'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<ActionButton onClick={() => setShowVariableInspectPanel(false)}>
|
||||
<RiCloseLine className="size-4" />
|
||||
<ActionButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
onClick={() => setShowVariableInspectPanel(false)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line size-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="grow p-2">
|
||||
@@ -193,8 +194,11 @@ const Panel: FC = () => {
|
||||
<div className="system-sm-semibold-uppercase text-text-primary">
|
||||
{t(($) => $['debug.variableInspect.title'], { ns: 'workflow' })}
|
||||
</div>
|
||||
<ActionButton onClick={() => setShowVariableInspectPanel(false)}>
|
||||
<RiCloseLine className="size-4" />
|
||||
<ActionButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
onClick={() => setShowVariableInspectPanel(false)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-close-line size-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="grow p-2">
|
||||
|
||||
Reference in New Issue
Block a user