Compare commits
54 Commits
feat/workf
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f4dff852 | ||
|
|
a85e583720 | ||
|
|
7522ae14b2 | ||
|
|
e09eee8fd1 | ||
|
|
b692ddd80c | ||
|
|
5ebb389d26 | ||
|
|
fff6f7cf2f | ||
|
|
516fd12b8d | ||
|
|
925ca49f21 | ||
|
|
a1577fc48c | ||
|
|
acda0341ed | ||
|
|
98378cfe03 | ||
|
|
3f89a3c742 | ||
|
|
e036075e22 | ||
|
|
5a84cde198 | ||
|
|
4e18ab0ad4 | ||
|
|
2a742486c5 | ||
|
|
d362bdbb4e | ||
|
|
60d6be5b13 | ||
|
|
cc1b55aaf7 | ||
|
|
d1fa17032e | ||
|
|
916d7e1c96 | ||
|
|
c094117ca2 | ||
|
|
c6b1311aa3 | ||
|
|
515c3d0c62 | ||
|
|
fe94ddaaeb | ||
|
|
3e8c0fdcef | ||
|
|
055462fe6e | ||
|
|
06e229e8b2 | ||
|
|
fa498ad7a2 | ||
|
|
a279be91a8 | ||
|
|
12300f3833 | ||
|
|
b439b6a800 | ||
|
|
7a7d8fe19e | ||
|
|
2635d1b3df | ||
|
|
a952487789 | ||
|
|
c9e6db5ef2 | ||
|
|
b31e66107c | ||
|
|
42d6589ce2 | ||
|
|
f00d0329d4 | ||
|
|
af9d2d00a3 | ||
|
|
ae8cb996c7 | ||
|
|
adb89a8926 | ||
|
|
3a4920c595 | ||
|
|
276c3b6e02 | ||
|
|
7ff5c85796 | ||
|
|
b8330df96e | ||
|
|
7422052759 | ||
|
|
ab0e131e87 | ||
|
|
1375dc3864 | ||
|
|
6530f2bcdb | ||
|
|
7039217c99 | ||
|
|
924d787e41 | ||
|
|
baba36d47d |
101
.github/workflows/accessibility-e2e.yml
vendored
Normal file
101
.github/workflows/accessibility-e2e.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
name: Manual WCAG Accessibility Audit
|
||||
run-name: WCAG ${{ inputs.level }} · ${{ inputs.page }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
page:
|
||||
description: Page to scan
|
||||
required: true
|
||||
default: home
|
||||
type: choice
|
||||
options:
|
||||
- home
|
||||
- sign-in
|
||||
- studio
|
||||
- agents
|
||||
- knowledge
|
||||
- integrations
|
||||
- all
|
||||
level:
|
||||
description: WCAG level to scan
|
||||
required: true
|
||||
default: aa
|
||||
type: choice
|
||||
options:
|
||||
- aa
|
||||
- a
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: accessibility-e2e-${{ github.ref }}-${{ inputs.page }}-${{ inputs.level }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
accessibility:
|
||||
name: WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} · ${{ inputs.page }}
|
||||
runs-on: depot-ubuntu-24.04-4
|
||||
timeout-minutes: 120
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup web dependencies
|
||||
uses: ./.github/actions/setup-web
|
||||
|
||||
- name: Setup UV and Python
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
enable-cache: true
|
||||
python-version: '3.12'
|
||||
cache-dependency-glob: |
|
||||
api/uv.lock
|
||||
|
||||
- name: Install API dependencies
|
||||
run: uv sync --project api --dev
|
||||
|
||||
- name: Install Chromium for accessibility E2E
|
||||
timeout-minutes: 15
|
||||
working-directory: ./e2e
|
||||
run: vp run e2e:install:ci:chromium
|
||||
|
||||
- name: Run automated WCAG Level ${{ inputs.level == 'a' && 'A' || 'AA' }} checks
|
||||
working-directory: ./e2e
|
||||
env:
|
||||
E2E_ADMIN_EMAIL: e2e-admin@example.com
|
||||
E2E_ADMIN_NAME: E2E Admin
|
||||
E2E_ADMIN_PASSWORD: E2eAdmin12345
|
||||
E2E_FORCE_WEB_BUILD: '1'
|
||||
E2E_INIT_PASSWORD: E2eInit12345
|
||||
WCAG_PAGE: ${{ inputs.page }}
|
||||
run: |
|
||||
if [[ "$WCAG_PAGE" == "all" ]]; then
|
||||
vp run e2e:accessibility:${{ inputs.level }}
|
||||
else
|
||||
pnpm exec tsx ./scripts/run-cucumber.ts --full -- --tags "@axe and @wcag-${{ inputs.level }} and @wcag-page-$WCAG_PAGE"
|
||||
fi
|
||||
|
||||
- name: Upload accessibility Cucumber report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: cucumber-report-accessibility-${{ inputs.level }}-${{ inputs.page }}
|
||||
path: e2e/cucumber-report
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload accessibility E2E logs
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-logs-accessibility-${{ inputs.level }}-${{ inputs.page }}
|
||||
path: e2e/.logs/*.log
|
||||
include-hidden-files: true
|
||||
retention-days: 7
|
||||
@@ -209,12 +209,11 @@ At the same time, please consider supporting Dify by sharing it on social media
|
||||
|
||||
## Star History
|
||||
|
||||
<!-- GitHub token name: star-history -->
|
||||
<a href="https://www.star-history.com/?repos=langgenius%2Fdify&type=date&legend=top-left">
|
||||
<a href="https://star-history.dera.page/#langgenius/dify&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&theme=dark&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=langgenius/dify&type=date&legend=top-left&sealed_token=p-SWD-UXZEDc5a2d0EMfgMmyCwVyRlSof0Qox68v7k4PvQPKRlCx0jDIlNztw7mbA6DEn96R50DojO9pCi5LQUlDBAoIhRswt8-GuC8K3rZQ3naJUXbuHqR_oItIW2F0NNM-7npevzw5SXp7L5mwixgqJncAvAGzuGq5zmhQfiDRSW_Jd6y8TQCZMJbt" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=langgenius/dify&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -95,3 +95,33 @@ forbidden_modules =
|
||||
services.workspace_member_role_resolver
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:setup-service-boundary]
|
||||
name = Setup application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.setup_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
[importlinter:contract:schema-definition-service-boundary]
|
||||
name = Schema definition application service is framework and persistence neutral
|
||||
type = forbidden
|
||||
source_modules =
|
||||
services.schema_definition_service
|
||||
forbidden_modules =
|
||||
configs
|
||||
controllers
|
||||
extensions
|
||||
flask
|
||||
models
|
||||
repositories
|
||||
sqlalchemy
|
||||
werkzeug
|
||||
|
||||
@@ -10,6 +10,8 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dify_agent.protocol import RunFailureType
|
||||
|
||||
|
||||
class AgentBackendError(Exception):
|
||||
"""Base error for API-side Agent backend integration failures."""
|
||||
@@ -54,6 +56,7 @@ class AgentBackendRunFailedError(AgentBackendError):
|
||||
|
||||
run_id: str
|
||||
detail: Any
|
||||
error_type: RunFailureType | None
|
||||
reason: str | None
|
||||
source_event_id: str | None
|
||||
|
||||
@@ -63,11 +66,13 @@ class AgentBackendRunFailedError(AgentBackendError):
|
||||
detail: Any,
|
||||
*,
|
||||
message: str | None = None,
|
||||
error_type: RunFailureType | None = None,
|
||||
reason: str | None = None,
|
||||
source_event_id: str | None = None,
|
||||
) -> None:
|
||||
self.run_id = run_id
|
||||
self.detail = detail
|
||||
self.error_type = error_type
|
||||
self.reason = reason
|
||||
self.source_event_id = source_event_id
|
||||
display_message = message or f"Agent backend run failed: {run_id}"
|
||||
|
||||
@@ -22,6 +22,7 @@ from dify_agent.protocol import (
|
||||
RunCancelledEvent,
|
||||
RunEvent,
|
||||
RunFailedEvent,
|
||||
RunFailureType,
|
||||
RunStartedEvent,
|
||||
RunSucceededEvent,
|
||||
)
|
||||
@@ -96,6 +97,7 @@ class AgentBackendRunFailedInternalEvent(AgentBackendInternalEventBase):
|
||||
|
||||
type: Literal[AgentBackendInternalEventType.RUN_FAILED] = AgentBackendInternalEventType.RUN_FAILED
|
||||
error: str
|
||||
error_type: RunFailureType | None = None
|
||||
reason: str | None = None
|
||||
|
||||
|
||||
@@ -180,6 +182,7 @@ class AgentBackendRunEventAdapter:
|
||||
run_id=event.run_id,
|
||||
source_event_id=event.id,
|
||||
error=event.data.error,
|
||||
error_type=event.data.error_type,
|
||||
reason=event.data.reason,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -159,8 +159,12 @@ class AgentBackendModelConfig(BaseModel):
|
||||
# ``DifyPluginLLMLayerConfig.model_settings`` is pydantic_ai's ``ModelSettings``
|
||||
# TypedDict (closed: unknown keys are rejected, explicit ``None`` values fail the
|
||||
# per-field type checks). Agent Soul model settings carry a wider, nullable shape
|
||||
# (``stop`` / ``response_format`` plus null-padded fields), so the layer config
|
||||
# only receives the keys the runtime contract accepts.
|
||||
# (``stop`` / ``response_format`` plus null-padded fields, plus arbitrary
|
||||
# plugin-declared parameters such as Qwen's ``enable_thinking``), so the layer
|
||||
# config only receives the keys the runtime contract accepts directly; anything
|
||||
# else is forwarded through ``extra_body``, the TypedDict's own escape hatch for
|
||||
# provider-specific parameters (see
|
||||
# ``dify_agent.adapters.llm.model._map_model_settings_to_parameters``).
|
||||
_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
|
||||
"temperature",
|
||||
"top_p",
|
||||
@@ -168,6 +172,7 @@ _AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS = (
|
||||
"frequency_penalty",
|
||||
"max_tokens",
|
||||
)
|
||||
_AGENT_MODEL_SETTINGS_KNOWN_KEYS = frozenset({*_AGENT_MODEL_SETTINGS_PASSTHROUGH_KEYS, "stop", "response_format"})
|
||||
|
||||
|
||||
def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None:
|
||||
@@ -177,6 +182,15 @@ def _agent_model_settings(settings: Mapping[str, JsonValue]) -> dict[str, JsonVa
|
||||
stop = settings.get("stop")
|
||||
if isinstance(stop, list) and stop:
|
||||
sanitized["stop_sequences"] = stop
|
||||
|
||||
extra_body: dict[str, JsonValue] = {
|
||||
key: value
|
||||
for key, value in settings.items()
|
||||
if key not in _AGENT_MODEL_SETTINGS_KNOWN_KEYS and value is not None
|
||||
}
|
||||
if extra_body:
|
||||
sanitized["extra_body"] = extra_body
|
||||
|
||||
return sanitized or None
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -10,7 +9,7 @@ from controllers.common.schema import (
|
||||
register_response_schema_models,
|
||||
)
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from services.advanced_prompt_template_service import AdvancedPromptTemplateArgs, AdvancedPromptTemplateService
|
||||
@@ -49,12 +48,12 @@ class AdvancedPromptTemplateList(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
args = AdvancedPromptTemplateQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(AdvancedPromptTemplateQuery)
|
||||
def get(self, req_data: AdvancedPromptTemplateQuery):
|
||||
prompt_args: AdvancedPromptTemplateArgs = {
|
||||
"app_mode": args.app_mode,
|
||||
"model_mode": args.model_mode,
|
||||
"model_name": args.model_name,
|
||||
"has_context": args.has_context,
|
||||
"app_mode": req_data.app_mode,
|
||||
"model_mode": req_data.model_mode,
|
||||
"model_name": req_data.model_name,
|
||||
"has_context": req_data.has_context,
|
||||
}
|
||||
return AdvancedPromptTemplateService.get_prompt(prompt_args)
|
||||
|
||||
@@ -21,6 +21,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -211,12 +212,12 @@ def _upload_skill_for_app(*, session: Session, current_user: Account, app_model:
|
||||
|
||||
|
||||
def _commit_drive_file_for_app(*, session: Session, current_user: Account, app_model: App, allow_node_id: bool = True):
|
||||
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
|
||||
query = query_params_from_request(AgentDriveMutationQuery)
|
||||
node_id = query.node_id if allow_node_id else None
|
||||
agent_id = _resolve_agent_id(session, app_model, node_id)
|
||||
if not agent_id:
|
||||
return _agent_not_bound()
|
||||
payload = AgentDriveFilePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
upload_file = session.scalar(
|
||||
select(UploadFile).where(
|
||||
@@ -341,11 +342,11 @@ class AgentLogApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.AGENT_CHAT])
|
||||
def get(self, session: Session, app_model: App):
|
||||
@model_validate(AgentLogQuery)
|
||||
def get(self, req_data: AgentLogQuery, session: Session, app_model: App):
|
||||
"""Get agent logs"""
|
||||
args = AgentLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
return AgentService.get_agent_logs(app_model, args.conversation_id, args.message_id, session)
|
||||
return AgentService.get_agent_logs(app_model, req_data.conversation_id, req_data.message_id, session)
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/skills/upload")
|
||||
|
||||
@@ -25,6 +25,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -87,14 +88,21 @@ class AgentAppFeatureConfigResource(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
@model_validate(AgentAppFeaturesPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentAppFeaturesPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
args = AgentAppFeaturesPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
new_app_model_config = AgentAppFeatureConfigService.update_features(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
config=args.model_dump(exclude_none=True),
|
||||
config=req_data.model_dump(exclude_none=True),
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Console routes for Agent App and workflow Agent sandbox file access.
|
||||
|
||||
The API accepts product-facing Conversation, Build Draft, or Workflow Node
|
||||
Execution locators and proxies list/read/upload to the agent backend's
|
||||
``/sandbox`` contract.
|
||||
Execution locators and proxies list/read/download to the agent backend's
|
||||
``/execution-bindings/files`` contract.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -11,10 +11,8 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from dify_agent.client import DifyAgentClientError, DifyAgentHTTPError, DifyAgentTimeoutError
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.common.schema import (
|
||||
query_params_from_model,
|
||||
@@ -22,12 +20,12 @@ from controllers.common.schema import (
|
||||
register_response_schema_models,
|
||||
register_schema_models,
|
||||
)
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.agent.app_helpers import resolve_agent_runtime_app_model
|
||||
from controllers.console.app.error import AppNotFoundError
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -43,11 +41,17 @@ from services.agent_app_sandbox_service import (
|
||||
WorkflowAgentSandboxService,
|
||||
)
|
||||
|
||||
_BINDING_PATH_DESCRIPTION = (
|
||||
"Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; "
|
||||
"`~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside "
|
||||
"Workspace are governed by backend isolation, not a Workspace-root restriction"
|
||||
)
|
||||
|
||||
|
||||
class AgentSandboxListQuery(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
|
||||
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class AgentSandboxInfoQuery(BaseModel):
|
||||
@@ -58,28 +62,28 @@ class AgentSandboxInfoQuery(BaseModel):
|
||||
class AgentSandboxFileQuery(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class AgentSandboxUploadPayload(BaseModel):
|
||||
class AgentSandboxDownloadPayload(BaseModel):
|
||||
caller_type: Literal["conversation", "build_draft"]
|
||||
caller_id: str = Field(min_length=1, description="Agent App caller ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxListQuery(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(default=".", description="Directory path relative to the sandbox workspace")
|
||||
path: str = Field(default=".", description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxFileQuery(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class WorkflowAgentSandboxUploadPayload(BaseModel):
|
||||
class WorkflowAgentSandboxDownloadPayload(BaseModel):
|
||||
node_execution_id: str = Field(min_length=1, description="Workflow node execution ID")
|
||||
path: str = Field(min_length=1, description="File path relative to the sandbox workspace")
|
||||
path: str = Field(min_length=1, description=_BINDING_PATH_DESCRIPTION)
|
||||
|
||||
|
||||
class SandboxFileEntryResponse(ResponseModel):
|
||||
@@ -107,21 +111,21 @@ class SandboxReadResponse(ResponseModel):
|
||||
text: str | None = None
|
||||
|
||||
|
||||
class SandboxUploadResponse(ResponseModel):
|
||||
class SandboxDownloadResponse(ResponseModel):
|
||||
url: str
|
||||
|
||||
|
||||
register_schema_models(
|
||||
console_ns,
|
||||
AgentSandboxUploadPayload,
|
||||
WorkflowAgentSandboxUploadPayload,
|
||||
AgentSandboxDownloadPayload,
|
||||
WorkflowAgentSandboxDownloadPayload,
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
SandboxInfoResponse,
|
||||
SandboxListResponse,
|
||||
SandboxReadResponse,
|
||||
SandboxUploadResponse,
|
||||
SandboxDownloadResponse,
|
||||
)
|
||||
|
||||
|
||||
@@ -152,14 +156,14 @@ class AgentAppSandboxInfoResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxInfoQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().get_info(
|
||||
result = service.get_info(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -181,14 +185,14 @@ class AgentAppSandboxListResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxListQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().list_files(
|
||||
result = service.list_files(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -211,14 +215,14 @@ class AgentAppSandboxReadResource(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
def get(self, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
query = query_params_from_request(AgentSandboxFileQuery)
|
||||
try:
|
||||
result = AgentAppSandboxService().read_file(
|
||||
result = service.read_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=query.caller_type,
|
||||
caller_id=query.caller_id,
|
||||
@@ -230,30 +234,36 @@ class AgentAppSandboxReadResource(Resource):
|
||||
return result.model_dump()
|
||||
|
||||
|
||||
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/upload")
|
||||
class AgentAppSandboxUploadResource(Resource):
|
||||
@console_ns.doc("upload_agent_app_sandbox_file")
|
||||
@console_ns.doc(description="Upload one Agent App sandbox file and return a signed download URL")
|
||||
@console_ns.expect(console_ns.models[AgentSandboxUploadPayload.__name__])
|
||||
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
|
||||
@console_ns.route("/agent/<uuid:agent_id>/sandbox/files/download")
|
||||
class AgentAppSandboxDownloadResource(Resource):
|
||||
@console_ns.doc("download_agent_app_sandbox_file")
|
||||
@console_ns.doc(description="Create a ToolFile from one Agent App Binding file and return its download URL")
|
||||
@console_ns.expect(console_ns.models[AgentSandboxDownloadPayload.__name__])
|
||||
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_current_user
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, current_user: Account, tenant_id: str, agent_id: UUID):
|
||||
app_model = resolve_agent_runtime_app_model(session=session, tenant_id=tenant_id, agent_id=agent_id)
|
||||
payload = AgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
|
||||
@model_validate(AgentSandboxDownloadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentSandboxDownloadPayload,
|
||||
current_user: Account,
|
||||
tenant_id: str,
|
||||
agent_id: UUID,
|
||||
):
|
||||
service = AgentAppSandboxService()
|
||||
app_id = service.resolve_app_id(tenant_id=tenant_id, agent_id=str(agent_id))
|
||||
try:
|
||||
result = AgentAppSandboxService().upload_file(
|
||||
result = service.download_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=app_id,
|
||||
agent_id=str(agent_id),
|
||||
caller_type=payload.caller_type,
|
||||
caller_id=payload.caller_id,
|
||||
caller_type=req_data.caller_type,
|
||||
caller_id=req_data.caller_id,
|
||||
account_id=current_user.id,
|
||||
path=payload.path,
|
||||
path=req_data.path,
|
||||
)
|
||||
except Exception as exc:
|
||||
return _handle(exc)
|
||||
@@ -333,29 +343,41 @@ class WorkflowAgentSandboxReadResource(Resource):
|
||||
|
||||
|
||||
@console_ns.route(
|
||||
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/upload"
|
||||
"/apps/<uuid:app_id>/workflow-runs/<uuid:workflow_run_id>/agent-nodes/<string:node_id>/sandbox/files/download"
|
||||
)
|
||||
class WorkflowAgentSandboxUploadResource(Resource):
|
||||
@console_ns.doc("upload_workflow_agent_sandbox_file")
|
||||
@console_ns.doc(description="Upload one workflow Agent sandbox file and return a signed download URL")
|
||||
@console_ns.expect(console_ns.models[WorkflowAgentSandboxUploadPayload.__name__])
|
||||
@console_ns.response(200, "Uploaded", console_ns.models[SandboxUploadResponse.__name__])
|
||||
class WorkflowAgentSandboxDownloadResource(Resource):
|
||||
@console_ns.doc("download_workflow_agent_sandbox_file")
|
||||
@console_ns.doc(description="Create a ToolFile from one workflow Agent Binding file and return its download URL")
|
||||
@console_ns.expect(console_ns.models[WorkflowAgentSandboxDownloadPayload.__name__])
|
||||
@console_ns.response(200, "Download URL returned", console_ns.models[SandboxDownloadResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, app_model: App, workflow_run_id: UUID, node_id: str):
|
||||
payload = WorkflowAgentSandboxUploadPayload.model_validate(request.get_json(silent=True) or {})
|
||||
@model_validate(WorkflowAgentSandboxDownloadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowAgentSandboxDownloadPayload,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
app_id: UUID,
|
||||
workflow_run_id: UUID,
|
||||
node_id: str,
|
||||
):
|
||||
service = WorkflowAgentSandboxService()
|
||||
resolved_app_id = service.resolve_app_id(tenant_id=tenant_id, app_id=str(app_id))
|
||||
if resolved_app_id is None:
|
||||
raise AppNotFoundError()
|
||||
try:
|
||||
result = WorkflowAgentSandboxService().upload_file(
|
||||
result = service.download_file(
|
||||
tenant_id=tenant_id,
|
||||
app_id=app_model.id,
|
||||
app_id=resolved_app_id,
|
||||
workflow_run_id=str(workflow_run_id),
|
||||
node_id=node_id,
|
||||
node_execution_id=payload.node_execution_id,
|
||||
path=payload.path,
|
||||
session=db.session(),
|
||||
node_execution_id=req_data.node_execution_id,
|
||||
account_id=current_user.id,
|
||||
path=req_data.path,
|
||||
)
|
||||
except Exception as exc:
|
||||
return _handle(exc)
|
||||
|
||||
@@ -30,6 +30,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -810,14 +811,21 @@ class AgentConfigFilesByAgentApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, tenant_id: str, current_user: Account, agent_id: UUID):
|
||||
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentConfigFileUploadPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AgentConfigFileUploadPayload,
|
||||
session: Session,
|
||||
tenant_id: str,
|
||||
current_user: Account,
|
||||
agent_id: UUID,
|
||||
):
|
||||
return _with_agent_route_target(
|
||||
session=session,
|
||||
tenant_id=tenant_id,
|
||||
agent_id=agent_id,
|
||||
current_user=current_user,
|
||||
action=lambda target: _file_upload_response(target, payload),
|
||||
action=lambda target: _file_upload_response(target, req_data),
|
||||
)
|
||||
|
||||
|
||||
@@ -849,13 +857,13 @@ class AgentConfigFilesApi(Resource):
|
||||
@with_current_user
|
||||
@with_session
|
||||
@get_app_model(mode=_WORKFLOW_APP_MODES)
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
payload = AgentConfigFileUploadPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AgentConfigFileUploadPayload)
|
||||
def post(self, req_data: AgentConfigFileUploadPayload, session: Session, current_user: Account, app_model: App):
|
||||
return _with_app_route_target(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
current_user=current_user,
|
||||
action=lambda target: _file_upload_response(target, payload),
|
||||
action=lambda target: _file_upload_response(target, req_data),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from controllers.console.wraps import (
|
||||
annotation_import_rate_limit,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -180,14 +181,14 @@ class AnnotationReplyActionApi(Resource):
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
def post(self, app_id: UUID, action: Literal["enable", "disable"]):
|
||||
args = AnnotationReplyPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AnnotationReplyPayload)
|
||||
def post(self, req_data: AnnotationReplyPayload, app_id: UUID, action: Literal["enable", "disable"]):
|
||||
match action:
|
||||
case "enable":
|
||||
enable_args: EnableAnnotationArgs = {
|
||||
"score_threshold": args.score_threshold,
|
||||
"embedding_provider_name": args.embedding_provider_name,
|
||||
"embedding_model_name": args.embedding_model_name,
|
||||
"score_threshold": req_data.score_threshold,
|
||||
"embedding_provider_name": req_data.embedding_provider_name,
|
||||
"embedding_model_name": req_data.embedding_model_name,
|
||||
}
|
||||
result = AppAnnotationService.enable_app_annotation(enable_args, str(app_id))
|
||||
case "disable":
|
||||
@@ -231,12 +232,17 @@ class AppAnnotationSettingUpdateApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID, annotation_setting_id: UUID):
|
||||
@model_validate(AnnotationSettingUpdatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: AnnotationSettingUpdatePayload,
|
||||
session: Session,
|
||||
app_id: UUID,
|
||||
annotation_setting_id: UUID,
|
||||
):
|
||||
annotation_setting_id_str = str(annotation_setting_id)
|
||||
|
||||
args = AnnotationSettingUpdatePayload.model_validate(console_ns.payload)
|
||||
|
||||
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": args.score_threshold}
|
||||
setting_args: UpdateAnnotationSettingArgs = {"score_threshold": req_data.score_threshold}
|
||||
result = AppAnnotationService.update_app_annotation_setting(
|
||||
str(app_id), annotation_setting_id_str, setting_args, session
|
||||
)
|
||||
@@ -290,11 +296,11 @@ class AnnotationApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, app_id: UUID):
|
||||
args = AnnotationListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
page = args.page
|
||||
limit = args.limit
|
||||
keyword = args.keyword
|
||||
@model_validate(AnnotationListQuery)
|
||||
def get(self, req_data: AnnotationListQuery, session: Session, app_id: UUID):
|
||||
page = req_data.page
|
||||
limit = req_data.limit
|
||||
keyword = req_data.keyword
|
||||
|
||||
annotation_list, total = AppAnnotationService.get_annotation_list_by_app_id(
|
||||
str(app_id), page, limit, keyword, session
|
||||
@@ -317,17 +323,17 @@ class AnnotationApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID):
|
||||
args = CreateAnnotationPayload.model_validate(console_ns.payload)
|
||||
@model_validate(CreateAnnotationPayload)
|
||||
def post(self, req_data: CreateAnnotationPayload, session: Session, app_id: UUID):
|
||||
upsert_args: UpsertAnnotationArgs = {}
|
||||
if args.answer is not None:
|
||||
upsert_args["answer"] = args.answer
|
||||
if args.content is not None:
|
||||
upsert_args["content"] = args.content
|
||||
if args.message_id is not None:
|
||||
upsert_args["message_id"] = args.message_id
|
||||
if args.question is not None:
|
||||
upsert_args["question"] = args.question
|
||||
if req_data.answer is not None:
|
||||
upsert_args["answer"] = req_data.answer
|
||||
if req_data.content is not None:
|
||||
upsert_args["content"] = req_data.content
|
||||
if req_data.message_id is not None:
|
||||
upsert_args["message_id"] = req_data.message_id
|
||||
if req_data.question is not None:
|
||||
upsert_args["question"] = req_data.question
|
||||
annotation = AppAnnotationService.up_insert_app_annotation_from_message(upsert_args, str(app_id), session)
|
||||
return dump_response(Annotation, annotation), 201
|
||||
|
||||
@@ -407,13 +413,13 @@ class AnnotationUpdateDeleteApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, app_id: UUID, annotation_id: UUID):
|
||||
args = UpdateAnnotationPayload.model_validate(console_ns.payload)
|
||||
@model_validate(UpdateAnnotationPayload)
|
||||
def post(self, req_data: UpdateAnnotationPayload, session: Session, app_id: UUID, annotation_id: UUID):
|
||||
update_args: UpdateAnnotationArgs = {}
|
||||
if args.answer is not None:
|
||||
update_args["answer"] = args.answer
|
||||
if args.question is not None:
|
||||
update_args["question"] = args.question
|
||||
if req_data.answer is not None:
|
||||
update_args["answer"] = req_data.answer
|
||||
if req_data.question is not None:
|
||||
update_args["question"] = req_data.question
|
||||
app_ref = _get_app_ref(session, str(app_id))
|
||||
annotation_ref = AppRefService.create_annotation_ref(app_ref, str(annotation_id))
|
||||
annotation = AppAnnotationService.update_app_annotation_directly(update_args, annotation_ref, session)
|
||||
|
||||
@@ -4,7 +4,6 @@ from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import AliasChoices, BaseModel, Field, ValidationInfo, computed_field, field_validator, model_validator
|
||||
from sqlalchemy import select
|
||||
@@ -33,6 +32,7 @@ from controllers.console.wraps import (
|
||||
edit_permission_required,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -697,16 +697,16 @@ class AppListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
@model_validate(CreateAppPayload)
|
||||
def post(self, req_data: CreateAppPayload, session: Session, current_tenant_id: str, current_user: Account):
|
||||
"""Create app"""
|
||||
args = CreateAppPayload.model_validate(console_ns.payload)
|
||||
params = CreateAppParams(
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
mode=args.mode,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
mode=req_data.mode,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
|
||||
app_service = AppService()
|
||||
@@ -908,20 +908,20 @@ class AppApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def put(self, session: Session, app_model: App):
|
||||
@model_validate(UpdateAppPayload)
|
||||
def put(self, req_data: UpdateAppPayload, session: Session, app_model: App):
|
||||
"""Update app"""
|
||||
args = UpdateAppPayload.model_validate(console_ns.payload)
|
||||
|
||||
app_service = AppService()
|
||||
|
||||
args_dict: AppService.ArgsDict = {
|
||||
"name": args.name,
|
||||
"description": args.description or "",
|
||||
"icon_type": args.icon_type,
|
||||
"icon": args.icon or "",
|
||||
"icon_background": args.icon_background or "",
|
||||
"use_icon_as_answer_icon": args.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": args.max_active_requests or 0,
|
||||
"name": req_data.name,
|
||||
"description": req_data.description or "",
|
||||
"icon_type": req_data.icon_type,
|
||||
"icon": req_data.icon or "",
|
||||
"icon_background": req_data.icon_background or "",
|
||||
"use_icon_as_answer_icon": req_data.use_icon_as_answer_icon or False,
|
||||
"max_active_requests": req_data.max_active_requests or 0,
|
||||
}
|
||||
app_model = app_service.update_app(app_model, args_dict, session=session)
|
||||
return AppDetailWithSite.model_validate(
|
||||
@@ -969,10 +969,10 @@ class AppCopyApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=None)
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
@model_validate(CopyAppPayload)
|
||||
def post(self, req_data: CopyAppPayload, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
"""Copy app"""
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
args = CopyAppPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
import_service = AppDslService(session)
|
||||
@@ -982,11 +982,11 @@ class AppCopyApi(Resource):
|
||||
account=current_user,
|
||||
import_mode=ImportMode.YAML_CONTENT,
|
||||
yaml_content=yaml_content,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
)
|
||||
except NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
@@ -1045,16 +1045,16 @@ class AppExportApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL)
|
||||
@agent_manage_required_for_agent_app
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
@model_validate(AppExportQuery)
|
||||
def get(self, req_data: AppExportQuery, app_model: App):
|
||||
"""Export app"""
|
||||
args = AppExportQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
response = AppExportResponse(
|
||||
data=AppDslService.export_dsl(
|
||||
app_model=app_model,
|
||||
session=db.session(),
|
||||
include_secret=args.include_secret,
|
||||
workflow_id=args.workflow_id,
|
||||
include_secret=req_data.include_secret,
|
||||
workflow_id=req_data.workflow_id,
|
||||
)
|
||||
)
|
||||
return response.model_dump(mode="json")
|
||||
@@ -1102,11 +1102,11 @@ class AppNameApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppNamePayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppNamePayload)
|
||||
def post(self, req_data: AppNamePayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_name(app_model, args.name, session=session)
|
||||
app_model = app_service.update_app_name(app_model, req_data.name, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1130,15 +1130,15 @@ class AppIconApi(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppIconPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AppIconPayload)
|
||||
def post(self, req_data: AppIconPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_icon(
|
||||
app_model,
|
||||
args.icon or "",
|
||||
args.icon_background or "",
|
||||
args.icon_type,
|
||||
req_data.icon or "",
|
||||
req_data.icon_background or "",
|
||||
req_data.icon_type,
|
||||
session=session,
|
||||
)
|
||||
return AppDetail.model_validate(
|
||||
@@ -1164,11 +1164,11 @@ class AppSiteStatus(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppSiteStatusPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppSiteStatusPayload)
|
||||
def post(self, req_data: AppSiteStatusPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_site_status(app_model, args.enable_site, session=session)
|
||||
app_model = app_service.update_app_site_status(app_model, req_data.enable_site, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1192,11 +1192,11 @@ class AppApiStatus(Resource):
|
||||
@agent_manage_required_for_agent_app
|
||||
@with_session
|
||||
@get_app_model(mode=None)
|
||||
def post(self, session: Session, app_model: App):
|
||||
args = AppApiStatusPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppApiStatusPayload)
|
||||
def post(self, req_data: AppApiStatusPayload, session: Session, app_model: App):
|
||||
|
||||
app_service = AppService()
|
||||
app_model = app_service.update_app_api_status(app_model, args.enable_api, session=session)
|
||||
app_model = app_service.update_app_api_status(app_model, req_data.enable_api, session=session)
|
||||
return AppDetail.model_validate(
|
||||
app_model,
|
||||
from_attributes=True,
|
||||
@@ -1242,14 +1242,14 @@ class AppTraceApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(AppTracePayload)
|
||||
def post(self, req_data: AppTracePayload, app_model: App):
|
||||
# add app trace
|
||||
args = AppTracePayload.model_validate(console_ns.payload)
|
||||
|
||||
OpsTraceManager.update_app_tracing_config(
|
||||
app_id=app_model.id,
|
||||
enabled=args.enabled,
|
||||
tracing_provider=args.tracing_provider,
|
||||
enabled=req_data.enabled,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -83,8 +84,8 @@ class AppImportApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_IMPORT_EXPORT_DSL, resource_required=False)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account | None = None):
|
||||
args = AppImportPayload.model_validate(console_ns.payload)
|
||||
@model_validate(AppImportPayload)
|
||||
def post(self, req_data: AppImportPayload, current_user: Account | None = None):
|
||||
current_user = current_user if current_user is not None else _current_user_and_tenant_id(None)[0]
|
||||
|
||||
# AppDslService performs internal commits for some creation paths, so use a plain
|
||||
@@ -96,15 +97,15 @@ class AppImportApi(Resource):
|
||||
try:
|
||||
result = import_service.import_app(
|
||||
account=account,
|
||||
import_mode=args.mode,
|
||||
yaml_content=args.yaml_content,
|
||||
yaml_url=args.yaml_url,
|
||||
name=args.name,
|
||||
description=args.description,
|
||||
icon_type=args.icon_type,
|
||||
icon=args.icon,
|
||||
icon_background=args.icon_background,
|
||||
app_id=args.app_id,
|
||||
import_mode=req_data.mode,
|
||||
yaml_content=req_data.yaml_content,
|
||||
yaml_url=req_data.yaml_url,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
icon_type=req_data.icon_type,
|
||||
icon=req_data.icon,
|
||||
icon_background=req_data.icon_background,
|
||||
app_id=req_data.app_id,
|
||||
)
|
||||
except NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
@@ -113,7 +114,7 @@ class AppImportApi(Resource):
|
||||
else:
|
||||
session.commit()
|
||||
|
||||
is_created_app = args.app_id is None and result.status in {
|
||||
is_created_app = req_data.app_id is None and result.status in {
|
||||
ImportStatus.COMPLETED,
|
||||
ImportStatus.COMPLETED_WITH_WARNINGS,
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -276,15 +277,15 @@ class ChatMessageTextApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(TextToSpeechPayload)
|
||||
def post(self, req_data: TextToSpeechPayload, app_model: App):
|
||||
try:
|
||||
payload = TextToSpeechPayload.model_validate(console_ns.payload)
|
||||
message_ref = None
|
||||
if payload.message_id:
|
||||
if req_data.message_id:
|
||||
app_ref = AppRefService.create_app_ref(app_model)
|
||||
message_ref = AppRefService.create_message_ref(
|
||||
app_ref,
|
||||
payload.message_id,
|
||||
req_data.message_id,
|
||||
account_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -292,8 +293,8 @@ class ChatMessageTextApi(Resource):
|
||||
return AudioService.transcript_tts(
|
||||
app_model=app_model,
|
||||
session=db.session(),
|
||||
text=payload.text,
|
||||
voice=payload.voice,
|
||||
text=req_data.text,
|
||||
voice=req_data.voice,
|
||||
message_ref=message_ref,
|
||||
is_draft=True,
|
||||
)
|
||||
@@ -339,13 +340,12 @@ class TextModesApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
@model_validate(TextToSpeechVoiceQuery)
|
||||
def get(self, req_data: TextToSpeechVoiceQuery, app_model: App):
|
||||
try:
|
||||
args = TextToSpeechVoiceQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
response = AudioService.transcript_tts_voices(
|
||||
tenant_id=app_model.tenant_id,
|
||||
language=args.language,
|
||||
language=req_data.language,
|
||||
)
|
||||
|
||||
return dump_response(TextToSpeechVoiceListResponse, response)
|
||||
|
||||
@@ -29,6 +29,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -160,11 +161,11 @@ class CompletionMessageApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TEST_AND_RUN)
|
||||
@with_session
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
args_model = CompletionMessagePayload.model_validate(console_ns.payload)
|
||||
args = args_model.model_dump(exclude_none=True, by_alias=True)
|
||||
@model_validate(CompletionMessagePayload)
|
||||
def post(self, req_data: CompletionMessagePayload, session: Session, current_user: Account, app_model: App):
|
||||
args = req_data.model_dump(exclude_none=True, by_alias=True)
|
||||
|
||||
streaming = args_model.response_mode != "blocking"
|
||||
streaming = req_data.response_mode != "blocking"
|
||||
args["auto_generate_name"] = False
|
||||
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import abort, request
|
||||
from flask import abort
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import func, or_
|
||||
@@ -18,6 +18,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -108,17 +109,17 @@ class CompletionConversationApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, session: Session, current_user: Account, app_model: App):
|
||||
args = CompletionConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(CompletionConversationQuery)
|
||||
def get(self, req_data: CompletionConversationQuery, session: Session, current_user: Account, app_model: App):
|
||||
|
||||
query = sa.select(Conversation).where(
|
||||
Conversation.app_id == app_model.id, Conversation.mode == "completion", Conversation.is_deleted.is_(False)
|
||||
)
|
||||
|
||||
if args.keyword:
|
||||
if req_data.keyword:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
escaped_keyword = escape_like_pattern(args.keyword)
|
||||
escaped_keyword = escape_like_pattern(req_data.keyword)
|
||||
query = query.join(Message, Message.conversation_id == Conversation.id).where(
|
||||
or_(
|
||||
Message.query.ilike(f"%{escaped_keyword}%", escape="\\"),
|
||||
@@ -130,7 +131,7 @@ class CompletionConversationApi(Resource):
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -142,7 +143,7 @@ class CompletionConversationApi(Resource):
|
||||
query = query.where(Conversation.created_at < end_datetime_utc)
|
||||
|
||||
# FIXME, the type ignore in this file
|
||||
if args.annotation_status == "annotated":
|
||||
if req_data.annotation_status == "annotated":
|
||||
query = (
|
||||
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
|
||||
.join( # type: ignore
|
||||
@@ -150,7 +151,7 @@ class CompletionConversationApi(Resource):
|
||||
)
|
||||
.group_by(Conversation.id)
|
||||
)
|
||||
elif args.annotation_status == "not_annotated":
|
||||
elif req_data.annotation_status == "not_annotated":
|
||||
query = (
|
||||
query.outerjoin(MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id)
|
||||
.group_by(Conversation.id)
|
||||
@@ -159,7 +160,7 @@ class CompletionConversationApi(Resource):
|
||||
|
||||
query = query.order_by(Conversation.created_at.desc())
|
||||
|
||||
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
|
||||
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
|
||||
|
||||
return dump_response(
|
||||
ConversationPaginationResponse,
|
||||
@@ -238,8 +239,8 @@ class ChatConversationApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@with_session(write=False)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, session: Session, current_user: Account, app_model: App):
|
||||
args = ChatConversationQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ChatConversationQuery)
|
||||
def get(self, req_data: ChatConversationQuery, session: Session, current_user: Account, app_model: App):
|
||||
|
||||
subquery = (
|
||||
sa.select(Conversation.id.label("conversation_id"), EndUser.session_id.label("from_end_user_session_id"))
|
||||
@@ -249,10 +250,10 @@ class ChatConversationApi(Resource):
|
||||
|
||||
query = sa.select(Conversation).where(Conversation.app_id == app_model.id, Conversation.is_deleted.is_(False))
|
||||
|
||||
if args.keyword:
|
||||
if req_data.keyword:
|
||||
from libs.helper import escape_like_pattern
|
||||
|
||||
escaped_keyword = escape_like_pattern(args.keyword)
|
||||
escaped_keyword = escape_like_pattern(req_data.keyword)
|
||||
keyword_filter = f"%{escaped_keyword}%"
|
||||
query = (
|
||||
query.join(
|
||||
@@ -276,12 +277,12 @@ class ChatConversationApi(Resource):
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
if start_datetime_utc:
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "updated_at" | "-updated_at":
|
||||
query = query.where(Conversation.updated_at >= start_datetime_utc)
|
||||
case "created_at" | "-created_at" | _:
|
||||
@@ -289,13 +290,13 @@ class ChatConversationApi(Resource):
|
||||
|
||||
if end_datetime_utc:
|
||||
end_datetime_utc = end_datetime_utc.replace(second=59)
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "updated_at" | "-updated_at":
|
||||
query = query.where(Conversation.updated_at <= end_datetime_utc)
|
||||
case "created_at" | "-created_at" | _:
|
||||
query = query.where(Conversation.created_at <= end_datetime_utc)
|
||||
|
||||
match args.annotation_status:
|
||||
match req_data.annotation_status:
|
||||
case "annotated":
|
||||
query = (
|
||||
query.options(selectinload(Conversation.message_annotations)) # type: ignore[arg-type]
|
||||
@@ -316,7 +317,7 @@ class ChatConversationApi(Resource):
|
||||
if app_model.mode == AppMode.ADVANCED_CHAT:
|
||||
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER)
|
||||
|
||||
match args.sort_by:
|
||||
match req_data.sort_by:
|
||||
case "created_at":
|
||||
query = query.order_by(Conversation.created_at.asc())
|
||||
case "-created_at":
|
||||
@@ -328,7 +329,7 @@ class ChatConversationApi(Resource):
|
||||
case _:
|
||||
query = query.order_by(Conversation.created_at.desc())
|
||||
|
||||
conversations = paginate_query(query, session=session, page=args.page, per_page=args.limit)
|
||||
conversations = paginate_query(query, session=session, page=req_data.page, per_page=req_data.limit)
|
||||
|
||||
return dump_response(
|
||||
ConversationWithSummaryPaginationResponse,
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
@@ -16,6 +15,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -101,15 +101,15 @@ class ConversationVariablesApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def get(self, app_model: App):
|
||||
args = ConversationVariablesQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ConversationVariablesQuery)
|
||||
def get(self, req_data: ConversationVariablesQuery, app_model: App):
|
||||
|
||||
stmt = (
|
||||
select(ConversationVariable)
|
||||
.where(ConversationVariable.app_id == app_model.id)
|
||||
.order_by(ConversationVariable.created_at)
|
||||
)
|
||||
stmt = stmt.where(ConversationVariable.conversation_id == args.conversation_id)
|
||||
stmt = stmt.where(ConversationVariable.conversation_id == req_data.conversation_id)
|
||||
|
||||
# NOTE: This is a temporary solution to avoid performance issues.
|
||||
page = 1
|
||||
|
||||
@@ -17,7 +17,12 @@ from controllers.console.app.error import (
|
||||
ProviderQuotaExceededError,
|
||||
)
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_tenant_id
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
)
|
||||
from core.app.app_config.entities import ModelConfig
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.helper.code_executor.code_node_provider import CodeNodeProvider
|
||||
@@ -238,11 +243,11 @@ class RuleGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleGeneratePayload)
|
||||
def post(self, req_data: RuleGeneratePayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=args)
|
||||
rules = LLMGenerator.generate_rule_config(tenant_id=current_tenant_id, args=req_data)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
except QuotaExceededError:
|
||||
@@ -267,13 +272,13 @@ class RuleCodeGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleCodeGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleCodeGeneratePayload)
|
||||
def post(self, req_data: RuleCodeGeneratePayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
code_result = LLMGenerator.generate_code(
|
||||
tenant_id=current_tenant_id,
|
||||
args=args,
|
||||
args=req_data,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@@ -299,13 +304,13 @@ class RuleStructuredOutputGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = RuleStructuredOutputPayload.model_validate(console_ns.payload)
|
||||
@model_validate(RuleStructuredOutputPayload)
|
||||
def post(self, req_data: RuleStructuredOutputPayload, current_tenant_id: str):
|
||||
|
||||
try:
|
||||
structured_output = LLMGenerator.generate_structured_output(
|
||||
tenant_id=current_tenant_id,
|
||||
args=args,
|
||||
args=req_data,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ProviderNotInitializeError(ex.description)
|
||||
@@ -332,36 +337,36 @@ class InstructionGenerateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def post(self, session: Session, current_tenant_id: str):
|
||||
args = InstructionGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(InstructionGeneratePayload)
|
||||
def post(self, req_data: InstructionGeneratePayload, session: Session, current_tenant_id: str):
|
||||
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
|
||||
code_provider: type[CodeNodeProvider] | None = next(
|
||||
(p for p in providers if p.is_accept_language(args.language)), None
|
||||
(p for p in providers if p.is_accept_language(req_data.language)), None
|
||||
)
|
||||
code_template = code_provider.get_default_code() if code_provider else ""
|
||||
try:
|
||||
# Generate from nothing for a workflow node
|
||||
if (args.current in (code_template, "")) and args.node_id != "":
|
||||
if (req_data.current in (code_template, "")) and req_data.node_id != "":
|
||||
app = session.scalar(
|
||||
select(App).where(App.id == args.flow_id, App.tenant_id == current_tenant_id).limit(1)
|
||||
select(App).where(App.id == req_data.flow_id, App.tenant_id == current_tenant_id).limit(1)
|
||||
)
|
||||
if not app:
|
||||
return {"error": f"app {args.flow_id} not found"}, 400
|
||||
return {"error": f"app {req_data.flow_id} not found"}, 400
|
||||
workflow = WorkflowService().get_draft_workflow(app_model=app, session=session)
|
||||
if not workflow:
|
||||
return {"error": f"workflow {args.flow_id} not found"}, 400
|
||||
return {"error": f"workflow {req_data.flow_id} not found"}, 400
|
||||
nodes: Sequence = workflow.graph_dict["nodes"]
|
||||
node = [node for node in nodes if node["id"] == args.node_id]
|
||||
node = [node for node in nodes if node["id"] == req_data.node_id]
|
||||
if len(node) == 0:
|
||||
return {"error": f"node {args.node_id} not found"}, 400
|
||||
return {"error": f"node {req_data.node_id} not found"}, 400
|
||||
node_type = node[0]["data"]["type"]
|
||||
match node_type:
|
||||
case "llm":
|
||||
return LLMGenerator.generate_rule_config(
|
||||
current_tenant_id,
|
||||
args=RuleGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
no_variable=True,
|
||||
),
|
||||
)
|
||||
@@ -369,8 +374,8 @@ class InstructionGenerateApi(Resource):
|
||||
return LLMGenerator.generate_rule_config(
|
||||
current_tenant_id,
|
||||
args=RuleGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
no_variable=True,
|
||||
),
|
||||
)
|
||||
@@ -378,31 +383,31 @@ class InstructionGenerateApi(Resource):
|
||||
return LLMGenerator.generate_code(
|
||||
tenant_id=current_tenant_id,
|
||||
args=RuleCodeGeneratePayload(
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
code_language=args.language,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
code_language=req_data.language,
|
||||
),
|
||||
)
|
||||
case _:
|
||||
return {"error": f"invalid node type: {node_type}"}
|
||||
if args.node_id == "" and args.current != "": # For legacy app without a workflow
|
||||
if req_data.node_id == "" and req_data.current != "": # For legacy app without a workflow
|
||||
return LLMGenerator.instruction_modify_legacy(
|
||||
tenant_id=current_tenant_id,
|
||||
flow_id=args.flow_id,
|
||||
current=args.current,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
flow_id=req_data.flow_id,
|
||||
current=req_data.current,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
)
|
||||
if args.node_id != "" and args.current != "": # For workflow node
|
||||
if req_data.node_id != "" and req_data.current != "": # For workflow node
|
||||
return LLMGenerator.instruction_modify_workflow(
|
||||
tenant_id=current_tenant_id,
|
||||
flow_id=args.flow_id,
|
||||
node_id=args.node_id,
|
||||
current=args.current,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
flow_id=req_data.flow_id,
|
||||
node_id=req_data.node_id,
|
||||
current=req_data.current,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
workflow_service=WorkflowService(),
|
||||
)
|
||||
return {"error": "incompatible parameters"}, 400
|
||||
@@ -426,9 +431,9 @@ class InstructionGenerationTemplateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
args = InstructionTemplatePayload.model_validate(console_ns.payload)
|
||||
match args.type:
|
||||
@model_validate(InstructionTemplatePayload)
|
||||
def post(self, req_data: InstructionTemplatePayload):
|
||||
match req_data.type:
|
||||
case "prompt":
|
||||
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT
|
||||
|
||||
@@ -438,7 +443,7 @@ class InstructionGenerationTemplateApi(Resource):
|
||||
|
||||
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
|
||||
case _:
|
||||
raise ValueError(f"Invalid type: {args.type}")
|
||||
raise ValueError(f"Invalid type: {req_data.type}")
|
||||
|
||||
|
||||
def _workflow_instruction_guard(args: WorkflowGeneratePayload) -> tuple[dict, int] | None:
|
||||
@@ -492,24 +497,24 @@ class WorkflowGenerateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowGeneratePayload)
|
||||
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
|
||||
|
||||
# Reject empty / over-length instructions at the boundary (shared with
|
||||
# the streaming endpoint) before spending a planner+builder roundtrip.
|
||||
guard = _workflow_instruction_guard(args)
|
||||
guard = _workflow_instruction_guard(req_data)
|
||||
if guard is not None:
|
||||
return guard
|
||||
|
||||
try:
|
||||
result = WorkflowGeneratorService.generate_workflow_graph(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
current_graph=args.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if args.current_graph
|
||||
mode=req_data.mode,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
current_graph=req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if req_data.current_graph
|
||||
else None,
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
@@ -547,13 +552,13 @@ class WorkflowInstructionSuggestionsApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowInstructionSuggestionsPayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowInstructionSuggestionsPayload)
|
||||
def post(self, req_data: WorkflowInstructionSuggestionsPayload, current_tenant_id: str):
|
||||
suggestions = LLMGenerator.generate_workflow_instruction_suggestions(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
language=args.language,
|
||||
count=args.count,
|
||||
mode=req_data.mode,
|
||||
language=req_data.language,
|
||||
count=req_data.count,
|
||||
)
|
||||
return dump_response(WorkflowInstructionSuggestionsResponse, {"suggestions": suggestions})
|
||||
|
||||
@@ -583,12 +588,12 @@ class WorkflowGenerateStreamApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
args = WorkflowGeneratePayload.model_validate(console_ns.payload)
|
||||
@model_validate(WorkflowGeneratePayload)
|
||||
def post(self, req_data: WorkflowGeneratePayload, current_tenant_id: str):
|
||||
|
||||
# Same boundary guards as the blocking endpoint — return a normal 400
|
||||
# JSON for these BEFORE opening the stream.
|
||||
guard = _workflow_instruction_guard(args)
|
||||
guard = _workflow_instruction_guard(req_data)
|
||||
if guard is not None:
|
||||
return guard
|
||||
|
||||
@@ -596,12 +601,14 @@ class WorkflowGenerateStreamApi(Resource):
|
||||
try:
|
||||
for event_name, payload in WorkflowGeneratorService.generate_workflow_graph_stream(
|
||||
tenant_id=current_tenant_id,
|
||||
mode=args.mode,
|
||||
instruction=args.instruction,
|
||||
model_config=args.model_config_data,
|
||||
ideal_output=args.ideal_output,
|
||||
mode=req_data.mode,
|
||||
instruction=req_data.instruction,
|
||||
model_config=req_data.model_config_data,
|
||||
ideal_output=req_data.ideal_output,
|
||||
current_graph=(
|
||||
args.current_graph.model_dump(by_alias=True, exclude_none=True) if args.current_graph else None
|
||||
req_data.current_graph.model_dump(by_alias=True, exclude_none=True)
|
||||
if req_data.current_graph
|
||||
else None
|
||||
),
|
||||
):
|
||||
body = {"event": event_name, **payload}
|
||||
|
||||
@@ -15,6 +15,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -109,17 +110,16 @@ class AppMCPServerController(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_tenant_id
|
||||
@get_app_model
|
||||
def post(self, current_tenant_id: str, app_model: App):
|
||||
payload = MCPServerCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
description = payload.description
|
||||
@model_validate(MCPServerCreatePayload)
|
||||
def post(self, req_data: MCPServerCreatePayload, current_tenant_id: str, app_model: App):
|
||||
description = req_data.description
|
||||
if not description:
|
||||
description = app_model.description or ""
|
||||
|
||||
server = AppMCPServer(
|
||||
name=app_model.name,
|
||||
description=description,
|
||||
parameters=json.dumps(payload.parameters, ensure_ascii=False),
|
||||
parameters=json.dumps(req_data.parameters, ensure_ascii=False),
|
||||
status=AppMCPServerStatus.ACTIVE,
|
||||
app_id=app_model.id,
|
||||
tenant_id=current_tenant_id,
|
||||
@@ -144,10 +144,10 @@ class AppMCPServerController(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@get_app_model
|
||||
def put(self, app_model: App):
|
||||
payload = MCPServerUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(MCPServerUpdatePayload)
|
||||
def put(self, req_data: MCPServerUpdatePayload, app_model: App):
|
||||
app_ref = AppRefService.create_app_ref(app_model)
|
||||
server_ref = AppRefService.create_mcp_server_ref(app_ref, payload.id)
|
||||
server_ref = AppRefService.create_mcp_server_ref(app_ref, req_data.id)
|
||||
server = db.session.scalar(
|
||||
select(AppMCPServer)
|
||||
.where(
|
||||
@@ -160,7 +160,7 @@ class AppMCPServerController(Resource):
|
||||
if not server:
|
||||
raise NotFound()
|
||||
|
||||
description = payload.description
|
||||
description = req_data.description
|
||||
if description is None or not description:
|
||||
server.description = app_model.description or ""
|
||||
else:
|
||||
@@ -168,10 +168,10 @@ class AppMCPServerController(Resource):
|
||||
|
||||
server.name = app_model.name
|
||||
|
||||
server.parameters = json.dumps(payload.parameters, ensure_ascii=False)
|
||||
if payload.status:
|
||||
server.parameters = json.dumps(req_data.parameters, ensure_ascii=False)
|
||||
if req_data.status:
|
||||
try:
|
||||
server.status = AppMCPServerStatus(payload.status)
|
||||
server.status = AppMCPServerStatus(req_data.status)
|
||||
except ValueError:
|
||||
raise ValueError("Invalid status")
|
||||
db.session.commit()
|
||||
|
||||
@@ -28,6 +28,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -320,8 +321,8 @@ class MessageFeedbackExportApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = FeedbackExportQuery.model_validate(request.args.to_dict())
|
||||
@model_validate(FeedbackExportQuery)
|
||||
def get(self, req_data: FeedbackExportQuery, app_model: App):
|
||||
|
||||
# Import the service function
|
||||
from services.feedback_service import FeedbackService
|
||||
@@ -330,12 +331,12 @@ class MessageFeedbackExportApi(Resource):
|
||||
export_data = FeedbackService.export_feedbacks(
|
||||
app_model.id,
|
||||
session=db.session(),
|
||||
from_source=args.from_source,
|
||||
rating=args.rating,
|
||||
has_comment=args.has_comment,
|
||||
start_date=args.start_date,
|
||||
end_date=args.end_date,
|
||||
format_type=args.format,
|
||||
from_source=req_data.from_source,
|
||||
rating=req_data.rating,
|
||||
has_comment=req_data.has_comment,
|
||||
start_date=req_data.start_date,
|
||||
end_date=req_data.end_date,
|
||||
format_type=req_data.format,
|
||||
)
|
||||
return export_data
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from werkzeug.exceptions import BadRequest
|
||||
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -74,12 +74,11 @@ class TraceAppConfigApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def get(self, app_model: App):
|
||||
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore
|
||||
|
||||
@model_validate(TraceProviderQuery)
|
||||
def get(self, req_data: TraceProviderQuery, app_model: App):
|
||||
try:
|
||||
trace_config = OpsService.get_tracing_app_config(
|
||||
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
|
||||
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
|
||||
)
|
||||
if not trace_config:
|
||||
return {"has_not_configured": True}
|
||||
@@ -104,15 +103,14 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def post(self, app_model: App):
|
||||
@model_validate(TraceConfigPayload)
|
||||
def post(self, req_data: TraceConfigPayload, app_model: App):
|
||||
"""Create a new trace app configuration"""
|
||||
args = TraceConfigPayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
result = OpsService.create_tracing_app_config(
|
||||
app_id=app_model.id,
|
||||
tracing_provider=args.tracing_provider,
|
||||
tracing_config=args.tracing_config,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
tracing_config=req_data.tracing_config,
|
||||
session=db.session(),
|
||||
)
|
||||
if not result:
|
||||
@@ -140,15 +138,14 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def patch(self, app_model: App):
|
||||
@model_validate(TraceConfigPayload)
|
||||
def patch(self, req_data: TraceConfigPayload, app_model: App):
|
||||
"""Update an existing trace app configuration"""
|
||||
args = TraceConfigPayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
result = OpsService.update_tracing_app_config(
|
||||
app_id=app_model.id,
|
||||
tracing_provider=args.tracing_provider,
|
||||
tracing_config=args.tracing_config,
|
||||
tracing_provider=req_data.tracing_provider,
|
||||
tracing_config=req_data.tracing_config,
|
||||
session=db.session(),
|
||||
)
|
||||
if not result:
|
||||
@@ -170,13 +167,12 @@ class TraceAppConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_TRACING_CONFIG)
|
||||
@get_app_model
|
||||
def delete(self, app_model: App):
|
||||
@model_validate(TraceProviderQuery)
|
||||
def delete(self, req_data: TraceProviderQuery, app_model: App):
|
||||
"""Delete an existing trace app configuration"""
|
||||
args = TraceProviderQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
try:
|
||||
result = OpsService.delete_tracing_app_config(
|
||||
app_id=app_model.id, tracing_provider=args.tracing_provider, session=db.session()
|
||||
app_id=app_model.id, tracing_provider=req_data.tracing_provider, session=db.session()
|
||||
)
|
||||
if not result:
|
||||
raise TracingConfigNotExist()
|
||||
|
||||
@@ -17,6 +17,7 @@ from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -98,8 +99,8 @@ class AppSite(Resource):
|
||||
@with_current_user
|
||||
@with_session
|
||||
@get_app_model
|
||||
def post(self, session: Session, current_user: Account, app_model: App):
|
||||
args = AppSiteUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(AppSiteUpdatePayload)
|
||||
def post(self, req_data: AppSiteUpdatePayload, session: Session, current_user: Account, app_model: App):
|
||||
site = session.scalar(select(Site).where(Site.app_id == app_model.id).limit(1))
|
||||
if not site:
|
||||
raise NotFound
|
||||
@@ -123,7 +124,7 @@ class AppSite(Resource):
|
||||
"show_workflow_steps",
|
||||
"use_icon_as_answer_icon",
|
||||
]:
|
||||
value = getattr(args, attr_name)
|
||||
value = getattr(req_data, attr_name)
|
||||
if value is not None:
|
||||
setattr(site, attr_name, value)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import sqlalchemy as sa
|
||||
from flask import abort, request
|
||||
from flask import abort
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
@@ -12,6 +12,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -157,8 +158,8 @@ class DailyMessageStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -177,7 +178,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -217,8 +218,8 @@ class DailyConversationStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -237,7 +238,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -276,8 +277,8 @@ class DailyTerminalsStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -296,7 +297,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -336,8 +337,8 @@ class DailyTokenCostStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -357,7 +358,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -399,8 +400,8 @@ class AverageSessionInteractionStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT])
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("c.created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -427,7 +428,7 @@ FROM
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -478,8 +479,8 @@ class UserSatisfactionRateStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("m.created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -502,7 +503,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -547,8 +548,8 @@ class AverageResponseTimeStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=AppMode.COMPLETION)
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -567,7 +568,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -607,8 +608,8 @@ class TokensPerSecondStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = StatisticTimeRangeQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(StatisticTimeRangeQuery)
|
||||
def get(self, req_data: StatisticTimeRangeQuery, account: Account, app_model: App):
|
||||
|
||||
converted_created_at = convert_datetime_to_date("created_at")
|
||||
sql_query = f"""SELECT
|
||||
@@ -630,7 +631,7 @@ WHERE
|
||||
}
|
||||
|
||||
try:
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_datetime_utc, end_datetime_utc = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from dateutil.parser import isoparse
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -14,6 +13,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
)
|
||||
@@ -183,11 +183,11 @@ class WorkflowAppLogApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowAppLogQuery)
|
||||
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
|
||||
"""
|
||||
Get workflow app logs
|
||||
"""
|
||||
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
# get paginate workflow app logs
|
||||
workflow_app_service = WorkflowAppService()
|
||||
@@ -195,15 +195,15 @@ class WorkflowAppLogApi(Resource):
|
||||
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_app_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
keyword=args.keyword,
|
||||
status=args.status,
|
||||
created_at_before=args.created_at__before,
|
||||
created_at_after=args.created_at__after,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
detail=args.detail,
|
||||
created_by_end_user_session_id=args.created_by_end_user_session_id,
|
||||
created_by_account=args.created_by_account,
|
||||
keyword=req_data.keyword,
|
||||
status=req_data.status,
|
||||
created_at_before=req_data.created_at__before,
|
||||
created_at_after=req_data.created_at__after,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
detail=req_data.detail,
|
||||
created_by_end_user_session_id=req_data.created_by_end_user_session_id,
|
||||
created_by_account=req_data.created_by_account,
|
||||
)
|
||||
|
||||
return WorkflowAppLogPaginationResponse.model_validate(
|
||||
@@ -227,19 +227,19 @@ class WorkflowArchivedLogApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_LOG_AND_ANNOTATION)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowAppLogQuery)
|
||||
def get(self, req_data: WorkflowAppLogQuery, app_model: App):
|
||||
"""
|
||||
Get workflow archived logs
|
||||
"""
|
||||
args = WorkflowAppLogQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
workflow_app_service = WorkflowAppService()
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
workflow_app_log_pagination = workflow_app_service.get_paginate_workflow_archive_logs(
|
||||
session=session,
|
||||
app_model=app_model,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
)
|
||||
|
||||
return WorkflowArchivedLogPaginationResponse.model_validate(
|
||||
|
||||
@@ -10,6 +10,7 @@ from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -239,18 +240,24 @@ class WorkflowCommentListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App):
|
||||
@model_validate(WorkflowCommentCreatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowCommentCreatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
):
|
||||
"""Create a new workflow comment."""
|
||||
payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.create_comment(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
created_by=current_user.id,
|
||||
content=payload.content,
|
||||
position_x=payload.position_x,
|
||||
position_y=payload.position_y,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
position_x=req_data.position_x,
|
||||
position_y=req_data.position_y,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentCreate, result), 201
|
||||
@@ -289,19 +296,26 @@ class WorkflowCommentDetailApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
@model_validate(WorkflowCommentUpdatePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: WorkflowCommentUpdatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
):
|
||||
"""Update a workflow comment."""
|
||||
payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.update_comment(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
comment_id=comment_id,
|
||||
user_id=current_user.id,
|
||||
content=payload.content,
|
||||
position_x=payload.position_x,
|
||||
position_y=payload.position_y,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
position_x=req_data.position_x,
|
||||
position_y=req_data.position_y,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentUpdate, result)
|
||||
@@ -372,20 +386,26 @@ class WorkflowCommentReplyApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def post(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str):
|
||||
@model_validate(WorkflowCommentReplyPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowCommentReplyPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
):
|
||||
"""Add a reply to a workflow comment."""
|
||||
# Validate comment access first
|
||||
WorkflowCommentService.validate_comment_access(
|
||||
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
|
||||
)
|
||||
|
||||
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
result = WorkflowCommentService.create_reply(
|
||||
comment_id=comment_id,
|
||||
content=payload.content,
|
||||
content=req_data.content,
|
||||
created_by=current_user.id,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentReplyCreate, result), 201
|
||||
@@ -407,23 +427,30 @@ class WorkflowCommentReplyDetailApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@get_app_model()
|
||||
def put(self, current_tenant_id: str, current_user: Account, app_model: App, comment_id: str, reply_id: str):
|
||||
@model_validate(WorkflowCommentReplyPayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: WorkflowCommentReplyPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
comment_id: str,
|
||||
reply_id: str,
|
||||
):
|
||||
"""Update a comment reply."""
|
||||
# Validate comment access first
|
||||
WorkflowCommentService.validate_comment_access(
|
||||
comment_id=comment_id, tenant_id=current_tenant_id, app_id=app_model.id
|
||||
)
|
||||
|
||||
payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
reply = WorkflowCommentService.update_reply(
|
||||
tenant_id=current_tenant_id,
|
||||
app_id=app_model.id,
|
||||
comment_id=comment_id,
|
||||
reply_id=reply_id,
|
||||
user_id=current_user.id,
|
||||
content=payload.content,
|
||||
mentioned_user_ids=payload.mentioned_user_ids,
|
||||
content=req_data.content,
|
||||
mentioned_user_ids=req_data.mentioned_user_ids,
|
||||
)
|
||||
|
||||
return dump_response(WorkflowCommentReplyUpdate, reply)
|
||||
|
||||
@@ -4,7 +4,7 @@ from functools import wraps
|
||||
from typing import Any, Concatenate, Self, TypedDict, override
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Response, request
|
||||
from flask import Response
|
||||
from flask_restx import Resource, fields, marshal, marshal_with
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -22,6 +22,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -359,11 +360,11 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_without_value_model)
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
def get(self, current_user: Account, app_model: App):
|
||||
@model_validate(WorkflowDraftVariableListQuery)
|
||||
def get(self, req_data: WorkflowDraftVariableListQuery, current_user: Account, app_model: App):
|
||||
"""
|
||||
Get draft workflow
|
||||
"""
|
||||
args = WorkflowDraftVariableListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
# fetch draft workflow by app_model
|
||||
workflow_service = WorkflowService()
|
||||
@@ -378,8 +379,8 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=app_model.id,
|
||||
page=args.page,
|
||||
limit=args.limit,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -479,7 +480,14 @@ class VariableApi(Resource):
|
||||
@console_ns.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_model)
|
||||
def patch(self, current_user: Account, app_model: App, variable_id: UUID):
|
||||
@model_validate(WorkflowDraftVariableUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: WorkflowDraftVariableUpdatePayload,
|
||||
current_user: Account,
|
||||
app_model: App,
|
||||
variable_id: UUID,
|
||||
):
|
||||
# Request payload for file types:
|
||||
#
|
||||
# Local File:
|
||||
@@ -504,7 +512,6 @@ class VariableApi(Resource):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
variable_id_str = str(variable_id)
|
||||
variable = ensure_variable_access(
|
||||
@@ -514,8 +521,8 @@ class VariableApi(Resource):
|
||||
current_user_id=current_user.id,
|
||||
)
|
||||
|
||||
new_name = args_model.name
|
||||
raw_value = args_model.value
|
||||
new_name = req_data.name
|
||||
raw_value = req_data.value
|
||||
if new_name is None and raw_value is None:
|
||||
return variable
|
||||
|
||||
@@ -660,13 +667,13 @@ class ConversationVariableCollectionApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=AppMode.ADVANCED_CHAT)
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ConversationVariableUpdatePayload)
|
||||
def post(self, req_data: ConversationVariableUpdatePayload, current_user: Account, app_model: App):
|
||||
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
conversation_variables_list = [
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.conversation_variables
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.conversation_variables
|
||||
]
|
||||
conversation_variables = [
|
||||
variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list
|
||||
@@ -755,24 +762,24 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_EDIT)
|
||||
@with_current_user
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def post(self, current_user: Account, app_model: App):
|
||||
payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(EnvironmentVariableUpdatePayload)
|
||||
def post(self, req_data: EnvironmentVariableUpdatePayload, current_user: Account, app_model: App):
|
||||
|
||||
workflow_service = WorkflowService()
|
||||
|
||||
environment_variables_list = [
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in payload.environment_variables
|
||||
variable.model_dump(mode="json", exclude_unset=True) for variable in req_data.environment_variables
|
||||
]
|
||||
environment_variables = [
|
||||
variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list
|
||||
]
|
||||
|
||||
if payload.patch:
|
||||
if req_data.patch:
|
||||
workflow_service.patch_draft_workflow_environment_variables(
|
||||
app_model=app_model,
|
||||
account=current_user,
|
||||
environment_variables=environment_variables,
|
||||
deleted_environment_variable_ids=payload.deleted_environment_variable_ids,
|
||||
deleted_environment_variable_ids=req_data.deleted_environment_variable_ids,
|
||||
session=db.session(),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,6 @@ from datetime import UTC, datetime, timedelta
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -160,21 +160,21 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunListQuery)
|
||||
def get(self, req_data: WorkflowRunListQuery, app_model: App):
|
||||
"""
|
||||
Get advanced chat app workflow run list
|
||||
"""
|
||||
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args: WorkflowRunListArgs = {"limit": args_model.limit}
|
||||
if args_model.last_id is not None:
|
||||
args["last_id"] = args_model.last_id
|
||||
if args_model.status is not None:
|
||||
args["status"] = args_model.status
|
||||
args: WorkflowRunListArgs = {"limit": req_data.limit}
|
||||
if req_data.last_id is not None:
|
||||
args["last_id"] = req_data.last_id
|
||||
if req_data.status is not None:
|
||||
args["status"] = req_data.status
|
||||
|
||||
# Default to DEBUGGING if not specified
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -258,17 +258,17 @@ class AdvancedChatAppWorkflowRunCountApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunCountQuery)
|
||||
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
|
||||
"""
|
||||
Get advanced chat workflow runs count statistics
|
||||
"""
|
||||
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args = args_model.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
# Default to DEBUGGING if not specified
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -299,21 +299,21 @@ class WorkflowRunListApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunListQuery)
|
||||
def get(self, req_data: WorkflowRunListQuery, app_model: App):
|
||||
"""
|
||||
Get workflow run list
|
||||
"""
|
||||
args_model = WorkflowRunListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args: WorkflowRunListArgs = {"limit": args_model.limit}
|
||||
if args_model.last_id is not None:
|
||||
args["last_id"] = args_model.last_id
|
||||
if args_model.status is not None:
|
||||
args["status"] = args_model.status
|
||||
args: WorkflowRunListArgs = {"limit": req_data.limit}
|
||||
if req_data.last_id is not None:
|
||||
args["last_id"] = req_data.last_id
|
||||
if req_data.status is not None:
|
||||
args["status"] = req_data.status
|
||||
|
||||
# Default to DEBUGGING for workflow if not specified (backward compatibility)
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
@@ -341,17 +341,17 @@ class WorkflowRunCountApi(Resource):
|
||||
@account_initialization_required
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_CREATE_AND_MANAGEMENT)
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
def get(self, app_model: App):
|
||||
@model_validate(WorkflowRunCountQuery)
|
||||
def get(self, req_data: WorkflowRunCountQuery, app_model: App):
|
||||
"""
|
||||
Get workflow runs count statistics
|
||||
"""
|
||||
args_model = WorkflowRunCountQuery.model_validate(request.args.to_dict(flat=True))
|
||||
args = args_model.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
# Default to DEBUGGING for workflow if not specified (backward compatibility)
|
||||
triggered_from = (
|
||||
WorkflowRunTriggeredFrom(args_model.triggered_from)
|
||||
if args_model.triggered_from
|
||||
WorkflowRunTriggeredFrom(req_data.triggered_from)
|
||||
if req_data.triggered_from
|
||||
else WorkflowRunTriggeredFrom.DEBUGGING
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from flask import abort, jsonify, request
|
||||
from flask import abort, jsonify
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -10,6 +10,7 @@ from controllers.console.wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -104,13 +105,13 @@ class WorkflowDailyRunsStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -148,13 +149,13 @@ class WorkflowDailyTerminalsStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -192,13 +193,13 @@ class WorkflowDailyTokenCostStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
@@ -236,13 +237,13 @@ class WorkflowAverageAppInteractionStatistic(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_MONITOR)
|
||||
@get_app_model(mode=[AppMode.WORKFLOW])
|
||||
def get(self, account: Account, app_model: App):
|
||||
args = WorkflowStatisticQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(WorkflowStatisticQuery)
|
||||
def get(self, req_data: WorkflowStatisticQuery, account: Account, app_model: App):
|
||||
|
||||
assert account.timezone is not None
|
||||
|
||||
try:
|
||||
start_date, end_date = parse_time_range(args.start, args.end, account.timezone)
|
||||
start_date, end_date = parse_time_range(req_data.start, req_data.end, account.timezone)
|
||||
except ValueError as e:
|
||||
abort(400, description=str(e))
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import select
|
||||
@@ -25,6 +24,7 @@ from ..wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -102,11 +102,11 @@ class WebhookTriggerApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[WebhookTriggerResponse.__name__])
|
||||
@rbac_permission_required(RBACResourceScope.APP, RBACPermission.APP_VIEW_LAYOUT)
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
def get(self, app_model: App):
|
||||
@model_validate(Parser)
|
||||
def get(self, req_data: Parser, app_model: App):
|
||||
"""Get webhook trigger for a node"""
|
||||
args = Parser.model_validate(request.args.to_dict(flat=True))
|
||||
|
||||
node_id = args.node_id
|
||||
node_id = req_data.node_id
|
||||
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
# Get webhook trigger for this app and node
|
||||
@@ -175,11 +175,11 @@ class AppTriggerEnableApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[WorkflowTriggerResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
@get_app_model(mode=AppMode.WORKFLOW)
|
||||
def post(self, current_tenant_id: str, app_model: App):
|
||||
@model_validate(ParserEnable)
|
||||
def post(self, req_data: ParserEnable, current_tenant_id: str, app_model: App):
|
||||
"""Update app trigger (enable/disable)"""
|
||||
args = ParserEnable.model_validate(console_ns.payload)
|
||||
|
||||
trigger_id = args.trigger_id
|
||||
trigger_id = req_data.trigger_id
|
||||
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
|
||||
# Find the trigger using select
|
||||
trigger = session.execute(
|
||||
@@ -194,7 +194,7 @@ class AppTriggerEnableApi(Resource):
|
||||
raise NotFound("Trigger not found")
|
||||
|
||||
# Update status based on enable_trigger boolean
|
||||
trigger.status = AppTriggerStatus.ENABLED if args.enable_trigger else AppTriggerStatus.DISABLED
|
||||
trigger.status = AppTriggerStatus.ENABLED if req_data.enable_trigger else AppTriggerStatus.DISABLED
|
||||
|
||||
# Add computed icon field
|
||||
url_prefix = dify_config.CONSOLE_API_URL + "/console/api/workspaces/current/tool-provider/builtin/"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -87,10 +88,10 @@ class ApiKeyAuthDataSourceBinding(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
|
||||
@console_ns.expect(console_ns.models[ApiKeyAuthBindingPayload.__name__])
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
@model_validate(ApiKeyAuthBindingPayload)
|
||||
def post(self, req_data: ApiKeyAuthBindingPayload, current_tenant_id: str):
|
||||
# The role of the current user in the table must be admin or owner
|
||||
payload = ApiKeyAuthBindingPayload.model_validate(console_ns.payload)
|
||||
data = payload.model_dump()
|
||||
data = req_data.model_dump()
|
||||
ApiKeyAuthService.validate_api_key_auth_args(data)
|
||||
try:
|
||||
ApiKeyAuthService.create_provider_auth(current_tenant_id, data, session=db.session())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -9,7 +9,7 @@ from pydantic import BaseModel
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
|
||||
from core.db.session_factory import session_factory
|
||||
from graphon.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.login import login_required
|
||||
@@ -154,8 +154,8 @@ class OAuthServerAppApi(Resource):
|
||||
@console_ns.expect(console_ns.models[OAuthProviderRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[OAuthProviderAppResponse.__name__])
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
payload = OAuthProviderRequest.model_validate(request.get_json())
|
||||
@model_validate(OAuthProviderRequest)
|
||||
def post(self, payload: OAuthProviderRequest, oauth_provider_app: OAuthProviderApp):
|
||||
redirect_uri = payload.redirect_uri
|
||||
|
||||
# check if redirect_uri is valid
|
||||
@@ -196,9 +196,8 @@ class OAuthServerUserTokenApi(Resource):
|
||||
@console_ns.expect(console_ns.models[OAuthTokenRequest.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[OAuthProviderTokenResponse.__name__])
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
payload = OAuthTokenRequest.model_validate(request.get_json())
|
||||
|
||||
@model_validate(OAuthTokenRequest)
|
||||
def post(self, payload: OAuthTokenRequest, oauth_provider_app: OAuthProviderApp):
|
||||
try:
|
||||
grant_type = OAuthGrantType(payload.grant_type)
|
||||
except ValueError:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -36,6 +36,7 @@ from ..wraps import (
|
||||
RBACPermission,
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -229,12 +230,18 @@ class DataSourceNotionListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_tenant_id: str, current_user: Account) -> tuple[dict[str, Any], int]:
|
||||
query = DataSourceNotionListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(DataSourceNotionListQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: DataSourceNotionListQuery,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
) -> tuple[dict[str, Any], int]:
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
credential = datasource_provider_service.get_datasource_credentials(
|
||||
tenant_id=current_tenant_id,
|
||||
credential_id=query.credential_id,
|
||||
credential_id=req_data.credential_id,
|
||||
provider="notion_datasource",
|
||||
plugin_id="langgenius/notion_datasource",
|
||||
)
|
||||
@@ -242,8 +249,8 @@ class DataSourceNotionListApi(Resource):
|
||||
raise NotFound("Credential not found.")
|
||||
exist_page_ids = []
|
||||
# import notion in the exist dataset
|
||||
if query.dataset_id:
|
||||
dataset = DatasetService.get_dataset(query.dataset_id, session)
|
||||
if req_data.dataset_id:
|
||||
dataset = DatasetService.get_dataset(req_data.dataset_id, session)
|
||||
if not dataset:
|
||||
raise NotFound("Dataset not found.")
|
||||
if dataset.data_source_type != "notion_import":
|
||||
@@ -251,7 +258,7 @@ class DataSourceNotionListApi(Resource):
|
||||
|
||||
documents = session.scalars(
|
||||
select(Document).where(
|
||||
Document.dataset_id == query.dataset_id,
|
||||
Document.dataset_id == req_data.dataset_id,
|
||||
Document.tenant_id == current_tenant_id,
|
||||
Document.data_source_type == "notion_import",
|
||||
Document.enabled.is_(True),
|
||||
@@ -318,13 +325,19 @@ class DataSourceNotionPreviewApi(Resource):
|
||||
@console_ns.doc(params=query_params_from_model(DataSourceNotionPreviewQuery))
|
||||
@console_ns.response(200, "Success", console_ns.models[TextContentResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, page_id: UUID, page_type: str) -> tuple[dict[str, str], int]:
|
||||
query = DataSourceNotionPreviewQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(DataSourceNotionPreviewQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: DataSourceNotionPreviewQuery,
|
||||
current_tenant_id: str,
|
||||
page_id: UUID,
|
||||
page_type: str,
|
||||
) -> tuple[dict[str, str], int]:
|
||||
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
credential = datasource_provider_service.get_datasource_credentials(
|
||||
tenant_id=current_tenant_id,
|
||||
credential_id=query.credential_id,
|
||||
credential_id=req_data.credential_id,
|
||||
provider="notion_datasource",
|
||||
plugin_id="langgenius/notion_datasource",
|
||||
)
|
||||
@@ -354,12 +367,17 @@ class DataSourceNotionIndexingEstimateApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[IndexingEstimate.__name__])
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str) -> tuple[dict[str, Any], int]:
|
||||
payload = NotionEstimatePayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump()
|
||||
@model_validate(NotionEstimatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: NotionEstimatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
) -> tuple[dict[str, Any], int]:
|
||||
args = req_data.model_dump()
|
||||
# validate args
|
||||
DocumentService.estimate_args_validate(args)
|
||||
notion_info_list = payload.notion_info_list
|
||||
notion_info_list = req_data.notion_info_list
|
||||
extract_settings = []
|
||||
for notion_info in notion_info_list:
|
||||
workspace_id = notion_info["workspace_id"]
|
||||
|
||||
@@ -26,6 +26,7 @@ from controllers.console.wraps import (
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
enterprise_license_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -572,9 +573,8 @@ class DatasetListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
payload = DatasetCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(DatasetCreatePayload)
|
||||
def post(self, req_data: DatasetCreatePayload, session: Session, current_tenant_id: str, current_user: Account):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
@@ -582,20 +582,20 @@ class DatasetListApi(Resource):
|
||||
if dify_config.RBAC_ENABLED:
|
||||
permission = DatasetPermissionEnum.ALL_TEAM
|
||||
else:
|
||||
permission = payload.permission or DatasetPermissionEnum.ONLY_ME
|
||||
permission = req_data.permission or DatasetPermissionEnum.ONLY_ME
|
||||
|
||||
try:
|
||||
dataset = DatasetService.create_empty_dataset(
|
||||
session=session,
|
||||
tenant_id=current_tenant_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
indexing_technique=payload.indexing_technique,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
indexing_technique=req_data.indexing_technique,
|
||||
account=current_user,
|
||||
permission=permission,
|
||||
provider=payload.provider,
|
||||
external_knowledge_api_id=payload.external_knowledge_api_id,
|
||||
external_knowledge_id=payload.external_knowledge_id,
|
||||
provider=req_data.provider,
|
||||
external_knowledge_api_id=req_data.external_knowledge_api_id,
|
||||
external_knowledge_id=req_data.external_knowledge_id,
|
||||
)
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
raise DatasetNameDuplicateError()
|
||||
@@ -715,28 +715,35 @@ class DatasetApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
def patch(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
@model_validate(DatasetUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: DatasetUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
dataset_id: UUID,
|
||||
):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str, session)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
|
||||
payload = DatasetUpdatePayload.model_validate(console_ns.payload or {})
|
||||
# check embedding model setting
|
||||
if (
|
||||
payload.indexing_technique == IndexTechniqueType.HIGH_QUALITY
|
||||
and payload.embedding_model_provider is not None
|
||||
and payload.embedding_model is not None
|
||||
req_data.indexing_technique == IndexTechniqueType.HIGH_QUALITY
|
||||
and req_data.embedding_model_provider is not None
|
||||
and req_data.embedding_model is not None
|
||||
):
|
||||
is_multimodal = DatasetService.check_is_multimodal_model(
|
||||
dataset.tenant_id, payload.embedding_model_provider, payload.embedding_model
|
||||
dataset.tenant_id, req_data.embedding_model_provider, req_data.embedding_model
|
||||
)
|
||||
payload.is_multimodal = is_multimodal
|
||||
payload_data = payload.model_dump(exclude_unset=True)
|
||||
req_data.is_multimodal = is_multimodal
|
||||
payload_data = req_data.model_dump(exclude_unset=True)
|
||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
DatasetPermissionService.check_permission(
|
||||
current_user, dataset, payload.permission, payload.partial_member_list, session=session
|
||||
current_user, dataset, req_data.permission, req_data.partial_member_list, session=session
|
||||
)
|
||||
|
||||
dataset = DatasetService.update_dataset(dataset_id_str, payload_data, current_user, session=session)
|
||||
@@ -754,12 +761,12 @@ class DatasetApi(Resource):
|
||||
result_data["permission_keys"] = permission_keys_map.get(dataset_id_str, [])
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
if payload.partial_member_list is not None and payload.permission == DatasetPermissionEnum.PARTIAL_TEAM:
|
||||
if req_data.partial_member_list is not None and req_data.permission == DatasetPermissionEnum.PARTIAL_TEAM:
|
||||
DatasetPermissionService.update_partial_member_list(
|
||||
tenant_id, dataset_id_str, payload.partial_member_list, session
|
||||
tenant_id, dataset_id_str, req_data.partial_member_list, session
|
||||
)
|
||||
# clear partial member list when permission is only_me or all_team_members
|
||||
elif payload.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
|
||||
elif req_data.permission in {DatasetPermissionEnum.ONLY_ME, DatasetPermissionEnum.ALL_TEAM}:
|
||||
DatasetPermissionService.clear_partial_member_list(dataset_id_str, session)
|
||||
|
||||
partial_member_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str, session)
|
||||
@@ -872,9 +879,9 @@ class DatasetIndexingEstimateApi(Resource):
|
||||
@console_ns.expect(console_ns.models[IndexingEstimatePayload.__name__])
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str):
|
||||
payload = IndexingEstimatePayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump()
|
||||
@model_validate(IndexingEstimatePayload)
|
||||
def post(self, req_data: IndexingEstimatePayload, session: Session, current_tenant_id: str):
|
||||
args = req_data.model_dump()
|
||||
# validate args
|
||||
DocumentService.estimate_args_validate(args)
|
||||
extract_settings = []
|
||||
|
||||
@@ -21,7 +21,7 @@ from controllers.common.fields import SimpleResultMessageResponse, SimpleResultR
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.common.session import with_session
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
|
||||
from core.entities.knowledge_entities import IndexingEstimate
|
||||
from core.errors.error import (
|
||||
LLMBadRequestError,
|
||||
@@ -1267,13 +1267,20 @@ class DocumentMetadataApi(DocumentResource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
def put(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID, document_id: UUID):
|
||||
@model_validate(DocumentMetadataUpdatePayload)
|
||||
def put(
|
||||
self,
|
||||
req_data: DocumentMetadataUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
dataset_id: UUID,
|
||||
document_id: UUID,
|
||||
):
|
||||
dataset_id_str = str(dataset_id)
|
||||
document_id_str = str(document_id)
|
||||
document = self.get_document(session, dataset_id_str, document_id_str, current_user, current_tenant_id)
|
||||
|
||||
req_data = DocumentMetadataUpdatePayload.model_validate(request.get_json() or {})
|
||||
|
||||
doc_type = req_data.doc_type
|
||||
doc_metadata = req_data.doc_metadata
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ from controllers.console.wraps import (
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -414,8 +415,10 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(SegmentCreatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: SegmentCreatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -455,8 +458,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
# validate args
|
||||
payload = SegmentCreatePayload.model_validate(console_ns.payload or {})
|
||||
payload_dict = payload.model_dump(exclude_none=True)
|
||||
payload_dict = req_data.model_dump(exclude_none=True)
|
||||
SegmentService.segment_create_args_validate(payload_dict, document)
|
||||
segment = type_cast(DocumentSegment, SegmentService.create_segment(payload_dict, document, dataset, session))
|
||||
summary = SummaryIndexService.get_segment_summary(
|
||||
@@ -485,8 +487,10 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(SegmentUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: SegmentUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -532,13 +536,12 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
segment_id_str = str(segment_id)
|
||||
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
|
||||
# validate args
|
||||
payload = SegmentUpdatePayload.model_validate(console_ns.payload or {})
|
||||
payload_dict = payload.model_dump(exclude_none=True)
|
||||
payload_dict = req_data.model_dump(exclude_none=True)
|
||||
SegmentService.segment_create_args_validate(payload_dict, document)
|
||||
|
||||
# Update segment (summary update with change detection is handled in SegmentService.update_segment)
|
||||
segment = SegmentService.update_segment(
|
||||
SegmentUpdateArgs.model_validate(payload.model_dump(exclude_none=True)),
|
||||
SegmentUpdateArgs.model_validate(req_data.model_dump(exclude_none=True)),
|
||||
segment,
|
||||
document,
|
||||
dataset,
|
||||
@@ -616,8 +619,10 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(BatchImportPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: BatchImportPayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -635,8 +640,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
if not document:
|
||||
raise NotFound("Document not found.")
|
||||
|
||||
payload = BatchImportPayload.model_validate(console_ns.payload or {})
|
||||
upload_file_id = payload.upload_file_id
|
||||
upload_file_id = req_data.upload_file_id
|
||||
|
||||
upload_file = session.scalar(select(UploadFile).where(UploadFile.id == upload_file_id).limit(1))
|
||||
if not upload_file:
|
||||
@@ -697,8 +701,10 @@ class ChildChunkAddApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(ChildChunkCreatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: ChildChunkCreatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -742,8 +748,7 @@ class ChildChunkAddApi(Resource):
|
||||
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
|
||||
# validate args
|
||||
try:
|
||||
payload = ChildChunkCreatePayload.model_validate(console_ns.payload or {})
|
||||
child_chunk = SegmentService.create_child_chunk(payload.content, segment, document, dataset, session)
|
||||
child_chunk = SegmentService.create_child_chunk(req_data.content, segment, document, dataset, session)
|
||||
except ChildChunkIndexingServiceError as e:
|
||||
raise ChildChunkIndexingError(str(e))
|
||||
return dump_response(ChildChunkDetailResponse, {"data": child_chunk}), 200
|
||||
@@ -812,8 +817,10 @@ class ChildChunkAddApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(ChildChunkBatchUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: ChildChunkBatchUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -843,9 +850,8 @@ class ChildChunkAddApi(Resource):
|
||||
segment_id_str = str(segment_id)
|
||||
_, segment = _get_segment_for_document(session, dataset, document, segment_id_str)
|
||||
# validate args
|
||||
payload = ChildChunkBatchUpdatePayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
child_chunks = SegmentService.update_child_chunks(payload.chunks, segment, document, dataset, session)
|
||||
child_chunks = SegmentService.update_child_chunks(req_data.chunks, segment, document, dataset, session)
|
||||
except ChildChunkIndexingServiceError as e:
|
||||
raise ChildChunkIndexingError(str(e))
|
||||
return dump_response(ChildChunkBatchUpdateResponse, {"data": child_chunks}), 200
|
||||
@@ -918,8 +924,10 @@ class ChildChunkUpdateApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(ChildChunkUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: ChildChunkUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
@@ -955,9 +963,8 @@ class ChildChunkUpdateApi(Resource):
|
||||
raise NotFound("Child chunk not found.")
|
||||
# validate args
|
||||
try:
|
||||
payload = ChildChunkUpdatePayload.model_validate(console_ns.payload or {})
|
||||
child_chunk = SegmentService.update_child_chunk(
|
||||
payload.content, child_chunk, segment, document, dataset, session
|
||||
req_data.content, child_chunk, segment, document, dataset, session
|
||||
)
|
||||
except ChildChunkIndexingServiceError as e:
|
||||
raise ChildChunkIndexingError(str(e))
|
||||
|
||||
@@ -5,7 +5,6 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import AliasChoices, BaseModel, Field, field_validator
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -26,6 +25,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -186,18 +186,18 @@ class ExternalApiTemplateListApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@account_initialization_required
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_tenant_id: str):
|
||||
query = ExternalApiTemplateListQuery.model_validate(request.args.to_dict())
|
||||
@model_validate(ExternalApiTemplateListQuery)
|
||||
def get(self, req_data: ExternalApiTemplateListQuery, session: Session, current_tenant_id: str):
|
||||
|
||||
external_knowledge_apis, total = ExternalDatasetService.get_external_knowledge_apis(
|
||||
query.page, query.limit, current_tenant_id, query.keyword, session=session
|
||||
req_data.page, req_data.limit, current_tenant_id, req_data.keyword, session=session
|
||||
)
|
||||
return ExternalKnowledgeApiListResponse(
|
||||
data=[external_knowledge_api_response(item, session=session) for item in external_knowledge_apis],
|
||||
has_more=len(external_knowledge_apis) == query.limit,
|
||||
limit=query.limit,
|
||||
has_more=len(external_knowledge_apis) == req_data.limit,
|
||||
limit=req_data.limit,
|
||||
total=total,
|
||||
page=query.page,
|
||||
page=req_data.page,
|
||||
).model_dump(mode="json"), 200
|
||||
|
||||
@console_ns.doc("create_external_api_template")
|
||||
@@ -215,10 +215,16 @@ class ExternalApiTemplateListApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(ExternalKnowledgeApiPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: ExternalKnowledgeApiPayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
):
|
||||
|
||||
ExternalDatasetService.validate_api_list(payload.settings)
|
||||
ExternalDatasetService.validate_api_list(req_data.settings)
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
@@ -228,7 +234,7 @@ class ExternalApiTemplateListApi(Resource):
|
||||
external_knowledge_api = ExternalDatasetService.create_external_knowledge_api(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
args=payload.model_dump(),
|
||||
args=req_data.model_dump(),
|
||||
session=session,
|
||||
)
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
@@ -279,17 +285,24 @@ class ExternalApiTemplateApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def patch(self, session: Session, current_tenant_id: str, current_user: Account, external_knowledge_api_id: UUID):
|
||||
@model_validate(ExternalKnowledgeApiPayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: ExternalKnowledgeApiPayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
external_knowledge_api_id: UUID,
|
||||
):
|
||||
external_knowledge_api_id_str = str(external_knowledge_api_id)
|
||||
|
||||
payload = ExternalKnowledgeApiPayload.model_validate(console_ns.payload or {})
|
||||
ExternalDatasetService.validate_api_list(payload.settings)
|
||||
ExternalDatasetService.validate_api_list(req_data.settings)
|
||||
|
||||
external_knowledge_api = ExternalDatasetService.update_external_knowledge_api(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
external_knowledge_api_id=external_knowledge_api_id_str,
|
||||
args=payload.model_dump(),
|
||||
args=req_data.model_dump(),
|
||||
session=session,
|
||||
)
|
||||
|
||||
@@ -354,9 +367,15 @@ class ExternalDatasetCreateApi(Resource):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account):
|
||||
@model_validate(ExternalDatasetCreatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: ExternalDatasetCreatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
payload = ExternalDatasetCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
@@ -366,7 +385,7 @@ class ExternalDatasetCreateApi(Resource):
|
||||
dataset = ExternalDatasetService.create_external_dataset(
|
||||
tenant_id=current_tenant_id,
|
||||
user_id=current_user.id,
|
||||
args=payload,
|
||||
args=req_data,
|
||||
session=session,
|
||||
)
|
||||
except services.errors.dataset.DatasetNameDuplicateError:
|
||||
@@ -405,7 +424,8 @@ class ExternalKnowledgeHitTestingApi(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_PIPELINE_TEST)
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, dataset_id: UUID):
|
||||
@model_validate(ExternalHitTestingPayload)
|
||||
def post(self, req_data: ExternalHitTestingPayload, session: Session, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str, session)
|
||||
if dataset is None:
|
||||
@@ -416,17 +436,16 @@ class ExternalKnowledgeHitTestingApi(Resource):
|
||||
except services.errors.account.NoPermissionError as e:
|
||||
raise Forbidden(str(e))
|
||||
|
||||
payload = ExternalHitTestingPayload.model_validate(console_ns.payload or {})
|
||||
HitTestingService.hit_testing_args_check(payload.model_dump())
|
||||
HitTestingService.hit_testing_args_check(req_data.model_dump())
|
||||
|
||||
try:
|
||||
response = HitTestingService.external_retrieve(
|
||||
session=session,
|
||||
dataset=dataset,
|
||||
query=payload.query,
|
||||
query=req_data.query,
|
||||
account=current_user,
|
||||
external_retrieval_model=payload.external_retrieval_model,
|
||||
metadata_filtering_conditions=payload.metadata_filtering_conditions,
|
||||
external_retrieval_model=req_data.external_retrieval_model,
|
||||
metadata_filtering_conditions=req_data.metadata_filtering_conditions,
|
||||
)
|
||||
|
||||
return dump_response(ExternalHitTestingResponse, response)
|
||||
@@ -441,11 +460,11 @@ class BedrockRetrievalApi(Resource):
|
||||
@console_ns.doc(description="Bedrock retrieval test (internal use only)")
|
||||
@console_ns.expect(console_ns.models[BedrockRetrievalPayload.__name__])
|
||||
@console_ns.response(200, "Bedrock retrieval test completed", console_ns.models[BedrockRetrievalResponse.__name__])
|
||||
def post(self):
|
||||
payload = BedrockRetrievalPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(BedrockRetrievalPayload)
|
||||
def post(self, req_data: BedrockRetrievalPayload):
|
||||
|
||||
# Call the knowledge retrieval service
|
||||
result = ExternalDatasetTestService.knowledge_retrieval(
|
||||
payload.retrieval_setting, payload.query, payload.knowledge_id
|
||||
req_data.retrieval_setting, req_data.query, req_data.knowledge_id
|
||||
)
|
||||
return dump_response(BedrockRetrievalResponse, result), 200
|
||||
|
||||
@@ -14,6 +14,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
enterprise_license_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -59,9 +60,15 @@ class DatasetMetadataCreateApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, current_tenant_id: str, current_user: Account, dataset_id: UUID):
|
||||
metadata_args = MetadataArgs.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(MetadataArgs)
|
||||
def post(
|
||||
self,
|
||||
req_data: MetadataArgs,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
dataset_id: UUID,
|
||||
):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str, session)
|
||||
if dataset is None:
|
||||
@@ -69,7 +76,7 @@ class DatasetMetadataCreateApi(Resource):
|
||||
DatasetService.check_dataset_permission(dataset, current_user, session)
|
||||
|
||||
metadata = MetadataService.create_metadata(
|
||||
dataset_id_str, metadata_args, current_user, current_tenant_id, session=session
|
||||
dataset_id_str, req_data, current_user, current_tenant_id, session=session
|
||||
)
|
||||
return dump_response(DatasetMetadataResponse, metadata), 201
|
||||
|
||||
@@ -103,16 +110,17 @@ class DatasetMetadataApi(Resource):
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
@model_validate(MetadataUpdatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: MetadataUpdatePayload,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
dataset_id: UUID,
|
||||
metadata_id: UUID,
|
||||
):
|
||||
payload = MetadataUpdatePayload.model_validate(console_ns.payload or {})
|
||||
name = payload.name
|
||||
name = req_data.name
|
||||
|
||||
dataset_id_str = str(dataset_id)
|
||||
metadata_id_str = str(metadata_id)
|
||||
@@ -203,16 +211,15 @@ class DocumentMetadataEditApi(Resource):
|
||||
@with_current_user
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, dataset_id: UUID):
|
||||
@model_validate(MetadataOperationData)
|
||||
def post(self, req_data: MetadataOperationData, session: Session, current_user: Account, dataset_id: UUID):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str, session)
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
DatasetService.check_dataset_permission(dataset, current_user, session)
|
||||
|
||||
metadata_args = MetadataOperationData.model_validate(console_ns.payload or {})
|
||||
|
||||
MetadataService.update_documents_metadata(dataset, metadata_args, current_user, session=session)
|
||||
MetadataService.update_documents_metadata(dataset, req_data, current_user, session=session)
|
||||
|
||||
# Frontend callers only await success and invalidate caches; no response body is consumed.
|
||||
return "", 204
|
||||
|
||||
@@ -14,6 +14,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -275,8 +276,8 @@ class DatasourceAuth(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceCredentialPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(DatasourceCredentialPayload)
|
||||
def post(self, req_data: DatasourceCredentialPayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
|
||||
@@ -284,8 +285,8 @@ class DatasourceAuth(Resource):
|
||||
datasource_provider_service.add_datasource_api_key_provider(
|
||||
tenant_id=current_tenant_id,
|
||||
provider_id=datasource_provider_id,
|
||||
credentials=payload.credentials,
|
||||
name=payload.name,
|
||||
credentials=req_data.credentials,
|
||||
name=req_data.name,
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ValueError(str(ex))
|
||||
@@ -325,16 +326,16 @@ class DatasourceAuthDeleteApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
@model_validate(DatasourceCredentialDeletePayload)
|
||||
def post(self, req_data: DatasourceCredentialDeletePayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
plugin_id = datasource_provider_id.plugin_id
|
||||
provider_name = datasource_provider_id.provider_name
|
||||
|
||||
payload = DatasourceCredentialDeletePayload.model_validate(console_ns.payload or {})
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.remove_datasource_credentials(
|
||||
tenant_id=current_tenant_id,
|
||||
auth_id=payload.credential_id,
|
||||
auth_id=req_data.credential_id,
|
||||
provider=provider_name,
|
||||
plugin_id=plugin_id,
|
||||
session=db.session(),
|
||||
@@ -354,18 +355,18 @@ class DatasourceAuthUpdateApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
@model_validate(DatasourceCredentialUpdatePayload)
|
||||
def post(self, req_data: DatasourceCredentialUpdatePayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
payload = DatasourceCredentialUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.update_datasource_credentials(
|
||||
tenant_id=current_tenant_id,
|
||||
auth_id=payload.credential_id,
|
||||
auth_id=req_data.credential_id,
|
||||
provider=datasource_provider_id.provider_name,
|
||||
plugin_id=datasource_provider_id.plugin_id,
|
||||
credentials=payload.credentials or {},
|
||||
name=payload.name,
|
||||
credentials=req_data.credentials or {},
|
||||
name=req_data.name,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 201
|
||||
|
||||
@@ -420,15 +421,15 @@ class DatasourceAuthOauthCustomClient(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceCustomClientPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(DatasourceCustomClientPayload)
|
||||
def post(self, req_data: DatasourceCustomClientPayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.setup_oauth_custom_client_params(
|
||||
tenant_id=current_tenant_id,
|
||||
datasource_provider_id=datasource_provider_id,
|
||||
client_params=payload.client_params or {},
|
||||
enabled=payload.enable_oauth_custom_client or False,
|
||||
client_params=req_data.client_params or {},
|
||||
enabled=req_data.enable_oauth_custom_client or False,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@@ -457,14 +458,14 @@ class DatasourceAuthDefaultApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceDefaultPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(DatasourceDefaultPayload)
|
||||
def post(self, req_data: DatasourceDefaultPayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.set_default_datasource_provider(
|
||||
tenant_id=current_tenant_id,
|
||||
datasource_provider_id=datasource_provider_id,
|
||||
credential_id=payload.id,
|
||||
credential_id=req_data.id,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@@ -479,14 +480,14 @@ class DatasourceUpdateProviderNameApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider_id: str):
|
||||
payload = DatasourceUpdateNamePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(DatasourceUpdateNamePayload)
|
||||
def post(self, req_data: DatasourceUpdateNamePayload, current_tenant_id: str, provider_id: str):
|
||||
datasource_provider_id = DatasourceProviderID(provider_id)
|
||||
datasource_provider_service = DatasourceProviderService()
|
||||
datasource_provider_service.update_datasource_provider_name(
|
||||
tenant_id=current_tenant_id,
|
||||
datasource_provider_id=datasource_provider_id,
|
||||
name=payload.name,
|
||||
credential_id=payload.credential_id,
|
||||
name=req_data.name,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel
|
||||
from controllers.common.schema import register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
|
||||
from extensions.ext_database import db
|
||||
from libs.login import login_required
|
||||
from models import Account
|
||||
@@ -34,14 +34,14 @@ class DataSourceContentPreviewApi(Resource):
|
||||
@account_initialization_required
|
||||
@get_rag_pipeline
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(Parser)
|
||||
def post(self, req_data: Parser, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run datasource content preview
|
||||
"""
|
||||
args = Parser.model_validate(console_ns.payload)
|
||||
|
||||
inputs = args.inputs
|
||||
datasource_type = args.datasource_type
|
||||
inputs = req_data.inputs
|
||||
datasource_type = req_data.datasource_type
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
preview_content = rag_pipeline_service.run_datasource_node_preview(
|
||||
pipeline=pipeline,
|
||||
@@ -50,6 +50,6 @@ class DataSourceContentPreviewApi(Resource):
|
||||
account=current_user,
|
||||
datasource_type=datasource_type,
|
||||
is_published=True,
|
||||
credential_id=args.credential_id,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
return preview_content, 200
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
@@ -21,6 +20,7 @@ from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
enterprise_license_required,
|
||||
knowledge_pipeline_publish_enabled,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -104,12 +104,17 @@ class PipelineTemplateListApi(Resource):
|
||||
@enterprise_license_required
|
||||
@with_current_tenant_id
|
||||
@with_session
|
||||
def get(self, session: Session, current_tenant_id: str) -> JsonResponseWithStatus:
|
||||
query = PipelineTemplateListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(PipelineTemplateListQuery)
|
||||
def get(
|
||||
self,
|
||||
req_data: PipelineTemplateListQuery,
|
||||
session: Session,
|
||||
current_tenant_id: str,
|
||||
) -> JsonResponseWithStatus:
|
||||
# get pipeline templates
|
||||
pipeline_templates = RagPipelineService.get_pipeline_templates(
|
||||
type=query.type,
|
||||
language=query.language,
|
||||
type=req_data.type,
|
||||
language=req_data.language,
|
||||
current_tenant_id=current_tenant_id,
|
||||
session=session,
|
||||
)
|
||||
@@ -125,11 +130,11 @@ class PipelineTemplateDetailApi(Resource):
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
@with_session
|
||||
def get(self, session: Session, template_id: str) -> JsonResponseWithStatus:
|
||||
query = PipelineTemplateDetailQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(PipelineTemplateDetailQuery)
|
||||
def get(self, req_data: PipelineTemplateDetailQuery, session: Session, template_id: str) -> JsonResponseWithStatus:
|
||||
pipeline_template = RagPipelineService.get_pipeline_template_detail(
|
||||
template_id,
|
||||
type=query.type,
|
||||
type=req_data.type,
|
||||
session=session,
|
||||
)
|
||||
if pipeline_template is None:
|
||||
@@ -147,9 +152,15 @@ class CustomizedPipelineTemplateApi(Resource):
|
||||
@enterprise_license_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, template_id: str) -> tuple[str, int]:
|
||||
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
|
||||
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(payload.model_dump())
|
||||
@model_validate(CustomizedPipelineTemplatePayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: CustomizedPipelineTemplatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
template_id: str,
|
||||
) -> tuple[str, int]:
|
||||
pipeline_template_info = PipelineTemplateInfoEntity.model_validate(req_data.model_dump())
|
||||
RagPipelineService.update_customized_pipeline_template(
|
||||
template_id, pipeline_template_info, current_user, current_tenant_id, session=db.session()
|
||||
)
|
||||
@@ -192,10 +203,16 @@ class PublishCustomizedPipelineTemplateApi(Resource):
|
||||
@knowledge_pipeline_publish_enabled
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, pipeline_id: str) -> tuple[str, int]:
|
||||
payload = CustomizedPipelineTemplatePayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(CustomizedPipelineTemplatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: CustomizedPipelineTemplatePayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
pipeline_id: str,
|
||||
) -> tuple[str, int]:
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
rag_pipeline_service.publish_customized_pipeline_template(
|
||||
pipeline_id, payload.model_dump(), current_user, current_tenant_id, session=db.session()
|
||||
pipeline_id, req_data.model_dump(), current_user, current_tenant_id, session=db.session()
|
||||
)
|
||||
return "", 204
|
||||
|
||||
@@ -10,6 +10,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_import import RagPip
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -47,8 +48,13 @@ class CreateRagPipelineDatasetApi(Resource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account) -> JsonResponseWithStatus:
|
||||
payload = RagPipelineDatasetImportPayload.model_validate(console_ns.payload or {})
|
||||
@model_validate(RagPipelineDatasetImportPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: RagPipelineDatasetImportPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
) -> JsonResponseWithStatus:
|
||||
# The role of the current user in the ta table must be admin, owner, or editor, or dataset_operator
|
||||
if not current_user.is_dataset_editor:
|
||||
raise Forbidden()
|
||||
@@ -62,7 +68,7 @@ class CreateRagPipelineDatasetApi(Resource):
|
||||
),
|
||||
permission=DatasetPermissionEnum.ONLY_ME,
|
||||
partial_member_list=None,
|
||||
yaml_content=payload.yaml_content,
|
||||
yaml_content=req_data.yaml_content,
|
||||
)
|
||||
try:
|
||||
rag_pipeline_dsl_service = RagPipelineDslService(db.session())
|
||||
|
||||
@@ -4,7 +4,7 @@ from functools import wraps
|
||||
from typing import Any, Concatenate, NoReturn
|
||||
from uuid import UUID
|
||||
|
||||
from flask import Response, request
|
||||
from flask import Response
|
||||
from flask_restx import Resource, marshal, marshal_with
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
@@ -24,7 +24,7 @@ from controllers.console.app.workflow_draft_variable import (
|
||||
workflow_draft_variable_model,
|
||||
)
|
||||
from controllers.console.datasets.wraps import get_rag_pipeline
|
||||
from controllers.console.wraps import account_initialization_required, setup_required, with_current_user
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required, with_current_user
|
||||
from core.app.file_access import DatabaseFileAccessController
|
||||
from core.workflow.llm_environment_variable import LLMEnvironmentVariable, environment_variable_value_type
|
||||
from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID
|
||||
@@ -92,11 +92,11 @@ class RagPipelineVariableCollectionApi(Resource):
|
||||
)
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_list_without_value_model)
|
||||
def get(self, current_user: Account, pipeline: Pipeline):
|
||||
@model_validate(PaginationQuery)
|
||||
def get(self, req_data: PaginationQuery, current_user: Account, pipeline: Pipeline):
|
||||
"""
|
||||
Get draft workflow
|
||||
"""
|
||||
query = PaginationQuery.model_validate(request.args.to_dict())
|
||||
|
||||
# fetch draft workflow by app_model
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
@@ -111,8 +111,8 @@ class RagPipelineVariableCollectionApi(Resource):
|
||||
)
|
||||
workflow_vars = draft_var_srv.list_variables_without_values(
|
||||
app_id=pipeline.id,
|
||||
page=query.page,
|
||||
limit=query.limit,
|
||||
page=req_data.page,
|
||||
limit=req_data.limit,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
@@ -196,7 +196,14 @@ class RagPipelineVariableApi(Resource):
|
||||
@_api_prerequisite
|
||||
@marshal_with(workflow_draft_variable_model)
|
||||
@console_ns.expect(console_ns.models[WorkflowDraftVariablePatchPayload.__name__])
|
||||
def patch(self, _current_user: Account, pipeline: Pipeline, variable_id: UUID):
|
||||
@model_validate(WorkflowDraftVariablePatchPayload)
|
||||
def patch(
|
||||
self,
|
||||
req_data: WorkflowDraftVariablePatchPayload,
|
||||
_current_user: Account,
|
||||
pipeline: Pipeline,
|
||||
variable_id: UUID,
|
||||
):
|
||||
# Request payload for file types:
|
||||
#
|
||||
# Local File:
|
||||
@@ -221,8 +228,7 @@ class RagPipelineVariableApi(Resource):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
session=db.session(),
|
||||
)
|
||||
payload = WorkflowDraftVariablePatchPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
variable_id_str = str(variable_id)
|
||||
variable = draft_var_srv.get_variable(variable_id=variable_id_str)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -85,9 +85,9 @@ class RagPipelineImportApi(Resource):
|
||||
RBACResourceScope.DATASET, RBACPermission.DATASET_CREATE_AND_MANAGEMENT, resource_required=False
|
||||
)
|
||||
@with_current_user
|
||||
def post(self, current_user: Account) -> JsonResponseWithStatus:
|
||||
@model_validate(RagPipelineImportPayload)
|
||||
def post(self, req_data: RagPipelineImportPayload, current_user: Account) -> JsonResponseWithStatus:
|
||||
# Check user role first
|
||||
payload = RagPipelineImportPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
# Use a plain Session so that caught exceptions inside the service
|
||||
# (which return FAILED status instead of re-raising) do not leave the
|
||||
@@ -98,11 +98,11 @@ class RagPipelineImportApi(Resource):
|
||||
account = current_user
|
||||
result = import_service.import_rag_pipeline(
|
||||
account=account,
|
||||
import_mode=payload.mode,
|
||||
yaml_content=payload.yaml_content,
|
||||
yaml_url=payload.yaml_url,
|
||||
pipeline_id=payload.pipeline_id,
|
||||
dataset_name=payload.name,
|
||||
import_mode=req_data.mode,
|
||||
yaml_content=req_data.yaml_content,
|
||||
yaml_url=req_data.yaml_url,
|
||||
pipeline_id=req_data.pipeline_id,
|
||||
dataset_name=req_data.name,
|
||||
)
|
||||
if result.status == ImportStatus.FAILED:
|
||||
session.rollback()
|
||||
@@ -179,14 +179,14 @@ class RagPipelineExportApi(Resource):
|
||||
@account_initialization_required
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_IMPORT_EXPORT_DSL)
|
||||
def get(self, pipeline: Pipeline) -> JsonResponseWithStatus:
|
||||
@model_validate(IncludeSecretQuery)
|
||||
def get(self, req_data: IncludeSecretQuery, pipeline: Pipeline) -> JsonResponseWithStatus:
|
||||
# Add include_secret params
|
||||
query = IncludeSecretQuery.model_validate(request.args.to_dict())
|
||||
|
||||
with Session(db.engine, expire_on_commit=False) as session:
|
||||
export_service = RagPipelineDslService(session)
|
||||
result = export_service.export_rag_pipeline_dsl(
|
||||
pipeline=pipeline, include_secret=query.include_secret == "true"
|
||||
pipeline=pipeline, include_secret=req_data.include_secret == "true"
|
||||
)
|
||||
|
||||
return dump_response(SimpleDataResponse, {"data": result}), 200
|
||||
|
||||
@@ -33,6 +33,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -274,12 +275,12 @@ class RagPipelineDraftRunIterationNodeApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(NodeRunPayload)
|
||||
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run draft workflow iteration node
|
||||
"""
|
||||
payload = NodeRunPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
try:
|
||||
response = PipelineGenerateService.generate_single_iteration(
|
||||
@@ -309,12 +310,12 @@ class RagPipelineDraftRunLoopNodeApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(NodeRunPayload)
|
||||
def post(self, req_data: NodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run draft workflow loop node
|
||||
"""
|
||||
payload = NodeRunPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
try:
|
||||
response = PipelineGenerateService.generate_single_loop(
|
||||
@@ -344,13 +345,13 @@ class DraftRagPipelineRunApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
|
||||
@model_validate(DraftWorkflowRunPayload)
|
||||
def post(self, req_data: DraftWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
|
||||
"""
|
||||
Run draft workflow
|
||||
"""
|
||||
pipeline = load_rag_pipeline(session, str(pipeline_id))
|
||||
payload = DraftWorkflowRunPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump()
|
||||
args = req_data.model_dump()
|
||||
|
||||
try:
|
||||
response = PipelineGenerateService.generate(
|
||||
@@ -378,14 +379,14 @@ class PublishedRagPipelineRunApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, pipeline_id: UUID):
|
||||
@model_validate(PublishedWorkflowRunPayload)
|
||||
def post(self, req_data: PublishedWorkflowRunPayload, session: Session, current_user: Account, pipeline_id: UUID):
|
||||
"""
|
||||
Run published workflow
|
||||
"""
|
||||
pipeline = load_rag_pipeline(session, str(pipeline_id))
|
||||
payload = PublishedWorkflowRunPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
streaming = payload.response_mode == "streaming"
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
streaming = req_data.response_mode == "streaming"
|
||||
|
||||
try:
|
||||
response = PipelineGenerateService.generate(
|
||||
@@ -393,7 +394,7 @@ class PublishedRagPipelineRunApi(Resource):
|
||||
pipeline=pipeline,
|
||||
user=current_user,
|
||||
args=args,
|
||||
invoke_from=InvokeFrom.DEBUGGER if payload.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
|
||||
invoke_from=InvokeFrom.DEBUGGER if req_data.is_preview else InvokeFrom.PUBLISHED_PIPELINE,
|
||||
streaming=streaming,
|
||||
)
|
||||
|
||||
@@ -413,11 +414,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(DatasourceNodeRunPayload)
|
||||
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run rag pipeline datasource
|
||||
"""
|
||||
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
return helper.compact_generate_response(
|
||||
@@ -425,11 +426,11 @@ class RagPipelinePublishedDatasourceNodeRunApi(Resource):
|
||||
rag_pipeline_service.run_datasource_workflow_node(
|
||||
pipeline=pipeline,
|
||||
node_id=node_id,
|
||||
user_inputs=payload.inputs,
|
||||
user_inputs=req_data.inputs,
|
||||
account=current_user,
|
||||
datasource_type=payload.datasource_type,
|
||||
datasource_type=req_data.datasource_type,
|
||||
is_published=False,
|
||||
credential_id=payload.credential_id,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -446,11 +447,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(DatasourceNodeRunPayload)
|
||||
def post(self, req_data: DatasourceNodeRunPayload, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run rag pipeline datasource
|
||||
"""
|
||||
payload = DatasourceNodeRunPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
return helper.compact_generate_response(
|
||||
@@ -458,11 +459,11 @@ class RagPipelineDraftDatasourceNodeRunApi(Resource):
|
||||
rag_pipeline_service.run_datasource_workflow_node(
|
||||
pipeline=pipeline,
|
||||
node_id=node_id,
|
||||
user_inputs=payload.inputs,
|
||||
user_inputs=req_data.inputs,
|
||||
account=current_user,
|
||||
datasource_type=payload.datasource_type,
|
||||
datasource_type=req_data.datasource_type,
|
||||
is_published=False,
|
||||
credential_id=payload.credential_id,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -483,12 +484,12 @@ class RagPipelineDraftNodeRunApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def post(self, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
@model_validate(NodeRunRequiredPayload)
|
||||
def post(self, req_data: NodeRunRequiredPayload, current_user: Account, pipeline: Pipeline, node_id: str):
|
||||
"""
|
||||
Run draft workflow node
|
||||
"""
|
||||
payload = NodeRunRequiredPayload.model_validate(console_ns.payload or {})
|
||||
inputs = payload.inputs
|
||||
inputs = req_data.inputs
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
workflow_node_execution = rag_pipeline_service.run_draft_workflow_node(
|
||||
@@ -617,16 +618,16 @@ class DefaultRagPipelineBlockConfigApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@get_rag_pipeline
|
||||
def get(self, pipeline: Pipeline, block_type: str):
|
||||
@model_validate(DefaultBlockConfigQuery)
|
||||
def get(self, req_data: DefaultBlockConfigQuery, pipeline: Pipeline, block_type: str):
|
||||
"""
|
||||
Get default block config
|
||||
"""
|
||||
query = DefaultBlockConfigQuery.model_validate(request.args.to_dict())
|
||||
|
||||
filters = None
|
||||
if query.q:
|
||||
if req_data.q:
|
||||
try:
|
||||
filters = json.loads(query.q)
|
||||
filters = json.loads(req_data.q)
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError("Invalid filters")
|
||||
|
||||
@@ -651,16 +652,16 @@ class PublishedAllRagPipelineApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
def get(self, current_user: Account, pipeline: Pipeline):
|
||||
@model_validate(WorkflowListQuery)
|
||||
def get(self, req_data: WorkflowListQuery, current_user: Account, pipeline: Pipeline):
|
||||
"""
|
||||
Get published workflows
|
||||
"""
|
||||
query = WorkflowListQuery.model_validate(request.args.to_dict())
|
||||
|
||||
page = query.page
|
||||
limit = query.limit
|
||||
user_id = query.user_id
|
||||
named_only = query.named_only
|
||||
page = req_data.page
|
||||
limit = req_data.limit
|
||||
user_id = req_data.user_id
|
||||
named_only = req_data.named_only
|
||||
|
||||
if user_id:
|
||||
if user_id != current_user.id:
|
||||
@@ -733,12 +734,12 @@ class RagPipelineByIdApi(Resource):
|
||||
@with_current_user
|
||||
@get_rag_pipeline
|
||||
@console_ns.expect(console_ns.models[WorkflowUpdatePayload.__name__])
|
||||
def patch(self, current_user: Account, pipeline: Pipeline, workflow_id: str):
|
||||
@model_validate(WorkflowUpdatePayload)
|
||||
def patch(self, req_data: WorkflowUpdatePayload, current_user: Account, pipeline: Pipeline, workflow_id: str):
|
||||
"""
|
||||
Update workflow attributes
|
||||
"""
|
||||
payload = WorkflowUpdatePayload.model_validate(console_ns.payload or {})
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
update_data = req_data.model_dump(exclude_unset=True)
|
||||
|
||||
if not update_data:
|
||||
return {"message": "No valid fields to update"}, 400
|
||||
@@ -803,12 +804,12 @@ class PublishedRagPipelineSecondStepApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
@model_validate(NodeIdQuery)
|
||||
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
|
||||
"""
|
||||
Get second step parameters of rag pipeline
|
||||
"""
|
||||
query = NodeIdQuery.model_validate(request.args.to_dict())
|
||||
node_id = query.node_id
|
||||
node_id = req_data.node_id
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
|
||||
return {
|
||||
@@ -826,12 +827,12 @@ class PublishedRagPipelineFirstStepApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
@model_validate(NodeIdQuery)
|
||||
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
|
||||
"""
|
||||
Get first step parameters of rag pipeline
|
||||
"""
|
||||
query = NodeIdQuery.model_validate(request.args.to_dict())
|
||||
node_id = query.node_id
|
||||
node_id = req_data.node_id
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=False)
|
||||
return {
|
||||
@@ -849,12 +850,12 @@ class DraftRagPipelineFirstStepApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
@model_validate(NodeIdQuery)
|
||||
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
|
||||
"""
|
||||
Get first step parameters of rag pipeline
|
||||
"""
|
||||
query = NodeIdQuery.model_validate(request.args.to_dict())
|
||||
node_id = query.node_id
|
||||
node_id = req_data.node_id
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
variables = rag_pipeline_service.get_first_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
|
||||
return {
|
||||
@@ -872,12 +873,12 @@ class DraftRagPipelineSecondStepApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def get(self, pipeline: Pipeline):
|
||||
@model_validate(NodeIdQuery)
|
||||
def get(self, req_data: NodeIdQuery, pipeline: Pipeline):
|
||||
"""
|
||||
Get second step parameters of rag pipeline
|
||||
"""
|
||||
query = NodeIdQuery.model_validate(request.args.to_dict())
|
||||
node_id = query.node_id
|
||||
node_id = req_data.node_id
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
variables = rag_pipeline_service.get_second_step_parameters(pipeline=pipeline, node_id=node_id, is_draft=True)
|
||||
@@ -1045,11 +1046,12 @@ class RagPipelineDatasourceVariableApi(Resource):
|
||||
@get_rag_pipeline
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.DATASET, RBACPermission.DATASET_EDIT)
|
||||
def post(self, current_user: Account, pipeline: Pipeline):
|
||||
@model_validate(DatasourceVariablesPayload)
|
||||
def post(self, req_data: DatasourceVariablesPayload, current_user: Account, pipeline: Pipeline):
|
||||
"""
|
||||
Set datasource variables
|
||||
"""
|
||||
args = DatasourceVariablesPayload.model_validate(console_ns.payload or {}).model_dump()
|
||||
args = req_data.model_dump()
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
workflow_node_execution = rag_pipeline_service.set_datasource_variables(
|
||||
@@ -1071,9 +1073,11 @@ class RagPipelineRecommendedPluginApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, current_tenant_id: str, current_user: Account):
|
||||
query = RagPipelineRecommendedPluginQuery.model_validate(request.args.to_dict())
|
||||
@model_validate(RagPipelineRecommendedPluginQuery)
|
||||
def get(self, req_data: RagPipelineRecommendedPluginQuery, current_tenant_id: str, current_user: Account):
|
||||
|
||||
rag_pipeline_service = RagPipelineService(db.session())
|
||||
recommended_plugins = rag_pipeline_service.get_recommended_plugins(query.type, current_user, current_tenant_id)
|
||||
recommended_plugins = rag_pipeline_service.get_recommended_plugins(
|
||||
req_data.type, current_user, current_tenant_id
|
||||
)
|
||||
return recommended_plugins
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, RootModel
|
||||
|
||||
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.datasets.error import WebsiteCrawlError
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import account_initialization_required, model_validate, setup_required
|
||||
from libs.login import login_required
|
||||
from services.website_service import WebsiteCrawlApiRequest, WebsiteCrawlStatusApiRequest, WebsiteService
|
||||
|
||||
@@ -40,12 +39,11 @@ class WebsiteCrawlApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def post(self):
|
||||
payload = WebsiteCrawlPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(WebsiteCrawlPayload)
|
||||
def post(self, req_data: WebsiteCrawlPayload):
|
||||
# Create typed request and validate
|
||||
try:
|
||||
api_request = WebsiteCrawlApiRequest.from_args(payload.model_dump())
|
||||
api_request = WebsiteCrawlApiRequest.from_args(req_data.model_dump())
|
||||
except ValueError as e:
|
||||
raise WebsiteCrawlError(str(e))
|
||||
|
||||
@@ -69,12 +67,11 @@ class WebsiteCrawlStatusApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self, job_id: str):
|
||||
args = WebsiteCrawlStatusQuery.model_validate(request.args.to_dict())
|
||||
|
||||
@model_validate(WebsiteCrawlStatusQuery)
|
||||
def get(self, req_data: WebsiteCrawlStatusQuery, job_id: str):
|
||||
# Create typed request and validate
|
||||
try:
|
||||
api_request = WebsiteCrawlStatusApiRequest.from_args(args.model_dump(), job_id)
|
||||
api_request = WebsiteCrawlStatusApiRequest.from_args(req_data.model_dump(), job_id)
|
||||
except ValueError as e:
|
||||
raise WebsiteCrawlError(str(e))
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from controllers.console.app.error import (
|
||||
UnsupportedAudioTypeError,
|
||||
)
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import model_validate
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from extensions.ext_database import db
|
||||
from graphon.model_runtime.errors.invoke import InvokeError
|
||||
@@ -100,16 +101,15 @@ class ChatAudioApi(InstalledAppResource):
|
||||
class ChatTextApi(InstalledAppResource):
|
||||
@console_ns.expect(console_ns.models[TextToAudioPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[AudioBinaryResponse.__name__])
|
||||
def post(self, installed_app: InstalledApp):
|
||||
@model_validate(TextToAudioPayload)
|
||||
def post(self, req_data: TextToAudioPayload, installed_app: InstalledApp):
|
||||
app_model = installed_app.app_with_session(session=db.session())
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
try:
|
||||
payload = TextToAudioPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
message_id = payload.message_id
|
||||
text = payload.text
|
||||
voice = payload.voice
|
||||
message_id = req_data.message_id
|
||||
text = req_data.text
|
||||
voice = req_data.voice
|
||||
message_ref = None
|
||||
if message_id:
|
||||
current_user, _ = current_account_with_tenant()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -20,7 +20,7 @@ from controllers.console.app.error import (
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.console.explore.error import NotChatAppError, NotCompletionAppError
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import with_current_user, with_current_user_id
|
||||
from controllers.console.wraps import model_validate, with_current_user, with_current_user_id
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.errors.error import (
|
||||
@@ -89,17 +89,23 @@ class CompletionApi(InstalledAppResource):
|
||||
@console_ns.response(200, "Success")
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
|
||||
@model_validate(CompletionMessageExplorePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: CompletionMessageExplorePayload,
|
||||
session: Session,
|
||||
current_user: Account,
|
||||
installed_app: InstalledApp,
|
||||
):
|
||||
app_model = installed_app.app_with_session(session=session)
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
if app_model.mode != AppMode.COMPLETION:
|
||||
raise NotCompletionAppError()
|
||||
|
||||
payload = CompletionMessageExplorePayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
streaming = payload.response_mode == "streaming"
|
||||
streaming = req_data.response_mode == "streaming"
|
||||
args["auto_generate_name"] = False
|
||||
|
||||
installed_app.last_used_at = naive_utc_now()
|
||||
@@ -173,7 +179,8 @@ class ChatApi(InstalledAppResource):
|
||||
@console_ns.response(200, "Success")
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
|
||||
@model_validate(ChatMessagePayload)
|
||||
def post(self, req_data: ChatMessagePayload, session: Session, current_user: Account, installed_app: InstalledApp):
|
||||
app_model = installed_app.app_with_session(session=session)
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
@@ -181,8 +188,7 @@ class ChatApi(InstalledAppResource):
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
payload = ChatMessagePayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
|
||||
args["auto_generate_name"] = False
|
||||
|
||||
@@ -191,10 +197,10 @@ class ChatApi(InstalledAppResource):
|
||||
|
||||
try:
|
||||
# Eagerly validate conversation to avoid hanging on invalid conversation_id
|
||||
if payload.conversation_id:
|
||||
if req_data.conversation_id:
|
||||
ConversationService.get_conversation(
|
||||
app_model=app_model,
|
||||
conversation_id=payload.conversation_id,
|
||||
conversation_id=req_data.conversation_id,
|
||||
user=current_user,
|
||||
session=session,
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from controllers.common.schema import query_params_from_model, register_response
|
||||
from controllers.console.app.error import AppUnavailableError
|
||||
from controllers.console.explore.error import NotChatAppError
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import with_current_user
|
||||
from controllers.console.wraps import model_validate, with_current_user
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from extensions.ext_database import db
|
||||
from fields.conversation_fields import (
|
||||
@@ -133,7 +133,8 @@ class ConversationRenameApi(InstalledAppResource):
|
||||
@console_ns.expect(console_ns.models[ConversationRenamePayload.__name__])
|
||||
@console_ns.response(200, "Conversation renamed successfully", console_ns.models[SimpleConversation.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, installed_app: InstalledApp, c_id: UUID):
|
||||
@model_validate(ConversationRenamePayload)
|
||||
def post(self, req_data: ConversationRenamePayload, current_user: Account, installed_app: InstalledApp, c_id: UUID):
|
||||
app_model = installed_app.app_with_session(session=db.session())
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
@@ -143,12 +144,10 @@ class ConversationRenameApi(InstalledAppResource):
|
||||
|
||||
conversation_id = str(c_id)
|
||||
|
||||
payload = ConversationRenamePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
session = db.session()
|
||||
conversation = ConversationService.rename(
|
||||
app_model, conversation_id, current_user, payload.name, payload.auto_generate, session=session
|
||||
app_model, conversation_id, current_user, req_data.name, req_data.auto_generate, session=session
|
||||
)
|
||||
return (
|
||||
TypeAdapter(SimpleConversation)
|
||||
|
||||
@@ -15,6 +15,7 @@ from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
model_validate,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
@@ -195,16 +196,15 @@ class InstalledAppsListApi(Resource):
|
||||
@console_ns.expect(console_ns.models[InstalledAppCreatePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleMessageResponse.__name__])
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
payload = InstalledAppCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(InstalledAppCreatePayload)
|
||||
def post(self, req_data: InstalledAppCreatePayload, current_tenant_id: str):
|
||||
recommended_app = db.session.scalar(
|
||||
select(RecommendedApp).where(RecommendedApp.app_id == payload.app_id).limit(1)
|
||||
select(RecommendedApp).where(RecommendedApp.app_id == req_data.app_id).limit(1)
|
||||
)
|
||||
if recommended_app is None:
|
||||
raise NotFound("Recommended app not found")
|
||||
|
||||
app = db.session.get(App, payload.app_id)
|
||||
app = db.session.get(App, req_data.app_id)
|
||||
|
||||
if app is None:
|
||||
raise NotFound("App entity not found")
|
||||
@@ -214,7 +214,7 @@ class InstalledAppsListApi(Resource):
|
||||
|
||||
installed_app = db.session.scalar(
|
||||
select(InstalledApp)
|
||||
.where(and_(InstalledApp.app_id == payload.app_id, InstalledApp.tenant_id == current_tenant_id))
|
||||
.where(and_(InstalledApp.app_id == req_data.app_id, InstalledApp.tenant_id == current_tenant_id))
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@@ -223,7 +223,7 @@ class InstalledAppsListApi(Resource):
|
||||
recommended_app.install_count += 1
|
||||
|
||||
new_installed_app = InstalledApp(
|
||||
app_id=payload.app_id,
|
||||
app_id=req_data.app_id,
|
||||
tenant_id=current_tenant_id,
|
||||
app_owner_tenant_id=app.tenant_id,
|
||||
is_pinned=False,
|
||||
@@ -281,12 +281,11 @@ class InstalledAppApi(InstalledAppResource):
|
||||
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultMessageResponse.__name__])
|
||||
@console_ns.expect(console_ns.models[InstalledAppUpdatePayload.__name__])
|
||||
def patch(self, installed_app: InstalledApp):
|
||||
payload = InstalledAppUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(InstalledAppUpdatePayload)
|
||||
def patch(self, req_data: InstalledAppUpdatePayload, installed_app: InstalledApp):
|
||||
commit_args = False
|
||||
if payload.is_pinned is not None:
|
||||
installed_app.is_pinned = payload.is_pinned
|
||||
if req_data.is_pinned is not None:
|
||||
installed_app.is_pinned = req_data.is_pinned
|
||||
commit_args = True
|
||||
|
||||
if commit_args:
|
||||
|
||||
@@ -24,7 +24,7 @@ from controllers.console.explore.error import (
|
||||
NotCompletionAppError,
|
||||
)
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import with_current_user
|
||||
from controllers.console.wraps import model_validate, with_current_user
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from extensions.ext_database import db
|
||||
@@ -119,22 +119,23 @@ class MessageFeedbackApi(InstalledAppResource):
|
||||
@console_ns.expect(console_ns.models[MessageFeedbackPayload.__name__])
|
||||
@console_ns.response(200, "Feedback submitted successfully", console_ns.models[ResultResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, installed_app: InstalledApp, message_id: UUID):
|
||||
@model_validate(MessageFeedbackPayload)
|
||||
def post(
|
||||
self, req_data: MessageFeedbackPayload, current_user: Account, installed_app: InstalledApp, message_id: UUID
|
||||
):
|
||||
app_model = installed_app.app_with_session(session=db.session())
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
|
||||
message_id_str = str(message_id)
|
||||
|
||||
payload = MessageFeedbackPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
MessageService.create_feedback(
|
||||
app_model=app_model,
|
||||
message_id=message_id_str,
|
||||
user=current_user,
|
||||
rating=FeedbackRating(payload.rating) if payload.rating else None,
|
||||
content=payload.content,
|
||||
rating=FeedbackRating(req_data.rating) if req_data.rating else None,
|
||||
content=req_data.content,
|
||||
session=db.session(),
|
||||
)
|
||||
except MessageNotExistsError:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,7 +10,7 @@ from controllers.console import console_ns
|
||||
from controllers.console.app.error import AppUnavailableError
|
||||
from controllers.console.explore.error import NotCompletionAppError
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import with_current_user
|
||||
from controllers.console.wraps import model_validate, with_current_user
|
||||
from extensions.ext_database import db
|
||||
from fields.conversation_fields import MessageResponseSource, ResultResponse
|
||||
from fields.message_fields import SavedMessageInfiniteScrollPagination, SavedMessageItem
|
||||
@@ -55,17 +55,16 @@ class SavedMessageListApi(InstalledAppResource):
|
||||
@console_ns.expect(console_ns.models[SavedMessageCreatePayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[ResultResponse.__name__])
|
||||
@with_current_user
|
||||
def post(self, current_user: Account, installed_app: InstalledApp):
|
||||
@model_validate(SavedMessageCreatePayload)
|
||||
def post(self, req_data: SavedMessageCreatePayload, current_user: Account, installed_app: InstalledApp):
|
||||
app_model = installed_app.app_with_session(session=db.session())
|
||||
if app_model is None:
|
||||
raise AppUnavailableError()
|
||||
if app_model.mode != "completion":
|
||||
raise NotCompletionAppError()
|
||||
|
||||
payload = SavedMessageCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
SavedMessageService.save(app_model, current_user, str(payload.message_id), session=db.session())
|
||||
SavedMessageService.save(app_model, current_user, str(req_data.message_id), session=db.session())
|
||||
except MessageNotExistsError:
|
||||
raise NotFound("Message Not Exists.")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ from controllers.console.app.error import (
|
||||
from controllers.console.app.wraps import with_session
|
||||
from controllers.console.explore.error import NotWorkflowAppError
|
||||
from controllers.console.explore.wraps import InstalledAppResource
|
||||
from controllers.console.wraps import with_current_user
|
||||
from controllers.console.wraps import model_validate, with_current_user
|
||||
from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError
|
||||
from core.app.apps.base_app_queue_manager import AppQueueManager
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
@@ -47,7 +47,14 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
|
||||
@console_ns.response(200, "Success")
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account, installed_app: InstalledApp):
|
||||
@model_validate(WorkflowRunPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: WorkflowRunPayload,
|
||||
session: Session,
|
||||
current_user: Account,
|
||||
installed_app: InstalledApp,
|
||||
):
|
||||
"""
|
||||
Run workflow
|
||||
"""
|
||||
@@ -58,8 +65,7 @@ class InstalledAppWorkflowRunApi(InstalledAppResource):
|
||||
if app_mode != AppMode.WORKFLOW:
|
||||
raise NotWorkflowAppError()
|
||||
|
||||
payload = WorkflowRunPayload.model_validate(console_ns.payload or {})
|
||||
args = payload.model_dump(exclude_none=True)
|
||||
args = req_data.model_dump(exclude_none=True)
|
||||
try:
|
||||
response = AppGenerateService.generate(
|
||||
session=session,
|
||||
|
||||
@@ -17,7 +17,7 @@ from services.code_based_extension_service import CodeBasedExtensionService
|
||||
|
||||
from ..common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from . import console_ns
|
||||
from .wraps import account_initialization_required, setup_required, with_current_tenant_id
|
||||
from .wraps import account_initialization_required, model_validate, setup_required, with_current_tenant_id
|
||||
|
||||
|
||||
class CodeBasedExtensionQuery(BaseModel):
|
||||
@@ -123,14 +123,13 @@ class APIBasedExtensionAPI(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str):
|
||||
payload = APIBasedExtensionPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
@model_validate(APIBasedExtensionPayload)
|
||||
def post(self, req_data: APIBasedExtensionPayload, current_tenant_id: str):
|
||||
extension_data = APIBasedExtension(
|
||||
tenant_id=current_tenant_id,
|
||||
name=payload.name,
|
||||
api_endpoint=payload.api_endpoint,
|
||||
api_key=payload.api_key,
|
||||
name=req_data.name,
|
||||
api_endpoint=req_data.api_endpoint,
|
||||
api_key=req_data.api_key,
|
||||
)
|
||||
|
||||
extension = APIBasedExtensionService.save(extension_data, session=db.session())
|
||||
@@ -138,7 +137,7 @@ class APIBasedExtensionAPI(Resource):
|
||||
id=extension.id,
|
||||
name=extension.name,
|
||||
api_endpoint=extension.api_endpoint,
|
||||
api_key=payload.api_key,
|
||||
api_key=req_data.api_key,
|
||||
created_at=to_timestamp(extension.created_at),
|
||||
).model_dump(mode="json"), 201
|
||||
|
||||
@@ -172,22 +171,22 @@ class APIBasedExtensionDetailAPI(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, id: UUID):
|
||||
@model_validate(APIBasedExtensionPayload)
|
||||
def post(self, req_data: APIBasedExtensionPayload, current_tenant_id: str, id: UUID):
|
||||
api_based_extension_id = str(id)
|
||||
|
||||
extension_data_from_db = APIBasedExtensionService.get_with_tenant_id(
|
||||
current_tenant_id, api_based_extension_id, session=db.session()
|
||||
)
|
||||
|
||||
payload = APIBasedExtensionPayload.model_validate(console_ns.payload or {})
|
||||
api_key_for_response = extension_data_from_db.api_key
|
||||
|
||||
extension_data_from_db.name = payload.name
|
||||
extension_data_from_db.api_endpoint = payload.api_endpoint
|
||||
extension_data_from_db.name = req_data.name
|
||||
extension_data_from_db.api_endpoint = req_data.api_endpoint
|
||||
|
||||
if payload.api_key != HIDDEN_VALUE:
|
||||
extension_data_from_db.api_key = payload.api_key
|
||||
api_key_for_response = payload.api_key
|
||||
if req_data.api_key != HIDDEN_VALUE:
|
||||
extension_data_from_db.api_key = req_data.api_key
|
||||
api_key_for_response = req_data.api_key
|
||||
|
||||
APIBasedExtensionService.save(extension_data_from_db, session=db.session())
|
||||
return APIBasedExtensionResponse(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import TypedDict
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -10,6 +9,7 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
only_edition_cloud,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -141,8 +141,8 @@ class NotificationDismissApi(Resource):
|
||||
@only_edition_cloud
|
||||
@console_ns.expect(console_ns.models[DismissNotificationPayload.__name__])
|
||||
@console_ns.response(200, "Success", console_ns.models[SimpleResultResponse.__name__])
|
||||
def post(self, current_user: Account):
|
||||
payload = DismissNotificationPayload.model_validate(request.get_json())
|
||||
@model_validate(DismissNotificationPayload)
|
||||
def post(self, payload: DismissNotificationPayload, current_user: Account):
|
||||
BillingService.dismiss_notification(
|
||||
notification_id=payload.notification_id,
|
||||
account_id=str(current_user.id),
|
||||
|
||||
@@ -21,7 +21,13 @@ from models import Account
|
||||
from services.step_by_step_tour_service import StepByStepTourPatch, StepByStepTourService
|
||||
|
||||
from . import console_ns
|
||||
from .wraps import account_initialization_required, setup_required, with_current_tenant_id, with_current_user
|
||||
from .wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
)
|
||||
|
||||
StepByStepTourAction = Literal[
|
||||
"skip",
|
||||
@@ -92,9 +98,9 @@ class StepByStepTourStateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account):
|
||||
payload = StepByStepTourStatePatchPayload.model_validate(console_ns.payload or {})
|
||||
patch = cast(StepByStepTourPatch, payload.model_dump(exclude_unset=True, exclude_none=True))
|
||||
@model_validate(StepByStepTourStatePatchPayload)
|
||||
def patch(self, req_data: StepByStepTourStatePatchPayload, current_tenant_id: str, current_user: Account):
|
||||
patch = cast(StepByStepTourPatch, req_data.model_dump(exclude_unset=True, exclude_none=True))
|
||||
return dump_response(
|
||||
StepByStepTourStateResponse,
|
||||
StepByStepTourService.patch_state(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,15 +2,16 @@ from typing import Literal
|
||||
|
||||
from flask import request
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.fastopenapi import console_router
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_application_services import application_services
|
||||
from libs.helper import EmailStr, extract_remote_ip
|
||||
from libs.password import valid_password
|
||||
from models.model import DifySetup, db
|
||||
from services.account_service import RegisterService, TenantService
|
||||
from services.setup_service import (
|
||||
InitializationValidationRequiredError,
|
||||
SetupAlreadyCompletedError,
|
||||
SetupInput,
|
||||
)
|
||||
|
||||
from .error import AlreadySetupError, NotInitValidateError
|
||||
from .init_validate import get_init_validate_status
|
||||
@@ -53,14 +54,12 @@ def get_setup_status_api() -> SetupStatusResponse:
|
||||
|
||||
Only bootstrap-safe status information should be returned by this endpoint.
|
||||
"""
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
setup_status = get_setup_status()
|
||||
if setup_status and not isinstance(setup_status, bool):
|
||||
return SetupStatusResponse(step="finished", setup_at=setup_status.setup_at.isoformat())
|
||||
if setup_status:
|
||||
return SetupStatusResponse(step="finished")
|
||||
setup_status = application_services().setup.get_status()
|
||||
if not setup_status.completed:
|
||||
return SetupStatusResponse(step="not_started")
|
||||
return SetupStatusResponse(step="finished")
|
||||
|
||||
setup_at = setup_status.setup_at.isoformat() if setup_status.setup_at is not None else None
|
||||
return SetupStatusResponse(step="finished", setup_at=setup_at)
|
||||
|
||||
|
||||
@console_router.post(
|
||||
@@ -77,33 +76,22 @@ def setup_system(payload: SetupRequestPayload) -> SetupResponse:
|
||||
Access is restricted by deployment mode (`SELF_HOSTED`), one-time setup guards,
|
||||
and init-password validation rather than user session authentication.
|
||||
"""
|
||||
if get_setup_status():
|
||||
raise AlreadySetupError()
|
||||
try:
|
||||
application_services().setup.initialize(
|
||||
SetupInput(
|
||||
email=payload.email,
|
||||
name=payload.name,
|
||||
password=payload.password,
|
||||
ip_address=extract_remote_ip(request),
|
||||
language=payload.language,
|
||||
),
|
||||
initialization_validated=get_init_validate_status(),
|
||||
)
|
||||
except SetupAlreadyCompletedError:
|
||||
raise AlreadySetupError() from None
|
||||
except InitializationValidationRequiredError:
|
||||
raise NotInitValidateError() from None
|
||||
|
||||
tenant_count = TenantService.get_tenant_count(session=db.session())
|
||||
if tenant_count > 0:
|
||||
raise AlreadySetupError()
|
||||
|
||||
if not get_init_validate_status():
|
||||
raise NotInitValidateError()
|
||||
|
||||
normalized_email = payload.email.lower()
|
||||
|
||||
RegisterService.setup(
|
||||
email=normalized_email,
|
||||
name=payload.name,
|
||||
password=payload.password,
|
||||
ip_address=extract_remote_ip(request),
|
||||
language=payload.language,
|
||||
session=db.session(),
|
||||
)
|
||||
mark_setup_completed()
|
||||
|
||||
return SetupResponse(result="success")
|
||||
|
||||
|
||||
def get_setup_status() -> DifySetup | bool | None:
|
||||
if dify_config.DEPLOYMENT_EDITION != DeploymentEdition.CLOUD:
|
||||
return db.session.scalar(select(DifySetup).limit(1))
|
||||
|
||||
return True
|
||||
|
||||
@@ -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,23 +1,18 @@
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from flask_restx import Resource
|
||||
from pydantic import Field, RootModel
|
||||
|
||||
from controllers.common.schema import register_response_schema_models
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.schemas.schema_manager import SchemaManager
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from extensions.ext_application_services import application_services
|
||||
from fields.base import ResponseModel
|
||||
from libs.login import login_required
|
||||
from machinery.context import RequestContext
|
||||
|
||||
from . import console_ns
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SchemaDefinitionItemResponse(ResponseModel):
|
||||
name: str
|
||||
@@ -34,20 +29,13 @@ register_response_schema_models(console_ns, SchemaDefinitionItemResponse, Schema
|
||||
|
||||
@console_ns.route("/spec/schema-definitions")
|
||||
class SpecSchemaDefinitionsApi(Resource):
|
||||
@console_ns.response(200, "Success", console_ns.models[SchemaDefinitionsResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[SchemaDefinitionsResponse.__name__])
|
||||
@console_account_admission()
|
||||
def get(self, _request_context: RequestContext):
|
||||
"""
|
||||
Get system JSON Schema definitions specification
|
||||
Used for frontend component type mapping
|
||||
"""
|
||||
try:
|
||||
schema_manager = SchemaManager()
|
||||
schema_definitions = schema_manager.get_all_schema_definitions()
|
||||
return schema_definitions, 200
|
||||
except Exception:
|
||||
logger.exception("Failed to get schema definitions from local registry")
|
||||
# Return empty array as fallback
|
||||
return [], 200
|
||||
schema_definitions = application_services().schema_definitions.list()
|
||||
response = SchemaDefinitionsResponse.model_validate(schema_definitions).model_dump(mode="json")
|
||||
return response, HTTPStatus.OK
|
||||
|
||||
@@ -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
|
||||
@@ -17,6 +16,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -134,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
|
||||
|
||||
@@ -147,14 +146,14 @@ class TagListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
def post(self, current_user: Account):
|
||||
@model_validate(TagBasePayload)
|
||||
def post(self, req_data: TagBasePayload, current_user: Account):
|
||||
# Allow users with edit permission, or dataset editors (including dataset operators).
|
||||
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||
raise Forbidden()
|
||||
|
||||
payload = TagBasePayload.model_validate(console_ns.payload or {})
|
||||
_enforce_snippet_tag_rbac_if_needed(payload.type)
|
||||
tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type), db.session())
|
||||
_enforce_snippet_tag_rbac_if_needed(req_data.type)
|
||||
tag = TagService.save_tags(SaveTagPayload(name=req_data.name, type=req_data.type), db.session())
|
||||
|
||||
return dump_response(TagResponse, {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0}), 200
|
||||
|
||||
@@ -167,15 +166,15 @@ class TagUpdateDeleteApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
def patch(self, current_user: Account, tag_id: UUID):
|
||||
@model_validate(TagUpdateRequestPayload)
|
||||
def patch(self, req_data: TagUpdateRequestPayload, current_user: Account, tag_id: UUID):
|
||||
tag_id_str = str(tag_id)
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||
raise Forbidden()
|
||||
|
||||
payload = TagUpdateRequestPayload.model_validate(console_ns.payload or {})
|
||||
_enforce_snippet_tag_rbac_by_tag_id(tag_id_str)
|
||||
tag = TagService.update_tags(UpdateTagPayload(name=payload.name), tag_id_str, db.session())
|
||||
tag = TagService.update_tags(UpdateTagPayload(name=req_data.name), tag_id_str, db.session())
|
||||
|
||||
binding_count = TagService.get_tag_binding_count(tag_id_str, db.session())
|
||||
|
||||
@@ -212,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(
|
||||
@@ -228,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(
|
||||
@@ -255,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")
|
||||
@@ -271,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)
|
||||
|
||||
@@ -13,6 +13,7 @@ from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_enabled,
|
||||
cloud_edition_billing_paid_plan_required,
|
||||
model_validate,
|
||||
only_edition_cloud,
|
||||
setup_required,
|
||||
)
|
||||
@@ -144,16 +145,16 @@ class WorkflowRunArchiveDownloadsApi(Resource):
|
||||
@only_edition_cloud
|
||||
@cloud_edition_billing_enabled
|
||||
@cloud_edition_billing_paid_plan_required
|
||||
def post(self):
|
||||
@model_validate(WorkflowRunArchiveDownloadPayload)
|
||||
def post(self, req_data: WorkflowRunArchiveDownloadPayload):
|
||||
tenant_id, account_id = _current_owner_or_admin_ids()
|
||||
payload = WorkflowRunArchiveDownloadPayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
task = create_workflow_run_archive_download_task(
|
||||
db.session(),
|
||||
tenant_id=tenant_id,
|
||||
requested_by=account_id,
|
||||
year=payload.year,
|
||||
month=payload.month,
|
||||
year=req_data.year,
|
||||
month=req_data.month,
|
||||
)
|
||||
except WorkflowRunArchiveNotFoundError as exc:
|
||||
raise NotFound(str(exc)) from exc
|
||||
|
||||
@@ -41,6 +41,7 @@ from controllers.console.wraps import (
|
||||
cloud_edition_billing_enabled,
|
||||
enable_change_email,
|
||||
enterprise_license_required,
|
||||
model_validate,
|
||||
only_edition_cloud,
|
||||
setup_required,
|
||||
with_current_user,
|
||||
@@ -332,9 +333,9 @@ class AccountAvatarApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
def get(self, current_user: Account):
|
||||
args = AccountAvatarQuery.model_validate(request.args.to_dict(flat=True))
|
||||
avatar = args.avatar
|
||||
@model_validate(AccountAvatarQuery)
|
||||
def get(self, req_data: AccountAvatarQuery, current_user: Account):
|
||||
avatar = req_data.avatar
|
||||
|
||||
if avatar.startswith(("http://", "https://")):
|
||||
return AvatarUrlResponse(avatar_url=avatar).model_dump(mode="json")
|
||||
|
||||
@@ -11,7 +11,6 @@ from enum import StrEnum
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -23,6 +22,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,
|
||||
@@ -166,43 +166,38 @@ register_response_schema_models(
|
||||
)
|
||||
|
||||
|
||||
def _create_endpoint(tenant_id: str, user_id: str) -> bool:
|
||||
def _create_endpoint(tenant_id: str, user_id: str, req_data: EndpointCreatePayload) -> bool:
|
||||
"""Create a plugin endpoint for the injected workspace and user."""
|
||||
args = EndpointCreatePayload.model_validate(console_ns.payload)
|
||||
|
||||
try:
|
||||
return EndpointService.create_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_unique_identifier=args.plugin_unique_identifier,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
plugin_unique_identifier=req_data.plugin_unique_identifier,
|
||||
name=req_data.name,
|
||||
settings=req_data.settings,
|
||||
)
|
||||
except PluginPermissionDeniedError as e:
|
||||
raise ValueError(e.description) from e
|
||||
|
||||
|
||||
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
|
||||
def _update_endpoint(tenant_id: str, user_id: str, endpoint_id: str, req_data: EndpointUpdatePayload) -> bool:
|
||||
"""Update a plugin endpoint identified by the canonical path parameter."""
|
||||
args = EndpointUpdatePayload.model_validate(console_ns.payload)
|
||||
|
||||
return EndpointService.update_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=endpoint_id,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
name=req_data.name,
|
||||
settings=req_data.settings,
|
||||
)
|
||||
|
||||
|
||||
def _legacy_update_endpoint(tenant_id: str, user_id: str) -> bool:
|
||||
args = LegacyEndpointUpdatePayload.model_validate(console_ns.payload)
|
||||
def _legacy_update_endpoint(tenant_id: str, user_id: str, req_data: LegacyEndpointUpdatePayload) -> bool:
|
||||
return EndpointService.update_endpoint(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
endpoint_id=args.endpoint_id,
|
||||
name=args.name,
|
||||
settings=args.settings,
|
||||
endpoint_id=req_data.endpoint_id,
|
||||
name=req_data.name,
|
||||
settings=req_data.settings,
|
||||
)
|
||||
|
||||
|
||||
@@ -215,15 +210,13 @@ def _delete_endpoint(tenant_id: str, user_id: str, endpoint_id: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _delete_endpoint_from_payload(tenant_id: str, user_id: str) -> bool:
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
def _delete_endpoint_from_payload(tenant_id: str, user_id: str, req_data: EndpointIdPayload) -> bool:
|
||||
return _delete_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=req_data.endpoint_id)
|
||||
|
||||
|
||||
def _set_endpoint_enabled(tenant_id: str, user_id: str, *, enabled: bool) -> bool:
|
||||
args = EndpointIdPayload.model_validate(console_ns.payload)
|
||||
def _set_endpoint_enabled(tenant_id: str, user_id: str, req_data: EndpointIdPayload, *, enabled: bool) -> bool:
|
||||
action = EndpointService.enable_endpoint if enabled else EndpointService.disable_endpoint
|
||||
return action(tenant_id=tenant_id, user_id=user_id, endpoint_id=args.endpoint_id)
|
||||
return action(tenant_id=tenant_id, user_id=user_id, endpoint_id=req_data.endpoint_id)
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints")
|
||||
@@ -246,8 +239,11 @@ class EndpointCollectionApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
|
||||
@model_validate(EndpointCreatePayload)
|
||||
def post(self, req_data: EndpointCreatePayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_create_endpoint(tenant_id=tenant_id, user_id=user_id, req_data=req_data)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/create")
|
||||
@@ -275,8 +271,11 @@ class DeprecatedEndpointCreateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(success=_create_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(mode="json")
|
||||
@model_validate(EndpointCreatePayload)
|
||||
def post(self, req_data: EndpointCreatePayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_create_endpoint(tenant_id=tenant_id, user_id=user_id, req_data=req_data)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/list")
|
||||
@@ -294,14 +293,14 @@ class EndpointListApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, user_id: str):
|
||||
args = EndpointListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(EndpointListQuery)
|
||||
def get(self, req_data: EndpointListQuery, tenant_id: str, user_id: str):
|
||||
|
||||
endpoints = EndpointService.list_endpoints(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
page=args.page,
|
||||
page_size=args.page_size,
|
||||
page=req_data.page,
|
||||
page_size=req_data.page_size,
|
||||
)
|
||||
|
||||
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
|
||||
@@ -322,15 +321,15 @@ class EndpointListForSinglePluginApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, user_id: str):
|
||||
args = EndpointListForPluginQuery.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(EndpointListForPluginQuery)
|
||||
def get(self, req_data: EndpointListForPluginQuery, tenant_id: str, user_id: str):
|
||||
|
||||
endpoints = EndpointService.list_endpoints_for_single_plugin(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
plugin_id=args.plugin_id,
|
||||
page=args.page,
|
||||
page_size=args.page_size,
|
||||
plugin_id=req_data.plugin_id,
|
||||
page=req_data.page,
|
||||
page_size=req_data.page_size,
|
||||
)
|
||||
|
||||
return EndpointListResponse(endpoints=endpoints).model_dump(mode="json")
|
||||
@@ -378,9 +377,10 @@ class EndpointItemApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def patch(self, tenant_id: str, user_id: str, id: str):
|
||||
@model_validate(EndpointUpdatePayload)
|
||||
def patch(self, req_data: EndpointUpdatePayload, tenant_id: str, user_id: str, id: str):
|
||||
return SuccessResponse(
|
||||
success=_update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id)
|
||||
success=_update_endpoint(tenant_id=tenant_id, user_id=user_id, endpoint_id=id, req_data=req_data)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@@ -410,10 +410,11 @@ class DeprecatedEndpointDeleteApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(success=_delete_endpoint_from_payload(tenant_id=tenant_id, user_id=user_id)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
@model_validate(EndpointIdPayload)
|
||||
def post(self, req_data: EndpointIdPayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_delete_endpoint_from_payload(tenant_id=tenant_id, user_id=user_id, req_data=req_data)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/update")
|
||||
@@ -442,10 +443,11 @@ class DeprecatedEndpointUpdateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(success=_legacy_update_endpoint(tenant_id=tenant_id, user_id=user_id)).model_dump(
|
||||
mode="json"
|
||||
)
|
||||
@model_validate(LegacyEndpointUpdatePayload)
|
||||
def post(self, req_data: LegacyEndpointUpdatePayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_legacy_update_endpoint(tenant_id=tenant_id, user_id=user_id, req_data=req_data)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current/endpoints/enable")
|
||||
@@ -466,9 +468,10 @@ class EndpointEnableApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
@model_validate(EndpointIdPayload)
|
||||
def post(self, req_data: EndpointIdPayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=True)
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, req_data=req_data, enabled=True)
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
@@ -490,7 +493,8 @@ class EndpointDisableApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user_id
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user_id: str):
|
||||
@model_validate(EndpointIdPayload)
|
||||
def post(self, req_data: EndpointIdPayload, tenant_id: str, user_id: str):
|
||||
return SuccessResponse(
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, enabled=False)
|
||||
success=_set_endpoint_enabled(tenant_id=tenant_id, user_id=user_id, req_data=req_data, enabled=False)
|
||||
).model_dump(mode="json")
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from libs.exception import BaseHTTPException
|
||||
|
||||
|
||||
class CurrentWorkspaceArchivedError(BaseHTTPException):
|
||||
error_code = "current_workspace_archived"
|
||||
description = "The current workspace has been archived."
|
||||
code = 409
|
||||
|
||||
|
||||
class RepeatPasswordNotMatchError(BaseHTTPException):
|
||||
error_code = "repeat_password_not_match"
|
||||
description = "New password and repeat password does not match."
|
||||
|
||||
@@ -6,6 +6,7 @@ from controllers.common.schema import register_response_schema_models, register_
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
model_validate,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
with_current_user,
|
||||
@@ -49,14 +50,15 @@ class LoadBalancingCredentialsValidateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, provider: str):
|
||||
@model_validate(LoadBalancingCredentialPayload)
|
||||
def post(
|
||||
self, req_data: LoadBalancingCredentialPayload, current_tenant_id: str, current_user: Account, provider: str
|
||||
):
|
||||
if not TenantAccountRole.is_privileged_role(current_user.current_role):
|
||||
raise Forbidden()
|
||||
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
payload = LoadBalancingCredentialPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
# validate model load balancing credentials
|
||||
model_load_balancing_service = ModelLoadBalancingService()
|
||||
|
||||
@@ -67,9 +69,9 @@ class LoadBalancingCredentialsValidateApi(Resource):
|
||||
model_load_balancing_service.validate_load_balancing_credentials(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=payload.model,
|
||||
model_type=payload.model_type,
|
||||
credentials=payload.credentials,
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
credentials=req_data.credentials,
|
||||
session=db.session(),
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
@@ -99,14 +101,20 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account, provider: str, config_id: str):
|
||||
@model_validate(LoadBalancingCredentialPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: LoadBalancingCredentialPayload,
|
||||
current_tenant_id: str,
|
||||
current_user: Account,
|
||||
provider: str,
|
||||
config_id: str,
|
||||
):
|
||||
if not TenantAccountRole.is_privileged_role(current_user.current_role):
|
||||
raise Forbidden()
|
||||
|
||||
tenant_id = current_tenant_id
|
||||
|
||||
payload = LoadBalancingCredentialPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
# validate model load balancing config credentials
|
||||
model_load_balancing_service = ModelLoadBalancingService()
|
||||
|
||||
@@ -117,9 +125,9 @@ class LoadBalancingConfigCredentialsValidateApi(Resource):
|
||||
model_load_balancing_service.validate_load_balancing_credentials(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=payload.model,
|
||||
model_type=payload.model_type,
|
||||
credentials=payload.credentials,
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
credentials=req_data.credentials,
|
||||
session=db.session(),
|
||||
config_id=config_id,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
@@ -18,6 +17,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -212,12 +212,12 @@ class DefaultModelApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str):
|
||||
args = ParserGetDefault.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ParserGetDefault)
|
||||
def get(self, req_data: ParserGetDefault, tenant_id: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
default_model_entity = model_provider_service.get_default_model_of_model_type(
|
||||
tenant_id=tenant_id, model_type=args.model_type
|
||||
tenant_id=tenant_id, model_type=req_data.model_type
|
||||
)
|
||||
|
||||
return DefaultModelDataResponse(data=default_model_entity).model_dump(mode="json")
|
||||
@@ -230,10 +230,10 @@ class DefaultModelApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str):
|
||||
args = ParserPostDefault.model_validate(console_ns.payload)
|
||||
@model_validate(ParserPostDefault)
|
||||
def post(self, req_data: ParserPostDefault, tenant_id: str):
|
||||
model_provider_service = ModelProviderService()
|
||||
model_settings = args.model_settings
|
||||
model_settings = req_data.model_settings
|
||||
for model_setting in model_settings:
|
||||
if model_setting.provider is None:
|
||||
continue
|
||||
@@ -279,43 +279,43 @@ class ModelProviderModelApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@model_validate(ParserPostModels)
|
||||
def post(self, req_data: ParserPostModels, tenant_id: str, provider: str):
|
||||
# To save the model's load balance configs
|
||||
args = ParserPostModels.model_validate(console_ns.payload)
|
||||
|
||||
if args.config_from == "custom-model":
|
||||
if not args.credential_id:
|
||||
if req_data.config_from == "custom-model":
|
||||
if not req_data.credential_id:
|
||||
raise ValueError("credential_id is required when configuring a custom-model")
|
||||
service = ModelProviderService()
|
||||
service.switch_active_custom_model_credential(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
credential_id=args.credential_id,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
|
||||
model_load_balancing_service = ModelLoadBalancingService()
|
||||
|
||||
if args.load_balancing and args.load_balancing.configs:
|
||||
if req_data.load_balancing and req_data.load_balancing.configs:
|
||||
# save load balancing configs
|
||||
model_load_balancing_service.update_load_balancing_configs(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=args.model,
|
||||
model_type=args.model_type,
|
||||
configs=args.load_balancing.configs,
|
||||
config_from=args.config_from or "",
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
configs=req_data.load_balancing.configs,
|
||||
config_from=req_data.config_from or "",
|
||||
session=db.session(),
|
||||
)
|
||||
|
||||
if args.load_balancing.enabled:
|
||||
if req_data.load_balancing.enabled:
|
||||
model_load_balancing_service.enable_model_load_balancing(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
|
||||
)
|
||||
else:
|
||||
model_load_balancing_service.disable_model_load_balancing(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json"), 200
|
||||
@@ -328,12 +328,12 @@ class ModelProviderModelApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, tenant_id: str, provider: str):
|
||||
args = ParserDeleteModels.model_validate(console_ns.payload)
|
||||
@model_validate(ParserDeleteModels)
|
||||
def delete(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.remove_model(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
|
||||
)
|
||||
|
||||
return "", 204
|
||||
@@ -352,29 +352,29 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, user: Account, provider: str):
|
||||
args = ParserGetCredentials.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ParserGetCredentials)
|
||||
def get(self, req_data: ParserGetCredentials, tenant_id: str, user: Account, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
current_credential = model_provider_service.get_model_credential(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
credential_id=args.credential_id,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
|
||||
model_load_balancing_service = ModelLoadBalancingService()
|
||||
is_load_balancing_enabled, load_balancing_configs = model_load_balancing_service.get_load_balancing_configs(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=args.model,
|
||||
model_type=args.model_type,
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
session=db.session(),
|
||||
config_from=args.config_from or "",
|
||||
config_from=req_data.config_from or "",
|
||||
)
|
||||
|
||||
if args.config_from == "predefined-model":
|
||||
if req_data.config_from == "predefined-model":
|
||||
# Only the predefined-model branch needs visibility filtering by user.
|
||||
# The account is injected once by the handler and only passed into the
|
||||
# service branch that needs user-scoped credential visibility.
|
||||
@@ -387,8 +387,8 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
available_credentials = model_provider_service.get_provider_model_available_credentials(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
)
|
||||
|
||||
credentials: dict[str, Any] = {}
|
||||
@@ -414,8 +414,8 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_CREATE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
args = ParserCreateCredential.model_validate(console_ns.payload)
|
||||
@model_validate(ParserCreateCredential)
|
||||
def post(self, req_data: ParserCreateCredential, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
|
||||
@@ -423,17 +423,17 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
model_provider_service.create_model_credential(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=args.model,
|
||||
model_type=args.model_type,
|
||||
credentials=args.credentials,
|
||||
credential_name=args.name,
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
credentials=req_data.credentials,
|
||||
credential_name=req_data.name,
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
logger.exception(
|
||||
"Failed to save model credentials, tenant_id: %s, model: %s, model_type: %s",
|
||||
tenant_id,
|
||||
args.model,
|
||||
args.model_type,
|
||||
req_data.model,
|
||||
req_data.model_type,
|
||||
)
|
||||
raise ValueError(str(ex))
|
||||
|
||||
@@ -447,8 +447,8 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def put(self, current_tenant_id: str, provider: str):
|
||||
args = ParserUpdateCredential.model_validate(console_ns.payload)
|
||||
@model_validate(ParserUpdateCredential)
|
||||
def put(self, req_data: ParserUpdateCredential, current_tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
|
||||
@@ -456,11 +456,11 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
model_provider_service.update_model_credential(
|
||||
tenant_id=current_tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
credentials=args.credentials,
|
||||
credential_id=args.credential_id,
|
||||
credential_name=args.name,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
credentials=req_data.credentials,
|
||||
credential_id=req_data.credential_id,
|
||||
credential_name=req_data.name,
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ValueError(str(ex))
|
||||
@@ -475,16 +475,16 @@ class ModelProviderModelCredentialApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def delete(self, current_tenant_id: str, provider: str):
|
||||
args = ParserDeleteCredential.model_validate(console_ns.payload)
|
||||
@model_validate(ParserDeleteCredential)
|
||||
def delete(self, req_data: ParserDeleteCredential, current_tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.remove_model_credential(
|
||||
tenant_id=current_tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
credential_id=args.credential_id,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
|
||||
return "", 204
|
||||
@@ -500,16 +500,16 @@ class ModelProviderModelCredentialSwitchApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_USE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, provider: str):
|
||||
args = ParserSwitch.model_validate(console_ns.payload)
|
||||
@model_validate(ParserSwitch)
|
||||
def post(self, req_data: ParserSwitch, current_tenant_id: str, provider: str):
|
||||
|
||||
service = ModelProviderService()
|
||||
service.add_model_credential_to_model_list(
|
||||
tenant_id=current_tenant_id,
|
||||
provider=provider,
|
||||
model_type=args.model_type,
|
||||
model=args.model,
|
||||
credential_id=args.credential_id,
|
||||
model_type=req_data.model_type,
|
||||
model=req_data.model,
|
||||
credential_id=req_data.credential_id,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -525,12 +525,12 @@ class ModelProviderModelEnableApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
def patch(self, tenant_id: str, provider: str):
|
||||
args = ParserDeleteModels.model_validate(console_ns.payload)
|
||||
@model_validate(ParserDeleteModels)
|
||||
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.enable_model(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
@@ -547,12 +547,12 @@ class ModelProviderModelDisableApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
def patch(self, tenant_id: str, provider: str):
|
||||
args = ParserDeleteModels.model_validate(console_ns.payload)
|
||||
@model_validate(ParserDeleteModels)
|
||||
def patch(self, req_data: ParserDeleteModels, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
model_provider_service.disable_model(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model, model_type=args.model_type
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model, model_type=req_data.model_type
|
||||
)
|
||||
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
@@ -579,8 +579,8 @@ class ModelProviderModelValidateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
args = ParserValidate.model_validate(console_ns.payload)
|
||||
@model_validate(ParserValidate)
|
||||
def post(self, req_data: ParserValidate, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
|
||||
@@ -591,9 +591,9 @@ class ModelProviderModelValidateApi(Resource):
|
||||
model_provider_service.validate_model_credentials(
|
||||
tenant_id=tenant_id,
|
||||
provider=provider,
|
||||
model=args.model,
|
||||
model_type=args.model_type,
|
||||
credentials=args.credentials,
|
||||
model=req_data.model,
|
||||
model_type=req_data.model_type,
|
||||
credentials=req_data.credentials,
|
||||
)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
result = False
|
||||
@@ -617,12 +617,12 @@ class ModelProviderModelParameterRuleApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, provider: str):
|
||||
args = ParserParameter.model_validate(request.args.to_dict(flat=True))
|
||||
@model_validate(ParserParameter)
|
||||
def get(self, req_data: ParserParameter, tenant_id: str, provider: str):
|
||||
|
||||
model_provider_service = ModelProviderService()
|
||||
parameter_rules = model_provider_service.get_model_parameter_rules(
|
||||
tenant_id=tenant_id, provider=provider, model=args.model
|
||||
tenant_id=tenant_id, provider=provider, model=req_data.model
|
||||
)
|
||||
|
||||
return ModelParameterRuleListResponse(data=parameter_rules).model_dump(mode="json")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from werkzeug.exceptions import NotFound
|
||||
from configs import dify_config
|
||||
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
|
||||
from controllers.console import console_ns
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, rbac_permission_required
|
||||
from controllers.console.wraps import RBACPermission, RBACResourceScope, model_validate, rbac_permission_required
|
||||
from core.db.session_factory import session_factory
|
||||
from core.rbac import RBACResourceWhitelistScope
|
||||
from extensions.ext_database import db
|
||||
@@ -310,19 +310,19 @@ class RBACRolesApi(Resource):
|
||||
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
|
||||
)
|
||||
@console_ns.response(200, "Success", console_ns.models[_RBACRoleList.__name__])
|
||||
def get(self):
|
||||
@model_validate(_RolesListQuery)
|
||||
def get(self, req_data: _RolesListQuery):
|
||||
tenant_id, account_id = _current_ids()
|
||||
query = _RolesListQuery.model_validate(request.args.to_dict(flat=True))
|
||||
options = query.to_inner_options()
|
||||
options = req_data.to_inner_options()
|
||||
if not dify_config.RBAC_ENABLED:
|
||||
result = _legacy_workspace_roles(
|
||||
options, include_owner=query.include_owner, billing_enabled=dify_config.BILLING_ENABLED
|
||||
options, include_owner=req_data.include_owner, billing_enabled=dify_config.BILLING_ENABLED
|
||||
)
|
||||
else:
|
||||
result = svc.RBACService.Roles.list(
|
||||
tenant_id,
|
||||
account_id,
|
||||
include_owner=query.include_owner,
|
||||
include_owner=req_data.include_owner,
|
||||
biiling_enabled=dify_config.BILLING_ENABLED,
|
||||
options=options,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,7 @@ from controllers.console.wraps import (
|
||||
RBACResourceScope,
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -151,27 +152,26 @@ class CustomizedSnippetsApi(Resource):
|
||||
)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, current_tenant_id: str, current_user: Account):
|
||||
@model_validate(CreateSnippetPayload)
|
||||
def post(self, req_data: CreateSnippetPayload, current_tenant_id: str, current_user: Account):
|
||||
"""Create a new customized snippet."""
|
||||
payload = CreateSnippetPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
snippet_type = SnippetType(payload.type)
|
||||
snippet_type = SnippetType(req_data.type)
|
||||
except ValueError:
|
||||
snippet_type = SnippetType.NODE
|
||||
|
||||
try:
|
||||
if payload.graph is not None:
|
||||
SnippetService.validate_snippet_graph_forbidden_nodes(payload.graph)
|
||||
if req_data.graph is not None:
|
||||
SnippetService.validate_snippet_graph_forbidden_nodes(req_data.graph)
|
||||
|
||||
snippet_service = _snippet_service()
|
||||
snippet = snippet_service.create_snippet(
|
||||
tenant_id=current_tenant_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
snippet_type=snippet_type,
|
||||
icon_info=payload.icon_info.model_dump() if payload.icon_info else None,
|
||||
input_fields=[f.model_dump() for f in payload.input_fields] if payload.input_fields else None,
|
||||
icon_info=req_data.icon_info.model_dump() if req_data.icon_info else None,
|
||||
input_fields=[f.model_dump() for f in req_data.input_fields] if req_data.input_fields else None,
|
||||
account=current_user,
|
||||
)
|
||||
except ValueError as e:
|
||||
@@ -216,7 +216,8 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
)
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def patch(self, current_tenant_id: str, current_user: Account, snippet_id: str):
|
||||
@model_validate(UpdateSnippetPayload)
|
||||
def patch(self, req_data: UpdateSnippetPayload, current_tenant_id: str, current_user: Account, snippet_id: str):
|
||||
"""Update customized snippet."""
|
||||
snippet_service = _snippet_service()
|
||||
snippet = snippet_service.get_snippet_by_id(
|
||||
@@ -227,11 +228,10 @@ class CustomizedSnippetDetailApi(Resource):
|
||||
if not snippet:
|
||||
raise NotFound("Snippet not found")
|
||||
|
||||
payload = UpdateSnippetPayload.model_validate(console_ns.payload or {})
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
update_data = req_data.model_dump(exclude_unset=True)
|
||||
|
||||
if "icon_info" in update_data and update_data["icon_info"] is not None:
|
||||
update_data["icon_info"] = payload.icon_info.model_dump() if payload.icon_info else None
|
||||
update_data["icon_info"] = req_data.icon_info.model_dump() if req_data.icon_info else None
|
||||
|
||||
if not update_data:
|
||||
return {"message": "No valid fields to update"}, 400
|
||||
@@ -349,19 +349,18 @@ class CustomizedSnippetImportApi(Resource):
|
||||
)
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account):
|
||||
@model_validate(SnippetImportPayload)
|
||||
def post(self, req_data: SnippetImportPayload, session: Session, current_user: Account):
|
||||
"""Import snippet from DSL."""
|
||||
payload = SnippetImportPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
import_service = SnippetDslService(session)
|
||||
result = import_service.import_snippet(
|
||||
account=current_user,
|
||||
import_mode=payload.mode,
|
||||
yaml_content=payload.yaml_content,
|
||||
yaml_url=payload.yaml_url,
|
||||
snippet_id=payload.snippet_id,
|
||||
name=payload.name,
|
||||
description=payload.description,
|
||||
import_mode=req_data.mode,
|
||||
yaml_content=req_data.yaml_content,
|
||||
yaml_url=req_data.yaml_url,
|
||||
snippet_id=req_data.snippet_id,
|
||||
name=req_data.name,
|
||||
description=req_data.description,
|
||||
)
|
||||
|
||||
# Return appropriate status code based on result
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ from ..wraps import (
|
||||
account_initialization_required,
|
||||
edit_permission_required,
|
||||
is_admin_or_owner_required,
|
||||
model_validate,
|
||||
rbac_permission_required,
|
||||
setup_required,
|
||||
with_current_tenant_id,
|
||||
@@ -233,13 +234,12 @@ class TriggerSubscriptionBuilderCreateApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user: Account, provider: str):
|
||||
@model_validate(TriggerSubscriptionBuilderCreatePayload)
|
||||
def post(self, req_data: TriggerSubscriptionBuilderCreatePayload, tenant_id: str, user: Account, provider: str):
|
||||
"""Add a new subscription instance for a trigger provider"""
|
||||
|
||||
payload = TriggerSubscriptionBuilderCreatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
credential_type = CredentialType.of(payload.credential_type)
|
||||
credential_type = CredentialType.of(req_data.credential_type)
|
||||
subscription_builder = TriggerSubscriptionBuilderService.create_trigger_subscription_builder(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
@@ -268,9 +268,16 @@ class TriggerSubscriptionBuilderGetApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
def get(self, provider: str, subscription_builder_id: str):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
|
||||
"""Get a subscription instance for a trigger provider"""
|
||||
subscription_builder = TriggerSubscriptionBuilderService.get_subscription_builder_by_id(subscription_builder_id)
|
||||
subscription_builder = TriggerSubscriptionBuilderService.get_subscription_builder_by_id(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
)
|
||||
return subscription_builder.model_dump(mode="json")
|
||||
|
||||
|
||||
@@ -291,11 +298,17 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
|
||||
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: TriggerSubscriptionBuilderVerifyPayload,
|
||||
tenant_id: str,
|
||||
user: Account,
|
||||
provider: str,
|
||||
subscription_builder_id: str,
|
||||
):
|
||||
"""Verify and update a subscription instance for a trigger provider"""
|
||||
|
||||
payload = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
# Use atomic update_and_verify to prevent race conditions
|
||||
result = TriggerSubscriptionBuilderService.update_and_verify_builder(
|
||||
@@ -304,7 +317,7 @@ class TriggerSubscriptionBuilderVerifyApi(Resource):
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
credentials=payload.credentials,
|
||||
credentials=req_data.credentials,
|
||||
),
|
||||
)
|
||||
return dump_response(TriggerVerificationResponse, result)
|
||||
@@ -328,21 +341,30 @@ class TriggerSubscriptionBuilderUpdateApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.CREDENTIAL_MANAGE, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str, subscription_builder_id: str):
|
||||
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: TriggerSubscriptionBuilderUpdatePayload,
|
||||
tenant_id: str,
|
||||
user: Account,
|
||||
provider: str,
|
||||
subscription_builder_id: str,
|
||||
):
|
||||
"""Update a subscription instance for a trigger provider"""
|
||||
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
return TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
credentials=payload.credentials,
|
||||
name=req_data.name,
|
||||
parameters=req_data.parameters,
|
||||
properties=req_data.properties,
|
||||
credentials=req_data.credentials,
|
||||
),
|
||||
).model_dump(mode="json")
|
||||
except Exception as e:
|
||||
@@ -364,11 +386,18 @@ class TriggerSubscriptionBuilderLogsApi(Resource):
|
||||
@edit_permission_required
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
def get(self, provider: str, subscription_builder_id: str):
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def get(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
|
||||
"""Get the request logs for a subscription instance for a trigger provider"""
|
||||
|
||||
try:
|
||||
logs = TriggerSubscriptionBuilderService.list_logs(subscription_builder_id)
|
||||
logs = TriggerSubscriptionBuilderService.list_logs(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
)
|
||||
return dump_response(TriggerSubscriptionBuilderLogsResponse, {"logs": logs})
|
||||
except Exception as e:
|
||||
logger.exception("Error getting request logs for subscription builder", exc_info=e)
|
||||
@@ -390,9 +419,16 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user: Account, provider: str, subscription_builder_id: str):
|
||||
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: TriggerSubscriptionBuilderUpdatePayload,
|
||||
tenant_id: str,
|
||||
user: Account,
|
||||
provider: str,
|
||||
subscription_builder_id: str,
|
||||
):
|
||||
"""Build a subscription instance for a trigger provider"""
|
||||
payload = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
try:
|
||||
# Use atomic update_and_build to prevent race conditions
|
||||
TriggerSubscriptionBuilderService.update_and_build_builder(
|
||||
@@ -401,9 +437,9 @@ class TriggerSubscriptionBuilderBuildApi(Resource):
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
name=payload.name,
|
||||
parameters=payload.parameters,
|
||||
properties=payload.properties,
|
||||
name=req_data.name,
|
||||
parameters=req_data.parameters,
|
||||
properties=req_data.properties,
|
||||
),
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
@@ -426,11 +462,10 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, subscription_id: str):
|
||||
@model_validate(TriggerSubscriptionBuilderUpdatePayload)
|
||||
def post(self, req_data: TriggerSubscriptionBuilderUpdatePayload, tenant_id: str, subscription_id: str):
|
||||
"""Update a subscription instance"""
|
||||
|
||||
request = TriggerSubscriptionBuilderUpdatePayload.model_validate(console_ns.payload or {})
|
||||
|
||||
subscription = TriggerProviderService.get_subscription_by_id(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
@@ -442,7 +477,9 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
|
||||
try:
|
||||
# For rename only, just update the name
|
||||
rename = request.name is not None and not any((request.credentials, request.parameters, request.properties))
|
||||
rename = req_data.name is not None and not any(
|
||||
(req_data.credentials, req_data.parameters, req_data.properties)
|
||||
)
|
||||
# When credential type is UNAUTHORIZED, it indicates the subscription was manually created
|
||||
# For Manually created subscription, they dont have credentials, parameters
|
||||
# They only have name and properties(which is input by user)
|
||||
@@ -451,8 +488,8 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
TriggerProviderService.update_trigger_subscription(
|
||||
tenant_id=tenant_id,
|
||||
subscription_id=subscription_id,
|
||||
name=request.name,
|
||||
properties=request.properties,
|
||||
name=req_data.name,
|
||||
properties=req_data.properties,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
|
||||
@@ -460,11 +497,11 @@ class TriggerSubscriptionUpdateApi(Resource):
|
||||
# we need to call third party provider(e.g. GitHub) to rebuild the subscription
|
||||
TriggerProviderService.rebuild_trigger_subscription(
|
||||
tenant_id=tenant_id,
|
||||
name=request.name,
|
||||
name=req_data.name,
|
||||
provider_id=provider_id,
|
||||
subscription_id=subscription_id,
|
||||
credentials=request.credentials or subscription.credentials,
|
||||
parameters=request.parameters or subscription.parameters,
|
||||
credentials=req_data.credentials or subscription.credentials,
|
||||
parameters=req_data.parameters or subscription.parameters,
|
||||
)
|
||||
return SimpleResultResponse(result="success").model_dump(mode="json")
|
||||
except ValueError as e:
|
||||
@@ -652,6 +689,7 @@ class TriggerOAuthCallbackApi(Resource):
|
||||
# Update subscription builder
|
||||
TriggerSubscriptionBuilderService.update_trigger_subscription_builder(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
provider_id=provider_id,
|
||||
subscription_builder_id=subscription_builder_id,
|
||||
subscription_builder_updater=SubscriptionBuilderUpdater(
|
||||
@@ -723,18 +761,17 @@ class TriggerOAuthClientManageApi(Resource):
|
||||
@rbac_permission_required(RBACResourceScope.WORKSPACE, RBACPermission.PLUGIN_PREFERENCES, resource_required=False)
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, provider: str):
|
||||
@model_validate(TriggerOAuthClientPayload)
|
||||
def post(self, req_data: TriggerOAuthClientPayload, tenant_id: str, provider: str):
|
||||
"""Configure custom OAuth client for a provider"""
|
||||
|
||||
payload = TriggerOAuthClientPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
provider_id = TriggerProviderID(provider)
|
||||
result = TriggerProviderService.save_custom_oauth_client_params(
|
||||
tenant_id=tenant_id,
|
||||
provider_id=provider_id,
|
||||
client_params=payload.client_params,
|
||||
enabled=payload.enabled,
|
||||
client_params=req_data.client_params,
|
||||
enabled=req_data.enabled,
|
||||
)
|
||||
return dump_response(SimpleResultResponse, result)
|
||||
|
||||
@@ -788,18 +825,24 @@ class TriggerSubscriptionVerifyApi(Resource):
|
||||
@account_initialization_required
|
||||
@with_current_user
|
||||
@with_current_tenant_id
|
||||
def post(self, tenant_id: str, user: Account, provider: str, subscription_id: str):
|
||||
@model_validate(TriggerSubscriptionBuilderVerifyPayload)
|
||||
def post(
|
||||
self,
|
||||
req_data: TriggerSubscriptionBuilderVerifyPayload,
|
||||
tenant_id: str,
|
||||
user: Account,
|
||||
provider: str,
|
||||
subscription_id: str,
|
||||
):
|
||||
"""Verify credentials for an existing subscription (edit mode only)"""
|
||||
|
||||
verify_request = TriggerSubscriptionBuilderVerifyPayload.model_validate(console_ns.payload or {})
|
||||
|
||||
try:
|
||||
result = TriggerProviderService.verify_subscription_credentials(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
provider_id=TriggerProviderID(provider),
|
||||
subscription_id=subscription_id,
|
||||
credentials=verify_request.credentials,
|
||||
credentials=req_data.credentials,
|
||||
)
|
||||
return dump_response(TriggerVerificationResponse, result)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -7,9 +7,10 @@ from flask_restx import Resource
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound, Unauthorized
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.common.errors import (
|
||||
FilenameNotExistsError,
|
||||
FileTooLargeError,
|
||||
@@ -28,6 +29,7 @@ from controllers.console import console_ns
|
||||
from controllers.console.admin import admin_required
|
||||
from controllers.console.error import AccountNotLinkTenantError
|
||||
from controllers.console.flask_admission import console_account_admission
|
||||
from controllers.console.workspace.error import CurrentWorkspaceArchivedError
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
@@ -43,7 +45,7 @@ from libs.helper import dump_response, to_timestamp
|
||||
from libs.login import login_required
|
||||
from libs.pagination import paginate_query
|
||||
from machinery.context import RequestContext
|
||||
from models.account import Account, Tenant, TenantCustomConfigDict, TenantStatus
|
||||
from models.account import Account, Tenant, TenantAccountRole, TenantCustomConfigDict, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
from services.file_service import FileService
|
||||
@@ -105,6 +107,14 @@ class TenantInfoResponse(ResponseModel):
|
||||
return to_timestamp(value)
|
||||
|
||||
|
||||
class CurrentWorkspaceSummaryResponse(ResponseModel):
|
||||
id: str
|
||||
name: str
|
||||
role: TenantAccountRole
|
||||
plan: str | None
|
||||
credits: int | None = Field(description="Remaining credits in the effective pool; -1 means unlimited.")
|
||||
|
||||
|
||||
class TenantListItemResponse(ResponseModel):
|
||||
id: str
|
||||
name: str | None = None
|
||||
@@ -201,6 +211,7 @@ register_schema_models(
|
||||
)
|
||||
register_response_schema_models(
|
||||
console_ns,
|
||||
CurrentWorkspaceSummaryResponse,
|
||||
TenantInfoResponse,
|
||||
TenantListItemResponse,
|
||||
TenantListResponse,
|
||||
@@ -245,35 +256,31 @@ class WorkspaceListApi(Resource):
|
||||
).model_dump(mode="json"), HTTPStatus.OK
|
||||
|
||||
|
||||
@console_ns.route("/workspaces/current", endpoint="workspaces_current")
|
||||
@console_ns.route("/info", endpoint="info") # Deprecated
|
||||
class TenantApi(Resource):
|
||||
@console_ns.route("/workspaces/current/summary")
|
||||
class CurrentWorkspaceSummaryApi(Resource):
|
||||
@console_ns.response(
|
||||
HTTPStatus.OK,
|
||||
"Success",
|
||||
console_ns.models[CurrentWorkspaceSummaryResponse.__name__],
|
||||
)
|
||||
@console_ns.response(HTTPStatus.CONFLICT, "Current workspace is archived")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[TenantInfoResponse.__name__])
|
||||
@with_current_user
|
||||
@with_session
|
||||
def post(self, session: Session, current_user: Account):
|
||||
if request.path == "/info":
|
||||
logger.warning("Deprecated URL /info was used.")
|
||||
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_user: Account):
|
||||
tenant = current_user.current_tenant
|
||||
if not tenant:
|
||||
raise ValueError("No current tenant")
|
||||
|
||||
if tenant.status == TenantStatus.ARCHIVE:
|
||||
tenants = TenantService.get_join_tenants(current_user, session=session)
|
||||
# if there is any tenant, switch to the first one
|
||||
if len(tenants) > 0:
|
||||
TenantService.switch_tenant(current_user, tenants[0].id, session=session)
|
||||
tenant = tenants[0]
|
||||
# else, raise Unauthorized
|
||||
else:
|
||||
raise Unauthorized("workspace is archived")
|
||||
raise CurrentWorkspaceArchivedError()
|
||||
|
||||
return (
|
||||
dump_response(TenantInfoResponse, WorkspaceService.get_tenant_info(tenant, session=session)),
|
||||
dump_response(
|
||||
CurrentWorkspaceSummaryResponse,
|
||||
WorkspaceService.get_current_workspace_summary(tenant, current_user.id, session=session),
|
||||
),
|
||||
HTTPStatus.OK,
|
||||
)
|
||||
|
||||
@@ -308,6 +315,31 @@ class SwitchWorkspaceApi(Resource):
|
||||
|
||||
@console_ns.route("/workspaces/custom-config")
|
||||
class CustomConfigWorkspaceApi(Resource):
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceCustomConfigResponse.__name__])
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@with_current_tenant_id
|
||||
@with_session(write=False)
|
||||
def get(self, session: Session, current_tenant_id: str):
|
||||
tenant = TenantService.get_tenant_by_id(current_tenant_id, session=session)
|
||||
if tenant is None:
|
||||
raise NotFound()
|
||||
|
||||
custom_config = tenant.custom_config_dict
|
||||
replace_webapp_logo = (
|
||||
f"{dify_config.FILES_URL}/files/workspaces/{tenant.id}/webapp-logo"
|
||||
if custom_config.get("replace_webapp_logo")
|
||||
else None
|
||||
)
|
||||
return dump_response(
|
||||
WorkspaceCustomConfigResponse,
|
||||
{
|
||||
"remove_webapp_brand": custom_config.get("remove_webapp_brand", False),
|
||||
"replace_webapp_logo": replace_webapp_logo,
|
||||
},
|
||||
)
|
||||
|
||||
@console_ns.expect(console_ns.models[WorkspaceCustomConfigPayload.__name__])
|
||||
@console_ns.response(HTTPStatus.OK, "Success", console_ns.models[WorkspaceTenantResultResponse.__name__])
|
||||
@setup_required
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from flask_restx.api import HTTPStatus
|
||||
@@ -5,10 +7,12 @@ from pydantic import BaseModel, Field
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
import services
|
||||
from core.db.session_factory import session_factory
|
||||
from core.tools.signature import verify_plugin_file_signature
|
||||
from core.tools.tool_file_manager import ToolFileManager, resolve_extension
|
||||
from core.workflow.file_reference import build_file_reference
|
||||
from fields.file_fields import FileResponse
|
||||
from services.account_service import TenantService
|
||||
|
||||
from ..common.errors import (
|
||||
FileTooLargeError,
|
||||
@@ -26,6 +30,7 @@ class PluginUploadQuery(BaseModel):
|
||||
sign: str = Field(..., description="HMAC signature")
|
||||
tenant_id: str = Field(..., description="Tenant identifier")
|
||||
user_id: str | None = Field(default=None, description="User identifier")
|
||||
user_from: Literal["account", "end-user"] | None = Field(default=None, description="User identity type")
|
||||
conversation_id: str | None = Field(default=None, description="Conversation identifier")
|
||||
|
||||
|
||||
@@ -77,8 +82,20 @@ class PluginUploadFileApi(Resource):
|
||||
nonce = args.nonce
|
||||
sign = args.sign
|
||||
tenant_id = args.tenant_id
|
||||
user_id = args.user_id
|
||||
user = get_user(tenant_id, user_id)
|
||||
if args.user_from == "account":
|
||||
if args.user_id is None:
|
||||
raise Forbidden("Invalid request.")
|
||||
with session_factory.create_session() as session:
|
||||
is_tenant_member = TenantService.account_belongs_to_tenant(
|
||||
args.user_id,
|
||||
tenant_id,
|
||||
session=session,
|
||||
)
|
||||
if not is_tenant_member:
|
||||
raise Forbidden("Invalid request.")
|
||||
owner_id = args.user_id
|
||||
else:
|
||||
owner_id = get_user(tenant_id, args.user_id).id
|
||||
|
||||
filename = file.filename
|
||||
mimetype = file.mimetype
|
||||
@@ -90,8 +107,9 @@ class PluginUploadFileApi(Resource):
|
||||
filename=filename,
|
||||
mimetype=mimetype,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user.id,
|
||||
user_id=owner_id,
|
||||
conversation_id=args.conversation_id,
|
||||
user_from=args.user_from,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
sign=sign,
|
||||
@@ -100,7 +118,7 @@ class PluginUploadFileApi(Resource):
|
||||
|
||||
try:
|
||||
tool_file = ToolFileManager().create_file_by_raw(
|
||||
user_id=user.id,
|
||||
user_id=owner_id,
|
||||
tenant_id=tenant_id,
|
||||
file_binary=file.stream.read(),
|
||||
mimetype=mimetype,
|
||||
|
||||
@@ -38,6 +38,7 @@ class AgentFileRequestHttpError(BaseHTTPException):
|
||||
class AgentFileUploadRequestPayload(RequestRequestUploadFile):
|
||||
tenant_id: str
|
||||
user_id: str
|
||||
user_from: Literal["account", "end-user"] | None = None
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
@@ -113,13 +114,19 @@ class AgentFileUploadRequestApi(Resource):
|
||||
status_code=404,
|
||||
)
|
||||
try:
|
||||
user = get_user(tenant.id, payload.user_id)
|
||||
if payload.user_from == "account":
|
||||
if not TenantService.account_belongs_to_tenant(payload.user_id, tenant.id, session=session):
|
||||
raise ValueError("account not found")
|
||||
owner_id = payload.user_id
|
||||
else:
|
||||
owner_id = get_user(tenant.id, payload.user_id).id
|
||||
upload_uri = get_signed_file_uri_for_plugin(
|
||||
filename=payload.filename,
|
||||
mimetype=payload.mimetype,
|
||||
tenant_id=tenant.id,
|
||||
user_id=user.id,
|
||||
user_id=owner_id,
|
||||
conversation_id=payload.conversation_id,
|
||||
user_from=payload.user_from,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise AgentFileRequestHttpError(
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
|
||||
These endpoints are called by the dify-agent server with the inner API key.
|
||||
They resolve the requested Agent config version directly from Agent Soul JSON
|
||||
and never expose signed download URLs or drive-owned metadata.
|
||||
and authorize downloads with the existing Config target, tenant, and source
|
||||
ownership semantics. Download requests return metadata plus a short-lived,
|
||||
origin-free ``/files/*`` URI, never file bytes; the Sandbox fetches those bytes
|
||||
directly from the Dify API data plane.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import Literal
|
||||
|
||||
from flask import request, send_file
|
||||
from flask import request
|
||||
from flask_restx import Resource
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError, model_validator
|
||||
|
||||
from controllers.console.wraps import setup_required
|
||||
from controllers.inner_api import inner_api_ns
|
||||
from controllers.inner_api.wraps import plugin_inner_api_only
|
||||
from models.agent_config_entities import validate_config_name, validate_config_skill_name
|
||||
from services.agent_config_service import (
|
||||
AgentConfigService,
|
||||
AgentConfigServiceError,
|
||||
@@ -38,6 +42,24 @@ class _ConfigMutationRequest(BaseModel):
|
||||
config_version_kind: AgentConfigVersionKind
|
||||
|
||||
|
||||
class _ConfigDownloadSource(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
kind: Literal["file", "skill"]
|
||||
name: str
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_name(self) -> _ConfigDownloadSource:
|
||||
self.name = validate_config_skill_name(self.name) if self.kind == "skill" else validate_config_name(self.name)
|
||||
return self
|
||||
|
||||
|
||||
class _ConfigDownloadRequest(_ConfigTargetQuery):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
config: _ConfigDownloadSource
|
||||
|
||||
|
||||
class _ConfigPushRequest(_ConfigMutationRequest):
|
||||
files: list[dict] = []
|
||||
skills: list[dict] = []
|
||||
@@ -99,28 +121,29 @@ class AgentConfigManifestApi(Resource):
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@inner_api_ns.route("/agent-config/<string:agent_id>/skills/<string:name>/pull")
|
||||
class AgentConfigSkillPullApi(Resource):
|
||||
@inner_api_ns.route("/agent-config/<string:agent_id>/download-request")
|
||||
class AgentConfigDownloadRequestApi(Resource):
|
||||
@setup_required
|
||||
@plugin_inner_api_only
|
||||
@inner_api_ns.doc("agent_config_skill_pull")
|
||||
def get(self, agent_id: str, name: str):
|
||||
@inner_api_ns.doc("agent_config_download_request")
|
||||
def post(self, agent_id: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = AgentConfigService().pull_skill(
|
||||
tenant_id=query.tenant_id,
|
||||
body = _ConfigDownloadRequest.model_validate(request.get_json(silent=True) or {})
|
||||
result = AgentConfigService().request_download(
|
||||
tenant_id=body.tenant_id,
|
||||
agent_id=agent_id,
|
||||
user_id=query.user_id,
|
||||
config_version_id=query.config_version_id,
|
||||
config_version_kind=query.config_version_kind,
|
||||
name=name,
|
||||
)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
user_id=body.user_id,
|
||||
config_version_id=body.config_version_id,
|
||||
config_version_kind=body.config_version_kind,
|
||||
kind=body.config.kind,
|
||||
name=body.config.name,
|
||||
)
|
||||
return {
|
||||
"filename": result.filename,
|
||||
"mime_type": result.mime_type,
|
||||
"size": result.size,
|
||||
"download_uri": result.download_uri,
|
||||
}
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except AgentConfigServiceError as exc:
|
||||
@@ -149,34 +172,6 @@ class AgentConfigSkillInspectApi(Resource):
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@inner_api_ns.route("/agent-config/<string:agent_id>/files/<string:name>/pull")
|
||||
class AgentConfigFilePullApi(Resource):
|
||||
@setup_required
|
||||
@plugin_inner_api_only
|
||||
@inner_api_ns.doc("agent_config_file_pull")
|
||||
def get(self, agent_id: str, name: str):
|
||||
try:
|
||||
query = _target_query_from_request()
|
||||
result = AgentConfigService().pull_file(
|
||||
tenant_id=query.tenant_id,
|
||||
agent_id=agent_id,
|
||||
user_id=query.user_id,
|
||||
config_version_id=query.config_version_id,
|
||||
config_version_kind=query.config_version_kind,
|
||||
name=name,
|
||||
)
|
||||
return send_file(
|
||||
io.BytesIO(result.payload),
|
||||
mimetype=result.mime_type,
|
||||
as_attachment=True,
|
||||
download_name=result.filename,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
return {"code": "invalid_request", "message": str(exc)}, 400
|
||||
except AgentConfigServiceError as exc:
|
||||
return _error_response(exc)
|
||||
|
||||
|
||||
@inner_api_ns.route("/agent-config/<string:agent_id>/push")
|
||||
class AgentConfigPushApi(Resource):
|
||||
@setup_required
|
||||
|
||||
@@ -16,6 +16,7 @@ from configs import dify_config
|
||||
from controllers.common.errors import NotFoundError
|
||||
from controllers.common.human_input import HumanInputFormSubmitPayload, stringify_form_default_values
|
||||
from controllers.common.schema import register_response_schema_models, register_schema_models
|
||||
from controllers.console.wraps import model_validate
|
||||
from controllers.web import web_ns
|
||||
from controllers.web.error import WebFormRateLimitExceededError
|
||||
from controllers.web.site import WebAppSiteResponse
|
||||
@@ -235,7 +236,8 @@ class HumanInputFormApi(Resource):
|
||||
"Form submitted successfully",
|
||||
web_ns.models[HumanInputFormSubmitResponse.__name__],
|
||||
)
|
||||
def post(self, form_token: str):
|
||||
@model_validate(HumanInputFormSubmitPayload)
|
||||
def post(self, payload: HumanInputFormSubmitPayload, form_token: str):
|
||||
"""
|
||||
Submit human input form by token.
|
||||
|
||||
@@ -249,8 +251,6 @@ class HumanInputFormApi(Resource):
|
||||
"action": "Approve"
|
||||
}
|
||||
"""
|
||||
payload = HumanInputFormSubmitPayload.model_validate(request.get_json())
|
||||
|
||||
ip_address = extract_remote_ip(request)
|
||||
if _FORM_SUBMIT_RATE_LIMITER.is_rate_limited(ip_address):
|
||||
raise WebFormRateLimitExceededError()
|
||||
|
||||
@@ -89,9 +89,10 @@ _AGENT_BACKEND_INVOKE_ERROR_BY_REASON: Mapping[str, type[InvokeError]] = {
|
||||
|
||||
|
||||
def _agent_backend_failure_to_exception(event: AgentBackendRunFailedInternalEvent) -> Exception:
|
||||
err_cls = _AGENT_BACKEND_INVOKE_ERROR_BY_REASON.get(event.reason or "")
|
||||
if err_cls is not None:
|
||||
return err_cls(event.error)
|
||||
if event.error_type is None:
|
||||
err_cls = _AGENT_BACKEND_INVOKE_ERROR_BY_REASON.get(event.reason or "")
|
||||
if err_cls is not None:
|
||||
return err_cls(event.error)
|
||||
message = event.error or "Agent backend run did not complete successfully."
|
||||
return AgentBackendRunFailedError(
|
||||
event.run_id,
|
||||
@@ -101,6 +102,7 @@ def _agent_backend_failure_to_exception(event: AgentBackendRunFailedInternalEven
|
||||
"source_event_id": event.source_event_id,
|
||||
},
|
||||
message=message,
|
||||
error_type=event.error_type,
|
||||
reason=event.reason,
|
||||
source_event_id=event.source_event_id,
|
||||
)
|
||||
@@ -701,7 +703,7 @@ class AgentAppRunner:
|
||||
if not isinstance(terminal, AgentBackendRunSucceededInternalEvent):
|
||||
if isinstance(terminal, AgentBackendRunFailedInternalEvent):
|
||||
reason = terminal.reason
|
||||
if reason == "binding_lost":
|
||||
if terminal.error_type is None and reason == "binding_lost":
|
||||
raise AgentBackendError("The retained agent working environment is no longer available.")
|
||||
raise _agent_backend_failure_to_exception(terminal)
|
||||
raise AgentBackendError("Agent backend run did not complete successfully.")
|
||||
|
||||
@@ -3,9 +3,10 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Generator, Mapping
|
||||
from typing import Any, Union, cast
|
||||
|
||||
from dify_agent.protocol import RunFailureType
|
||||
from pydantic import JsonValue
|
||||
|
||||
from clients.agent_backend.errors import AgentBackendError
|
||||
from clients.agent_backend.errors import AgentBackendError, AgentBackendRunFailedError
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.app.entities.task_entities import AppBlockingResponse, AppStreamResponse
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
@@ -117,6 +118,13 @@ class AppGenerateResponseConverter[TBlockingResponse: AppBlockingResponse](ABC):
|
||||
:param e: exception
|
||||
:return:
|
||||
"""
|
||||
if isinstance(e, AgentBackendRunFailedError) and e.error_type == RunFailureType.AGENT_RUN_LIMIT_EXCEEDED:
|
||||
return {
|
||||
"code": RunFailureType.AGENT_RUN_LIMIT_EXCEEDED.value,
|
||||
"status": 400,
|
||||
"message": str(e),
|
||||
}
|
||||
|
||||
error_responses: dict[type[Exception], dict[str, JsonValue]] = {
|
||||
ValueError: {"code": "invalid_param", "status": 400},
|
||||
ProviderTokenNotInitError: {"code": "provider_not_initialize", "status": 400},
|
||||
|
||||
@@ -4,6 +4,7 @@ import hmac
|
||||
import os
|
||||
import time
|
||||
import urllib.parse
|
||||
from typing import Literal
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from configs import dify_config
|
||||
@@ -88,13 +89,27 @@ def verify_tool_file_signature(file_id: str, timestamp: str, nonce: str, sign: s
|
||||
|
||||
|
||||
def get_signed_file_uri_for_plugin(
|
||||
filename: str, mimetype: str, tenant_id: str, user_id: str, conversation_id: str | None = None
|
||||
filename: str,
|
||||
mimetype: str,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
conversation_id: str | None = None,
|
||||
user_from: Literal["account", "end-user"] | None = None,
|
||||
) -> str:
|
||||
"""Build a signed plugin-upload URI without selecting a network origin."""
|
||||
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = os.urandom(16).hex()
|
||||
data_to_sign = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{conversation_id or ''}|{timestamp}|{nonce}"
|
||||
data_to_sign = _plugin_upload_signature_payload(
|
||||
filename=filename,
|
||||
mimetype=mimetype,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
conversation_id=conversation_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
user_from=user_from,
|
||||
)
|
||||
sign = hmac.new(_secret_key(), data_to_sign.encode(), hashlib.sha256).digest()
|
||||
encoded_sign = base64.urlsafe_b64encode(sign).decode()
|
||||
query_params = {
|
||||
@@ -106,6 +121,8 @@ def get_signed_file_uri_for_plugin(
|
||||
}
|
||||
if conversation_id:
|
||||
query_params["conversation_id"] = conversation_id
|
||||
if user_from is not None:
|
||||
query_params["user_from"] = user_from
|
||||
query = urllib.parse.urlencode(query_params)
|
||||
return f"/files/upload/for-plugin?{query}"
|
||||
|
||||
@@ -117,13 +134,23 @@ def verify_plugin_file_signature(
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
conversation_id: str | None = None,
|
||||
user_from: Literal["account", "end-user"] | None = None,
|
||||
timestamp: str,
|
||||
nonce: str,
|
||||
sign: str,
|
||||
) -> bool:
|
||||
"""Verify the signature used by the plugin-facing file upload endpoint."""
|
||||
|
||||
data_to_sign = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{conversation_id or ''}|{timestamp}|{nonce}"
|
||||
data_to_sign = _plugin_upload_signature_payload(
|
||||
filename=filename,
|
||||
mimetype=mimetype,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
conversation_id=conversation_id,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
user_from=user_from,
|
||||
)
|
||||
recalculated_sign = hmac.new(_secret_key(), data_to_sign.encode(), hashlib.sha256).digest()
|
||||
recalculated_encoded_sign = base64.urlsafe_b64encode(recalculated_sign).decode()
|
||||
|
||||
@@ -132,3 +159,27 @@ def verify_plugin_file_signature(
|
||||
|
||||
current_time = int(time.time())
|
||||
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT
|
||||
|
||||
|
||||
def _plugin_upload_signature_payload(
|
||||
*,
|
||||
filename: str,
|
||||
mimetype: str,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
conversation_id: str | None,
|
||||
timestamp: str,
|
||||
nonce: str,
|
||||
user_from: Literal["account", "end-user"] | None,
|
||||
) -> str:
|
||||
"""Build the compatible upload signature payload with optional identity ownership.
|
||||
|
||||
Omitting ``user_from`` preserves the legacy payload. When present, the
|
||||
identity kind is appended and HMAC-protected so account/end-user ownership
|
||||
cannot be altered.
|
||||
"""
|
||||
|
||||
payload = f"upload|{filename}|{mimetype}|{tenant_id}|{user_id}|{conversation_id or ''}|{timestamp}|{nonce}"
|
||||
if user_from is not None:
|
||||
payload = f"{payload}|{user_from}"
|
||||
return payload
|
||||
|
||||
@@ -123,7 +123,7 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
|
||||
for hit_callback in self.hit_callbacks:
|
||||
hit_callback.return_retriever_resource_info(context_list)
|
||||
|
||||
return str("\n".join([item.page_content for item in results]))
|
||||
return "\n".join([item.page_content for item in results])
|
||||
else:
|
||||
if metadata_condition and not document_ids_filter:
|
||||
return ""
|
||||
@@ -139,7 +139,7 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
|
||||
top_k=self.top_k,
|
||||
document_ids_filter=document_ids_filter,
|
||||
)
|
||||
return str("\n".join([document.page_content for document in documents]))
|
||||
return "\n".join([document.page_content for document in documents])
|
||||
else:
|
||||
if self.top_k > 0:
|
||||
# retrieval source
|
||||
@@ -241,5 +241,5 @@ class DatasetRetrieverTool(DatasetRetrieverBaseTool):
|
||||
hit_callback.return_retriever_resource_info(retrieval_resource_list)
|
||||
if document_context_list:
|
||||
document_context_list = sorted(document_context_list, key=lambda x: x.score or 0.0, reverse=True)
|
||||
return str("\n".join([document_context.content for document_context in document_context_list]))
|
||||
return "\n".join([document_context.content for document_context in document_context_list])
|
||||
return ""
|
||||
|
||||
@@ -98,7 +98,7 @@ class WorkflowAgentOutputAdapter:
|
||||
match event:
|
||||
case AgentBackendRunFailedInternalEvent():
|
||||
error = event.error
|
||||
error_type = event.reason or "agent_backend_run_failed"
|
||||
error_type = event.error_type or event.reason or "agent_backend_run_failed"
|
||||
terminal_status = "failed"
|
||||
case AgentBackendRunCancelledInternalEvent():
|
||||
error = event.message or "Agent backend run was cancelled."
|
||||
|
||||
@@ -836,7 +836,12 @@ def _knowledge_metadata_filtering_config(
|
||||
return DifyKnowledgeMetadataFilteringConfig(
|
||||
mode=metadata_filtering.mode,
|
||||
model_config=_knowledge_model_config(metadata_filtering.metadata_model_config),
|
||||
conditions=cast(Any, metadata_filtering.conditions.model_dump(mode="json"))
|
||||
conditions=cast(
|
||||
Any,
|
||||
metadata_filtering.conditions.model_dump(
|
||||
mode="json", exclude={"conditions": {"__all__": {"id", "metadata_id"}}}
|
||||
),
|
||||
)
|
||||
if metadata_filtering.conditions is not None
|
||||
else None,
|
||||
)
|
||||
|
||||
@@ -6,13 +6,21 @@ from typing import cast
|
||||
from flask import Flask, current_app
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from constants.dsl_version import CURRENT_APP_DSL_VERSION
|
||||
from core.db.session_factory import get_session_maker
|
||||
from core.schemas.schema_manager import SchemaManager
|
||||
from enums.deployment_edition import DeploymentEdition
|
||||
from extensions.ext_redis import RedisClientWrapper, redis_client
|
||||
from repositories.installation_state_repository import InstallationStateRepository
|
||||
from repositories.workspace_member_query_repository import WorkspaceMemberQueryRepository
|
||||
from repositories.workspace_query_repository import WorkspaceQueryRepository
|
||||
from services.feature_query_service import FeatureQueryService
|
||||
from services.feature_service import FeatureService
|
||||
from services.feature_service_gateway import FeatureServiceGateway
|
||||
from services.schema_definition_service import SchemaDefinitionService
|
||||
from services.setup_adapters import RedisSetupLock, RegisterServiceAccountProvisioner
|
||||
from services.setup_service import SetupService
|
||||
from services.workspace_member_query_service import WorkspaceMemberQueryService
|
||||
from services.workspace_member_role_resolver import DeploymentWorkspaceMemberRoleResolver
|
||||
from services.workspace_plan_gateway import DeploymentWorkspacePlanGateway
|
||||
@@ -23,6 +31,8 @@ _EXTENSION_KEY = "application_services"
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApplicationServices:
|
||||
schema_definitions: SchemaDefinitionService
|
||||
setup: SetupService
|
||||
feature_queries: FeatureQueryService
|
||||
workspace_queries: WorkspaceQueryService
|
||||
workspace_member_queries: WorkspaceMemberQueryService
|
||||
@@ -31,8 +41,18 @@ class ApplicationServices:
|
||||
def build_application_services(
|
||||
*,
|
||||
database_client: sessionmaker[Session],
|
||||
deployment_edition: DeploymentEdition,
|
||||
redis: RedisClientWrapper,
|
||||
) -> ApplicationServices:
|
||||
installation_state = InstallationStateRepository(client=database_client)
|
||||
return ApplicationServices(
|
||||
schema_definitions=SchemaDefinitionService(source_factory=SchemaManager),
|
||||
setup=SetupService(
|
||||
state=installation_state,
|
||||
accounts=RegisterServiceAccountProvisioner(client=database_client),
|
||||
lock=RedisSetupLock(client=redis),
|
||||
setup_required=deployment_edition != DeploymentEdition.CLOUD,
|
||||
),
|
||||
feature_queries=FeatureQueryService(
|
||||
features=FeatureServiceGateway(),
|
||||
trial_models=FeatureService.get_trial_models(),
|
||||
@@ -56,6 +76,8 @@ def build_application_services(
|
||||
def init_app(app: Flask) -> None:
|
||||
app.extensions[_EXTENSION_KEY] = build_application_services(
|
||||
database_client=get_session_maker(),
|
||||
deployment_edition=dify_config.DEPLOYMENT_EDITION,
|
||||
redis=redis_client,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -420,8 +420,19 @@ class AgentKnowledgeRetrievalConfig(BaseModel):
|
||||
|
||||
|
||||
class AgentKnowledgeMetadataCondition(BaseModel):
|
||||
"""One manual metadata filter clause.
|
||||
|
||||
``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
|
||||
every save (a stable row key and a reference to the selected metadata
|
||||
field). They are persisted here for round-tripping the composer's draft
|
||||
state but are stripped before building the Agent runtime request, whose
|
||||
DTO only accepts ``name``/``comparison_operator``/``value``.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: str | None = None
|
||||
metadata_id: str | None = None
|
||||
name: str = Field(min_length=1, max_length=255)
|
||||
comparison_operator: SupportedComparisonOperator
|
||||
value: ConditionValue = None
|
||||
@@ -527,8 +538,14 @@ class AgentModelResponseFormatConfig(AgentFlexibleConfig):
|
||||
type: str | None = Field(default=None, max_length=64)
|
||||
|
||||
|
||||
class AgentSoulModelSettings(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
class AgentSoulModelSettings(AgentFlexibleConfig):
|
||||
"""Model parameters for the Agent Soul model.
|
||||
|
||||
Model plugins can declare arbitrary parameters via ``parameter_rules``
|
||||
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
|
||||
fields typed below, so extra keys must round-trip through persistence
|
||||
rather than being dropped.
|
||||
"""
|
||||
|
||||
temperature: float | None = None
|
||||
top_p: float | None = None
|
||||
|
||||
@@ -1274,7 +1274,7 @@ List a directory in an Agent App conversation sandbox
|
||||
| agent_id | path | Agent ID | Yes | string (uuid) |
|
||||
| caller_id | query | Agent App caller ID | Yes | string |
|
||||
| caller_type | query | | Yes | string, <br>**Available values:** "build_draft", "conversation" |
|
||||
| path | query | Directory path relative to the sandbox workspace | No | string, <br>**Default:** . |
|
||||
| path | query | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | No | string, <br>**Default:** . |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -1282,26 +1282,8 @@ List a directory in an Agent App conversation sandbox
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)<br> |
|
||||
|
||||
### [GET] /agent/{agent_id}/sandbox/files/read
|
||||
Read a text/binary preview file in an Agent App conversation sandbox
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | Agent ID | Yes | string (uuid) |
|
||||
| caller_id | query | Agent App caller ID | Yes | string |
|
||||
| caller_type | query | | Yes | string, <br>**Available values:** "build_draft", "conversation" |
|
||||
| path | query | File path relative to the sandbox workspace | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [POST] /agent/{agent_id}/sandbox/files/upload
|
||||
Upload one Agent App sandbox file and return a signed download URL
|
||||
### [POST] /agent/{agent_id}/sandbox/files/download
|
||||
Create a ToolFile from one Agent App Binding file and return its download URL
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -1313,13 +1295,31 @@ Upload one Agent App sandbox file and return a signed download URL
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [AgentSandboxUploadPayload](#agentsandboxuploadpayload)<br> |
|
||||
| Yes | **application/json**: [AgentSandboxDownloadPayload](#agentsandboxdownloadpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)<br> |
|
||||
| 200 | Download URL returned | **application/json**: [SandboxDownloadResponse](#sandboxdownloadresponse)<br> |
|
||||
|
||||
### [GET] /agent/{agent_id}/sandbox/files/read
|
||||
Read a text/binary preview file in an Agent App conversation sandbox
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| agent_id | path | Agent ID | Yes | string (uuid) |
|
||||
| caller_id | query | Agent App caller ID | Yes | string |
|
||||
| caller_type | query | | Yes | string, <br>**Available values:** "build_draft", "conversation" |
|
||||
| path | query | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [POST] /agent/{agent_id}/skills/upload
|
||||
Upload + standardize a Skill into an Agent App drive
|
||||
@@ -3822,7 +3822,7 @@ List a directory in a workflow Agent node sandbox
|
||||
| node_id | path | Workflow Agent node ID | Yes | string |
|
||||
| workflow_run_id | path | Workflow run ID | Yes | string (uuid) |
|
||||
| node_execution_id | query | Workflow node execution ID | Yes | string |
|
||||
| path | query | Directory path relative to the sandbox workspace | No | string, <br>**Default:** . |
|
||||
| path | query | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | No | string, <br>**Default:** . |
|
||||
|
||||
#### Responses
|
||||
|
||||
@@ -3830,27 +3830,8 @@ List a directory in a workflow Agent node sandbox
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Listing returned | **application/json**: [SandboxListResponse](#sandboxlistresponse)<br> |
|
||||
|
||||
### [GET] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/read
|
||||
Read a text/binary preview file in a workflow Agent node sandbox
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | Application ID | Yes | string (uuid) |
|
||||
| node_id | path | Workflow Agent node ID | Yes | string |
|
||||
| workflow_run_id | path | Workflow run ID | Yes | string (uuid) |
|
||||
| node_execution_id | query | Workflow node execution ID | Yes | string |
|
||||
| path | query | File path relative to the sandbox workspace | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [POST] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/upload
|
||||
Upload one workflow Agent sandbox file and return a signed download URL
|
||||
### [POST] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/download
|
||||
Create a ToolFile from one workflow Agent Binding file and return its download URL
|
||||
|
||||
#### Parameters
|
||||
|
||||
@@ -3864,13 +3845,32 @@ Upload one workflow Agent sandbox file and return a signed download URL
|
||||
|
||||
| Required | Schema |
|
||||
| -------- | ------ |
|
||||
| Yes | **application/json**: [WorkflowAgentSandboxUploadPayload](#workflowagentsandboxuploadpayload)<br> |
|
||||
| Yes | **application/json**: [WorkflowAgentSandboxDownloadPayload](#workflowagentsandboxdownloadpayload)<br> |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Uploaded | **application/json**: [SandboxUploadResponse](#sandboxuploadresponse)<br> |
|
||||
| 200 | Download URL returned | **application/json**: [SandboxDownloadResponse](#sandboxdownloadresponse)<br> |
|
||||
|
||||
### [GET] /apps/{app_id}/workflow-runs/{workflow_run_id}/agent-nodes/{node_id}/sandbox/files/read
|
||||
Read a text/binary preview file in a workflow Agent node sandbox
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Located in | Description | Required | Schema |
|
||||
| ---- | ---------- | ----------- | -------- | ------ |
|
||||
| app_id | path | Application ID | Yes | string (uuid) |
|
||||
| node_id | path | Workflow Agent node ID | Yes | string |
|
||||
| workflow_run_id | path | Workflow run ID | Yes | string (uuid) |
|
||||
| node_execution_id | query | Workflow node execution ID | Yes | string |
|
||||
| path | query | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | Yes | string |
|
||||
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Preview returned | **application/json**: [SandboxReadResponse](#sandboxreadresponse)<br> |
|
||||
|
||||
### [GET] /apps/{app_id}/workflow/comments
|
||||
**Get all comments for a workflow**
|
||||
@@ -7037,13 +7037,6 @@ Request body:
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [ConsoleHumanInputFormSubmitResponse](#consolehumaninputformsubmitresponse)<br> |
|
||||
|
||||
### [POST] /info
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)<br> |
|
||||
|
||||
### [GET] /installed-apps
|
||||
#### Parameters
|
||||
|
||||
@@ -10069,13 +10062,6 @@ Returns information about why and where the workflow is paused.
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantListResponse](#tenantlistresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/current
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [TenantInfoResponse](#tenantinforesponse)<br> |
|
||||
|
||||
### [GET] /workspaces/current/agent-provider/{provider_name}
|
||||
Get specific agent provider details
|
||||
|
||||
@@ -12031,6 +12017,14 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceAccessMatrix](#workspaceaccessmatrix)<br> |
|
||||
|
||||
### [GET] /workspaces/current/summary
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [CurrentWorkspaceSummaryResponse](#currentworkspacesummaryresponse)<br> |
|
||||
| 409 | Current workspace is archived | |
|
||||
|
||||
### [GET] /workspaces/current/tool-labels
|
||||
#### Responses
|
||||
|
||||
@@ -12838,6 +12832,13 @@ Returns permission flags that control workspace features like member invitations
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Trigger providers retrieved successfully | **application/json**: [TriggerProviderListResponse](#triggerproviderlistresponse)<br> |
|
||||
|
||||
### [GET] /workspaces/custom-config
|
||||
#### Responses
|
||||
|
||||
| Code | Description | Schema |
|
||||
| ---- | ----------- | ------ |
|
||||
| 200 | Success | **application/json**: [WorkspaceCustomConfigResponse](#workspacecustomconfigresponse)<br> |
|
||||
|
||||
### [POST] /workspaces/custom-config
|
||||
#### Request Body
|
||||
|
||||
@@ -14301,9 +14302,19 @@ the current roster/workflow APIs scoped to Dify Agent.
|
||||
|
||||
#### AgentKnowledgeMetadataCondition
|
||||
|
||||
One manual metadata filter clause.
|
||||
|
||||
``id`` and ``metadata_id`` are UI-only bookkeeping the composer sends on
|
||||
every save (a stable row key and a reference to the selected metadata
|
||||
field). They are persisted here for round-tripping the composer's draft
|
||||
state but are stripped before building the Agent runtime request, whose
|
||||
DTO only accepts ``name``/``comparison_operator``/``value``.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| comparison_operator | string, <br>**Available values:** "<", "=", ">", "after", "before", "contains", "empty", "end with", "in", "is", "is not", "not contains", "not empty", "not in", "start with", "≠", "≤", "≥" | *Enum:* `"<"`, `"="`, `">"`, `"after"`, `"before"`, `"contains"`, `"empty"`, `"end with"`, `"in"`, `"is"`, `"is not"`, `"not contains"`, `"not empty"`, `"not in"`, `"start with"`, `"≠"`, `"≤"`, `"≥"` | Yes |
|
||||
| id | string | | No |
|
||||
| metadata_id | string | | No |
|
||||
| name | string | | Yes |
|
||||
| value | string<br>[ string ]<br>number | | No |
|
||||
|
||||
@@ -14702,6 +14713,14 @@ section may be empty, which is how callers express "no knowledge layer".
|
||||
| workflow_id | string | | No |
|
||||
| workflow_node_id | string | | No |
|
||||
|
||||
#### AgentSandboxDownloadPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| caller_id | string | Agent App caller ID | Yes |
|
||||
| caller_type | string, <br>**Available values:** "build_draft", "conversation" | *Enum:* `"build_draft"`, `"conversation"` | Yes |
|
||||
| path | string | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | Yes |
|
||||
|
||||
#### AgentSandboxProviderConfig
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -14711,14 +14730,6 @@ section may be empty, which is how callers express "no knowledge layer".
|
||||
| image | string | | No |
|
||||
| working_dir | string | | No |
|
||||
|
||||
#### AgentSandboxUploadPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| caller_id | string | Agent App caller ID | Yes |
|
||||
| caller_type | string, <br>**Available values:** "build_draft", "conversation" | *Enum:* `"build_draft"`, `"conversation"` | Yes |
|
||||
| path | string | File path relative to the sandbox workspace | Yes |
|
||||
|
||||
#### AgentScope
|
||||
|
||||
Visibility and lifecycle scope of an Agent record.
|
||||
@@ -14922,6 +14933,13 @@ Reference to model credentials resolved only at runtime.
|
||||
|
||||
#### AgentSoulModelSettings
|
||||
|
||||
Model parameters for the Agent Soul model.
|
||||
|
||||
Model plugins can declare arbitrary parameters via ``parameter_rules``
|
||||
(e.g. Qwen/Tongyi's ``enable_thinking``) beyond the common OpenAI-style
|
||||
fields typed below, so extra keys must round-trip through persistence
|
||||
rather than being dropped.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| frequency_penalty | number | | No |
|
||||
@@ -16619,6 +16637,16 @@ Model class for credential form schema.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| CredentialType | string | | |
|
||||
|
||||
#### CurrentWorkspaceSummaryResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| credits | integer | Remaining credits in the effective pool; -1 means unlimited. | Yes |
|
||||
| id | string | | Yes |
|
||||
| name | string | | Yes |
|
||||
| plan | string | | Yes |
|
||||
| role | [TenantAccountRole](#tenantaccountrole) | | Yes |
|
||||
|
||||
#### CustomConfigurationResponse
|
||||
|
||||
Model class for provider custom configuration response.
|
||||
@@ -18926,6 +18954,7 @@ Enum class for large language model mode.
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| expired_at | string | | Yes |
|
||||
| license_expiry_notice_enabled | boolean | | Yes |
|
||||
| seats | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
|
||||
| status | [LicenseStatus](#licensestatus) | | Yes |
|
||||
| workspaces | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
|
||||
@@ -21510,6 +21539,12 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs.
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| SSOProtocol | string | | |
|
||||
|
||||
#### SandboxDownloadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| url | string | | Yes |
|
||||
|
||||
#### SandboxFileEntryResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -21543,12 +21578,6 @@ Whitelist scopes accepted by RBAC app and dataset access config APIs.
|
||||
| text | string | | No |
|
||||
| truncated | boolean | | Yes |
|
||||
|
||||
#### SandboxUploadResponse
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| url | string | | Yes |
|
||||
|
||||
#### SavedMessageCreatePayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
@@ -22276,7 +22305,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
|
||||
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_explore_banner | boolean | | Yes |
|
||||
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_marketplace | boolean | | Yes |
|
||||
| enable_social_oauth_login | boolean | | Yes |
|
||||
| enable_step_by_step_tour | boolean | | Yes |
|
||||
@@ -23356,12 +23384,12 @@ How a workflow node is bound to an Agent.
|
||||
| variant | string | | Yes |
|
||||
| workflow_id | string | | No |
|
||||
|
||||
#### WorkflowAgentSandboxUploadPayload
|
||||
#### WorkflowAgentSandboxDownloadPayload
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| node_execution_id | string | Workflow node execution ID | Yes |
|
||||
| path | string | File path relative to the sandbox workspace | Yes |
|
||||
| path | string | Binding path: relative paths start in Workspace; exact `~` and paths beginning with `~/` start in Home; `~user` is an ordinary relative path from Workspace; absolute paths remain absolute; `..` and paths outside Workspace are governed by backend isolation, not a Workspace-root restriction | Yes |
|
||||
|
||||
#### WorkflowAppLogPaginationResponse
|
||||
|
||||
|
||||
@@ -1566,7 +1566,6 @@ Non-sensitive bootstrap snapshot exposed before Console or Web authentication.
|
||||
| enable_email_password_login | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_explore_banner | boolean | | Yes |
|
||||
| enable_learn_app | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_license_expiry_notice | boolean, <br>**Default:** true | | Yes |
|
||||
| enable_marketplace | boolean | | Yes |
|
||||
| enable_social_oauth_login | boolean | | Yes |
|
||||
| enable_step_by_step_tour | boolean | | Yes |
|
||||
|
||||
27
api/repositories/installation_state_repository.py
Normal file
27
api/repositories/installation_state_repository.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Persistence adapter for installation setup and tenant-existence state."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import exists, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from models.account import Tenant
|
||||
from models.model import DifySetup
|
||||
|
||||
|
||||
class InstallationStateRepository:
|
||||
"""Read persistent state shared by installation bootstrap use cases."""
|
||||
|
||||
def __init__(self, client: sessionmaker[Session]) -> None:
|
||||
self._client = client
|
||||
|
||||
def get_setup_at(self) -> datetime | None:
|
||||
with self._client() as session:
|
||||
return session.scalar(select(DifySetup.setup_at).limit(1))
|
||||
|
||||
def is_setup(self) -> bool:
|
||||
return self.get_setup_at() is not None
|
||||
|
||||
def has_tenants(self) -> bool:
|
||||
with self._client() as session:
|
||||
return session.scalar(select(exists().select_from(Tenant))) is True
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user