mirror of
https://github.com/langgenius/dify.git
synced 2026-04-25 17:47:30 +08:00
Merge remote-tracking branch 'origin/main' into release/e-1.8.1
This commit is contained in:
commit
aec72ba2d5
28
.github/workflows/autofix.yml
vendored
28
.github/workflows/autofix.yml
vendored
@ -20,14 +20,40 @@ jobs:
|
|||||||
cd api
|
cd api
|
||||||
uv sync --dev
|
uv sync --dev
|
||||||
# Fix lint errors
|
# Fix lint errors
|
||||||
uv run ruff check --fix-only .
|
uv run ruff check --fix .
|
||||||
# Format code
|
# Format code
|
||||||
uv run ruff format .
|
uv run ruff format .
|
||||||
|
|
||||||
- name: ast-grep
|
- name: ast-grep
|
||||||
run: |
|
run: |
|
||||||
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||||
uvx --from ast-grep-cli sg --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all
|
uvx --from ast-grep-cli sg --pattern 'session.query($WHATEVER).filter($HERE)' --rewrite 'session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||||
|
|
||||||
|
|
||||||
- name: mdformat
|
- name: mdformat
|
||||||
run: |
|
run: |
|
||||||
uvx mdformat .
|
uvx mdformat .
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
package_json_file: web/package.json
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Setup NodeJS
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: ./web/package.json
|
||||||
|
|
||||||
|
- name: Web dependencies
|
||||||
|
working-directory: ./web
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: oxlint
|
||||||
|
working-directory: ./web
|
||||||
|
run: |
|
||||||
|
pnpx oxlint --fix
|
||||||
|
|
||||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||||
|
|||||||
28
.github/workflows/deploy-enterprise.yml
vendored
28
.github/workflows/deploy-enterprise.yml
vendored
@ -19,11 +19,23 @@ jobs:
|
|||||||
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to server
|
- name: trigger deployments
|
||||||
uses: appleboy/ssh-action@v0.1.8
|
env:
|
||||||
with:
|
DEV_ENV_ADDRS: ${{ vars.DEV_ENV_ADDRS }}
|
||||||
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
|
DEPLOY_SECRET: ${{ secrets.DEPLOY_SECRET }}
|
||||||
username: ${{ secrets.ENTERPRISE_SSH_USER }}
|
run: |
|
||||||
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
|
IFS=',' read -ra ENDPOINTS <<< "${DEV_ENV_ADDRS:-}"
|
||||||
script: |
|
BODY='{"project":"dify-api","tag":"deploy-enterprise"}'
|
||||||
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}
|
|
||||||
|
for ENDPOINT in "${ENDPOINTS[@]}"; do
|
||||||
|
ENDPOINT="$(echo "$ENDPOINT" | xargs)"
|
||||||
|
[ -z "$ENDPOINT" ] && continue
|
||||||
|
|
||||||
|
API_SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$DEPLOY_SECRET" | awk '{print "sha256="$2}')
|
||||||
|
|
||||||
|
curl -sSf -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-Hub-Signature-256: $API_SIGNATURE" \
|
||||||
|
-d "$BODY" \
|
||||||
|
"$ENDPOINT"
|
||||||
|
done
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -227,3 +227,7 @@ web/public/fallback-*.js
|
|||||||
.roo/
|
.roo/
|
||||||
api/.env.backup
|
api/.env.backup
|
||||||
/clickzetta
|
/clickzetta
|
||||||
|
|
||||||
|
# Benchmark
|
||||||
|
scripts/stress-test/setup/config/
|
||||||
|
scripts/stress-test/reports/
|
||||||
34
Makefile
34
Makefile
@ -4,10 +4,13 @@ WEB_IMAGE=$(DOCKER_REGISTRY)/dify-web
|
|||||||
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
API_IMAGE=$(DOCKER_REGISTRY)/dify-api
|
||||||
VERSION=latest
|
VERSION=latest
|
||||||
|
|
||||||
|
# Default target - show help
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
# Backend Development Environment Setup
|
# Backend Development Environment Setup
|
||||||
.PHONY: dev-setup prepare-docker prepare-web prepare-api
|
.PHONY: dev-setup prepare-docker prepare-web prepare-api
|
||||||
|
|
||||||
# Default dev setup target
|
# Dev setup target
|
||||||
dev-setup: prepare-docker prepare-web prepare-api
|
dev-setup: prepare-docker prepare-web prepare-api
|
||||||
@echo "✅ Backend development environment setup complete!"
|
@echo "✅ Backend development environment setup complete!"
|
||||||
|
|
||||||
@ -46,6 +49,27 @@ dev-clean:
|
|||||||
@rm -rf api/storage
|
@rm -rf api/storage
|
||||||
@echo "✅ Cleanup complete"
|
@echo "✅ Cleanup complete"
|
||||||
|
|
||||||
|
# Backend Code Quality Commands
|
||||||
|
format:
|
||||||
|
@echo "🎨 Running ruff format..."
|
||||||
|
@uv run --project api --dev ruff format ./api
|
||||||
|
@echo "✅ Code formatting complete"
|
||||||
|
|
||||||
|
check:
|
||||||
|
@echo "🔍 Running ruff check..."
|
||||||
|
@uv run --project api --dev ruff check ./api
|
||||||
|
@echo "✅ Code check complete"
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@echo "🔧 Running ruff format and check with fixes..."
|
||||||
|
@uv run --directory api --dev sh -c 'ruff format ./api && ruff check --fix ./api'
|
||||||
|
@echo "✅ Linting complete"
|
||||||
|
|
||||||
|
type-check:
|
||||||
|
@echo "📝 Running type check with basedpyright..."
|
||||||
|
@uv run --directory api --dev basedpyright
|
||||||
|
@echo "✅ Type check complete"
|
||||||
|
|
||||||
# Build Docker images
|
# Build Docker images
|
||||||
build-web:
|
build-web:
|
||||||
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
@echo "Building web Docker image: $(WEB_IMAGE):$(VERSION)..."
|
||||||
@ -90,6 +114,12 @@ help:
|
|||||||
@echo " make prepare-api - Set up API environment"
|
@echo " make prepare-api - Set up API environment"
|
||||||
@echo " make dev-clean - Stop Docker middleware containers"
|
@echo " make dev-clean - Stop Docker middleware containers"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Backend Code Quality:"
|
||||||
|
@echo " make format - Format code with ruff"
|
||||||
|
@echo " make check - Check code with ruff"
|
||||||
|
@echo " make lint - Format and fix code with ruff"
|
||||||
|
@echo " make type-check - Run type checking with basedpyright"
|
||||||
|
@echo ""
|
||||||
@echo "Docker Build Targets:"
|
@echo "Docker Build Targets:"
|
||||||
@echo " make build-web - Build web Docker image"
|
@echo " make build-web - Build web Docker image"
|
||||||
@echo " make build-api - Build API Docker image"
|
@echo " make build-api - Build API Docker image"
|
||||||
@ -98,4 +128,4 @@ help:
|
|||||||
@echo " make build-push-all - Build and push all Docker images"
|
@echo " make build-push-all - Build and push all Docker images"
|
||||||
|
|
||||||
# Phony targets
|
# Phony targets
|
||||||
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help
|
.PHONY: build-web build-api push-web push-api build-all push-all build-push-all dev-setup prepare-docker prepare-web prepare-api dev-clean help format check lint type-check
|
||||||
|
|||||||
@ -530,6 +530,7 @@ ENDPOINT_URL_TEMPLATE=http://localhost:5002/e/{hook_id}
|
|||||||
|
|
||||||
# Reset password token expiry minutes
|
# Reset password token expiry minutes
|
||||||
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES=5
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES=5
|
||||||
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
OWNER_TRANSFER_TOKEN_EXPIRY_MINUTES=5
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,7 @@ select = [
|
|||||||
"G001", # don't use str format to logging messages
|
"G001", # don't use str format to logging messages
|
||||||
"G003", # don't use + in logging messages
|
"G003", # don't use + in logging messages
|
||||||
"G004", # don't use f-strings to format logging messages
|
"G004", # don't use f-strings to format logging messages
|
||||||
|
"UP042", # use StrEnum
|
||||||
]
|
]
|
||||||
|
|
||||||
ignore = [
|
ignore = [
|
||||||
|
|||||||
@ -212,7 +212,9 @@ def migrate_annotation_vector_database():
|
|||||||
if not dataset_collection_binding:
|
if not dataset_collection_binding:
|
||||||
click.echo(f"App annotation collection binding not found: {app.id}")
|
click.echo(f"App annotation collection binding not found: {app.id}")
|
||||||
continue
|
continue
|
||||||
annotations = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app.id).all()
|
annotations = db.session.scalars(
|
||||||
|
select(MessageAnnotation).where(MessageAnnotation.app_id == app.id)
|
||||||
|
).all()
|
||||||
dataset = Dataset(
|
dataset = Dataset(
|
||||||
id=app.id,
|
id=app.id,
|
||||||
tenant_id=app.tenant_id,
|
tenant_id=app.tenant_id,
|
||||||
@ -367,29 +369,25 @@ def migrate_knowledge_vector_database():
|
|||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
dataset_documents = (
|
dataset_documents = db.session.scalars(
|
||||||
db.session.query(DatasetDocument)
|
select(DatasetDocument).where(
|
||||||
.where(
|
|
||||||
DatasetDocument.dataset_id == dataset.id,
|
DatasetDocument.dataset_id == dataset.id,
|
||||||
DatasetDocument.indexing_status == "completed",
|
DatasetDocument.indexing_status == "completed",
|
||||||
DatasetDocument.enabled == True,
|
DatasetDocument.enabled == True,
|
||||||
DatasetDocument.archived == False,
|
DatasetDocument.archived == False,
|
||||||
)
|
)
|
||||||
.all()
|
).all()
|
||||||
)
|
|
||||||
|
|
||||||
documents = []
|
documents = []
|
||||||
segments_count = 0
|
segments_count = 0
|
||||||
for dataset_document in dataset_documents:
|
for dataset_document in dataset_documents:
|
||||||
segments = (
|
segments = db.session.scalars(
|
||||||
db.session.query(DocumentSegment)
|
select(DocumentSegment).where(
|
||||||
.where(
|
|
||||||
DocumentSegment.document_id == dataset_document.id,
|
DocumentSegment.document_id == dataset_document.id,
|
||||||
DocumentSegment.status == "completed",
|
DocumentSegment.status == "completed",
|
||||||
DocumentSegment.enabled == True,
|
DocumentSegment.enabled == True,
|
||||||
)
|
)
|
||||||
.all()
|
).all()
|
||||||
)
|
|
||||||
|
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
document = Document(
|
document = Document(
|
||||||
@ -479,12 +477,12 @@ def convert_to_agent_apps():
|
|||||||
click.echo(f"Converting app: {app.id}")
|
click.echo(f"Converting app: {app.id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
app.mode = AppMode.AGENT_CHAT.value
|
app.mode = AppMode.AGENT_CHAT
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# update conversation mode to agent
|
# update conversation mode to agent
|
||||||
db.session.query(Conversation).where(Conversation.app_id == app.id).update(
|
db.session.query(Conversation).where(Conversation.app_id == app.id).update(
|
||||||
{Conversation.mode: AppMode.AGENT_CHAT.value}
|
{Conversation.mode: AppMode.AGENT_CHAT}
|
||||||
)
|
)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -511,7 +509,7 @@ def add_qdrant_index(field: str):
|
|||||||
from qdrant_client.http.exceptions import UnexpectedResponse
|
from qdrant_client.http.exceptions import UnexpectedResponse
|
||||||
from qdrant_client.http.models import PayloadSchemaType
|
from qdrant_client.http.models import PayloadSchemaType
|
||||||
|
|
||||||
from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig
|
from core.rag.datasource.vdb.qdrant.qdrant_vector import PathQdrantParams, QdrantConfig
|
||||||
|
|
||||||
for binding in bindings:
|
for binding in bindings:
|
||||||
if dify_config.QDRANT_URL is None:
|
if dify_config.QDRANT_URL is None:
|
||||||
@ -525,7 +523,21 @@ def add_qdrant_index(field: str):
|
|||||||
prefer_grpc=dify_config.QDRANT_GRPC_ENABLED,
|
prefer_grpc=dify_config.QDRANT_GRPC_ENABLED,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
client = qdrant_client.QdrantClient(**qdrant_config.to_qdrant_params())
|
params = qdrant_config.to_qdrant_params()
|
||||||
|
# Check the type before using
|
||||||
|
if isinstance(params, PathQdrantParams):
|
||||||
|
# PathQdrantParams case
|
||||||
|
client = qdrant_client.QdrantClient(path=params.path)
|
||||||
|
else:
|
||||||
|
# UrlQdrantParams case - params is UrlQdrantParams
|
||||||
|
client = qdrant_client.QdrantClient(
|
||||||
|
url=params.url,
|
||||||
|
api_key=params.api_key,
|
||||||
|
timeout=int(params.timeout),
|
||||||
|
verify=params.verify,
|
||||||
|
grpc_port=params.grpc_port,
|
||||||
|
prefer_grpc=params.prefer_grpc,
|
||||||
|
)
|
||||||
# create payload index
|
# create payload index
|
||||||
client.create_payload_index(binding.collection_name, field, field_schema=PayloadSchemaType.KEYWORD)
|
client.create_payload_index(binding.collection_name, field, field_schema=PayloadSchemaType.KEYWORD)
|
||||||
create_count += 1
|
create_count += 1
|
||||||
|
|||||||
@ -31,6 +31,12 @@ class SecurityConfig(BaseSettings):
|
|||||||
description="Duration in minutes for which a password reset token remains valid",
|
description="Duration in minutes for which a password reset token remains valid",
|
||||||
default=5,
|
default=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EMAIL_REGISTER_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||||
|
description="Duration in minutes for which a email register token remains valid",
|
||||||
|
default=5,
|
||||||
|
)
|
||||||
|
|
||||||
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
CHANGE_EMAIL_TOKEN_EXPIRY_MINUTES: PositiveInt = Field(
|
||||||
description="Duration in minutes for which a change email token remains valid",
|
description="Duration in minutes for which a change email token remains valid",
|
||||||
default=5,
|
default=5,
|
||||||
@ -639,6 +645,11 @@ class AuthConfig(BaseSettings):
|
|||||||
default=86400,
|
default=86400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
EMAIL_REGISTER_LOCKOUT_DURATION: PositiveInt = Field(
|
||||||
|
description="Time (in seconds) a user must wait before retrying email register after exceeding the rate limit.",
|
||||||
|
default=86400,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModerationConfig(BaseSettings):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import enum
|
from enum import Enum
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from pydantic import Field, PositiveInt
|
from pydantic import Field, PositiveInt
|
||||||
@ -10,7 +10,7 @@ class OpenSearchConfig(BaseSettings):
|
|||||||
Configuration settings for OpenSearch
|
Configuration settings for OpenSearch
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class AuthMethod(enum.StrEnum):
|
class AuthMethod(Enum):
|
||||||
"""
|
"""
|
||||||
Authentication method for OpenSearch
|
Authentication method for OpenSearch
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -16,14 +16,14 @@ AUDIO_EXTENSIONS = ["mp3", "m4a", "wav", "amr", "mpga"]
|
|||||||
AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
|
AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
|
||||||
|
|
||||||
|
|
||||||
|
_doc_extensions: list[str]
|
||||||
if dify_config.ETL_TYPE == "Unstructured":
|
if dify_config.ETL_TYPE == "Unstructured":
|
||||||
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"]
|
_doc_extensions = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"]
|
||||||
DOCUMENT_EXTENSIONS.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
_doc_extensions.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
|
||||||
if dify_config.UNSTRUCTURED_API_URL:
|
if dify_config.UNSTRUCTURED_API_URL:
|
||||||
DOCUMENT_EXTENSIONS.append("ppt")
|
_doc_extensions.append("ppt")
|
||||||
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
|
||||||
else:
|
else:
|
||||||
DOCUMENT_EXTENSIONS = [
|
_doc_extensions = [
|
||||||
"txt",
|
"txt",
|
||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
@ -38,4 +38,4 @@ else:
|
|||||||
"vtt",
|
"vtt",
|
||||||
"properties",
|
"properties",
|
||||||
]
|
]
|
||||||
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
|
DOCUMENT_EXTENSIONS = _doc_extensions + [ext.upper() for ext in _doc_extensions]
|
||||||
|
|||||||
@ -7,7 +7,7 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
|||||||
# workflow default mode
|
# workflow default mode
|
||||||
AppMode.WORKFLOW: {
|
AppMode.WORKFLOW: {
|
||||||
"app": {
|
"app": {
|
||||||
"mode": AppMode.WORKFLOW.value,
|
"mode": AppMode.WORKFLOW,
|
||||||
"enable_site": True,
|
"enable_site": True,
|
||||||
"enable_api": True,
|
"enable_api": True,
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
|||||||
# completion default mode
|
# completion default mode
|
||||||
AppMode.COMPLETION: {
|
AppMode.COMPLETION: {
|
||||||
"app": {
|
"app": {
|
||||||
"mode": AppMode.COMPLETION.value,
|
"mode": AppMode.COMPLETION,
|
||||||
"enable_site": True,
|
"enable_site": True,
|
||||||
"enable_api": True,
|
"enable_api": True,
|
||||||
},
|
},
|
||||||
@ -44,7 +44,7 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
|||||||
# chat default mode
|
# chat default mode
|
||||||
AppMode.CHAT: {
|
AppMode.CHAT: {
|
||||||
"app": {
|
"app": {
|
||||||
"mode": AppMode.CHAT.value,
|
"mode": AppMode.CHAT,
|
||||||
"enable_site": True,
|
"enable_site": True,
|
||||||
"enable_api": True,
|
"enable_api": True,
|
||||||
},
|
},
|
||||||
@ -60,7 +60,7 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
|||||||
# advanced-chat default mode
|
# advanced-chat default mode
|
||||||
AppMode.ADVANCED_CHAT: {
|
AppMode.ADVANCED_CHAT: {
|
||||||
"app": {
|
"app": {
|
||||||
"mode": AppMode.ADVANCED_CHAT.value,
|
"mode": AppMode.ADVANCED_CHAT,
|
||||||
"enable_site": True,
|
"enable_site": True,
|
||||||
"enable_api": True,
|
"enable_api": True,
|
||||||
},
|
},
|
||||||
@ -68,7 +68,7 @@ default_app_templates: Mapping[AppMode, Mapping] = {
|
|||||||
# agent-chat default mode
|
# agent-chat default mode
|
||||||
AppMode.AGENT_CHAT: {
|
AppMode.AGENT_CHAT: {
|
||||||
"app": {
|
"app": {
|
||||||
"mode": AppMode.AGENT_CHAT.value,
|
"mode": AppMode.AGENT_CHAT,
|
||||||
"enable_site": True,
|
"enable_site": True,
|
||||||
"enable_api": True,
|
"enable_api": True,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,6 @@ if TYPE_CHECKING:
|
|||||||
from core.model_runtime.entities.model_entities import AIModelEntity
|
from core.model_runtime.entities.model_entities import AIModelEntity
|
||||||
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
||||||
from core.tools.plugin_tool.provider import PluginToolProviderController
|
from core.tools.plugin_tool.provider import PluginToolProviderController
|
||||||
from core.workflow.entities.variable_pool import VariablePool
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
|
from flask_restx import Namespace
|
||||||
|
|
||||||
from libs.external_api import ExternalApi
|
from libs.external_api import ExternalApi
|
||||||
|
|
||||||
@ -26,7 +27,16 @@ from .files import FileApi, FilePreviewApi, FileSupportTypeApi
|
|||||||
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
from .remote_files import RemoteFileInfoApi, RemoteFileUploadApi
|
||||||
|
|
||||||
bp = Blueprint("console", __name__, url_prefix="/console/api")
|
bp = Blueprint("console", __name__, url_prefix="/console/api")
|
||||||
api = ExternalApi(bp)
|
|
||||||
|
api = ExternalApi(
|
||||||
|
bp,
|
||||||
|
version="1.0",
|
||||||
|
title="Console API",
|
||||||
|
description="Console management APIs for app configuration, monitoring, and administration",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create namespace
|
||||||
|
console_ns = Namespace("console", description="Console management API operations", path="/")
|
||||||
|
|
||||||
# File
|
# File
|
||||||
api.add_resource(FileApi, "/files/upload")
|
api.add_resource(FileApi, "/files/upload")
|
||||||
@ -43,7 +53,16 @@ api.add_resource(AppImportConfirmApi, "/apps/imports/<string:import_id>/confirm"
|
|||||||
api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies")
|
api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/check-dependencies")
|
||||||
|
|
||||||
# Import other controllers
|
# Import other controllers
|
||||||
from . import admin, apikey, extension, feature, ping, setup, version
|
from . import (
|
||||||
|
admin,
|
||||||
|
apikey,
|
||||||
|
extension,
|
||||||
|
feature,
|
||||||
|
init_validate,
|
||||||
|
ping,
|
||||||
|
setup,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
|
||||||
# Import app controllers
|
# Import app controllers
|
||||||
from .app import (
|
from .app import (
|
||||||
@ -70,7 +89,16 @@ from .app import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Import auth controllers
|
# Import auth controllers
|
||||||
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth, oauth_server
|
from .auth import (
|
||||||
|
activate,
|
||||||
|
data_source_bearer_auth,
|
||||||
|
data_source_oauth,
|
||||||
|
email_register,
|
||||||
|
forgot_password,
|
||||||
|
login,
|
||||||
|
oauth,
|
||||||
|
oauth_server,
|
||||||
|
)
|
||||||
|
|
||||||
# Import billing controllers
|
# Import billing controllers
|
||||||
from .billing import billing, compliance
|
from .billing import billing, compliance
|
||||||
@ -95,6 +123,23 @@ from .explore import (
|
|||||||
saved_message,
|
saved_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Import tag controllers
|
||||||
|
from .tag import tags
|
||||||
|
|
||||||
|
# Import workspace controllers
|
||||||
|
from .workspace import (
|
||||||
|
account,
|
||||||
|
agent_providers,
|
||||||
|
endpoint,
|
||||||
|
load_balancing_config,
|
||||||
|
members,
|
||||||
|
model_providers,
|
||||||
|
models,
|
||||||
|
plugin,
|
||||||
|
tool_providers,
|
||||||
|
workspace,
|
||||||
|
)
|
||||||
|
|
||||||
# Explore Audio
|
# Explore Audio
|
||||||
api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio")
|
api.add_resource(ChatAudioApi, "/installed-apps/<uuid:installed_app_id>/audio-to-text", endpoint="installed_app_audio")
|
||||||
api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text")
|
api.add_resource(ChatTextApi, "/installed-apps/<uuid:installed_app_id>/text-to-audio", endpoint="installed_app_text")
|
||||||
@ -166,19 +211,71 @@ api.add_resource(
|
|||||||
InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop"
|
InstalledAppWorkflowTaskStopApi, "/installed-apps/<uuid:installed_app_id>/workflows/tasks/<string:task_id>/stop"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import tag controllers
|
api.add_namespace(console_ns)
|
||||||
from .tag import tags
|
|
||||||
|
|
||||||
# Import workspace controllers
|
__all__ = [
|
||||||
from .workspace import (
|
"account",
|
||||||
account,
|
"activate",
|
||||||
agent_providers,
|
"admin",
|
||||||
endpoint,
|
"advanced_prompt_template",
|
||||||
load_balancing_config,
|
"agent",
|
||||||
members,
|
"agent_providers",
|
||||||
model_providers,
|
"annotation",
|
||||||
models,
|
"api",
|
||||||
plugin,
|
"apikey",
|
||||||
tool_providers,
|
"app",
|
||||||
workspace,
|
"audio",
|
||||||
)
|
"billing",
|
||||||
|
"bp",
|
||||||
|
"completion",
|
||||||
|
"compliance",
|
||||||
|
"console_ns",
|
||||||
|
"conversation",
|
||||||
|
"conversation_variables",
|
||||||
|
"data_source",
|
||||||
|
"data_source_bearer_auth",
|
||||||
|
"data_source_oauth",
|
||||||
|
"datasets",
|
||||||
|
"datasets_document",
|
||||||
|
"datasets_segments",
|
||||||
|
"email_register",
|
||||||
|
"endpoint",
|
||||||
|
"extension",
|
||||||
|
"external",
|
||||||
|
"feature",
|
||||||
|
"forgot_password",
|
||||||
|
"generator",
|
||||||
|
"hit_testing",
|
||||||
|
"init_validate",
|
||||||
|
"installed_app",
|
||||||
|
"load_balancing_config",
|
||||||
|
"login",
|
||||||
|
"mcp_server",
|
||||||
|
"members",
|
||||||
|
"message",
|
||||||
|
"metadata",
|
||||||
|
"model_config",
|
||||||
|
"model_providers",
|
||||||
|
"models",
|
||||||
|
"oauth",
|
||||||
|
"oauth_server",
|
||||||
|
"ops_trace",
|
||||||
|
"parameter",
|
||||||
|
"ping",
|
||||||
|
"plugin",
|
||||||
|
"recommended_app",
|
||||||
|
"saved_message",
|
||||||
|
"setup",
|
||||||
|
"site",
|
||||||
|
"statistic",
|
||||||
|
"tags",
|
||||||
|
"tool_providers",
|
||||||
|
"version",
|
||||||
|
"website",
|
||||||
|
"workflow",
|
||||||
|
"workflow_app_log",
|
||||||
|
"workflow_draft_variable",
|
||||||
|
"workflow_run",
|
||||||
|
"workflow_statistic",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from functools import wraps
|
|||||||
from typing import ParamSpec, TypeVar
|
from typing import ParamSpec, TypeVar
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
@ -12,7 +12,7 @@ P = ParamSpec("P")
|
|||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import only_edition_cloud
|
from controllers.console.wraps import only_edition_cloud
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.model import App, InstalledApp, RecommendedApp
|
from models.model import App, InstalledApp, RecommendedApp
|
||||||
@ -45,7 +45,28 @@ def admin_required(view: Callable[P, R]):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/admin/insert-explore-apps")
|
||||||
class InsertExploreAppListApi(Resource):
|
class InsertExploreAppListApi(Resource):
|
||||||
|
@api.doc("insert_explore_app")
|
||||||
|
@api.doc(description="Insert or update an app in the explore list")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"InsertExploreAppRequest",
|
||||||
|
{
|
||||||
|
"app_id": fields.String(required=True, description="Application ID"),
|
||||||
|
"desc": fields.String(description="App description"),
|
||||||
|
"copyright": fields.String(description="Copyright information"),
|
||||||
|
"privacy_policy": fields.String(description="Privacy policy"),
|
||||||
|
"custom_disclaimer": fields.String(description="Custom disclaimer"),
|
||||||
|
"language": fields.String(required=True, description="Language code"),
|
||||||
|
"category": fields.String(required=True, description="App category"),
|
||||||
|
"position": fields.Integer(required=True, description="Display position"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "App updated successfully")
|
||||||
|
@api.response(201, "App inserted successfully")
|
||||||
|
@api.response(404, "App not found")
|
||||||
@only_edition_cloud
|
@only_edition_cloud
|
||||||
@admin_required
|
@admin_required
|
||||||
def post(self):
|
def post(self):
|
||||||
@ -115,7 +136,12 @@ class InsertExploreAppListApi(Resource):
|
|||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/admin/insert-explore-apps/<uuid:app_id>")
|
||||||
class InsertExploreAppApi(Resource):
|
class InsertExploreAppApi(Resource):
|
||||||
|
@api.doc("delete_explore_app")
|
||||||
|
@api.doc(description="Remove an app from the explore list")
|
||||||
|
@api.doc(params={"app_id": "Application ID to remove"})
|
||||||
|
@api.response(204, "App removed successfully")
|
||||||
@only_edition_cloud
|
@only_edition_cloud
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete(self, app_id):
|
def delete(self, app_id):
|
||||||
@ -152,7 +178,3 @@ class InsertExploreAppApi(Resource):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(InsertExploreAppListApi, "/admin/insert-explore-apps")
|
|
||||||
api.add_resource(InsertExploreAppApi, "/admin/insert-explore-apps/<uuid:app_id>")
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
import flask_restx
|
import flask_restx
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, fields, marshal_with
|
from flask_restx import Resource, fields, marshal_with
|
||||||
|
from flask_restx._http import HTTPStatus
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
@ -13,7 +14,7 @@ from libs.login import login_required
|
|||||||
from models.dataset import Dataset
|
from models.dataset import Dataset
|
||||||
from models.model import ApiToken, App
|
from models.model import ApiToken, App
|
||||||
|
|
||||||
from . import api
|
from . import api, console_ns
|
||||||
from .wraps import account_initialization_required, setup_required
|
from .wraps import account_initialization_required, setup_required
|
||||||
|
|
||||||
api_key_fields = {
|
api_key_fields = {
|
||||||
@ -40,7 +41,7 @@ def _get_resource(resource_id, tenant_id, resource_model):
|
|||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
|
|
||||||
if resource is None:
|
if resource is None:
|
||||||
flask_restx.abort(404, message=f"{resource_model.__name__} not found.")
|
flask_restx.abort(HTTPStatus.NOT_FOUND, message=f"{resource_model.__name__} not found.")
|
||||||
|
|
||||||
return resource
|
return resource
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ class BaseApiKeyListResource(Resource):
|
|||||||
method_decorators = [account_initialization_required, login_required, setup_required]
|
method_decorators = [account_initialization_required, login_required, setup_required]
|
||||||
|
|
||||||
resource_type: str | None = None
|
resource_type: str | None = None
|
||||||
resource_model: Optional[Any] = None
|
resource_model: Optional[type] = None
|
||||||
resource_id_field: str | None = None
|
resource_id_field: str | None = None
|
||||||
token_prefix: str | None = None
|
token_prefix: str | None = None
|
||||||
max_keys = 10
|
max_keys = 10
|
||||||
@ -59,11 +60,11 @@ class BaseApiKeyListResource(Resource):
|
|||||||
assert self.resource_id_field is not None, "resource_id_field must be set"
|
assert self.resource_id_field is not None, "resource_id_field must be set"
|
||||||
resource_id = str(resource_id)
|
resource_id = str(resource_id)
|
||||||
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
_get_resource(resource_id, current_user.current_tenant_id, self.resource_model)
|
||||||
keys = (
|
keys = db.session.scalars(
|
||||||
db.session.query(ApiToken)
|
select(ApiToken).where(
|
||||||
.where(ApiToken.type == self.resource_type, getattr(ApiToken, self.resource_id_field) == resource_id)
|
ApiToken.type == self.resource_type, getattr(ApiToken, self.resource_id_field) == resource_id
|
||||||
.all()
|
)
|
||||||
)
|
).all()
|
||||||
return {"items": keys}
|
return {"items": keys}
|
||||||
|
|
||||||
@marshal_with(api_key_fields)
|
@marshal_with(api_key_fields)
|
||||||
@ -82,7 +83,7 @@ class BaseApiKeyListResource(Resource):
|
|||||||
|
|
||||||
if current_key_count >= self.max_keys:
|
if current_key_count >= self.max_keys:
|
||||||
flask_restx.abort(
|
flask_restx.abort(
|
||||||
400,
|
HTTPStatus.BAD_REQUEST,
|
||||||
message=f"Cannot create more than {self.max_keys} API keys for this resource type.",
|
message=f"Cannot create more than {self.max_keys} API keys for this resource type.",
|
||||||
custom="max_keys_exceeded",
|
custom="max_keys_exceeded",
|
||||||
)
|
)
|
||||||
@ -102,7 +103,7 @@ class BaseApiKeyResource(Resource):
|
|||||||
method_decorators = [account_initialization_required, login_required, setup_required]
|
method_decorators = [account_initialization_required, login_required, setup_required]
|
||||||
|
|
||||||
resource_type: str | None = None
|
resource_type: str | None = None
|
||||||
resource_model: Optional[Any] = None
|
resource_model: Optional[type] = None
|
||||||
resource_id_field: str | None = None
|
resource_id_field: str | None = None
|
||||||
|
|
||||||
def delete(self, resource_id, api_key_id):
|
def delete(self, resource_id, api_key_id):
|
||||||
@ -126,7 +127,7 @@ class BaseApiKeyResource(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if key is None:
|
if key is None:
|
||||||
flask_restx.abort(404, message="API key not found")
|
flask_restx.abort(HTTPStatus.NOT_FOUND, message="API key not found")
|
||||||
|
|
||||||
db.session.query(ApiToken).where(ApiToken.id == api_key_id).delete()
|
db.session.query(ApiToken).where(ApiToken.id == api_key_id).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -134,7 +135,25 @@ class BaseApiKeyResource(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:resource_id>/api-keys")
|
||||||
class AppApiKeyListResource(BaseApiKeyListResource):
|
class AppApiKeyListResource(BaseApiKeyListResource):
|
||||||
|
@api.doc("get_app_api_keys")
|
||||||
|
@api.doc(description="Get all API keys for an app")
|
||||||
|
@api.doc(params={"resource_id": "App ID"})
|
||||||
|
@api.response(200, "Success", api_key_list)
|
||||||
|
def get(self, resource_id):
|
||||||
|
"""Get all API keys for an app"""
|
||||||
|
return super().get(resource_id)
|
||||||
|
|
||||||
|
@api.doc("create_app_api_key")
|
||||||
|
@api.doc(description="Create a new API key for an app")
|
||||||
|
@api.doc(params={"resource_id": "App ID"})
|
||||||
|
@api.response(201, "API key created successfully", api_key_fields)
|
||||||
|
@api.response(400, "Maximum keys exceeded")
|
||||||
|
def post(self, resource_id):
|
||||||
|
"""Create a new API key for an app"""
|
||||||
|
return super().post(resource_id)
|
||||||
|
|
||||||
def after_request(self, resp):
|
def after_request(self, resp):
|
||||||
resp.headers["Access-Control-Allow-Origin"] = "*"
|
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
@ -146,7 +165,16 @@ class AppApiKeyListResource(BaseApiKeyListResource):
|
|||||||
token_prefix = "app-"
|
token_prefix = "app-"
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:resource_id>/api-keys/<uuid:api_key_id>")
|
||||||
class AppApiKeyResource(BaseApiKeyResource):
|
class AppApiKeyResource(BaseApiKeyResource):
|
||||||
|
@api.doc("delete_app_api_key")
|
||||||
|
@api.doc(description="Delete an API key for an app")
|
||||||
|
@api.doc(params={"resource_id": "App ID", "api_key_id": "API key ID"})
|
||||||
|
@api.response(204, "API key deleted successfully")
|
||||||
|
def delete(self, resource_id, api_key_id):
|
||||||
|
"""Delete an API key for an app"""
|
||||||
|
return super().delete(resource_id, api_key_id)
|
||||||
|
|
||||||
def after_request(self, resp):
|
def after_request(self, resp):
|
||||||
resp.headers["Access-Control-Allow-Origin"] = "*"
|
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
@ -157,7 +185,25 @@ class AppApiKeyResource(BaseApiKeyResource):
|
|||||||
resource_id_field = "app_id"
|
resource_id_field = "app_id"
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:resource_id>/api-keys")
|
||||||
class DatasetApiKeyListResource(BaseApiKeyListResource):
|
class DatasetApiKeyListResource(BaseApiKeyListResource):
|
||||||
|
@api.doc("get_dataset_api_keys")
|
||||||
|
@api.doc(description="Get all API keys for a dataset")
|
||||||
|
@api.doc(params={"resource_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Success", api_key_list)
|
||||||
|
def get(self, resource_id):
|
||||||
|
"""Get all API keys for a dataset"""
|
||||||
|
return super().get(resource_id)
|
||||||
|
|
||||||
|
@api.doc("create_dataset_api_key")
|
||||||
|
@api.doc(description="Create a new API key for a dataset")
|
||||||
|
@api.doc(params={"resource_id": "Dataset ID"})
|
||||||
|
@api.response(201, "API key created successfully", api_key_fields)
|
||||||
|
@api.response(400, "Maximum keys exceeded")
|
||||||
|
def post(self, resource_id):
|
||||||
|
"""Create a new API key for a dataset"""
|
||||||
|
return super().post(resource_id)
|
||||||
|
|
||||||
def after_request(self, resp):
|
def after_request(self, resp):
|
||||||
resp.headers["Access-Control-Allow-Origin"] = "*"
|
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
@ -169,7 +215,16 @@ class DatasetApiKeyListResource(BaseApiKeyListResource):
|
|||||||
token_prefix = "ds-"
|
token_prefix = "ds-"
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:resource_id>/api-keys/<uuid:api_key_id>")
|
||||||
class DatasetApiKeyResource(BaseApiKeyResource):
|
class DatasetApiKeyResource(BaseApiKeyResource):
|
||||||
|
@api.doc("delete_dataset_api_key")
|
||||||
|
@api.doc(description="Delete an API key for a dataset")
|
||||||
|
@api.doc(params={"resource_id": "Dataset ID", "api_key_id": "API key ID"})
|
||||||
|
@api.response(204, "API key deleted successfully")
|
||||||
|
def delete(self, resource_id, api_key_id):
|
||||||
|
"""Delete an API key for a dataset"""
|
||||||
|
return super().delete(resource_id, api_key_id)
|
||||||
|
|
||||||
def after_request(self, resp):
|
def after_request(self, resp):
|
||||||
resp.headers["Access-Control-Allow-Origin"] = "*"
|
resp.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
resp.headers["Access-Control-Allow-Credentials"] = "true"
|
||||||
@ -178,9 +233,3 @@ class DatasetApiKeyResource(BaseApiKeyResource):
|
|||||||
resource_type = "dataset"
|
resource_type = "dataset"
|
||||||
resource_model = Dataset
|
resource_model = Dataset
|
||||||
resource_id_field = "dataset_id"
|
resource_id_field = "dataset_id"
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppApiKeyListResource, "/apps/<uuid:resource_id>/api-keys")
|
|
||||||
api.add_resource(AppApiKeyResource, "/apps/<uuid:resource_id>/api-keys/<uuid:api_key_id>")
|
|
||||||
api.add_resource(DatasetApiKeyListResource, "/datasets/<uuid:resource_id>/api-keys")
|
|
||||||
api.add_resource(DatasetApiKeyResource, "/datasets/<uuid:resource_id>/api-keys/<uuid:api_key_id>")
|
|
||||||
|
|||||||
@ -1,12 +1,26 @@
|
|||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.advanced_prompt_template_service import AdvancedPromptTemplateService
|
from services.advanced_prompt_template_service import AdvancedPromptTemplateService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/app/prompt-templates")
|
||||||
class AdvancedPromptTemplateList(Resource):
|
class AdvancedPromptTemplateList(Resource):
|
||||||
|
@api.doc("get_advanced_prompt_templates")
|
||||||
|
@api.doc(description="Get advanced prompt templates based on app mode and model configuration")
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("app_mode", type=str, required=True, location="args", help="Application mode")
|
||||||
|
.add_argument("model_mode", type=str, required=True, location="args", help="Model mode")
|
||||||
|
.add_argument("has_context", type=str, default="true", location="args", help="Whether has context")
|
||||||
|
.add_argument("model_name", type=str, required=True, location="args", help="Model name")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200, "Prompt templates retrieved successfully", fields.List(fields.Raw(description="Prompt template data"))
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -19,6 +33,3 @@ class AdvancedPromptTemplateList(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
return AdvancedPromptTemplateService.get_prompt(args)
|
return AdvancedPromptTemplateService.get_prompt(args)
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AdvancedPromptTemplateList, "/app/prompt-templates")
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
@ -9,7 +9,18 @@ from models.model import AppMode
|
|||||||
from services.agent_service import AgentService
|
from services.agent_service import AgentService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/agent/logs")
|
||||||
class AgentLogApi(Resource):
|
class AgentLogApi(Resource):
|
||||||
|
@api.doc("get_agent_logs")
|
||||||
|
@api.doc(description="Get agent execution logs for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("message_id", type=str, required=True, location="args", help="Message UUID")
|
||||||
|
.add_argument("conversation_id", type=str, required=True, location="args", help="Conversation UUID")
|
||||||
|
)
|
||||||
|
@api.response(200, "Agent logs retrieved successfully", fields.List(fields.Raw(description="Agent log entries")))
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -23,6 +34,3 @@ class AgentLogApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
return AgentService.get_agent_logs(app_model, args["conversation_id"], args["message_id"])
|
return AgentService.get_agent_logs(app_model, args["conversation_id"], args["message_id"])
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AgentLogApi, "/apps/<uuid:app_id>/agent/logs")
|
|
||||||
|
|||||||
@ -2,11 +2,11 @@ from typing import Literal
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, marshal, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.common.errors import NoFileUploadedError, TooManyFilesError
|
from controllers.common.errors import NoFileUploadedError, TooManyFilesError
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_resource_check,
|
cloud_edition_billing_resource_check,
|
||||||
@ -21,7 +21,23 @@ from libs.login import login_required
|
|||||||
from services.annotation_service import AppAnnotationService
|
from services.annotation_service import AppAnnotationService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotation-reply/<string:action>")
|
||||||
class AnnotationReplyActionApi(Resource):
|
class AnnotationReplyActionApi(Resource):
|
||||||
|
@api.doc("annotation_reply_action")
|
||||||
|
@api.doc(description="Enable or disable annotation reply for an app")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "action": "Action to perform (enable/disable)"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AnnotationReplyActionRequest",
|
||||||
|
{
|
||||||
|
"score_threshold": fields.Float(required=True, description="Score threshold for annotation matching"),
|
||||||
|
"embedding_provider_name": fields.String(required=True, description="Embedding provider name"),
|
||||||
|
"embedding_model_name": fields.String(required=True, description="Embedding model name"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Action completed successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -43,7 +59,13 @@ class AnnotationReplyActionApi(Resource):
|
|||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotation-setting")
|
||||||
class AppAnnotationSettingDetailApi(Resource):
|
class AppAnnotationSettingDetailApi(Resource):
|
||||||
|
@api.doc("get_annotation_setting")
|
||||||
|
@api.doc(description="Get annotation settings for an app")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Annotation settings retrieved successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -56,7 +78,23 @@ class AppAnnotationSettingDetailApi(Resource):
|
|||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotation-settings/<uuid:annotation_setting_id>")
|
||||||
class AppAnnotationSettingUpdateApi(Resource):
|
class AppAnnotationSettingUpdateApi(Resource):
|
||||||
|
@api.doc("update_annotation_setting")
|
||||||
|
@api.doc(description="Update annotation settings for an app")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "annotation_setting_id": "Annotation setting ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AnnotationSettingUpdateRequest",
|
||||||
|
{
|
||||||
|
"score_threshold": fields.Float(required=True, description="Score threshold"),
|
||||||
|
"embedding_provider_name": fields.String(required=True, description="Embedding provider"),
|
||||||
|
"embedding_model_name": fields.String(required=True, description="Embedding model"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Settings updated successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -75,7 +113,13 @@ class AppAnnotationSettingUpdateApi(Resource):
|
|||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>")
|
||||||
class AnnotationReplyActionStatusApi(Resource):
|
class AnnotationReplyActionStatusApi(Resource):
|
||||||
|
@api.doc("get_annotation_reply_action_status")
|
||||||
|
@api.doc(description="Get status of annotation reply action job")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "job_id": "Job ID", "action": "Action type"})
|
||||||
|
@api.response(200, "Job status retrieved successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -99,7 +143,19 @@ class AnnotationReplyActionStatusApi(Resource):
|
|||||||
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
|
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations")
|
||||||
class AnnotationApi(Resource):
|
class AnnotationApi(Resource):
|
||||||
|
@api.doc("list_annotations")
|
||||||
|
@api.doc(description="Get annotations for an app with pagination")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("page", type=int, location="args", default=1, help="Page number")
|
||||||
|
.add_argument("limit", type=int, location="args", default=20, help="Page size")
|
||||||
|
.add_argument("keyword", type=str, location="args", default="", help="Search keyword")
|
||||||
|
)
|
||||||
|
@api.response(200, "Annotations retrieved successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -122,6 +178,21 @@ class AnnotationApi(Resource):
|
|||||||
}
|
}
|
||||||
return response, 200
|
return response, 200
|
||||||
|
|
||||||
|
@api.doc("create_annotation")
|
||||||
|
@api.doc(description="Create a new annotation for an app")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CreateAnnotationRequest",
|
||||||
|
{
|
||||||
|
"question": fields.String(required=True, description="Question text"),
|
||||||
|
"answer": fields.String(required=True, description="Answer text"),
|
||||||
|
"annotation_reply": fields.Raw(description="Annotation reply data"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "Annotation created successfully", annotation_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -168,7 +239,13 @@ class AnnotationApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/export")
|
||||||
class AnnotationExportApi(Resource):
|
class AnnotationExportApi(Resource):
|
||||||
|
@api.doc("export_annotations")
|
||||||
|
@api.doc(description="Export all annotations for an app")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Annotations exported successfully", fields.List(fields.Nested(annotation_fields)))
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -182,7 +259,14 @@ class AnnotationExportApi(Resource):
|
|||||||
return response, 200
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
|
||||||
class AnnotationUpdateDeleteApi(Resource):
|
class AnnotationUpdateDeleteApi(Resource):
|
||||||
|
@api.doc("update_delete_annotation")
|
||||||
|
@api.doc(description="Update or delete an annotation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "annotation_id": "Annotation ID"})
|
||||||
|
@api.response(200, "Annotation updated successfully", annotation_fields)
|
||||||
|
@api.response(204, "Annotation deleted successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -214,7 +298,14 @@ class AnnotationUpdateDeleteApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/batch-import")
|
||||||
class AnnotationBatchImportApi(Resource):
|
class AnnotationBatchImportApi(Resource):
|
||||||
|
@api.doc("batch_import_annotations")
|
||||||
|
@api.doc(description="Batch import annotations from CSV file")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Batch import started successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(400, "No file uploaded or too many files")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -239,7 +330,13 @@ class AnnotationBatchImportApi(Resource):
|
|||||||
return AppAnnotationService.batch_import_app_annotations(app_id, file)
|
return AppAnnotationService.batch_import_app_annotations(app_id, file)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
|
||||||
class AnnotationBatchImportStatusApi(Resource):
|
class AnnotationBatchImportStatusApi(Resource):
|
||||||
|
@api.doc("get_batch_import_status")
|
||||||
|
@api.doc(description="Get status of batch import job")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "job_id": "Job ID"})
|
||||||
|
@api.response(200, "Job status retrieved successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -262,7 +359,20 @@ class AnnotationBatchImportStatusApi(Resource):
|
|||||||
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
|
return {"job_id": job_id, "job_status": job_status, "error_msg": error_msg}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/<uuid:annotation_id>/hit-histories")
|
||||||
class AnnotationHitHistoryListApi(Resource):
|
class AnnotationHitHistoryListApi(Resource):
|
||||||
|
@api.doc("list_annotation_hit_histories")
|
||||||
|
@api.doc(description="Get hit histories for an annotation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "annotation_id": "Annotation ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("page", type=int, location="args", default=1, help="Page number")
|
||||||
|
.add_argument("limit", type=int, location="args", default=20, help="Page size")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200, "Hit histories retrieved successfully", fields.List(fields.Nested(annotation_hit_history_fields))
|
||||||
|
)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -285,17 +395,3 @@ class AnnotationHitHistoryListApi(Resource):
|
|||||||
"page": page,
|
"page": page,
|
||||||
}
|
}
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AnnotationReplyActionApi, "/apps/<uuid:app_id>/annotation-reply/<string:action>")
|
|
||||||
api.add_resource(
|
|
||||||
AnnotationReplyActionStatusApi, "/apps/<uuid:app_id>/annotation-reply/<string:action>/status/<uuid:job_id>"
|
|
||||||
)
|
|
||||||
api.add_resource(AnnotationApi, "/apps/<uuid:app_id>/annotations")
|
|
||||||
api.add_resource(AnnotationExportApi, "/apps/<uuid:app_id>/annotations/export")
|
|
||||||
api.add_resource(AnnotationUpdateDeleteApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
|
|
||||||
api.add_resource(AnnotationBatchImportApi, "/apps/<uuid:app_id>/annotations/batch-import")
|
|
||||||
api.add_resource(AnnotationBatchImportStatusApi, "/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
|
|
||||||
api.add_resource(AnnotationHitHistoryListApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>/hit-histories")
|
|
||||||
api.add_resource(AppAnnotationSettingDetailApi, "/apps/<uuid:app_id>/annotation-setting")
|
|
||||||
api.add_resource(AppAnnotationSettingUpdateApi, "/apps/<uuid:app_id>/annotation-settings/<uuid:annotation_setting_id>")
|
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import uuid
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, inputs, marshal, marshal_with, reqparse
|
from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
@ -34,7 +34,27 @@ def _validate_description_length(description):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps")
|
||||||
class AppListApi(Resource):
|
class AppListApi(Resource):
|
||||||
|
@api.doc("list_apps")
|
||||||
|
@api.doc(description="Get list of applications with pagination and filtering")
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("page", type=int, location="args", help="Page number (1-99999)", default=1)
|
||||||
|
.add_argument("limit", type=int, location="args", help="Page size (1-100)", default=20)
|
||||||
|
.add_argument(
|
||||||
|
"mode",
|
||||||
|
type=str,
|
||||||
|
location="args",
|
||||||
|
choices=["completion", "chat", "advanced-chat", "workflow", "agent-chat", "channel", "all"],
|
||||||
|
default="all",
|
||||||
|
help="App mode filter",
|
||||||
|
)
|
||||||
|
.add_argument("name", type=str, location="args", help="Filter by app name")
|
||||||
|
.add_argument("tag_ids", type=str, location="args", help="Comma-separated tag IDs")
|
||||||
|
.add_argument("is_created_by_me", type=bool, location="args", help="Filter by creator")
|
||||||
|
)
|
||||||
|
@api.response(200, "Success", app_pagination_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -91,6 +111,24 @@ class AppListApi(Resource):
|
|||||||
|
|
||||||
return marshal(app_pagination, app_pagination_fields), 200
|
return marshal(app_pagination, app_pagination_fields), 200
|
||||||
|
|
||||||
|
@api.doc("create_app")
|
||||||
|
@api.doc(description="Create a new application")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CreateAppRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(required=True, description="App name"),
|
||||||
|
"description": fields.String(description="App description (max 400 chars)"),
|
||||||
|
"mode": fields.String(required=True, enum=ALLOW_CREATE_APP_MODES, description="App mode"),
|
||||||
|
"icon_type": fields.String(description="Icon type"),
|
||||||
|
"icon": fields.String(description="Icon"),
|
||||||
|
"icon_background": fields.String(description="Icon background color"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "App created successfully", app_detail_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -115,12 +153,21 @@ class AppListApi(Resource):
|
|||||||
raise BadRequest("mode is required")
|
raise BadRequest("mode is required")
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
|
if current_user.current_tenant_id is None:
|
||||||
|
raise ValueError("current_user.current_tenant_id cannot be None")
|
||||||
app = app_service.create_app(current_user.current_tenant_id, args, current_user)
|
app = app_service.create_app(current_user.current_tenant_id, args, current_user)
|
||||||
|
|
||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>")
|
||||||
class AppApi(Resource):
|
class AppApi(Resource):
|
||||||
|
@api.doc("get_app_detail")
|
||||||
|
@api.doc(description="Get application details")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Success", app_detail_fields_with_site)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -139,6 +186,26 @@ class AppApi(Resource):
|
|||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
@api.doc("update_app")
|
||||||
|
@api.doc(description="Update application details")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateAppRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(required=True, description="App name"),
|
||||||
|
"description": fields.String(description="App description (max 400 chars)"),
|
||||||
|
"icon_type": fields.String(description="Icon type"),
|
||||||
|
"icon": fields.String(description="Icon"),
|
||||||
|
"icon_background": fields.String(description="Icon background color"),
|
||||||
|
"use_icon_as_answer_icon": fields.Boolean(description="Use icon as answer icon"),
|
||||||
|
"max_active_requests": fields.Integer(description="Maximum active requests"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "App updated successfully", app_detail_fields_with_site)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -161,14 +228,31 @@ class AppApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_model = app_service.update_app(app_model, args)
|
# Construct ArgsDict from parsed arguments
|
||||||
|
from services.app_service import AppService as AppServiceType
|
||||||
|
|
||||||
|
args_dict: AppServiceType.ArgsDict = {
|
||||||
|
"name": args["name"],
|
||||||
|
"description": args.get("description", ""),
|
||||||
|
"icon_type": args.get("icon_type", ""),
|
||||||
|
"icon": args.get("icon", ""),
|
||||||
|
"icon_background": args.get("icon_background", ""),
|
||||||
|
"use_icon_as_answer_icon": args.get("use_icon_as_answer_icon", False),
|
||||||
|
"max_active_requests": args.get("max_active_requests", 0),
|
||||||
|
}
|
||||||
|
app_model = app_service.update_app(app_model, args_dict)
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
@api.doc("delete_app")
|
||||||
|
@api.doc(description="Delete application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(204, "App deleted successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def delete(self, app_model):
|
def delete(self, app_model):
|
||||||
"""Delete app"""
|
"""Delete app"""
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
@ -181,7 +265,25 @@ class AppApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/copy")
|
||||||
class AppCopyApi(Resource):
|
class AppCopyApi(Resource):
|
||||||
|
@api.doc("copy_app")
|
||||||
|
@api.doc(description="Create a copy of an existing application")
|
||||||
|
@api.doc(params={"app_id": "Application ID to copy"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CopyAppRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(description="Name for the copied app"),
|
||||||
|
"description": fields.String(description="Description for the copied app"),
|
||||||
|
"icon_type": fields.String(description="Icon type"),
|
||||||
|
"icon": fields.String(description="Icon"),
|
||||||
|
"icon_background": fields.String(description="Icon background color"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "App copied successfully", app_detail_fields_with_site)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -223,11 +325,26 @@ class AppCopyApi(Resource):
|
|||||||
return app, 201
|
return app, 201
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/export")
|
||||||
class AppExportApi(Resource):
|
class AppExportApi(Resource):
|
||||||
|
@api.doc("export_app")
|
||||||
|
@api.doc(description="Export application configuration as DSL")
|
||||||
|
@api.doc(params={"app_id": "Application ID to export"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("include_secret", type=bool, location="args", default=False, help="Include secrets in export")
|
||||||
|
.add_argument("workflow_id", type=str, location="args", help="Specific workflow ID to export")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"App exported successfully",
|
||||||
|
api.model("AppExportResponse", {"data": fields.String(description="DSL export data")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
"""Export app"""
|
"""Export app"""
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
@ -247,7 +364,13 @@ class AppExportApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/name")
|
||||||
class AppNameApi(Resource):
|
class AppNameApi(Resource):
|
||||||
|
@api.doc("check_app_name")
|
||||||
|
@api.doc(description="Check if app name is available")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(api.parser().add_argument("name", type=str, required=True, location="args", help="Name to check"))
|
||||||
|
@api.response(200, "Name availability checked")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -263,12 +386,28 @@ class AppNameApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_model = app_service.update_app_name(app_model, args.get("name"))
|
app_model = app_service.update_app_name(app_model, args["name"])
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/icon")
|
||||||
class AppIconApi(Resource):
|
class AppIconApi(Resource):
|
||||||
|
@api.doc("update_app_icon")
|
||||||
|
@api.doc(description="Update application icon")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AppIconRequest",
|
||||||
|
{
|
||||||
|
"icon": fields.String(required=True, description="Icon data"),
|
||||||
|
"icon_type": fields.String(description="Icon type"),
|
||||||
|
"icon_background": fields.String(description="Icon background color"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Icon updated successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -285,12 +424,23 @@ class AppIconApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_model = app_service.update_app_icon(app_model, args.get("icon"), args.get("icon_background"))
|
app_model = app_service.update_app_icon(app_model, args.get("icon") or "", args.get("icon_background") or "")
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/site-enable")
|
||||||
class AppSiteStatus(Resource):
|
class AppSiteStatus(Resource):
|
||||||
|
@api.doc("update_app_site_status")
|
||||||
|
@api.doc(description="Enable or disable app site")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AppSiteStatusRequest", {"enable_site": fields.Boolean(required=True, description="Enable or disable site")}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Site status updated successfully", app_detail_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -306,12 +456,23 @@ class AppSiteStatus(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_model = app_service.update_app_site_status(app_model, args.get("enable_site"))
|
app_model = app_service.update_app_site_status(app_model, args["enable_site"])
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/api-enable")
|
||||||
class AppApiStatus(Resource):
|
class AppApiStatus(Resource):
|
||||||
|
@api.doc("update_app_api_status")
|
||||||
|
@api.doc(description="Enable or disable app API")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AppApiStatusRequest", {"enable_api": fields.Boolean(required=True, description="Enable or disable API")}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "API status updated successfully", app_detail_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -327,12 +488,17 @@ class AppApiStatus(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_service = AppService()
|
app_service = AppService()
|
||||||
app_model = app_service.update_app_api_status(app_model, args.get("enable_api"))
|
app_model = app_service.update_app_api_status(app_model, args["enable_api"])
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/trace")
|
||||||
class AppTraceApi(Resource):
|
class AppTraceApi(Resource):
|
||||||
|
@api.doc("get_app_trace")
|
||||||
|
@api.doc(description="Get app tracing configuration")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Trace configuration retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -342,6 +508,20 @@ class AppTraceApi(Resource):
|
|||||||
|
|
||||||
return app_trace_config
|
return app_trace_config
|
||||||
|
|
||||||
|
@api.doc("update_app_trace")
|
||||||
|
@api.doc(description="Update app tracing configuration")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AppTraceRequest",
|
||||||
|
{
|
||||||
|
"enabled": fields.Boolean(required=True, description="Enable or disable tracing"),
|
||||||
|
"tracing_provider": fields.String(required=True, description="Tracing provider"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Trace configuration updated successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -361,14 +541,3 @@ class AppTraceApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppListApi, "/apps")
|
|
||||||
api.add_resource(AppApi, "/apps/<uuid:app_id>")
|
|
||||||
api.add_resource(AppCopyApi, "/apps/<uuid:app_id>/copy")
|
|
||||||
api.add_resource(AppExportApi, "/apps/<uuid:app_id>/export")
|
|
||||||
api.add_resource(AppNameApi, "/apps/<uuid:app_id>/name")
|
|
||||||
api.add_resource(AppIconApi, "/apps/<uuid:app_id>/icon")
|
|
||||||
api.add_resource(AppSiteStatus, "/apps/<uuid:app_id>/site-enable")
|
|
||||||
api.add_resource(AppApiStatus, "/apps/<uuid:app_id>/api-enable")
|
|
||||||
api.add_resource(AppTraceApi, "/apps/<uuid:app_id>/trace")
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import (
|
from controllers.console.app.error import (
|
||||||
AppUnavailableError,
|
AppUnavailableError,
|
||||||
AudioTooLargeError,
|
AudioTooLargeError,
|
||||||
@ -34,7 +34,18 @@ from services.errors.audio import (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/audio-to-text")
|
||||||
class ChatMessageAudioApi(Resource):
|
class ChatMessageAudioApi(Resource):
|
||||||
|
@api.doc("chat_message_audio_transcript")
|
||||||
|
@api.doc(description="Transcript audio to text for chat messages")
|
||||||
|
@api.doc(params={"app_id": "App ID"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Audio transcription successful",
|
||||||
|
api.model("AudioTranscriptResponse", {"text": fields.String(description="Transcribed text from audio")}),
|
||||||
|
)
|
||||||
|
@api.response(400, "Bad request - No audio uploaded or unsupported type")
|
||||||
|
@api.response(413, "Audio file too large")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -76,11 +87,28 @@ class ChatMessageAudioApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/text-to-audio")
|
||||||
class ChatMessageTextApi(Resource):
|
class ChatMessageTextApi(Resource):
|
||||||
|
@api.doc("chat_message_text_to_speech")
|
||||||
|
@api.doc(description="Convert text to speech for chat messages")
|
||||||
|
@api.doc(params={"app_id": "App ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"TextToSpeechRequest",
|
||||||
|
{
|
||||||
|
"message_id": fields.String(description="Message ID"),
|
||||||
|
"text": fields.String(required=True, description="Text to convert to speech"),
|
||||||
|
"voice": fields.String(description="Voice to use for TTS"),
|
||||||
|
"streaming": fields.Boolean(description="Whether to stream the audio"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Text to speech conversion successful")
|
||||||
|
@api.response(400, "Bad request - Invalid parameters")
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def post(self, app_model: App):
|
def post(self, app_model: App):
|
||||||
try:
|
try:
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -124,11 +152,18 @@ class ChatMessageTextApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/text-to-audio/voices")
|
||||||
class TextModesApi(Resource):
|
class TextModesApi(Resource):
|
||||||
|
@api.doc("get_text_to_speech_voices")
|
||||||
|
@api.doc(description="Get available TTS voices for a specific language")
|
||||||
|
@api.doc(params={"app_id": "App ID"})
|
||||||
|
@api.expect(api.parser().add_argument("language", type=str, required=True, location="args", help="Language code"))
|
||||||
|
@api.response(200, "TTS voices retrieved successfully", fields.List(fields.Raw(description="Available voices")))
|
||||||
|
@api.response(400, "Invalid language parameter")
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
try:
|
try:
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -164,8 +199,3 @@ class TextModesApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to handle get request to TextModesApi")
|
logger.exception("Failed to handle get request to TextModesApi")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ChatMessageAudioApi, "/apps/<uuid:app_id>/audio-to-text")
|
|
||||||
api.add_resource(ChatMessageTextApi, "/apps/<uuid:app_id>/text-to-audio")
|
|
||||||
api.add_resource(TextModesApi, "/apps/<uuid:app_id>/text-to-audio/voices")
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import flask_login
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import (
|
from controllers.console.app.error import (
|
||||||
AppUnavailableError,
|
AppUnavailableError,
|
||||||
CompletionRequestError,
|
CompletionRequestError,
|
||||||
@ -29,7 +28,8 @@ from core.helper.trace_id_helper import get_external_trace_id
|
|||||||
from core.model_runtime.errors.invoke import InvokeError
|
from core.model_runtime.errors.invoke import InvokeError
|
||||||
from libs import helper
|
from libs import helper
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from libs.login import login_required
|
from libs.login import current_user, login_required
|
||||||
|
from models import Account
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.llm import InvokeRateLimitError
|
from services.errors.llm import InvokeRateLimitError
|
||||||
@ -38,7 +38,27 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# define completion message api for user
|
# define completion message api for user
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/completion-messages")
|
||||||
class CompletionMessageApi(Resource):
|
class CompletionMessageApi(Resource):
|
||||||
|
@api.doc("create_completion_message")
|
||||||
|
@api.doc(description="Generate completion message for debugging")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CompletionMessageRequest",
|
||||||
|
{
|
||||||
|
"inputs": fields.Raw(required=True, description="Input variables"),
|
||||||
|
"query": fields.String(description="Query text", default=""),
|
||||||
|
"files": fields.List(fields.Raw(), description="Uploaded files"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
"response_mode": fields.String(enum=["blocking", "streaming"], description="Response mode"),
|
||||||
|
"retriever_from": fields.String(default="dev", description="Retriever source"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Completion generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(404, "App not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -56,11 +76,11 @@ class CompletionMessageApi(Resource):
|
|||||||
streaming = args["response_mode"] != "blocking"
|
streaming = args["response_mode"] != "blocking"
|
||||||
args["auto_generate_name"] = False
|
args["auto_generate_name"] = False
|
||||||
|
|
||||||
account = flask_login.current_user
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account or EndUser instance")
|
||||||
response = AppGenerateService.generate(
|
response = AppGenerateService.generate(
|
||||||
app_model=app_model, user=account, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
|
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
|
||||||
)
|
)
|
||||||
|
|
||||||
return helper.compact_generate_response(response)
|
return helper.compact_generate_response(response)
|
||||||
@ -86,25 +106,58 @@ class CompletionMessageApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/completion-messages/<string:task_id>/stop")
|
||||||
class CompletionMessageStopApi(Resource):
|
class CompletionMessageStopApi(Resource):
|
||||||
|
@api.doc("stop_completion_message")
|
||||||
|
@api.doc(description="Stop a running completion message generation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "task_id": "Task ID to stop"})
|
||||||
|
@api.response(200, "Task stopped successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model(mode=AppMode.COMPLETION)
|
@get_app_model(mode=AppMode.COMPLETION)
|
||||||
def post(self, app_model, task_id):
|
def post(self, app_model, task_id):
|
||||||
account = flask_login.current_user
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, account.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-messages")
|
||||||
class ChatMessageApi(Resource):
|
class ChatMessageApi(Resource):
|
||||||
|
@api.doc("create_chat_message")
|
||||||
|
@api.doc(description="Generate chat message for debugging")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ChatMessageRequest",
|
||||||
|
{
|
||||||
|
"inputs": fields.Raw(required=True, description="Input variables"),
|
||||||
|
"query": fields.String(required=True, description="User query"),
|
||||||
|
"files": fields.List(fields.Raw(), description="Uploaded files"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
"conversation_id": fields.String(description="Conversation ID"),
|
||||||
|
"parent_message_id": fields.String(description="Parent message ID"),
|
||||||
|
"response_mode": fields.String(enum=["blocking", "streaming"], description="Response mode"),
|
||||||
|
"retriever_from": fields.String(default="dev", description="Retriever source"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Chat message generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(404, "App or conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT])
|
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT])
|
||||||
def post(self, app_model):
|
def post(self, app_model):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not current_user.has_edit_permission:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("inputs", type=dict, required=True, location="json")
|
parser.add_argument("inputs", type=dict, required=True, location="json")
|
||||||
parser.add_argument("query", type=str, required=True, location="json")
|
parser.add_argument("query", type=str, required=True, location="json")
|
||||||
@ -123,11 +176,11 @@ class ChatMessageApi(Resource):
|
|||||||
if external_trace_id:
|
if external_trace_id:
|
||||||
args["external_trace_id"] = external_trace_id
|
args["external_trace_id"] = external_trace_id
|
||||||
|
|
||||||
account = flask_login.current_user
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account or EndUser instance")
|
||||||
response = AppGenerateService.generate(
|
response = AppGenerateService.generate(
|
||||||
app_model=app_model, user=account, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
|
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.DEBUGGER, streaming=streaming
|
||||||
)
|
)
|
||||||
|
|
||||||
return helper.compact_generate_response(response)
|
return helper.compact_generate_response(response)
|
||||||
@ -155,20 +208,19 @@ class ChatMessageApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-messages/<string:task_id>/stop")
|
||||||
class ChatMessageStopApi(Resource):
|
class ChatMessageStopApi(Resource):
|
||||||
|
@api.doc("stop_chat_message")
|
||||||
|
@api.doc(description="Stop a running chat message generation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "task_id": "Task ID to stop"})
|
||||||
|
@api.response(200, "Task stopped successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
||||||
def post(self, app_model, task_id):
|
def post(self, app_model, task_id):
|
||||||
account = flask_login.current_user
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, account.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CompletionMessageApi, "/apps/<uuid:app_id>/completion-messages")
|
|
||||||
api.add_resource(CompletionMessageStopApi, "/apps/<uuid:app_id>/completion-messages/<string:task_id>/stop")
|
|
||||||
api.add_resource(ChatMessageApi, "/apps/<uuid:app_id>/chat-messages")
|
|
||||||
api.add_resource(ChatMessageStopApi, "/apps/<uuid:app_id>/chat-messages/<string:task_id>/stop")
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from sqlalchemy import func, or_
|
|||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
@ -22,13 +22,35 @@ from fields.conversation_fields import (
|
|||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.helper import DatetimeString
|
from libs.helper import DatetimeString
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Conversation, EndUser, Message, MessageAnnotation
|
from models import Account, Conversation, EndUser, Message, MessageAnnotation
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.conversation_service import ConversationService
|
from services.conversation_service import ConversationService
|
||||||
from services.errors.conversation import ConversationNotExistsError
|
from services.errors.conversation import ConversationNotExistsError
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/completion-conversations")
|
||||||
class CompletionConversationApi(Resource):
|
class CompletionConversationApi(Resource):
|
||||||
|
@api.doc("list_completion_conversations")
|
||||||
|
@api.doc(description="Get completion conversations with pagination and filtering")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("keyword", type=str, location="args", help="Search keyword")
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument(
|
||||||
|
"annotation_status",
|
||||||
|
type=str,
|
||||||
|
location="args",
|
||||||
|
choices=["annotated", "not_annotated", "all"],
|
||||||
|
default="all",
|
||||||
|
help="Annotation status filter",
|
||||||
|
)
|
||||||
|
.add_argument("page", type=int, location="args", default=1, help="Page number")
|
||||||
|
.add_argument("limit", type=int, location="args", default=20, help="Page size (1-100)")
|
||||||
|
)
|
||||||
|
@api.response(200, "Success", conversation_pagination_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -101,7 +123,14 @@ class CompletionConversationApi(Resource):
|
|||||||
return conversations
|
return conversations
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/completion-conversations/<uuid:conversation_id>")
|
||||||
class CompletionConversationDetailApi(Resource):
|
class CompletionConversationDetailApi(Resource):
|
||||||
|
@api.doc("get_completion_conversation")
|
||||||
|
@api.doc(description="Get completion conversation details with messages")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
||||||
|
@api.response(200, "Success", conversation_message_detail_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -114,6 +143,12 @@ class CompletionConversationDetailApi(Resource):
|
|||||||
|
|
||||||
return _get_conversation(app_model, conversation_id)
|
return _get_conversation(app_model, conversation_id)
|
||||||
|
|
||||||
|
@api.doc("delete_completion_conversation")
|
||||||
|
@api.doc(description="Delete a completion conversation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
||||||
|
@api.response(204, "Conversation deleted successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -124,6 +159,8 @@ class CompletionConversationDetailApi(Resource):
|
|||||||
conversation_id = str(conversation_id)
|
conversation_id = str(conversation_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
ConversationService.delete(app_model, conversation_id, current_user)
|
ConversationService.delete(app_model, conversation_id, current_user)
|
||||||
except ConversationNotExistsError:
|
except ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
@ -131,7 +168,38 @@ class CompletionConversationDetailApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-conversations")
|
||||||
class ChatConversationApi(Resource):
|
class ChatConversationApi(Resource):
|
||||||
|
@api.doc("list_chat_conversations")
|
||||||
|
@api.doc(description="Get chat conversations with pagination, filtering and summary")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("keyword", type=str, location="args", help="Search keyword")
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument(
|
||||||
|
"annotation_status",
|
||||||
|
type=str,
|
||||||
|
location="args",
|
||||||
|
choices=["annotated", "not_annotated", "all"],
|
||||||
|
default="all",
|
||||||
|
help="Annotation status filter",
|
||||||
|
)
|
||||||
|
.add_argument("message_count_gte", type=int, location="args", help="Minimum message count")
|
||||||
|
.add_argument("page", type=int, location="args", default=1, help="Page number")
|
||||||
|
.add_argument("limit", type=int, location="args", default=20, help="Page size (1-100)")
|
||||||
|
.add_argument(
|
||||||
|
"sort_by",
|
||||||
|
type=str,
|
||||||
|
location="args",
|
||||||
|
choices=["created_at", "-created_at", "updated_at", "-updated_at"],
|
||||||
|
default="-updated_at",
|
||||||
|
help="Sort field and direction",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Success", conversation_with_summary_pagination_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -239,7 +307,7 @@ class ChatConversationApi(Resource):
|
|||||||
.having(func.count(Message.id) >= args["message_count_gte"])
|
.having(func.count(Message.id) >= args["message_count_gte"])
|
||||||
)
|
)
|
||||||
|
|
||||||
if app_model.mode == AppMode.ADVANCED_CHAT.value:
|
if app_model.mode == AppMode.ADVANCED_CHAT:
|
||||||
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER.value)
|
query = query.where(Conversation.invoke_from != InvokeFrom.DEBUGGER.value)
|
||||||
|
|
||||||
match args["sort_by"]:
|
match args["sort_by"]:
|
||||||
@ -259,7 +327,14 @@ class ChatConversationApi(Resource):
|
|||||||
return conversations
|
return conversations
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-conversations/<uuid:conversation_id>")
|
||||||
class ChatConversationDetailApi(Resource):
|
class ChatConversationDetailApi(Resource):
|
||||||
|
@api.doc("get_chat_conversation")
|
||||||
|
@api.doc(description="Get chat conversation details")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
||||||
|
@api.response(200, "Success", conversation_detail_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -272,6 +347,12 @@ class ChatConversationDetailApi(Resource):
|
|||||||
|
|
||||||
return _get_conversation(app_model, conversation_id)
|
return _get_conversation(app_model, conversation_id)
|
||||||
|
|
||||||
|
@api.doc("delete_chat_conversation")
|
||||||
|
@api.doc(description="Delete a chat conversation")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"})
|
||||||
|
@api.response(204, "Conversation deleted successfully")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
||||||
@ -282,6 +363,8 @@ class ChatConversationDetailApi(Resource):
|
|||||||
conversation_id = str(conversation_id)
|
conversation_id = str(conversation_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
ConversationService.delete(app_model, conversation_id, current_user)
|
ConversationService.delete(app_model, conversation_id, current_user)
|
||||||
except ConversationNotExistsError:
|
except ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
@ -289,12 +372,6 @@ class ChatConversationDetailApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CompletionConversationApi, "/apps/<uuid:app_id>/completion-conversations")
|
|
||||||
api.add_resource(CompletionConversationDetailApi, "/apps/<uuid:app_id>/completion-conversations/<uuid:conversation_id>")
|
|
||||||
api.add_resource(ChatConversationApi, "/apps/<uuid:app_id>/chat-conversations")
|
|
||||||
api.add_resource(ChatConversationDetailApi, "/apps/<uuid:app_id>/chat-conversations/<uuid:conversation_id>")
|
|
||||||
|
|
||||||
|
|
||||||
def _get_conversation(app_model, conversation_id):
|
def _get_conversation(app_model, conversation_id):
|
||||||
conversation = (
|
conversation = (
|
||||||
db.session.query(Conversation)
|
db.session.query(Conversation)
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from flask_restx import Resource, marshal_with, reqparse
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -12,7 +12,17 @@ from models import ConversationVariable
|
|||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/conversation-variables")
|
||||||
class ConversationVariablesApi(Resource):
|
class ConversationVariablesApi(Resource):
|
||||||
|
@api.doc("get_conversation_variables")
|
||||||
|
@api.doc(description="Get conversation variables for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser().add_argument(
|
||||||
|
"conversation_id", type=str, location="args", help="Conversation ID to filter variables"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Conversation variables retrieved successfully", paginated_conversation_variable_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -55,6 +65,3 @@ class ConversationVariablesApi(Resource):
|
|||||||
for row in rows
|
for row in rows
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ConversationVariablesApi, "/apps/<uuid:app_id>/conversation-variables")
|
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import (
|
from controllers.console.app.error import (
|
||||||
CompletionRequestError,
|
CompletionRequestError,
|
||||||
ProviderModelCurrentlyNotSupportError,
|
ProviderModelCurrentlyNotSupportError,
|
||||||
@ -19,7 +19,23 @@ from core.model_runtime.errors.invoke import InvokeError
|
|||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/rule-generate")
|
||||||
class RuleGenerateApi(Resource):
|
class RuleGenerateApi(Resource):
|
||||||
|
@api.doc("generate_rule_config")
|
||||||
|
@api.doc(description="Generate rule configuration using LLM")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"RuleGenerateRequest",
|
||||||
|
{
|
||||||
|
"instruction": fields.String(required=True, description="Rule generation instruction"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
"no_variable": fields.Boolean(required=True, default=False, description="Whether to exclude variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Rule configuration generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(402, "Provider quota exceeded")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -50,7 +66,26 @@ class RuleGenerateApi(Resource):
|
|||||||
return rules
|
return rules
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/rule-code-generate")
|
||||||
class RuleCodeGenerateApi(Resource):
|
class RuleCodeGenerateApi(Resource):
|
||||||
|
@api.doc("generate_rule_code")
|
||||||
|
@api.doc(description="Generate code rules using LLM")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"RuleCodeGenerateRequest",
|
||||||
|
{
|
||||||
|
"instruction": fields.String(required=True, description="Code generation instruction"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
"no_variable": fields.Boolean(required=True, default=False, description="Whether to exclude variables"),
|
||||||
|
"code_language": fields.String(
|
||||||
|
default="javascript", description="Programming language for code generation"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Code rules generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(402, "Provider quota exceeded")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -82,7 +117,22 @@ class RuleCodeGenerateApi(Resource):
|
|||||||
return code_result
|
return code_result
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/rule-structured-output-generate")
|
||||||
class RuleStructuredOutputGenerateApi(Resource):
|
class RuleStructuredOutputGenerateApi(Resource):
|
||||||
|
@api.doc("generate_structured_output")
|
||||||
|
@api.doc(description="Generate structured output rules using LLM")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"StructuredOutputGenerateRequest",
|
||||||
|
{
|
||||||
|
"instruction": fields.String(required=True, description="Structured output generation instruction"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Structured output generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(402, "Provider quota exceeded")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -111,7 +161,27 @@ class RuleStructuredOutputGenerateApi(Resource):
|
|||||||
return structured_output
|
return structured_output
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/instruction-generate")
|
||||||
class InstructionGenerateApi(Resource):
|
class InstructionGenerateApi(Resource):
|
||||||
|
@api.doc("generate_instruction")
|
||||||
|
@api.doc(description="Generate instruction for workflow nodes or general use")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"InstructionGenerateRequest",
|
||||||
|
{
|
||||||
|
"flow_id": fields.String(required=True, description="Workflow/Flow ID"),
|
||||||
|
"node_id": fields.String(description="Node ID for workflow context"),
|
||||||
|
"current": fields.String(description="Current instruction text"),
|
||||||
|
"language": fields.String(default="javascript", description="Programming language (javascript/python)"),
|
||||||
|
"instruction": fields.String(required=True, description="Instruction for generation"),
|
||||||
|
"model_config": fields.Raw(required=True, description="Model configuration"),
|
||||||
|
"ideal_output": fields.String(description="Expected ideal output"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Instruction generated successfully")
|
||||||
|
@api.response(400, "Invalid request parameters or flow/workflow not found")
|
||||||
|
@api.response(402, "Provider quota exceeded")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -203,7 +273,21 @@ class InstructionGenerateApi(Resource):
|
|||||||
raise CompletionRequestError(e.description)
|
raise CompletionRequestError(e.description)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/instruction-generate/template")
|
||||||
class InstructionGenerationTemplateApi(Resource):
|
class InstructionGenerationTemplateApi(Resource):
|
||||||
|
@api.doc("get_instruction_template")
|
||||||
|
@api.doc(description="Get instruction generation template")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"InstructionTemplateRequest",
|
||||||
|
{
|
||||||
|
"instruction": fields.String(required=True, description="Template instruction"),
|
||||||
|
"ideal_output": fields.String(description="Expected ideal output"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Template retrieved successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -222,10 +306,3 @@ class InstructionGenerationTemplateApi(Resource):
|
|||||||
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
|
return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Invalid type: {args['type']}")
|
raise ValueError(f"Invalid type: {args['type']}")
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(RuleGenerateApi, "/rule-generate")
|
|
||||||
api.add_resource(RuleCodeGenerateApi, "/rule-code-generate")
|
|
||||||
api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate")
|
|
||||||
api.add_resource(InstructionGenerateApi, "/instruction-generate")
|
|
||||||
api.add_resource(InstructionGenerationTemplateApi, "/instruction-generate/template")
|
|
||||||
|
|||||||
@ -2,10 +2,10 @@ import json
|
|||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -19,7 +19,12 @@ class AppMCPServerStatus(StrEnum):
|
|||||||
INACTIVE = "inactive"
|
INACTIVE = "inactive"
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/server")
|
||||||
class AppMCPServerController(Resource):
|
class AppMCPServerController(Resource):
|
||||||
|
@api.doc("get_app_mcp_server")
|
||||||
|
@api.doc(description="Get MCP server configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "MCP server configuration retrieved successfully", app_server_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -29,6 +34,20 @@ class AppMCPServerController(Resource):
|
|||||||
server = db.session.query(AppMCPServer).where(AppMCPServer.app_id == app_model.id).first()
|
server = db.session.query(AppMCPServer).where(AppMCPServer.app_id == app_model.id).first()
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
@api.doc("create_app_mcp_server")
|
||||||
|
@api.doc(description="Create MCP server configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"MCPServerCreateRequest",
|
||||||
|
{
|
||||||
|
"description": fields.String(description="Server description"),
|
||||||
|
"parameters": fields.Raw(required=True, description="Server parameters configuration"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "MCP server configuration created successfully", app_server_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -59,6 +78,23 @@ class AppMCPServerController(Resource):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
@api.doc("update_app_mcp_server")
|
||||||
|
@api.doc(description="Update MCP server configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"MCPServerUpdateRequest",
|
||||||
|
{
|
||||||
|
"id": fields.String(required=True, description="Server ID"),
|
||||||
|
"description": fields.String(description="Server description"),
|
||||||
|
"parameters": fields.Raw(required=True, description="Server parameters configuration"),
|
||||||
|
"status": fields.String(description="Server status"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "MCP server configuration updated successfully", app_server_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Server not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -94,7 +130,14 @@ class AppMCPServerController(Resource):
|
|||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:server_id>/server/refresh")
|
||||||
class AppMCPServerRefreshController(Resource):
|
class AppMCPServerRefreshController(Resource):
|
||||||
|
@api.doc("refresh_app_mcp_server")
|
||||||
|
@api.doc(description="Refresh MCP server configuration and regenerate server code")
|
||||||
|
@api.doc(params={"server_id": "Server ID"})
|
||||||
|
@api.response(200, "MCP server refreshed successfully", app_server_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "Server not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -113,7 +156,3 @@ class AppMCPServerRefreshController(Resource):
|
|||||||
server.server_code = AppMCPServer.generate_server_code(16)
|
server.server_code = AppMCPServer.generate_server_code(16)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return server
|
return server
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppMCPServerController, "/apps/<uuid:app_id>/server")
|
|
||||||
api.add_resource(AppMCPServerRefreshController, "/apps/<uuid:server_id>/server/refresh")
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_restx import Resource, fields, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from sqlalchemy import exists, select
|
from sqlalchemy import exists, select
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import (
|
from controllers.console.app.error import (
|
||||||
CompletionRequestError,
|
CompletionRequestError,
|
||||||
ProviderModelCurrentlyNotSupportError,
|
ProviderModelCurrentlyNotSupportError,
|
||||||
@ -27,7 +26,8 @@ from extensions.ext_database import db
|
|||||||
from fields.conversation_fields import annotation_fields, message_detail_fields
|
from fields.conversation_fields import annotation_fields, message_detail_fields
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||||
from libs.login import login_required
|
from libs.login import current_user, login_required
|
||||||
|
from models.account import Account
|
||||||
from models.model import AppMode, Conversation, Message, MessageAnnotation, MessageFeedback
|
from models.model import AppMode, Conversation, Message, MessageAnnotation, MessageFeedback
|
||||||
from services.annotation_service import AppAnnotationService
|
from services.annotation_service import AppAnnotationService
|
||||||
from services.errors.conversation import ConversationNotExistsError
|
from services.errors.conversation import ConversationNotExistsError
|
||||||
@ -37,6 +37,7 @@ from services.message_service import MessageService
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-messages")
|
||||||
class ChatMessageListApi(Resource):
|
class ChatMessageListApi(Resource):
|
||||||
message_infinite_scroll_pagination_fields = {
|
message_infinite_scroll_pagination_fields = {
|
||||||
"limit": fields.Integer,
|
"limit": fields.Integer,
|
||||||
@ -44,6 +45,17 @@ class ChatMessageListApi(Resource):
|
|||||||
"data": fields.List(fields.Nested(message_detail_fields)),
|
"data": fields.List(fields.Nested(message_detail_fields)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@api.doc("list_chat_messages")
|
||||||
|
@api.doc(description="Get chat messages for a conversation with pagination")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("conversation_id", type=str, required=True, location="args", help="Conversation ID")
|
||||||
|
.add_argument("first_id", type=str, location="args", help="First message ID for pagination")
|
||||||
|
.add_argument("limit", type=int, location="args", default=20, help="Number of messages to return (1-100)")
|
||||||
|
)
|
||||||
|
@api.response(200, "Success", message_infinite_scroll_pagination_fields)
|
||||||
|
@api.response(404, "Conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
|
||||||
@ -117,12 +129,31 @@ class ChatMessageListApi(Resource):
|
|||||||
return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more)
|
return InfiniteScrollPagination(data=history_messages, limit=args["limit"], has_more=has_more)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/feedbacks")
|
||||||
class MessageFeedbackApi(Resource):
|
class MessageFeedbackApi(Resource):
|
||||||
|
@api.doc("create_message_feedback")
|
||||||
|
@api.doc(description="Create or update message feedback (like/dislike)")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"MessageFeedbackRequest",
|
||||||
|
{
|
||||||
|
"message_id": fields.String(required=True, description="Message ID"),
|
||||||
|
"rating": fields.String(enum=["like", "dislike"], description="Feedback rating"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Feedback updated successfully")
|
||||||
|
@api.response(404, "Message not found")
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def post(self, app_model):
|
def post(self, app_model):
|
||||||
|
if current_user is None:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("message_id", required=True, type=uuid_value, location="json")
|
parser.add_argument("message_id", required=True, type=uuid_value, location="json")
|
||||||
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
|
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
|
||||||
@ -159,7 +190,24 @@ class MessageFeedbackApi(Resource):
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations")
|
||||||
class MessageAnnotationApi(Resource):
|
class MessageAnnotationApi(Resource):
|
||||||
|
@api.doc("create_message_annotation")
|
||||||
|
@api.doc(description="Create message annotation")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"MessageAnnotationRequest",
|
||||||
|
{
|
||||||
|
"message_id": fields.String(description="Message ID"),
|
||||||
|
"question": fields.String(required=True, description="Question text"),
|
||||||
|
"answer": fields.String(required=True, description="Answer text"),
|
||||||
|
"annotation_reply": fields.Raw(description="Annotation reply"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Annotation created successfully", annotation_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -167,7 +215,9 @@ class MessageAnnotationApi(Resource):
|
|||||||
@get_app_model
|
@get_app_model
|
||||||
@marshal_with(annotation_fields)
|
@marshal_with(annotation_fields)
|
||||||
def post(self, app_model):
|
def post(self, app_model):
|
||||||
if not current_user.is_editor:
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -181,18 +231,37 @@ class MessageAnnotationApi(Resource):
|
|||||||
return annotation
|
return annotation
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/annotations/count")
|
||||||
class MessageAnnotationCountApi(Resource):
|
class MessageAnnotationCountApi(Resource):
|
||||||
|
@api.doc("get_annotation_count")
|
||||||
|
@api.doc(description="Get count of message annotations for the app")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Annotation count retrieved successfully",
|
||||||
|
api.model("AnnotationCountResponse", {"count": fields.Integer(description="Number of annotations")}),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
count = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_model.id).count()
|
count = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_model.id).count()
|
||||||
|
|
||||||
return {"count": count}
|
return {"count": count}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/chat-messages/<uuid:message_id>/suggested-questions")
|
||||||
class MessageSuggestedQuestionApi(Resource):
|
class MessageSuggestedQuestionApi(Resource):
|
||||||
|
@api.doc("get_message_suggested_questions")
|
||||||
|
@api.doc(description="Get suggested questions for a message")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "message_id": "Message ID"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Suggested questions retrieved successfully",
|
||||||
|
api.model("SuggestedQuestionsResponse", {"data": fields.List(fields.String(description="Suggested question"))}),
|
||||||
|
)
|
||||||
|
@api.response(404, "Message or conversation not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -225,7 +294,13 @@ class MessageSuggestedQuestionApi(Resource):
|
|||||||
return {"data": questions}
|
return {"data": questions}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/messages/<uuid:message_id>")
|
||||||
class MessageApi(Resource):
|
class MessageApi(Resource):
|
||||||
|
@api.doc("get_message")
|
||||||
|
@api.doc(description="Get message details by ID")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "message_id": "Message ID"})
|
||||||
|
@api.response(200, "Message retrieved successfully", message_detail_fields)
|
||||||
|
@api.response(404, "Message not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -240,11 +315,3 @@ class MessageApi(Resource):
|
|||||||
raise NotFound("Message Not Exists.")
|
raise NotFound("Message Not Exists.")
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(MessageSuggestedQuestionApi, "/apps/<uuid:app_id>/chat-messages/<uuid:message_id>/suggested-questions")
|
|
||||||
api.add_resource(ChatMessageListApi, "/apps/<uuid:app_id>/chat-messages", endpoint="console_chat_messages")
|
|
||||||
api.add_resource(MessageFeedbackApi, "/apps/<uuid:app_id>/feedbacks")
|
|
||||||
api.add_resource(MessageAnnotationApi, "/apps/<uuid:app_id>/annotations")
|
|
||||||
api.add_resource(MessageAnnotationCountApi, "/apps/<uuid:app_id>/annotations/count")
|
|
||||||
api.add_resource(MessageApi, "/apps/<uuid:app_id>/messages/<uuid:message_id>", endpoint="console_message")
|
|
||||||
|
|||||||
@ -3,9 +3,10 @@ from typing import cast
|
|||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource, fields
|
||||||
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.agent.entities import AgentToolEntity
|
from core.agent.entities import AgentToolEntity
|
||||||
@ -14,17 +15,51 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager
|
|||||||
from events.app_event import app_model_config_was_updated
|
from events.app_event import app_model_config_was_updated
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
|
from models.account import Account
|
||||||
from models.model import AppMode, AppModelConfig
|
from models.model import AppMode, AppModelConfig
|
||||||
from services.app_model_config_service import AppModelConfigService
|
from services.app_model_config_service import AppModelConfigService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/model-config")
|
||||||
class ModelConfigResource(Resource):
|
class ModelConfigResource(Resource):
|
||||||
|
@api.doc("update_app_model_config")
|
||||||
|
@api.doc(description="Update application model configuration")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ModelConfigRequest",
|
||||||
|
{
|
||||||
|
"provider": fields.String(description="Model provider"),
|
||||||
|
"model": fields.String(description="Model name"),
|
||||||
|
"configs": fields.Raw(description="Model configuration parameters"),
|
||||||
|
"opening_statement": fields.String(description="Opening statement"),
|
||||||
|
"suggested_questions": fields.List(fields.String(), description="Suggested questions"),
|
||||||
|
"more_like_this": fields.Raw(description="More like this configuration"),
|
||||||
|
"speech_to_text": fields.Raw(description="Speech to text configuration"),
|
||||||
|
"text_to_speech": fields.Raw(description="Text to speech configuration"),
|
||||||
|
"retrieval_model": fields.Raw(description="Retrieval model configuration"),
|
||||||
|
"tools": fields.List(fields.Raw(), description="Available tools"),
|
||||||
|
"dataset_configs": fields.Raw(description="Dataset configurations"),
|
||||||
|
"agent_mode": fields.Raw(description="Agent mode configuration"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Model configuration updated successfully")
|
||||||
|
@api.response(400, "Invalid configuration")
|
||||||
|
@api.response(404, "App not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION])
|
@get_app_model(mode=[AppMode.AGENT_CHAT, AppMode.CHAT, AppMode.COMPLETION])
|
||||||
def post(self, app_model):
|
def post(self, app_model):
|
||||||
"""Modify app model config"""
|
"""Modify app model config"""
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not current_user.has_edit_permission:
|
||||||
|
raise Forbidden()
|
||||||
|
|
||||||
|
assert current_user.current_tenant_id is not None, "The tenant information should be loaded."
|
||||||
# validate config
|
# validate config
|
||||||
model_configuration = AppModelConfigService.validate_configuration(
|
model_configuration = AppModelConfigService.validate_configuration(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
@ -39,7 +74,7 @@ class ModelConfigResource(Resource):
|
|||||||
)
|
)
|
||||||
new_app_model_config = new_app_model_config.from_model_config_dict(model_configuration)
|
new_app_model_config = new_app_model_config.from_model_config_dict(model_configuration)
|
||||||
|
|
||||||
if app_model.mode == AppMode.AGENT_CHAT.value or app_model.is_agent:
|
if app_model.mode == AppMode.AGENT_CHAT or app_model.is_agent:
|
||||||
# get original app model config
|
# get original app model config
|
||||||
original_app_model_config = (
|
original_app_model_config = (
|
||||||
db.session.query(AppModelConfig).where(AppModelConfig.id == app_model.app_model_config_id).first()
|
db.session.query(AppModelConfig).where(AppModelConfig.id == app_model.app_model_config_id).first()
|
||||||
@ -142,6 +177,3 @@ class ModelConfigResource(Resource):
|
|||||||
app_model_config_was_updated.send(app_model, app_model_config=new_app_model_config)
|
app_model_config_was_updated.send(app_model, app_model_config=new_app_model_config)
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ModelConfigResource, "/apps/<uuid:app_id>/model-config")
|
|
||||||
|
|||||||
@ -1,18 +1,31 @@
|
|||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
from controllers.console.app.error import TracingConfigCheckError, TracingConfigIsExist, TracingConfigNotExist
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.ops_service import OpsService
|
from services.ops_service import OpsService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/trace-config")
|
||||||
class TraceAppConfigApi(Resource):
|
class TraceAppConfigApi(Resource):
|
||||||
"""
|
"""
|
||||||
Manage trace app configurations
|
Manage trace app configurations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@api.doc("get_trace_app_config")
|
||||||
|
@api.doc(description="Get tracing configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser().add_argument(
|
||||||
|
"tracing_provider", type=str, required=True, location="args", help="Tracing provider name"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200, "Tracing configuration retrieved successfully", fields.Raw(description="Tracing configuration data")
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -29,6 +42,22 @@ class TraceAppConfigApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
|
@api.doc("create_trace_app_config")
|
||||||
|
@api.doc(description="Create a new tracing configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"TraceConfigCreateRequest",
|
||||||
|
{
|
||||||
|
"tracing_provider": fields.String(required=True, description="Tracing provider name"),
|
||||||
|
"tracing_config": fields.Raw(required=True, description="Tracing configuration data"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
201, "Tracing configuration created successfully", fields.Raw(description="Created configuration data")
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid request parameters or configuration already exists")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -51,6 +80,20 @@ class TraceAppConfigApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
|
@api.doc("update_trace_app_config")
|
||||||
|
@api.doc(description="Update an existing tracing configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"TraceConfigUpdateRequest",
|
||||||
|
{
|
||||||
|
"tracing_provider": fields.String(required=True, description="Tracing provider name"),
|
||||||
|
"tracing_config": fields.Raw(required=True, description="Updated tracing configuration data"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Tracing configuration updated successfully", fields.Raw(description="Success response"))
|
||||||
|
@api.response(400, "Invalid request parameters or configuration not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -71,6 +114,16 @@ class TraceAppConfigApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
|
@api.doc("delete_trace_app_config")
|
||||||
|
@api.doc(description="Delete an existing tracing configuration for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser().add_argument(
|
||||||
|
"tracing_provider", type=str, required=True, location="args", help="Tracing provider name"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(204, "Tracing configuration deleted successfully")
|
||||||
|
@api.response(400, "Invalid request parameters or configuration not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -87,6 +140,3 @@ class TraceAppConfigApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(TraceAppConfigApi, "/apps/<uuid:app_id>/trace-config")
|
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.app_fields import app_site_fields
|
from fields.app_fields import app_site_fields
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Site
|
from models import Account, Site
|
||||||
|
|
||||||
|
|
||||||
def parse_app_site_args():
|
def parse_app_site_args():
|
||||||
@ -36,7 +36,39 @@ def parse_app_site_args():
|
|||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/site")
|
||||||
class AppSite(Resource):
|
class AppSite(Resource):
|
||||||
|
@api.doc("update_app_site")
|
||||||
|
@api.doc(description="Update application site configuration")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AppSiteRequest",
|
||||||
|
{
|
||||||
|
"title": fields.String(description="Site title"),
|
||||||
|
"icon_type": fields.String(description="Icon type"),
|
||||||
|
"icon": fields.String(description="Icon"),
|
||||||
|
"icon_background": fields.String(description="Icon background color"),
|
||||||
|
"description": fields.String(description="Site description"),
|
||||||
|
"default_language": fields.String(description="Default language"),
|
||||||
|
"chat_color_theme": fields.String(description="Chat color theme"),
|
||||||
|
"chat_color_theme_inverted": fields.Boolean(description="Inverted chat color theme"),
|
||||||
|
"customize_domain": fields.String(description="Custom domain"),
|
||||||
|
"copyright": fields.String(description="Copyright text"),
|
||||||
|
"privacy_policy": fields.String(description="Privacy policy"),
|
||||||
|
"custom_disclaimer": fields.String(description="Custom disclaimer"),
|
||||||
|
"customize_token_strategy": fields.String(
|
||||||
|
enum=["must", "allow", "not_allow"], description="Token strategy"
|
||||||
|
),
|
||||||
|
"prompt_public": fields.Boolean(description="Make prompt public"),
|
||||||
|
"show_workflow_steps": fields.Boolean(description="Show workflow steps"),
|
||||||
|
"use_icon_as_answer_icon": fields.Boolean(description="Use icon as answer icon"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Site configuration updated successfully", app_site_fields)
|
||||||
|
@api.response(403, "Insufficient permissions")
|
||||||
|
@api.response(404, "App not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -75,6 +107,8 @@ class AppSite(Resource):
|
|||||||
if value is not None:
|
if value is not None:
|
||||||
setattr(site, attr_name, value)
|
setattr(site, attr_name, value)
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
site.updated_by = current_user.id
|
site.updated_by = current_user.id
|
||||||
site.updated_at = naive_utc_now()
|
site.updated_at = naive_utc_now()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -82,7 +116,14 @@ class AppSite(Resource):
|
|||||||
return site
|
return site
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/site/access-token-reset")
|
||||||
class AppSiteAccessTokenReset(Resource):
|
class AppSiteAccessTokenReset(Resource):
|
||||||
|
@api.doc("reset_app_site_access_token")
|
||||||
|
@api.doc(description="Reset access token for application site")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Access token reset successfully", app_site_fields)
|
||||||
|
@api.response(403, "Insufficient permissions (admin/owner required)")
|
||||||
|
@api.response(404, "App or site not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -99,12 +140,10 @@ class AppSiteAccessTokenReset(Resource):
|
|||||||
raise NotFound
|
raise NotFound
|
||||||
|
|
||||||
site.code = Site.generate_code(16)
|
site.code = Site.generate_code(16)
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
site.updated_by = current_user.id
|
site.updated_by = current_user.id
|
||||||
site.updated_at = naive_utc_now()
|
site.updated_at = naive_utc_now()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return site
|
return site
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppSite, "/apps/<uuid:app_id>/site")
|
|
||||||
api.add_resource(AppSiteAccessTokenReset, "/apps/<uuid:app_id>/site/access-token-reset")
|
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import pytz
|
|||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
@ -17,11 +17,25 @@ from libs.login import login_required
|
|||||||
from models import AppMode, Message
|
from models import AppMode, Message
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/daily-messages")
|
||||||
class DailyMessageStatistic(Resource):
|
class DailyMessageStatistic(Resource):
|
||||||
|
@api.doc("get_daily_message_statistics")
|
||||||
|
@api.doc(description="Get daily message statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Daily message statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Daily message count data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -74,11 +88,25 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/daily-conversations")
|
||||||
class DailyConversationStatistic(Resource):
|
class DailyConversationStatistic(Resource):
|
||||||
|
@api.doc("get_daily_conversation_statistics")
|
||||||
|
@api.doc(description="Get daily conversation statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Daily conversation statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Daily conversation count data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -126,11 +154,25 @@ class DailyConversationStatistic(Resource):
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/daily-end-users")
|
||||||
class DailyTerminalsStatistic(Resource):
|
class DailyTerminalsStatistic(Resource):
|
||||||
|
@api.doc("get_daily_terminals_statistics")
|
||||||
|
@api.doc(description="Get daily terminal/end-user statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Daily terminal statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Daily terminal count data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -183,11 +225,25 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/token-costs")
|
||||||
class DailyTokenCostStatistic(Resource):
|
class DailyTokenCostStatistic(Resource):
|
||||||
|
@api.doc("get_daily_token_cost_statistics")
|
||||||
|
@api.doc(description="Get daily token cost statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Daily token cost statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Daily token cost data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -243,7 +299,21 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/average-session-interactions")
|
||||||
class AverageSessionInteractionStatistic(Resource):
|
class AverageSessionInteractionStatistic(Resource):
|
||||||
|
@api.doc("get_average_session_interaction_statistics")
|
||||||
|
@api.doc(description="Get average session interaction statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Average session interaction statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Average session interaction data")),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -319,11 +389,25 @@ ORDER BY
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/user-satisfaction-rate")
|
||||||
class UserSatisfactionRateStatistic(Resource):
|
class UserSatisfactionRateStatistic(Resource):
|
||||||
|
@api.doc("get_user_satisfaction_rate_statistics")
|
||||||
|
@api.doc(description="Get user satisfaction rate statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"User satisfaction rate statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="User satisfaction rate data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -385,7 +469,21 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/average-response-time")
|
||||||
class AverageResponseTimeStatistic(Resource):
|
class AverageResponseTimeStatistic(Resource):
|
||||||
|
@api.doc("get_average_response_time_statistics")
|
||||||
|
@api.doc(description="Get average response time statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Average response time statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Average response time data")),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -442,11 +540,25 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/statistics/tokens-per-second")
|
||||||
class TokensPerSecondStatistic(Resource):
|
class TokensPerSecondStatistic(Resource):
|
||||||
|
@api.doc("get_tokens_per_second_statistics")
|
||||||
|
@api.doc(description="Get tokens per second statistics for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("start", type=str, location="args", help="Start date (YYYY-MM-DD HH:MM)")
|
||||||
|
.add_argument("end", type=str, location="args", help="End date (YYYY-MM-DD HH:MM)")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Tokens per second statistics retrieved successfully",
|
||||||
|
fields.List(fields.Raw(description="Tokens per second data")),
|
||||||
|
)
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -500,13 +612,3 @@ WHERE
|
|||||||
response_data.append({"date": str(i.date), "tps": round(i.tokens_per_second, 4)})
|
response_data.append({"date": str(i.date), "tps": round(i.tokens_per_second, 4)})
|
||||||
|
|
||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DailyMessageStatistic, "/apps/<uuid:app_id>/statistics/daily-messages")
|
|
||||||
api.add_resource(DailyConversationStatistic, "/apps/<uuid:app_id>/statistics/daily-conversations")
|
|
||||||
api.add_resource(DailyTerminalsStatistic, "/apps/<uuid:app_id>/statistics/daily-end-users")
|
|
||||||
api.add_resource(DailyTokenCostStatistic, "/apps/<uuid:app_id>/statistics/token-costs")
|
|
||||||
api.add_resource(AverageSessionInteractionStatistic, "/apps/<uuid:app_id>/statistics/average-session-interactions")
|
|
||||||
api.add_resource(UserSatisfactionRateStatistic, "/apps/<uuid:app_id>/statistics/user-satisfaction-rate")
|
|
||||||
api.add_resource(AverageResponseTimeStatistic, "/apps/<uuid:app_id>/statistics/average-response-time")
|
|
||||||
api.add_resource(TokensPerSecondStatistic, "/apps/<uuid:app_id>/statistics/tokens-per-second")
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class DraftWorkflowApi(Resource):
|
|||||||
"""
|
"""
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
# fetch draft workflow by app_model
|
# fetch draft workflow by app_model
|
||||||
@ -92,7 +92,7 @@ class DraftWorkflowApi(Resource):
|
|||||||
"""
|
"""
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
content_type = request.headers.get("Content-Type", "")
|
content_type = request.headers.get("Content-Type", "")
|
||||||
@ -170,7 +170,7 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
"""
|
"""
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
@ -220,7 +220,7 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -256,7 +256,7 @@ class WorkflowDraftRunIterationNodeApi(Resource):
|
|||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -293,7 +293,7 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -330,7 +330,7 @@ class WorkflowDraftRunLoopNodeApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -367,7 +367,7 @@ class DraftWorkflowRunApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -406,7 +406,7 @@ class WorkflowTaskStopApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.DEBUGGER, current_user.id)
|
||||||
@ -428,7 +428,7 @@ class DraftWorkflowNodeRunApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -476,7 +476,7 @@ class PublishedWorkflowApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
# fetch published workflow by app_model
|
# fetch published workflow by app_model
|
||||||
@ -497,7 +497,7 @@ class PublishedWorkflowApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -547,7 +547,7 @@ class DefaultBlockConfigsApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
# Get default block configs
|
# Get default block configs
|
||||||
@ -567,7 +567,7 @@ class DefaultBlockConfigApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -602,7 +602,7 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# The role of the current user in the ta table must be admin, owner, or editor
|
# The role of the current user in the ta table must be admin, owner, or editor
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
if request.data:
|
if request.data:
|
||||||
@ -651,7 +651,7 @@ class PublishedAllWorkflowApi(Resource):
|
|||||||
|
|
||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -702,7 +702,7 @@ class WorkflowByIdApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# Check permission
|
# Check permission
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -715,7 +715,6 @@ class WorkflowByIdApi(Resource):
|
|||||||
raise ValueError("Marked name cannot exceed 20 characters")
|
raise ValueError("Marked name cannot exceed 20 characters")
|
||||||
if args.marked_comment and len(args.marked_comment) > 100:
|
if args.marked_comment and len(args.marked_comment) > 100:
|
||||||
raise ValueError("Marked comment cannot exceed 100 characters")
|
raise ValueError("Marked comment cannot exceed 100 characters")
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Prepare update data
|
# Prepare update data
|
||||||
update_data = {}
|
update_data = {}
|
||||||
@ -758,7 +757,7 @@ class WorkflowByIdApi(Resource):
|
|||||||
if not isinstance(current_user, Account):
|
if not isinstance(current_user, Account):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
# Check permission
|
# Check permission
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
workflow_service = WorkflowService()
|
workflow_service = WorkflowService()
|
||||||
|
|||||||
@ -137,7 +137,7 @@ def _api_prerequisite(f):
|
|||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -18,10 +18,10 @@ from models.model import AppMode
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowDailyRunsStatistic(Resource):
|
class WorkflowDailyRunsStatistic(Resource):
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -80,10 +80,10 @@ WHERE
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowDailyTerminalsStatistic(Resource):
|
class WorkflowDailyTerminalsStatistic(Resource):
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
@ -142,10 +142,10 @@ WHERE
|
|||||||
|
|
||||||
|
|
||||||
class WorkflowDailyTokenCostStatistic(Resource):
|
class WorkflowDailyTokenCostStatistic(Resource):
|
||||||
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model
|
|
||||||
def get(self, app_model):
|
def get(self, app_model):
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional, Union
|
from typing import Optional, ParamSpec, TypeVar, Union
|
||||||
|
|
||||||
from controllers.console.app.error import AppNotFoundError
|
from controllers.console.app.error import AppNotFoundError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -8,6 +8,9 @@ from libs.login import current_user
|
|||||||
from models import App, AppMode
|
from models import App, AppMode
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
def _load_app_model(app_id: str) -> Optional[App]:
|
def _load_app_model(app_id: str) -> Optional[App]:
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
@ -19,10 +22,10 @@ def _load_app_model(app_id: str) -> Optional[App]:
|
|||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
|
|
||||||
def get_app_model(view: Optional[Callable] = None, *, mode: Union[AppMode, list[AppMode], None] = None):
|
def get_app_model(view: Optional[Callable[P, R]] = None, *, mode: Union[AppMode, list[AppMode], None] = None):
|
||||||
def decorator(view_func):
|
def decorator(view_func: Callable[P, R]):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||||
if not kwargs.get("app_id"):
|
if not kwargs.get("app_id"):
|
||||||
raise ValueError("missing app_id in path parameters")
|
raise ValueError("missing app_id in path parameters")
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from constants.languages import supported_language
|
from constants.languages import supported_language
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.error import AlreadyActivateError
|
from controllers.console.error import AlreadyActivateError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
@ -10,14 +10,36 @@ from libs.helper import StrLen, email, extract_remote_ip, timezone
|
|||||||
from models.account import AccountStatus
|
from models.account import AccountStatus
|
||||||
from services.account_service import AccountService, RegisterService
|
from services.account_service import AccountService, RegisterService
|
||||||
|
|
||||||
|
active_check_parser = reqparse.RequestParser()
|
||||||
|
active_check_parser.add_argument(
|
||||||
|
"workspace_id", type=str, required=False, nullable=True, location="args", help="Workspace ID"
|
||||||
|
)
|
||||||
|
active_check_parser.add_argument(
|
||||||
|
"email", type=email, required=False, nullable=True, location="args", help="Email address"
|
||||||
|
)
|
||||||
|
active_check_parser.add_argument(
|
||||||
|
"token", type=str, required=True, nullable=False, location="args", help="Activation token"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/activate/check")
|
||||||
class ActivateCheckApi(Resource):
|
class ActivateCheckApi(Resource):
|
||||||
|
@api.doc("check_activation_token")
|
||||||
|
@api.doc(description="Check if activation token is valid")
|
||||||
|
@api.expect(active_check_parser)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model(
|
||||||
|
"ActivationCheckResponse",
|
||||||
|
{
|
||||||
|
"is_valid": fields.Boolean(description="Whether token is valid"),
|
||||||
|
"data": fields.Raw(description="Activation data if valid"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
parser = reqparse.RequestParser()
|
args = active_check_parser.parse_args()
|
||||||
parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="args")
|
|
||||||
parser.add_argument("email", type=email, required=False, nullable=True, location="args")
|
|
||||||
parser.add_argument("token", type=str, required=True, nullable=False, location="args")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
workspaceId = args["workspace_id"]
|
workspaceId = args["workspace_id"]
|
||||||
reg_email = args["email"]
|
reg_email = args["email"]
|
||||||
@ -38,18 +60,36 @@ class ActivateCheckApi(Resource):
|
|||||||
return {"is_valid": False}
|
return {"is_valid": False}
|
||||||
|
|
||||||
|
|
||||||
|
active_parser = reqparse.RequestParser()
|
||||||
|
active_parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json")
|
||||||
|
active_parser.add_argument("email", type=email, required=False, nullable=True, location="json")
|
||||||
|
active_parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
|
active_parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
|
||||||
|
active_parser.add_argument(
|
||||||
|
"interface_language", type=supported_language, required=True, nullable=False, location="json"
|
||||||
|
)
|
||||||
|
active_parser.add_argument("timezone", type=timezone, required=True, nullable=False, location="json")
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/activate")
|
||||||
class ActivateApi(Resource):
|
class ActivateApi(Resource):
|
||||||
|
@api.doc("activate_account")
|
||||||
|
@api.doc(description="Activate account with invitation token")
|
||||||
|
@api.expect(active_parser)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Account activated successfully",
|
||||||
|
api.model(
|
||||||
|
"ActivationResponse",
|
||||||
|
{
|
||||||
|
"result": fields.String(description="Operation result"),
|
||||||
|
"data": fields.Raw(description="Login token data"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@api.response(400, "Already activated or invalid token")
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
args = active_parser.parse_args()
|
||||||
parser.add_argument("workspace_id", type=str, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument("email", type=email, required=False, nullable=True, location="json")
|
|
||||||
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
|
||||||
parser.add_argument("name", type=StrLen(30), required=True, nullable=False, location="json")
|
|
||||||
parser.add_argument(
|
|
||||||
"interface_language", type=supported_language, required=True, nullable=False, location="json"
|
|
||||||
)
|
|
||||||
parser.add_argument("timezone", type=timezone, required=True, nullable=False, location="json")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
invitation = RegisterService.get_invitation_if_token_valid(args["workspace_id"], args["email"], args["token"])
|
invitation = RegisterService.get_invitation_if_token_valid(args["workspace_id"], args["email"], args["token"])
|
||||||
if invitation is None:
|
if invitation is None:
|
||||||
@ -70,7 +110,3 @@ class ActivateApi(Resource):
|
|||||||
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
token_pair = AccountService.login(account, ip_address=extract_remote_ip(request))
|
||||||
|
|
||||||
return {"result": "success", "data": token_pair.model_dump()}
|
return {"result": "success", "data": token_pair.model_dump()}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ActivateCheckApi, "/activate/check")
|
|
||||||
api.add_resource(ActivateApi, "/activate")
|
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
from flask import current_app, redirect, request
|
from flask import current_app, redirect, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource, fields
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from libs.oauth_data_source import NotionOAuth
|
from libs.oauth_data_source import NotionOAuth
|
||||||
|
|
||||||
@ -28,7 +28,21 @@ def get_oauth_providers():
|
|||||||
return OAUTH_PROVIDERS
|
return OAUTH_PROVIDERS
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/data-source/<string:provider>")
|
||||||
class OAuthDataSource(Resource):
|
class OAuthDataSource(Resource):
|
||||||
|
@api.doc("oauth_data_source")
|
||||||
|
@api.doc(description="Get OAuth authorization URL for data source provider")
|
||||||
|
@api.doc(params={"provider": "Data source provider name (notion)"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Authorization URL or internal setup success",
|
||||||
|
api.model(
|
||||||
|
"OAuthDataSourceResponse",
|
||||||
|
{"data": fields.Raw(description="Authorization URL or 'internal' for internal setup")},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid provider")
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
# The role of the current user in the table must be admin or owner
|
# The role of the current user in the table must be admin or owner
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
@ -49,7 +63,19 @@ class OAuthDataSource(Resource):
|
|||||||
return {"data": auth_url}, 200
|
return {"data": auth_url}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/data-source/callback/<string:provider>")
|
||||||
class OAuthDataSourceCallback(Resource):
|
class OAuthDataSourceCallback(Resource):
|
||||||
|
@api.doc("oauth_data_source_callback")
|
||||||
|
@api.doc(description="Handle OAuth callback from data source provider")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"provider": "Data source provider name (notion)",
|
||||||
|
"code": "Authorization code from OAuth provider",
|
||||||
|
"error": "Error message from OAuth provider",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(302, "Redirect to console with result")
|
||||||
|
@api.response(400, "Invalid provider")
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
@ -68,7 +94,19 @@ class OAuthDataSourceCallback(Resource):
|
|||||||
return redirect(f"{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied")
|
return redirect(f"{dify_config.CONSOLE_WEB_URL}?type=notion&error=Access denied")
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/data-source/binding/<string:provider>")
|
||||||
class OAuthDataSourceBinding(Resource):
|
class OAuthDataSourceBinding(Resource):
|
||||||
|
@api.doc("oauth_data_source_binding")
|
||||||
|
@api.doc(description="Bind OAuth data source with authorization code")
|
||||||
|
@api.doc(
|
||||||
|
params={"provider": "Data source provider name (notion)", "code": "Authorization code from OAuth provider"}
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Data source binding success",
|
||||||
|
api.model("OAuthDataSourceBindingResponse", {"result": fields.String(description="Operation result")}),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid provider or code")
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
OAUTH_DATASOURCE_PROVIDERS = get_oauth_providers()
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
@ -90,7 +128,17 @@ class OAuthDataSourceBinding(Resource):
|
|||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/data-source/<string:provider>/<uuid:binding_id>/sync")
|
||||||
class OAuthDataSourceSync(Resource):
|
class OAuthDataSourceSync(Resource):
|
||||||
|
@api.doc("oauth_data_source_sync")
|
||||||
|
@api.doc(description="Sync data from OAuth data source")
|
||||||
|
@api.doc(params={"provider": "Data source provider name (notion)", "binding_id": "Data source binding ID"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Data source sync success",
|
||||||
|
api.model("OAuthDataSourceSyncResponse", {"result": fields.String(description="Operation result")}),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid provider or sync failed")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -111,9 +159,3 @@ class OAuthDataSourceSync(Resource):
|
|||||||
return {"error": "OAuth data source process failed"}, 400
|
return {"error": "OAuth data source process failed"}, 400
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(OAuthDataSource, "/oauth/data-source/<string:provider>")
|
|
||||||
api.add_resource(OAuthDataSourceCallback, "/oauth/data-source/callback/<string:provider>")
|
|
||||||
api.add_resource(OAuthDataSourceBinding, "/oauth/data-source/binding/<string:provider>")
|
|
||||||
api.add_resource(OAuthDataSourceSync, "/oauth/data-source/<string:provider>/<uuid:binding_id>/sync")
|
|
||||||
|
|||||||
155
api/controllers/console/auth/email_register.py
Normal file
155
api/controllers/console/auth/email_register.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
from flask import request
|
||||||
|
from flask_restx import Resource, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from constants.languages import languages
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.auth.error import (
|
||||||
|
EmailAlreadyInUseError,
|
||||||
|
EmailCodeError,
|
||||||
|
EmailRegisterLimitError,
|
||||||
|
InvalidEmailError,
|
||||||
|
InvalidTokenError,
|
||||||
|
PasswordMismatchError,
|
||||||
|
)
|
||||||
|
from controllers.console.error import AccountInFreezeError, EmailSendIpLimitError
|
||||||
|
from controllers.console.wraps import email_password_login_enabled, email_register_enabled, setup_required
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from libs.helper import email, extract_remote_ip
|
||||||
|
from libs.password import valid_password
|
||||||
|
from models.account import Account
|
||||||
|
from services.account_service import AccountService
|
||||||
|
from services.billing_service import BillingService
|
||||||
|
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterSendEmailApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
parser.add_argument("language", type=str, required=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
ip_address = extract_remote_ip(request)
|
||||||
|
if AccountService.is_email_send_ip_limit(ip_address):
|
||||||
|
raise EmailSendIpLimitError()
|
||||||
|
language = "en-US"
|
||||||
|
if args["language"] in languages:
|
||||||
|
language = args["language"]
|
||||||
|
|
||||||
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
||||||
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
||||||
|
token = None
|
||||||
|
token = AccountService.send_email_register_email(email=args["email"], account=account, language=language)
|
||||||
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterCheckApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("code", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
user_email = args["email"]
|
||||||
|
|
||||||
|
is_email_register_error_rate_limit = AccountService.is_email_register_error_rate_limit(args["email"])
|
||||||
|
if is_email_register_error_rate_limit:
|
||||||
|
raise EmailRegisterLimitError()
|
||||||
|
|
||||||
|
token_data = AccountService.get_email_register_data(args["token"])
|
||||||
|
if token_data is None:
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
if user_email != token_data.get("email"):
|
||||||
|
raise InvalidEmailError()
|
||||||
|
|
||||||
|
if args["code"] != token_data.get("code"):
|
||||||
|
AccountService.add_email_register_error_rate_limit(args["email"])
|
||||||
|
raise EmailCodeError()
|
||||||
|
|
||||||
|
# Verified, revoke the first token
|
||||||
|
AccountService.revoke_email_register_token(args["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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
AccountService.reset_email_register_error_rate_limit(args["email"])
|
||||||
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterResetApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
|
@email_register_enabled
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("new_password", type=valid_password, required=True, nullable=False, location="json")
|
||||||
|
parser.add_argument("password_confirm", type=valid_password, required=True, nullable=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Validate passwords match
|
||||||
|
if args["new_password"] != args["password_confirm"]:
|
||||||
|
raise PasswordMismatchError()
|
||||||
|
|
||||||
|
# Validate token and get register data
|
||||||
|
register_data = AccountService.get_email_register_data(args["token"])
|
||||||
|
if not register_data:
|
||||||
|
raise InvalidTokenError()
|
||||||
|
# Must use token in reset phase
|
||||||
|
if register_data.get("phase", "") != "register":
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
# Revoke token to prevent reuse
|
||||||
|
AccountService.revoke_email_register_token(args["token"])
|
||||||
|
|
||||||
|
email = register_data.get("email", "")
|
||||||
|
|
||||||
|
with Session(db.engine) as session:
|
||||||
|
account = session.execute(select(Account).filter_by(email=email)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if account:
|
||||||
|
raise EmailAlreadyInUseError()
|
||||||
|
else:
|
||||||
|
account = self._create_new_account(email, args["password_confirm"])
|
||||||
|
if not account:
|
||||||
|
raise AccountNotFoundError()
|
||||||
|
token_pair = AccountService.login(account=account, ip_address=extract_remote_ip(request))
|
||||||
|
AccountService.reset_login_error_rate_limit(email)
|
||||||
|
|
||||||
|
return {"result": "success", "data": token_pair.model_dump()}
|
||||||
|
|
||||||
|
def _create_new_account(self, email, password) -> Account | None:
|
||||||
|
# Create new account if allowed
|
||||||
|
account = None
|
||||||
|
try:
|
||||||
|
account = AccountService.create_account_and_tenant(
|
||||||
|
email=email,
|
||||||
|
name=email,
|
||||||
|
password=password,
|
||||||
|
interface_language=languages[0],
|
||||||
|
)
|
||||||
|
except AccountRegisterError:
|
||||||
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(EmailRegisterSendEmailApi, "/email-register/send-email")
|
||||||
|
api.add_resource(EmailRegisterCheckApi, "/email-register/validity")
|
||||||
|
api.add_resource(EmailRegisterResetApi, "/email-register")
|
||||||
@ -27,21 +27,43 @@ class InvalidTokenError(BaseHTTPException):
|
|||||||
|
|
||||||
class PasswordResetRateLimitExceededError(BaseHTTPException):
|
class PasswordResetRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "password_reset_rate_limit_exceeded"
|
error_code = "password_reset_rate_limit_exceeded"
|
||||||
description = "Too many password reset emails have been sent. Please try again in 1 minute."
|
description = "Too many password reset emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterRateLimitExceededError(BaseHTTPException):
|
||||||
|
error_code = "email_register_rate_limit_exceeded"
|
||||||
|
description = "Too many email register emails have been sent. Please try again in {minutes} minutes."
|
||||||
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailChangeRateLimitExceededError(BaseHTTPException):
|
class EmailChangeRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_change_rate_limit_exceeded"
|
error_code = "email_change_rate_limit_exceeded"
|
||||||
description = "Too many email change emails have been sent. Please try again in 1 minute."
|
description = "Too many email change emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class OwnerTransferRateLimitExceededError(BaseHTTPException):
|
class OwnerTransferRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "owner_transfer_rate_limit_exceeded"
|
error_code = "owner_transfer_rate_limit_exceeded"
|
||||||
description = "Too many owner transfer emails have been sent. Please try again in 1 minute."
|
description = "Too many owner transfer emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 1):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailCodeError(BaseHTTPException):
|
class EmailCodeError(BaseHTTPException):
|
||||||
error_code = "email_code_error"
|
error_code = "email_code_error"
|
||||||
@ -69,15 +91,23 @@ class EmailPasswordLoginLimitError(BaseHTTPException):
|
|||||||
|
|
||||||
class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
|
class EmailCodeLoginRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_code_login_rate_limit_exceeded"
|
error_code = "email_code_login_rate_limit_exceeded"
|
||||||
description = "Too many login emails have been sent. Please try again in 5 minutes."
|
description = "Too many login emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 5):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
||||||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
||||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
description = "Too many account deletion emails have been sent. Please try again in {minutes} minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
def __init__(self, minutes: int = 5):
|
||||||
|
description = self.description.format(minutes=int(minutes)) if self.description else None
|
||||||
|
super().__init__(description=description)
|
||||||
|
|
||||||
|
|
||||||
class EmailPasswordResetLimitError(BaseHTTPException):
|
class EmailPasswordResetLimitError(BaseHTTPException):
|
||||||
error_code = "email_password_reset_limit"
|
error_code = "email_password_reset_limit"
|
||||||
@ -85,6 +115,12 @@ class EmailPasswordResetLimitError(BaseHTTPException):
|
|||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRegisterLimitError(BaseHTTPException):
|
||||||
|
error_code = "email_register_limit"
|
||||||
|
description = "Too many failed email register attempts. Please try again in 24 hours."
|
||||||
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
class EmailChangeLimitError(BaseHTTPException):
|
class EmailChangeLimitError(BaseHTTPException):
|
||||||
error_code = "email_change_limit"
|
error_code = "email_change_limit"
|
||||||
description = "Too many failed email change attempts. Please try again in 24 hours."
|
description = "Too many failed email change attempts. Please try again in 24 hours."
|
||||||
|
|||||||
@ -2,12 +2,11 @@ import base64
|
|||||||
import secrets
|
import secrets
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from constants.languages import languages
|
from controllers.console import api, console_ns
|
||||||
from controllers.console import api
|
|
||||||
from controllers.console.auth.error import (
|
from controllers.console.auth.error import (
|
||||||
EmailCodeError,
|
EmailCodeError,
|
||||||
EmailPasswordResetLimitError,
|
EmailPasswordResetLimitError,
|
||||||
@ -15,7 +14,7 @@ from controllers.console.auth.error import (
|
|||||||
InvalidTokenError,
|
InvalidTokenError,
|
||||||
PasswordMismatchError,
|
PasswordMismatchError,
|
||||||
)
|
)
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
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, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -23,12 +22,35 @@ from libs.helper import email, extract_remote_ip
|
|||||||
from libs.password import hash_password, valid_password
|
from libs.password import hash_password, valid_password
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, TenantService
|
from services.account_service import AccountService, TenantService
|
||||||
from services.errors.account import AccountRegisterError
|
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkspacesLimitExceededError
|
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/forgot-password")
|
||||||
class ForgotPasswordSendEmailApi(Resource):
|
class ForgotPasswordSendEmailApi(Resource):
|
||||||
|
@api.doc("send_forgot_password_email")
|
||||||
|
@api.doc(description="Send password reset email")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ForgotPasswordEmailRequest",
|
||||||
|
{
|
||||||
|
"email": fields.String(required=True, description="Email address"),
|
||||||
|
"language": fields.String(description="Language for email (zh-Hans/en-US)"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Email sent successfully",
|
||||||
|
api.model(
|
||||||
|
"ForgotPasswordEmailResponse",
|
||||||
|
{
|
||||||
|
"result": fields.String(description="Operation result"),
|
||||||
|
"data": fields.String(description="Reset token"),
|
||||||
|
"code": fields.String(description="Error code if account not found"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid email or rate limit exceeded")
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
@ -48,20 +70,44 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
|
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
account = session.execute(select(Account).filter_by(email=args["email"])).scalar_one_or_none()
|
||||||
token = None
|
|
||||||
if account is None:
|
token = AccountService.send_reset_password_email(
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
account=account,
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
email=args["email"],
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
language=language,
|
||||||
else:
|
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||||
raise AccountNotFound()
|
)
|
||||||
else:
|
|
||||||
token = AccountService.send_reset_password_email(account=account, email=args["email"], language=language)
|
|
||||||
|
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/forgot-password/validity")
|
||||||
class ForgotPasswordCheckApi(Resource):
|
class ForgotPasswordCheckApi(Resource):
|
||||||
|
@api.doc("check_forgot_password_code")
|
||||||
|
@api.doc(description="Verify password reset code")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ForgotPasswordCheckRequest",
|
||||||
|
{
|
||||||
|
"email": fields.String(required=True, description="Email address"),
|
||||||
|
"code": fields.String(required=True, description="Verification code"),
|
||||||
|
"token": fields.String(required=True, description="Reset token"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Code verified successfully",
|
||||||
|
api.model(
|
||||||
|
"ForgotPasswordCheckResponse",
|
||||||
|
{
|
||||||
|
"is_valid": fields.Boolean(description="Whether code is valid"),
|
||||||
|
"email": fields.String(description="Email address"),
|
||||||
|
"token": fields.String(description="New reset token"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid code or token")
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
@ -100,7 +146,26 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/forgot-password/resets")
|
||||||
class ForgotPasswordResetApi(Resource):
|
class ForgotPasswordResetApi(Resource):
|
||||||
|
@api.doc("reset_password")
|
||||||
|
@api.doc(description="Reset password with verification token")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ForgotPasswordResetRequest",
|
||||||
|
{
|
||||||
|
"token": fields.String(required=True, description="Verification token"),
|
||||||
|
"new_password": fields.String(required=True, description="New password"),
|
||||||
|
"password_confirm": fields.String(required=True, description="Password confirmation"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Password reset successfully",
|
||||||
|
api.model("ForgotPasswordResetResponse", {"result": fields.String(description="Operation result")}),
|
||||||
|
)
|
||||||
|
@api.response(400, "Invalid token or password mismatch")
|
||||||
@setup_required
|
@setup_required
|
||||||
@email_password_login_enabled
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
@ -137,7 +202,7 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
if account:
|
if account:
|
||||||
self._update_existing_account(account, password_hashed, salt, session)
|
self._update_existing_account(account, password_hashed, salt, session)
|
||||||
else:
|
else:
|
||||||
self._create_new_account(email, args["password_confirm"])
|
raise AccountNotFound()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
@ -157,22 +222,6 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
account.current_tenant = tenant
|
account.current_tenant = tenant
|
||||||
tenant_was_created.send(tenant)
|
tenant_was_created.send(tenant)
|
||||||
|
|
||||||
def _create_new_account(self, email, password):
|
|
||||||
# Create new account if allowed
|
|
||||||
try:
|
|
||||||
AccountService.create_account_and_tenant(
|
|
||||||
email=email,
|
|
||||||
name=email,
|
|
||||||
password=password,
|
|
||||||
interface_language=languages[0],
|
|
||||||
)
|
|
||||||
except WorkSpaceNotAllowedCreateError:
|
|
||||||
pass
|
|
||||||
except WorkspacesLimitExceededError:
|
|
||||||
pass
|
|
||||||
except AccountRegisterError:
|
|
||||||
raise AccountInFreezeError()
|
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
|
api.add_resource(ForgotPasswordSendEmailApi, "/forgot-password")
|
||||||
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
|
api.add_resource(ForgotPasswordCheckApi, "/forgot-password/validity")
|
||||||
|
|||||||
@ -26,7 +26,6 @@ from controllers.console.error import (
|
|||||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
from libs.password import valid_password
|
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
from services.billing_service import BillingService
|
from services.billing_service import BillingService
|
||||||
@ -44,10 +43,9 @@ class LoginApi(Resource):
|
|||||||
"""Authenticate user and login."""
|
"""Authenticate user and login."""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
parser.add_argument("password", type=valid_password, required=True, location="json")
|
parser.add_argument("password", type=str, required=True, location="json")
|
||||||
parser.add_argument("remember_me", type=bool, required=False, default=False, location="json")
|
parser.add_argument("remember_me", type=bool, required=False, default=False, location="json")
|
||||||
parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
|
parser.add_argument("invite_token", type=str, required=False, default=None, location="json")
|
||||||
parser.add_argument("language", type=str, required=False, default="en-US", location="json")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(args["email"]):
|
||||||
@ -61,11 +59,6 @@ class LoginApi(Resource):
|
|||||||
if invitation:
|
if invitation:
|
||||||
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)
|
invitation = RegisterService.get_invitation_if_token_valid(None, args["email"], invitation)
|
||||||
|
|
||||||
if args["language"] is not None and args["language"] == "zh-Hans":
|
|
||||||
language = "zh-Hans"
|
|
||||||
else:
|
|
||||||
language = "en-US"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if invitation:
|
if invitation:
|
||||||
data = invitation.get("data", {})
|
data = invitation.get("data", {})
|
||||||
@ -80,12 +73,6 @@ class LoginApi(Resource):
|
|||||||
except services.errors.account.AccountPasswordError:
|
except services.errors.account.AccountPasswordError:
|
||||||
AccountService.add_login_error_rate_limit(args["email"])
|
AccountService.add_login_error_rate_limit(args["email"])
|
||||||
raise AuthenticationFailedError()
|
raise AuthenticationFailedError()
|
||||||
except services.errors.account.AccountNotFoundError:
|
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
|
||||||
return {"result": "fail", "data": token, "code": "account_not_found"}
|
|
||||||
else:
|
|
||||||
raise AccountNotFound()
|
|
||||||
# SELF_HOSTED only have one workspace
|
# SELF_HOSTED only have one workspace
|
||||||
tenants = TenantService.get_join_tenants(account)
|
tenants = TenantService.get_join_tenants(account)
|
||||||
if len(tenants) == 0:
|
if len(tenants) == 0:
|
||||||
@ -133,13 +120,12 @@ class ResetPasswordSendEmailApi(Resource):
|
|||||||
except AccountRegisterError:
|
except AccountRegisterError:
|
||||||
raise AccountInFreezeError()
|
raise AccountInFreezeError()
|
||||||
|
|
||||||
if account is None:
|
token = AccountService.send_reset_password_email(
|
||||||
if FeatureService.get_system_features().is_allow_register:
|
email=args["email"],
|
||||||
token = AccountService.send_reset_password_email(email=args["email"], language=language)
|
account=account,
|
||||||
else:
|
language=language,
|
||||||
raise AccountNotFound()
|
is_allow_register=FeatureService.get_system_features().is_allow_register,
|
||||||
else:
|
)
|
||||||
token = AccountService.send_reset_password_email(account=account, language=language)
|
|
||||||
|
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|||||||
@ -18,11 +18,12 @@ from libs.oauth import GitHubOAuth, GoogleOAuth, OAuthUserInfo
|
|||||||
from models import Account
|
from models import Account
|
||||||
from models.account import AccountStatus
|
from models.account import AccountStatus
|
||||||
from services.account_service import AccountService, RegisterService, TenantService
|
from services.account_service import AccountService, RegisterService, TenantService
|
||||||
|
from services.billing_service import BillingService
|
||||||
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
from services.errors.account import AccountNotFoundError, AccountRegisterError
|
||||||
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
from services.errors.workspace import WorkSpaceNotAllowedCreateError, WorkSpaceNotFoundError
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
from .. import api
|
from .. import api, console_ns
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -50,7 +51,13 @@ def get_oauth_providers():
|
|||||||
return OAUTH_PROVIDERS
|
return OAUTH_PROVIDERS
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/login/<provider>")
|
||||||
class OAuthLogin(Resource):
|
class OAuthLogin(Resource):
|
||||||
|
@api.doc("oauth_login")
|
||||||
|
@api.doc(description="Initiate OAuth login process")
|
||||||
|
@api.doc(params={"provider": "OAuth provider name (github/google)", "invite_token": "Optional invitation token"})
|
||||||
|
@api.response(302, "Redirect to OAuth authorization URL")
|
||||||
|
@api.response(400, "Invalid provider")
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
invite_token = request.args.get("invite_token") or None
|
invite_token = request.args.get("invite_token") or None
|
||||||
OAUTH_PROVIDERS = get_oauth_providers()
|
OAUTH_PROVIDERS = get_oauth_providers()
|
||||||
@ -63,7 +70,19 @@ class OAuthLogin(Resource):
|
|||||||
return redirect(auth_url)
|
return redirect(auth_url)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/oauth/authorize/<provider>")
|
||||||
class OAuthCallback(Resource):
|
class OAuthCallback(Resource):
|
||||||
|
@api.doc("oauth_callback")
|
||||||
|
@api.doc(description="Handle OAuth callback and complete login process")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"provider": "OAuth provider name (github/google)",
|
||||||
|
"code": "Authorization code from OAuth provider",
|
||||||
|
"state": "Optional state parameter (used for invite token)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(302, "Redirect to console with access token")
|
||||||
|
@api.response(400, "OAuth process failed")
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
OAUTH_PROVIDERS = get_oauth_providers()
|
OAUTH_PROVIDERS = get_oauth_providers()
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
@ -77,6 +96,9 @@ class OAuthCallback(Resource):
|
|||||||
if state:
|
if state:
|
||||||
invite_token = state
|
invite_token = state
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
return {"error": "Authorization code is required"}, 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = oauth_provider.get_access_token(code)
|
token = oauth_provider.get_access_token(code)
|
||||||
user_info = oauth_provider.get_user_info(token)
|
user_info = oauth_provider.get_user_info(token)
|
||||||
@ -86,7 +108,7 @@ class OAuthCallback(Resource):
|
|||||||
return {"error": "OAuth process failed"}, 400
|
return {"error": "OAuth process failed"}, 400
|
||||||
|
|
||||||
if invite_token and RegisterService.is_valid_invite_token(invite_token):
|
if invite_token and RegisterService.is_valid_invite_token(invite_token):
|
||||||
invitation = RegisterService._get_invitation_by_token(token=invite_token)
|
invitation = RegisterService.get_invitation_by_token(token=invite_token)
|
||||||
if invitation:
|
if invitation:
|
||||||
invitation_email = invitation.get("email", None)
|
invitation_email = invitation.get("email", None)
|
||||||
if invitation_email != user_info.email:
|
if invitation_email != user_info.email:
|
||||||
@ -162,7 +184,15 @@ def _generate_account(provider: str, user_info: OAuthUserInfo):
|
|||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
if not FeatureService.get_system_features().is_allow_register:
|
if not FeatureService.get_system_features().is_allow_register:
|
||||||
raise AccountNotFoundError()
|
if dify_config.BILLING_ENABLED and BillingService.is_email_in_freeze(user_info.email):
|
||||||
|
raise AccountRegisterError(
|
||||||
|
description=(
|
||||||
|
"This email account has been deleted within the past "
|
||||||
|
"30 days and is temporarily unavailable for new account registration"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AccountRegisterError(description=("Invalid email or password"))
|
||||||
account_name = user_info.name or "Dify"
|
account_name = user_info.name or "Dify"
|
||||||
account = RegisterService.register(
|
account = RegisterService.register(
|
||||||
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
|
email=user_info.email, name=account_name, password=None, open_id=user_info.id, provider=provider
|
||||||
@ -181,7 +211,3 @@ def _generate_account(provider: str, user_info: OAuthUserInfo):
|
|||||||
AccountService.link_account_integrate(provider, user_info.id, account)
|
AccountService.link_account_integrate(provider, user_info.id, account)
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(OAuthLogin, "/oauth/login/<provider>")
|
|
||||||
api.add_resource(OAuthCallback, "/oauth/authorize/<provider>")
|
|
||||||
|
|||||||
@ -29,14 +29,12 @@ class DataSourceApi(Resource):
|
|||||||
@marshal_with(integrate_list_fields)
|
@marshal_with(integrate_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
# get workspace data source integrates
|
# get workspace data source integrates
|
||||||
data_source_integrates = (
|
data_source_integrates = db.session.scalars(
|
||||||
db.session.query(DataSourceOauthBinding)
|
select(DataSourceOauthBinding).where(
|
||||||
.where(
|
|
||||||
DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
|
DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
|
||||||
DataSourceOauthBinding.disabled == False,
|
DataSourceOauthBinding.disabled == False,
|
||||||
)
|
)
|
||||||
.all()
|
).all()
|
||||||
)
|
|
||||||
|
|
||||||
base_url = request.url_root.rstrip("/")
|
base_url = request.url_root.rstrip("/")
|
||||||
data_source_oauth_base_path = "/console/api/oauth/data-source"
|
data_source_oauth_base_path = "/console/api/oauth/data-source"
|
||||||
@ -249,7 +247,7 @@ class DataSourceNotionDatasetSyncApi(Resource):
|
|||||||
documents = DocumentService.get_document_by_dataset_id(dataset_id_str)
|
documents = DocumentService.get_document_by_dataset_id(dataset_id_str)
|
||||||
for document in documents:
|
for document in documents:
|
||||||
document_indexing_sync_task.delay(dataset_id_str, document.id)
|
document_indexing_sync_task.delay(dataset_id_str, document.id)
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
class DataSourceNotionDocumentSyncApi(Resource):
|
class DataSourceNotionDocumentSyncApi(Resource):
|
||||||
@ -267,7 +265,7 @@ class DataSourceNotionDocumentSyncApi(Resource):
|
|||||||
if document is None:
|
if document is None:
|
||||||
raise NotFound("Document not found.")
|
raise NotFound("Document not found.")
|
||||||
document_indexing_sync_task.delay(dataset_id_str, document_id_str)
|
document_indexing_sync_task.delay(dataset_id_str, document_id_str)
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DataSourceApi, "/data-source/integrates", "/data-source/integrates/<uuid:binding_id>/<string:action>")
|
api.add_resource(DataSourceApi, "/data-source/integrates", "/data-source/integrates/<uuid:binding_id>/<string:action>")
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import flask_restx
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, marshal, marshal_with, reqparse
|
from flask_restx import Resource, marshal, marshal_with, reqparse
|
||||||
|
from sqlalchemy import select
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
@ -411,11 +412,11 @@ class DatasetIndexingEstimateApi(Resource):
|
|||||||
extract_settings = []
|
extract_settings = []
|
||||||
if args["info_list"]["data_source_type"] == "upload_file":
|
if args["info_list"]["data_source_type"] == "upload_file":
|
||||||
file_ids = args["info_list"]["file_info_list"]["file_ids"]
|
file_ids = args["info_list"]["file_info_list"]["file_ids"]
|
||||||
file_details = (
|
file_details = db.session.scalars(
|
||||||
db.session.query(UploadFile)
|
select(UploadFile).where(
|
||||||
.where(UploadFile.tenant_id == current_user.current_tenant_id, UploadFile.id.in_(file_ids))
|
UploadFile.tenant_id == current_user.current_tenant_id, UploadFile.id.in_(file_ids)
|
||||||
.all()
|
)
|
||||||
)
|
).all()
|
||||||
|
|
||||||
if file_details is None:
|
if file_details is None:
|
||||||
raise NotFound("File not found.")
|
raise NotFound("File not found.")
|
||||||
@ -518,11 +519,11 @@ class DatasetIndexingStatusApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self, dataset_id):
|
def get(self, dataset_id):
|
||||||
dataset_id = str(dataset_id)
|
dataset_id = str(dataset_id)
|
||||||
documents = (
|
documents = db.session.scalars(
|
||||||
db.session.query(Document)
|
select(Document).where(
|
||||||
.where(Document.dataset_id == dataset_id, Document.tenant_id == current_user.current_tenant_id)
|
Document.dataset_id == dataset_id, Document.tenant_id == current_user.current_tenant_id
|
||||||
.all()
|
)
|
||||||
)
|
).all()
|
||||||
documents_status = []
|
documents_status = []
|
||||||
for document in documents:
|
for document in documents:
|
||||||
completed_segments = (
|
completed_segments = (
|
||||||
@ -569,11 +570,11 @@ class DatasetApiKeyApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(api_key_list)
|
@marshal_with(api_key_list)
|
||||||
def get(self):
|
def get(self):
|
||||||
keys = (
|
keys = db.session.scalars(
|
||||||
db.session.query(ApiToken)
|
select(ApiToken).where(
|
||||||
.where(ApiToken.type == self.resource_type, ApiToken.tenant_id == current_user.current_tenant_id)
|
ApiToken.type == self.resource_type, ApiToken.tenant_id == current_user.current_tenant_id
|
||||||
.all()
|
)
|
||||||
)
|
).all()
|
||||||
return {"items": keys}
|
return {"items": keys}
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from argparse import ArgumentTypeError
|
from argparse import ArgumentTypeError
|
||||||
|
from collections.abc import Sequence
|
||||||
from typing import Literal, cast
|
from typing import Literal, cast
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -79,7 +80,7 @@ class DocumentResource(Resource):
|
|||||||
|
|
||||||
return document
|
return document
|
||||||
|
|
||||||
def get_batch_documents(self, dataset_id: str, batch: str) -> list[Document]:
|
def get_batch_documents(self, dataset_id: str, batch: str) -> Sequence[Document]:
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
if not dataset:
|
if not dataset:
|
||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
|
|||||||
@ -113,7 +113,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
|
|||||||
MetadataService.enable_built_in_field(dataset)
|
MetadataService.enable_built_in_field(dataset)
|
||||||
elif action == "disable":
|
elif action == "disable":
|
||||||
MetadataService.disable_built_in_field(dataset)
|
MetadataService.disable_built_in_field(dataset)
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
class DocumentMetadataEditApi(Resource):
|
class DocumentMetadataEditApi(Resource):
|
||||||
@ -135,7 +135,7 @@ class DocumentMetadataEditApi(Resource):
|
|||||||
|
|
||||||
MetadataService.update_documents_metadata(dataset, metadata_args)
|
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||||
|
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DatasetMetadataCreateApi, "/datasets/<uuid:dataset_id>/metadata")
|
api.add_resource(DatasetMetadataCreateApi, "/datasets/<uuid:dataset_id>/metadata")
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_restx import reqparse
|
from flask_restx import reqparse
|
||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import InternalServerError, NotFound
|
||||||
|
|
||||||
@ -28,6 +27,8 @@ from extensions.ext_database import db
|
|||||||
from libs import helper
|
from libs import helper
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
|
from libs.login import current_user
|
||||||
|
from models import Account
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.llm import InvokeRateLimitError
|
from services.errors.llm import InvokeRateLimitError
|
||||||
@ -57,6 +58,8 @@ class CompletionApi(InstalledAppResource):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
response = AppGenerateService.generate(
|
response = AppGenerateService.generate(
|
||||||
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming
|
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=streaming
|
||||||
)
|
)
|
||||||
@ -90,6 +93,8 @@ class CompletionStopApi(InstalledAppResource):
|
|||||||
if app_model.mode != "completion":
|
if app_model.mode != "completion":
|
||||||
raise NotCompletionAppError()
|
raise NotCompletionAppError()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
@ -117,6 +122,8 @@ class ChatApi(InstalledAppResource):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
response = AppGenerateService.generate(
|
response = AppGenerateService.generate(
|
||||||
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True
|
app_model=app_model, user=current_user, args=args, invoke_from=InvokeFrom.EXPLORE, streaming=True
|
||||||
)
|
)
|
||||||
@ -153,6 +160,8 @@ class ChatStopApi(InstalledAppResource):
|
|||||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||||
raise NotChatAppError()
|
raise NotChatAppError()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.EXPLORE, current_user.id)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_restx import marshal_with, reqparse
|
from flask_restx import marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@ -10,6 +9,8 @@ from core.app.entities.app_invoke_entities import InvokeFrom
|
|||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.conversation_fields import conversation_infinite_scroll_pagination_fields, simple_conversation_fields
|
from fields.conversation_fields import conversation_infinite_scroll_pagination_fields, simple_conversation_fields
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
|
from libs.login import current_user
|
||||||
|
from models import Account
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.conversation_service import ConversationService
|
from services.conversation_service import ConversationService
|
||||||
from services.errors.conversation import ConversationNotExistsError, LastConversationNotExistsError
|
from services.errors.conversation import ConversationNotExistsError, LastConversationNotExistsError
|
||||||
@ -35,6 +36,8 @@ class ConversationListApi(InstalledAppResource):
|
|||||||
pinned = args["pinned"] == "true"
|
pinned = args["pinned"] == "true"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
return WebConversationService.pagination_by_last_id(
|
return WebConversationService.pagination_by_last_id(
|
||||||
session=session,
|
session=session,
|
||||||
@ -58,6 +61,8 @@ class ConversationApi(InstalledAppResource):
|
|||||||
|
|
||||||
conversation_id = str(c_id)
|
conversation_id = str(c_id)
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
ConversationService.delete(app_model, conversation_id, current_user)
|
ConversationService.delete(app_model, conversation_id, current_user)
|
||||||
except ConversationNotExistsError:
|
except ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
@ -81,6 +86,8 @@ class ConversationRenameApi(InstalledAppResource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
return ConversationService.rename(
|
return ConversationService.rename(
|
||||||
app_model, conversation_id, current_user, args["name"], args["auto_generate"]
|
app_model, conversation_id, current_user, args["name"], args["auto_generate"]
|
||||||
)
|
)
|
||||||
@ -98,6 +105,8 @@ class ConversationPinApi(InstalledAppResource):
|
|||||||
conversation_id = str(c_id)
|
conversation_id = str(c_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
WebConversationService.pin(app_model, conversation_id, current_user)
|
WebConversationService.pin(app_model, conversation_id, current_user)
|
||||||
except ConversationNotExistsError:
|
except ConversationNotExistsError:
|
||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
@ -113,6 +122,8 @@ class ConversationUnPinApi(InstalledAppResource):
|
|||||||
raise NotChatAppError()
|
raise NotChatAppError()
|
||||||
|
|
||||||
conversation_id = str(c_id)
|
conversation_id = str(c_id)
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
WebConversationService.unpin(app_model, conversation_id, current_user)
|
WebConversationService.unpin(app_model, conversation_id, current_user)
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|||||||
@ -2,9 +2,8 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user
|
|
||||||
from flask_restx import Resource, inputs, marshal_with, reqparse
|
from flask_restx import Resource, inputs, marshal_with, reqparse
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_, select
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
@ -13,8 +12,8 @@ from controllers.console.wraps import account_initialization_required, cloud_edi
|
|||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.installed_app_fields import installed_app_list_fields
|
from fields.installed_app_fields import installed_app_list_fields
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.login import login_required
|
from libs.login import current_user, login_required
|
||||||
from models import App, InstalledApp, RecommendedApp
|
from models import Account, App, InstalledApp, RecommendedApp
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
from services.enterprise.enterprise_service import EnterpriseService
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
@ -29,17 +28,23 @@ class InstalledAppsListApi(Resource):
|
|||||||
@marshal_with(installed_app_list_fields)
|
@marshal_with(installed_app_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
app_id = request.args.get("app_id", default=None, type=str)
|
app_id = request.args.get("app_id", default=None, type=str)
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
current_tenant_id = current_user.current_tenant_id
|
current_tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
if app_id:
|
if app_id:
|
||||||
installed_apps = (
|
installed_apps = db.session.scalars(
|
||||||
db.session.query(InstalledApp)
|
select(InstalledApp).where(
|
||||||
.where(and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id))
|
and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id)
|
||||||
.all()
|
)
|
||||||
)
|
).all()
|
||||||
else:
|
else:
|
||||||
installed_apps = db.session.query(InstalledApp).where(InstalledApp.tenant_id == current_tenant_id).all()
|
installed_apps = db.session.scalars(
|
||||||
|
select(InstalledApp).where(InstalledApp.tenant_id == current_tenant_id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if current_user.current_tenant is None:
|
||||||
|
raise ValueError("current_user.current_tenant must not be None")
|
||||||
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
|
||||||
installed_app_list: list[dict[str, Any]] = [
|
installed_app_list: list[dict[str, Any]] = [
|
||||||
{
|
{
|
||||||
@ -115,6 +120,8 @@ class InstalledAppsListApi(Resource):
|
|||||||
if recommended_app is None:
|
if recommended_app is None:
|
||||||
raise NotFound("App not found")
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
current_tenant_id = current_user.current_tenant_id
|
current_tenant_id = current_user.current_tenant_id
|
||||||
app = db.session.query(App).where(App.id == args["app_id"]).first()
|
app = db.session.query(App).where(App.id == args["app_id"]).first()
|
||||||
|
|
||||||
@ -154,6 +161,8 @@ class InstalledAppApi(InstalledAppResource):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def delete(self, installed_app):
|
def delete(self, installed_app):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
if installed_app.app_owner_tenant_id == current_user.current_tenant_id:
|
if installed_app.app_owner_tenant_id == current_user.current_tenant_id:
|
||||||
raise BadRequest("You can't uninstall an app owned by the current tenant")
|
raise BadRequest("You can't uninstall an app owned by the current tenant")
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask_login import current_user
|
|
||||||
from flask_restx import marshal_with, reqparse
|
from flask_restx import marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import InternalServerError, NotFound
|
||||||
@ -24,6 +23,8 @@ from core.model_runtime.errors.invoke import InvokeError
|
|||||||
from fields.message_fields import message_infinite_scroll_pagination_fields
|
from fields.message_fields import message_infinite_scroll_pagination_fields
|
||||||
from libs import helper
|
from libs import helper
|
||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
|
from libs.login import current_user
|
||||||
|
from models import Account
|
||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
from services.app_generate_service import AppGenerateService
|
from services.app_generate_service import AppGenerateService
|
||||||
from services.errors.app import MoreLikeThisDisabledError
|
from services.errors.app import MoreLikeThisDisabledError
|
||||||
@ -54,6 +55,8 @@ class MessageListApi(InstalledAppResource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
return MessageService.pagination_by_first_id(
|
return MessageService.pagination_by_first_id(
|
||||||
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
|
app_model, current_user, args["conversation_id"], args["first_id"], args["limit"]
|
||||||
)
|
)
|
||||||
@ -75,6 +78,8 @@ class MessageFeedbackApi(InstalledAppResource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
MessageService.create_feedback(
|
MessageService.create_feedback(
|
||||||
app_model=app_model,
|
app_model=app_model,
|
||||||
message_id=message_id,
|
message_id=message_id,
|
||||||
@ -105,6 +110,8 @@ class MessageMoreLikeThisApi(InstalledAppResource):
|
|||||||
streaming = args["response_mode"] == "streaming"
|
streaming = args["response_mode"] == "streaming"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
response = AppGenerateService.generate_more_like_this(
|
response = AppGenerateService.generate_more_like_this(
|
||||||
app_model=app_model,
|
app_model=app_model,
|
||||||
user=current_user,
|
user=current_user,
|
||||||
@ -142,6 +149,8 @@ class MessageSuggestedQuestionApi(InstalledAppResource):
|
|||||||
message_id = str(message_id)
|
message_id = str(message_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
questions = MessageService.get_suggested_questions_after_answer(
|
questions = MessageService.get_suggested_questions_after_answer(
|
||||||
app_model=app_model, user=current_user, message_id=message_id, invoke_from=InvokeFrom.EXPLORE
|
app_model=app_model, user=current_user, message_id=message_id, invoke_from=InvokeFrom.EXPLORE
|
||||||
)
|
)
|
||||||
|
|||||||
@ -20,7 +20,7 @@ class AppParameterApi(InstalledAppResource):
|
|||||||
if app_model is None:
|
if app_model is None:
|
||||||
raise AppUnavailableError()
|
raise AppUnavailableError()
|
||||||
|
|
||||||
if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
workflow = app_model.workflow
|
workflow = app_model.workflow
|
||||||
if workflow is None:
|
if workflow is None:
|
||||||
raise AppUnavailableError()
|
raise AppUnavailableError()
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_restx import Resource, fields, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.wraps import account_initialization_required
|
from controllers.console.wraps import account_initialization_required
|
||||||
from libs.helper import AppIconUrlField
|
from libs.helper import AppIconUrlField
|
||||||
from libs.login import login_required
|
from libs.login import current_user, login_required
|
||||||
from services.recommended_app_service import RecommendedAppService
|
from services.recommended_app_service import RecommendedAppService
|
||||||
|
|
||||||
app_fields = {
|
app_fields = {
|
||||||
@ -46,8 +45,9 @@ class RecommendedAppListApi(Resource):
|
|||||||
parser.add_argument("language", type=str, location="args")
|
parser.add_argument("language", type=str, location="args")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.get("language") and args.get("language") in languages:
|
language = args.get("language")
|
||||||
language_prefix = args.get("language")
|
if language and language in languages:
|
||||||
|
language_prefix = language
|
||||||
elif current_user and current_user.interface_language:
|
elif current_user and current_user.interface_language:
|
||||||
language_prefix = current_user.interface_language
|
language_prefix = current_user.interface_language
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
from flask_login import current_user
|
|
||||||
from flask_restx import fields, marshal_with, reqparse
|
from flask_restx import fields, marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
@ -8,6 +7,8 @@ from controllers.console.explore.error import NotCompletionAppError
|
|||||||
from controllers.console.explore.wraps import InstalledAppResource
|
from controllers.console.explore.wraps import InstalledAppResource
|
||||||
from fields.conversation_fields import message_file_fields
|
from fields.conversation_fields import message_file_fields
|
||||||
from libs.helper import TimestampField, uuid_value
|
from libs.helper import TimestampField, uuid_value
|
||||||
|
from libs.login import current_user
|
||||||
|
from models import Account
|
||||||
from services.errors.message import MessageNotExistsError
|
from services.errors.message import MessageNotExistsError
|
||||||
from services.saved_message_service import SavedMessageService
|
from services.saved_message_service import SavedMessageService
|
||||||
|
|
||||||
@ -42,6 +43,8 @@ class SavedMessageListApi(InstalledAppResource):
|
|||||||
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
|
parser.add_argument("limit", type=int_range(1, 100), required=False, default=20, location="args")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
return SavedMessageService.pagination_by_last_id(app_model, current_user, args["last_id"], args["limit"])
|
return SavedMessageService.pagination_by_last_id(app_model, current_user, args["last_id"], args["limit"])
|
||||||
|
|
||||||
def post(self, installed_app):
|
def post(self, installed_app):
|
||||||
@ -54,6 +57,8 @@ class SavedMessageListApi(InstalledAppResource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
SavedMessageService.save(app_model, current_user, args["message_id"])
|
SavedMessageService.save(app_model, current_user, args["message_id"])
|
||||||
except MessageNotExistsError:
|
except MessageNotExistsError:
|
||||||
raise NotFound("Message Not Exists.")
|
raise NotFound("Message Not Exists.")
|
||||||
@ -70,6 +75,8 @@ class SavedMessageApi(InstalledAppResource):
|
|||||||
if app_model.mode != "completion":
|
if app_model.mode != "completion":
|
||||||
raise NotCompletionAppError()
|
raise NotCompletionAppError()
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("current_user must be an Account instance")
|
||||||
SavedMessageService.delete(app_model, current_user, message_id)
|
SavedMessageService.delete(app_model, current_user, message_id)
|
||||||
|
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal_with, reqparse
|
||||||
|
|
||||||
from constants import HIDDEN_VALUE
|
from constants import HIDDEN_VALUE
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from fields.api_based_extension_fields import api_based_extension_fields
|
from fields.api_based_extension_fields import api_based_extension_fields
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
@ -11,7 +11,21 @@ from services.api_based_extension_service import APIBasedExtensionService
|
|||||||
from services.code_based_extension_service import CodeBasedExtensionService
|
from services.code_based_extension_service import CodeBasedExtensionService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/code-based-extension")
|
||||||
class CodeBasedExtensionAPI(Resource):
|
class CodeBasedExtensionAPI(Resource):
|
||||||
|
@api.doc("get_code_based_extension")
|
||||||
|
@api.doc(description="Get code-based extension data by module name")
|
||||||
|
@api.expect(
|
||||||
|
api.parser().add_argument("module", type=str, required=True, location="args", help="Extension module name")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model(
|
||||||
|
"CodeBasedExtensionResponse",
|
||||||
|
{"module": fields.String(description="Module name"), "data": fields.Raw(description="Extension data")},
|
||||||
|
),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -23,7 +37,11 @@ class CodeBasedExtensionAPI(Resource):
|
|||||||
return {"module": args["module"], "data": CodeBasedExtensionService.get_code_based_extension(args["module"])}
|
return {"module": args["module"], "data": CodeBasedExtensionService.get_code_based_extension(args["module"])}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/api-based-extension")
|
||||||
class APIBasedExtensionAPI(Resource):
|
class APIBasedExtensionAPI(Resource):
|
||||||
|
@api.doc("get_api_based_extensions")
|
||||||
|
@api.doc(description="Get all API-based extensions for current tenant")
|
||||||
|
@api.response(200, "Success", fields.List(fields.Nested(api_based_extension_fields)))
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -32,6 +50,19 @@ class APIBasedExtensionAPI(Resource):
|
|||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = current_user.current_tenant_id
|
||||||
return APIBasedExtensionService.get_all_by_tenant_id(tenant_id)
|
return APIBasedExtensionService.get_all_by_tenant_id(tenant_id)
|
||||||
|
|
||||||
|
@api.doc("create_api_based_extension")
|
||||||
|
@api.doc(description="Create a new API-based extension")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CreateAPIBasedExtensionRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(required=True, description="Extension name"),
|
||||||
|
"api_endpoint": fields.String(required=True, description="API endpoint URL"),
|
||||||
|
"api_key": fields.String(required=True, description="API key for authentication"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "Extension created successfully", api_based_extension_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -53,7 +84,12 @@ class APIBasedExtensionAPI(Resource):
|
|||||||
return APIBasedExtensionService.save(extension_data)
|
return APIBasedExtensionService.save(extension_data)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/api-based-extension/<uuid:id>")
|
||||||
class APIBasedExtensionDetailAPI(Resource):
|
class APIBasedExtensionDetailAPI(Resource):
|
||||||
|
@api.doc("get_api_based_extension")
|
||||||
|
@api.doc(description="Get API-based extension by ID")
|
||||||
|
@api.doc(params={"id": "Extension ID"})
|
||||||
|
@api.response(200, "Success", api_based_extension_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -64,6 +100,20 @@ class APIBasedExtensionDetailAPI(Resource):
|
|||||||
|
|
||||||
return APIBasedExtensionService.get_with_tenant_id(tenant_id, api_based_extension_id)
|
return APIBasedExtensionService.get_with_tenant_id(tenant_id, api_based_extension_id)
|
||||||
|
|
||||||
|
@api.doc("update_api_based_extension")
|
||||||
|
@api.doc(description="Update API-based extension")
|
||||||
|
@api.doc(params={"id": "Extension ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateAPIBasedExtensionRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(required=True, description="Extension name"),
|
||||||
|
"api_endpoint": fields.String(required=True, description="API endpoint URL"),
|
||||||
|
"api_key": fields.String(required=True, description="API key for authentication"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Extension updated successfully", api_based_extension_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -88,6 +138,10 @@ class APIBasedExtensionDetailAPI(Resource):
|
|||||||
|
|
||||||
return APIBasedExtensionService.save(extension_data_from_db)
|
return APIBasedExtensionService.save(extension_data_from_db)
|
||||||
|
|
||||||
|
@api.doc("delete_api_based_extension")
|
||||||
|
@api.doc(description="Delete API-based extension")
|
||||||
|
@api.doc(params={"id": "Extension ID"})
|
||||||
|
@api.response(204, "Extension deleted successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -100,9 +154,3 @@ class APIBasedExtensionDetailAPI(Resource):
|
|||||||
APIBasedExtensionService.delete(extension_data_from_db)
|
APIBasedExtensionService.delete(extension_data_from_db)
|
||||||
|
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CodeBasedExtensionAPI, "/code-based-extension")
|
|
||||||
|
|
||||||
api.add_resource(APIBasedExtensionAPI, "/api-based-extension")
|
|
||||||
api.add_resource(APIBasedExtensionDetailAPI, "/api-based-extension/<uuid:id>")
|
|
||||||
|
|||||||
@ -1,26 +1,40 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource, fields
|
||||||
|
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
from . import api
|
from . import api, console_ns
|
||||||
from .wraps import account_initialization_required, cloud_utm_record, setup_required
|
from .wraps import account_initialization_required, cloud_utm_record, setup_required
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/features")
|
||||||
class FeatureApi(Resource):
|
class FeatureApi(Resource):
|
||||||
|
@api.doc("get_tenant_features")
|
||||||
|
@api.doc(description="Get feature configuration for current tenant")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model("FeatureResponse", {"features": fields.Raw(description="Feature configuration object")}),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@cloud_utm_record
|
@cloud_utm_record
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Get feature configuration for current tenant"""
|
||||||
return FeatureService.get_features(current_user.current_tenant_id).model_dump()
|
return FeatureService.get_features(current_user.current_tenant_id).model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/system-features")
|
||||||
class SystemFeatureApi(Resource):
|
class SystemFeatureApi(Resource):
|
||||||
|
@api.doc("get_system_features")
|
||||||
|
@api.doc(description="Get system-wide feature configuration")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model("SystemFeatureResponse", {"features": fields.Raw(description="System feature configuration object")}),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Get system-wide feature configuration"""
|
||||||
return FeatureService.get_system_features().model_dump()
|
return FeatureService.get_system_features().model_dump()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(FeatureApi, "/features")
|
|
||||||
api.add_resource(SystemFeatureApi, "/system-features")
|
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from controllers.console.wraps import (
|
|||||||
)
|
)
|
||||||
from fields.file_fields import file_fields, upload_config_fields
|
from fields.file_fields import file_fields, upload_config_fields
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
|
from models import Account
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
PREVIEW_WORDS_LIMIT = 3000
|
PREVIEW_WORDS_LIMIT = 3000
|
||||||
@ -68,6 +69,8 @@ class FileApi(Resource):
|
|||||||
source = None
|
source = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
upload_file = FileService.upload_file(
|
upload_file = FileService.upload_file(
|
||||||
filename=file.filename,
|
filename=file.filename,
|
||||||
content=file.read(),
|
content=file.read(),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@ -11,20 +11,47 @@ from libs.helper import StrLen
|
|||||||
from models.model import DifySetup
|
from models.model import DifySetup
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
|
|
||||||
from . import api
|
from . import api, console_ns
|
||||||
from .error import AlreadySetupError, InitValidateFailedError
|
from .error import AlreadySetupError, InitValidateFailedError
|
||||||
from .wraps import only_edition_self_hosted
|
from .wraps import only_edition_self_hosted
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/init")
|
||||||
class InitValidateAPI(Resource):
|
class InitValidateAPI(Resource):
|
||||||
|
@api.doc("get_init_status")
|
||||||
|
@api.doc(description="Get initialization validation status")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
model=api.model(
|
||||||
|
"InitStatusResponse",
|
||||||
|
{"status": fields.String(description="Initialization status", enum=["finished", "not_started"])},
|
||||||
|
),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Get initialization validation status"""
|
||||||
init_status = get_init_validate_status()
|
init_status = get_init_validate_status()
|
||||||
if init_status:
|
if init_status:
|
||||||
return {"status": "finished"}
|
return {"status": "finished"}
|
||||||
return {"status": "not_started"}
|
return {"status": "not_started"}
|
||||||
|
|
||||||
|
@api.doc("validate_init_password")
|
||||||
|
@api.doc(description="Validate initialization password for self-hosted edition")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"InitValidateRequest",
|
||||||
|
{"password": fields.String(required=True, description="Initialization password", max_length=30)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
201,
|
||||||
|
"Success",
|
||||||
|
model=api.model("InitValidateResponse", {"result": fields.String(description="Operation result")}),
|
||||||
|
)
|
||||||
|
@api.response(400, "Already setup or validation failed")
|
||||||
@only_edition_self_hosted
|
@only_edition_self_hosted
|
||||||
def post(self):
|
def post(self):
|
||||||
|
"""Validate initialization password"""
|
||||||
# is tenant created
|
# is tenant created
|
||||||
tenant_count = TenantService.get_tenant_count()
|
tenant_count = TenantService.get_tenant_count()
|
||||||
if tenant_count > 0:
|
if tenant_count > 0:
|
||||||
@ -52,6 +79,3 @@ def get_init_validate_status():
|
|||||||
return db_session.execute(select(DifySetup)).scalar_one_or_none()
|
return db_session.execute(select(DifySetup)).scalar_one_or_none()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(InitValidateAPI, "/init")
|
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
from flask_restx import Resource
|
from flask_restx import Resource, fields
|
||||||
|
|
||||||
from controllers.console import api
|
from . import api, console_ns
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/ping")
|
||||||
class PingApi(Resource):
|
class PingApi(Resource):
|
||||||
|
@api.doc("health_check")
|
||||||
|
@api.doc(description="Health check endpoint for connection testing")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model("PingResponse", {"result": fields.String(description="Health check result", example="pong")}),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""Health check endpoint for connection testing"""
|
||||||
For connection health check
|
|
||||||
"""
|
|
||||||
return {"result": "pong"}
|
return {"result": "pong"}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(PingApi, "/ping")
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from libs.helper import StrLen, email, extract_remote_ip
|
from libs.helper import StrLen, email, extract_remote_ip
|
||||||
@ -7,23 +7,56 @@ from libs.password import valid_password
|
|||||||
from models.model import DifySetup, db
|
from models.model import DifySetup, db
|
||||||
from services.account_service import RegisterService, TenantService
|
from services.account_service import RegisterService, TenantService
|
||||||
|
|
||||||
from . import api
|
from . import api, console_ns
|
||||||
from .error import AlreadySetupError, NotInitValidateError
|
from .error import AlreadySetupError, NotInitValidateError
|
||||||
from .init_validate import get_init_validate_status
|
from .init_validate import get_init_validate_status
|
||||||
from .wraps import only_edition_self_hosted
|
from .wraps import only_edition_self_hosted
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/setup")
|
||||||
class SetupApi(Resource):
|
class SetupApi(Resource):
|
||||||
|
@api.doc("get_setup_status")
|
||||||
|
@api.doc(description="Get system setup status")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model(
|
||||||
|
"SetupStatusResponse",
|
||||||
|
{
|
||||||
|
"step": fields.String(description="Setup step status", enum=["not_started", "finished"]),
|
||||||
|
"setup_at": fields.String(description="Setup completion time (ISO format)", required=False),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Get system setup status"""
|
||||||
if dify_config.EDITION == "SELF_HOSTED":
|
if dify_config.EDITION == "SELF_HOSTED":
|
||||||
setup_status = get_setup_status()
|
setup_status = get_setup_status()
|
||||||
if setup_status:
|
# Check if setup_status is a DifySetup object rather than a bool
|
||||||
|
if setup_status and not isinstance(setup_status, bool):
|
||||||
return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()}
|
return {"step": "finished", "setup_at": setup_status.setup_at.isoformat()}
|
||||||
|
elif setup_status:
|
||||||
|
return {"step": "finished"}
|
||||||
return {"step": "not_started"}
|
return {"step": "not_started"}
|
||||||
return {"step": "finished"}
|
return {"step": "finished"}
|
||||||
|
|
||||||
|
@api.doc("setup_system")
|
||||||
|
@api.doc(description="Initialize system setup with admin account")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"SetupRequest",
|
||||||
|
{
|
||||||
|
"email": fields.String(required=True, description="Admin email address"),
|
||||||
|
"name": fields.String(required=True, description="Admin name (max 30 characters)"),
|
||||||
|
"password": fields.String(required=True, description="Admin password"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "Success", api.model("SetupResponse", {"result": fields.String(description="Setup result")}))
|
||||||
|
@api.response(400, "Already setup or validation failed")
|
||||||
@only_edition_self_hosted
|
@only_edition_self_hosted
|
||||||
def post(self):
|
def post(self):
|
||||||
|
"""Initialize system setup with admin account"""
|
||||||
# is set up
|
# is set up
|
||||||
if get_setup_status():
|
if get_setup_status():
|
||||||
raise AlreadySetupError()
|
raise AlreadySetupError()
|
||||||
@ -55,6 +88,3 @@ def get_setup_status():
|
|||||||
return db.session.query(DifySetup).first()
|
return db.session.query(DifySetup).first()
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(SetupApi, "/setup")
|
|
||||||
|
|||||||
@ -111,7 +111,7 @@ class TagBindingCreateApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
TagService.save_tag_binding(args)
|
TagService.save_tag_binding(args)
|
||||||
|
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
class TagBindingDeleteApi(Resource):
|
class TagBindingDeleteApi(Resource):
|
||||||
@ -132,7 +132,7 @@ class TagBindingDeleteApi(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
TagService.delete_tag_binding(args)
|
TagService.delete_tag_binding(args)
|
||||||
|
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(TagListApi, "/tags")
|
api.add_resource(TagListApi, "/tags")
|
||||||
|
|||||||
@ -2,18 +2,41 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from packaging import version
|
from packaging import version
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
|
|
||||||
from . import api
|
from . import api, console_ns
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/version")
|
||||||
class VersionApi(Resource):
|
class VersionApi(Resource):
|
||||||
|
@api.doc("check_version_update")
|
||||||
|
@api.doc(description="Check for application version updates")
|
||||||
|
@api.expect(
|
||||||
|
api.parser().add_argument(
|
||||||
|
"current_version", type=str, required=True, location="args", help="Current application version"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model(
|
||||||
|
"VersionResponse",
|
||||||
|
{
|
||||||
|
"version": fields.String(description="Latest version number"),
|
||||||
|
"release_date": fields.String(description="Release date of latest version"),
|
||||||
|
"release_notes": fields.String(description="Release notes for latest version"),
|
||||||
|
"can_auto_update": fields.Boolean(description="Whether auto-update is supported"),
|
||||||
|
"features": fields.Raw(description="Feature flags and capabilities"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
"""Check for application version updates"""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("current_version", type=str, required=True, location="args")
|
parser.add_argument("current_version", type=str, required=True, location="args")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -34,14 +57,14 @@ class VersionApi(Resource):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(check_update_url, {"current_version": args.get("current_version")}, timeout=(3, 10))
|
response = requests.get(check_update_url, {"current_version": args["current_version"]}, timeout=(3, 10))
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.warning("Check update version error: %s.", str(error))
|
logger.warning("Check update version error: %s.", str(error))
|
||||||
result["version"] = args.get("current_version")
|
result["version"] = args["current_version"]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
content = json.loads(response.content)
|
content = json.loads(response.content)
|
||||||
if _has_new_version(latest_version=content["version"], current_version=f"{args.get('current_version')}"):
|
if _has_new_version(latest_version=content["version"], current_version=f"{args['current_version']}"):
|
||||||
result["version"] = content["version"]
|
result["version"] = content["version"]
|
||||||
result["release_date"] = content["releaseDate"]
|
result["release_date"] = content["releaseDate"]
|
||||||
result["release_notes"] = content["releaseNotes"]
|
result["release_notes"] = content["releaseNotes"]
|
||||||
@ -59,6 +82,3 @@ def _has_new_version(*, latest_version: str, current_version: str) -> bool:
|
|||||||
except version.InvalidVersion:
|
except version.InvalidVersion:
|
||||||
logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version)
|
logger.warning("Invalid version format: latest=%s, current=%s", latest_version, current_version)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(VersionApi, "/version")
|
|
||||||
|
|||||||
@ -49,6 +49,8 @@ class AccountInitApi(Resource):
|
|||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
if account.status == "active":
|
if account.status == "active":
|
||||||
@ -102,6 +104,8 @@ class AccountProfileApi(Resource):
|
|||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
@enterprise_license_required
|
@enterprise_license_required
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
@ -111,6 +115,8 @@ class AccountNameApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=True, location="json")
|
parser.add_argument("name", type=str, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -130,6 +136,8 @@ class AccountAvatarApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("avatar", type=str, required=True, location="json")
|
parser.add_argument("avatar", type=str, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -145,6 +153,8 @@ class AccountInterfaceLanguageApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("interface_language", type=supported_language, required=True, location="json")
|
parser.add_argument("interface_language", type=supported_language, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -160,6 +170,8 @@ class AccountInterfaceThemeApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("interface_theme", type=str, choices=["light", "dark"], required=True, location="json")
|
parser.add_argument("interface_theme", type=str, choices=["light", "dark"], required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -175,6 +187,8 @@ class AccountTimezoneApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("timezone", type=str, required=True, location="json")
|
parser.add_argument("timezone", type=str, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -194,6 +208,8 @@ class AccountPasswordApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_fields)
|
@marshal_with(account_fields)
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("password", type=str, required=False, location="json")
|
parser.add_argument("password", type=str, required=False, location="json")
|
||||||
parser.add_argument("new_password", type=str, required=True, location="json")
|
parser.add_argument("new_password", type=str, required=True, location="json")
|
||||||
@ -228,9 +244,13 @@ class AccountIntegrateApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(integrate_list_fields)
|
@marshal_with(integrate_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
account_integrates = db.session.query(AccountIntegrate).where(AccountIntegrate.account_id == account.id).all()
|
account_integrates = db.session.scalars(
|
||||||
|
select(AccountIntegrate).where(AccountIntegrate.account_id == account.id)
|
||||||
|
).all()
|
||||||
|
|
||||||
base_url = request.url_root.rstrip("/")
|
base_url = request.url_root.rstrip("/")
|
||||||
oauth_base_path = "/console/api/oauth/login"
|
oauth_base_path = "/console/api/oauth/login"
|
||||||
@ -268,6 +288,8 @@ class AccountDeleteVerifyApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
token, code = AccountService.generate_account_deletion_verification_code(account)
|
token, code = AccountService.generate_account_deletion_verification_code(account)
|
||||||
@ -281,6 +303,8 @@ class AccountDeleteApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -321,6 +345,8 @@ class EducationVerifyApi(Resource):
|
|||||||
@cloud_edition_billing_enabled
|
@cloud_edition_billing_enabled
|
||||||
@marshal_with(verify_fields)
|
@marshal_with(verify_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
return BillingService.EducationIdentity.verify(account.id, account.email)
|
return BillingService.EducationIdentity.verify(account.id, account.email)
|
||||||
@ -340,6 +366,8 @@ class EducationApi(Resource):
|
|||||||
@only_edition_cloud
|
@only_edition_cloud
|
||||||
@cloud_edition_billing_enabled
|
@cloud_edition_billing_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -357,6 +385,8 @@ class EducationApi(Resource):
|
|||||||
@cloud_edition_billing_enabled
|
@cloud_edition_billing_enabled
|
||||||
@marshal_with(status_fields)
|
@marshal_with(status_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
account = current_user
|
account = current_user
|
||||||
|
|
||||||
res = BillingService.EducationIdentity.status(account.id)
|
res = BillingService.EducationIdentity.status(account.id)
|
||||||
@ -421,6 +451,8 @@ class ChangeEmailSendEmailApi(Resource):
|
|||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
user_email = reset_data.get("email", "")
|
user_email = reset_data.get("email", "")
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if user_email != current_user.email:
|
if user_email != current_user.email:
|
||||||
raise InvalidEmailError()
|
raise InvalidEmailError()
|
||||||
else:
|
else:
|
||||||
@ -501,6 +533,8 @@ class ChangeEmailResetApi(Resource):
|
|||||||
AccountService.revoke_change_email_token(args["token"])
|
AccountService.revoke_change_email_token(args["token"])
|
||||||
|
|
||||||
old_email = reset_data.get("old_email", "")
|
old_email = reset_data.get("old_email", "")
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if current_user.email != old_email:
|
if current_user.email != old_email:
|
||||||
raise AccountNotFound()
|
raise AccountNotFound()
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource, fields
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.agent_service import AgentService
|
from services.agent_service import AgentService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/agent-providers")
|
||||||
class AgentProviderListApi(Resource):
|
class AgentProviderListApi(Resource):
|
||||||
|
@api.doc("list_agent_providers")
|
||||||
|
@api.doc(description="Get list of available agent providers")
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
fields.List(fields.Raw(description="Agent provider information")),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -21,7 +29,16 @@ class AgentProviderListApi(Resource):
|
|||||||
return jsonable_encoder(AgentService.list_agent_providers(user_id, tenant_id))
|
return jsonable_encoder(AgentService.list_agent_providers(user_id, tenant_id))
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/agent-provider/<path:provider_name>")
|
||||||
class AgentProviderApi(Resource):
|
class AgentProviderApi(Resource):
|
||||||
|
@api.doc("get_agent_provider")
|
||||||
|
@api.doc(description="Get specific agent provider details")
|
||||||
|
@api.doc(params={"provider_name": "Agent provider name"})
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
fields.Raw(description="Agent provider details"),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -30,7 +47,3 @@ class AgentProviderApi(Resource):
|
|||||||
user_id = user.id
|
user_id = user.id
|
||||||
tenant_id = user.current_tenant_id
|
tenant_id = user.current_tenant_id
|
||||||
return jsonable_encoder(AgentService.get_agent_provider(user_id, tenant_id, provider_name))
|
return jsonable_encoder(AgentService.get_agent_provider(user_id, tenant_id, provider_name))
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AgentProviderListApi, "/workspaces/current/agent-providers")
|
|
||||||
api.add_resource(AgentProviderApi, "/workspaces/current/agent-provider/<path:provider_name>")
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, fields, reqparse
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
from core.plugin.impl.exc import PluginPermissionDeniedError
|
from core.plugin.impl.exc import PluginPermissionDeniedError
|
||||||
@ -10,7 +10,26 @@ from libs.login import login_required
|
|||||||
from services.plugin.endpoint_service import EndpointService
|
from services.plugin.endpoint_service import EndpointService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/create")
|
||||||
class EndpointCreateApi(Resource):
|
class EndpointCreateApi(Resource):
|
||||||
|
@api.doc("create_endpoint")
|
||||||
|
@api.doc(description="Create a new plugin endpoint")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"EndpointCreateRequest",
|
||||||
|
{
|
||||||
|
"plugin_unique_identifier": fields.String(required=True, description="Plugin unique identifier"),
|
||||||
|
"settings": fields.Raw(required=True, description="Endpoint settings"),
|
||||||
|
"name": fields.String(required=True, description="Endpoint name"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Endpoint created successfully",
|
||||||
|
api.model("EndpointCreateResponse", {"success": fields.Boolean(description="Operation success")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -43,7 +62,20 @@ class EndpointCreateApi(Resource):
|
|||||||
raise ValueError(e.description) from e
|
raise ValueError(e.description) from e
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/list")
|
||||||
class EndpointListApi(Resource):
|
class EndpointListApi(Resource):
|
||||||
|
@api.doc("list_endpoints")
|
||||||
|
@api.doc(description="List plugin endpoints with pagination")
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("page", type=int, required=True, location="args", help="Page number")
|
||||||
|
.add_argument("page_size", type=int, required=True, location="args", help="Page size")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model("EndpointListResponse", {"endpoints": fields.List(fields.Raw(description="Endpoint information"))}),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -70,7 +102,23 @@ class EndpointListApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/list/plugin")
|
||||||
class EndpointListForSinglePluginApi(Resource):
|
class EndpointListForSinglePluginApi(Resource):
|
||||||
|
@api.doc("list_plugin_endpoints")
|
||||||
|
@api.doc(description="List endpoints for a specific plugin")
|
||||||
|
@api.expect(
|
||||||
|
api.parser()
|
||||||
|
.add_argument("page", type=int, required=True, location="args", help="Page number")
|
||||||
|
.add_argument("page_size", type=int, required=True, location="args", help="Page size")
|
||||||
|
.add_argument("plugin_id", type=str, required=True, location="args", help="Plugin ID")
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Success",
|
||||||
|
api.model(
|
||||||
|
"PluginEndpointListResponse", {"endpoints": fields.List(fields.Raw(description="Endpoint information"))}
|
||||||
|
),
|
||||||
|
)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -100,7 +148,19 @@ class EndpointListForSinglePluginApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/delete")
|
||||||
class EndpointDeleteApi(Resource):
|
class EndpointDeleteApi(Resource):
|
||||||
|
@api.doc("delete_endpoint")
|
||||||
|
@api.doc(description="Delete a plugin endpoint")
|
||||||
|
@api.expect(
|
||||||
|
api.model("EndpointDeleteRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")})
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Endpoint deleted successfully",
|
||||||
|
api.model("EndpointDeleteResponse", {"success": fields.Boolean(description="Operation success")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -123,7 +183,26 @@ class EndpointDeleteApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/update")
|
||||||
class EndpointUpdateApi(Resource):
|
class EndpointUpdateApi(Resource):
|
||||||
|
@api.doc("update_endpoint")
|
||||||
|
@api.doc(description="Update a plugin endpoint")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"EndpointUpdateRequest",
|
||||||
|
{
|
||||||
|
"endpoint_id": fields.String(required=True, description="Endpoint ID"),
|
||||||
|
"settings": fields.Raw(required=True, description="Updated settings"),
|
||||||
|
"name": fields.String(required=True, description="Updated name"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Endpoint updated successfully",
|
||||||
|
api.model("EndpointUpdateResponse", {"success": fields.Boolean(description="Operation success")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -154,7 +233,19 @@ class EndpointUpdateApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/enable")
|
||||||
class EndpointEnableApi(Resource):
|
class EndpointEnableApi(Resource):
|
||||||
|
@api.doc("enable_endpoint")
|
||||||
|
@api.doc(description="Enable a plugin endpoint")
|
||||||
|
@api.expect(
|
||||||
|
api.model("EndpointEnableRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")})
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Endpoint enabled successfully",
|
||||||
|
api.model("EndpointEnableResponse", {"success": fields.Boolean(description="Operation success")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -177,7 +268,19 @@ class EndpointEnableApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/workspaces/current/endpoints/disable")
|
||||||
class EndpointDisableApi(Resource):
|
class EndpointDisableApi(Resource):
|
||||||
|
@api.doc("disable_endpoint")
|
||||||
|
@api.doc(description="Disable a plugin endpoint")
|
||||||
|
@api.expect(
|
||||||
|
api.model("EndpointDisableRequest", {"endpoint_id": fields.String(required=True, description="Endpoint ID")})
|
||||||
|
)
|
||||||
|
@api.response(
|
||||||
|
200,
|
||||||
|
"Endpoint disabled successfully",
|
||||||
|
api.model("EndpointDisableResponse", {"success": fields.Boolean(description="Operation success")}),
|
||||||
|
)
|
||||||
|
@api.response(403, "Admin privileges required")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -198,12 +301,3 @@ class EndpointDisableApi(Resource):
|
|||||||
tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id
|
tenant_id=user.current_tenant_id, user_id=user.id, endpoint_id=endpoint_id
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(EndpointCreateApi, "/workspaces/current/endpoints/create")
|
|
||||||
api.add_resource(EndpointListApi, "/workspaces/current/endpoints/list")
|
|
||||||
api.add_resource(EndpointListForSinglePluginApi, "/workspaces/current/endpoints/list/plugin")
|
|
||||||
api.add_resource(EndpointDeleteApi, "/workspaces/current/endpoints/delete")
|
|
||||||
api.add_resource(EndpointUpdateApi, "/workspaces/current/endpoints/update")
|
|
||||||
api.add_resource(EndpointEnableApi, "/workspaces/current/endpoints/enable")
|
|
||||||
api.add_resource(EndpointDisableApi, "/workspaces/current/endpoints/disable")
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from urllib import parse
|
from urllib import parse
|
||||||
|
|
||||||
from flask import request
|
from flask import abort, request
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from flask_restx import Resource, abort, marshal_with, reqparse
|
from flask_restx import Resource, marshal_with, reqparse
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
@ -41,6 +41,10 @@ class MemberListApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_with_role_list_fields)
|
@marshal_with(account_with_role_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
members = TenantService.get_tenant_members(current_user.current_tenant)
|
members = TenantService.get_tenant_members(current_user.current_tenant)
|
||||||
return {"result": "success", "accounts": members}, 200
|
return {"result": "success", "accounts": members}, 200
|
||||||
|
|
||||||
@ -65,7 +69,11 @@ class MemberInviteEmailApi(Resource):
|
|||||||
if not TenantAccountRole.is_non_owner_role(invitee_role):
|
if not TenantAccountRole.is_non_owner_role(invitee_role):
|
||||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
inviter = current_user
|
inviter = current_user
|
||||||
|
if not inviter.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
invitation_results = []
|
invitation_results = []
|
||||||
console_web_url = dify_config.CONSOLE_WEB_URL
|
console_web_url = dify_config.CONSOLE_WEB_URL
|
||||||
|
|
||||||
@ -76,6 +84,8 @@ class MemberInviteEmailApi(Resource):
|
|||||||
|
|
||||||
for invitee_email in invitee_emails:
|
for invitee_email in invitee_emails:
|
||||||
try:
|
try:
|
||||||
|
if not inviter.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
token = RegisterService.invite_new_member(
|
token = RegisterService.invite_new_member(
|
||||||
inviter.current_tenant, invitee_email, interface_language, role=invitee_role, inviter=inviter
|
inviter.current_tenant, invitee_email, interface_language, role=invitee_role, inviter=inviter
|
||||||
)
|
)
|
||||||
@ -97,7 +107,7 @@ class MemberInviteEmailApi(Resource):
|
|||||||
return {
|
return {
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"invitation_results": invitation_results,
|
"invitation_results": invitation_results,
|
||||||
"tenant_id": str(current_user.current_tenant.id),
|
"tenant_id": str(inviter.current_tenant.id) if inviter.current_tenant else "",
|
||||||
}, 201
|
}, 201
|
||||||
|
|
||||||
|
|
||||||
@ -108,6 +118,10 @@ class MemberCancelInviteApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def delete(self, member_id):
|
def delete(self, member_id):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
member = db.session.query(Account).where(Account.id == str(member_id)).first()
|
member = db.session.query(Account).where(Account.id == str(member_id)).first()
|
||||||
if member is None:
|
if member is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
@ -123,7 +137,10 @@ class MemberCancelInviteApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(str(e))
|
raise ValueError(str(e))
|
||||||
|
|
||||||
return {"result": "success", "tenant_id": str(current_user.current_tenant.id)}, 200
|
return {
|
||||||
|
"result": "success",
|
||||||
|
"tenant_id": str(current_user.current_tenant.id) if current_user.current_tenant else "",
|
||||||
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
class MemberUpdateRoleApi(Resource):
|
class MemberUpdateRoleApi(Resource):
|
||||||
@ -141,6 +158,10 @@ class MemberUpdateRoleApi(Resource):
|
|||||||
if not TenantAccountRole.is_valid_role(new_role):
|
if not TenantAccountRole.is_valid_role(new_role):
|
||||||
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
return {"code": "invalid-role", "message": "Invalid role"}, 400
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
member = db.session.get(Account, str(member_id))
|
member = db.session.get(Account, str(member_id))
|
||||||
if not member:
|
if not member:
|
||||||
abort(404)
|
abort(404)
|
||||||
@ -164,6 +185,10 @@ class DatasetOperatorMemberListApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@marshal_with(account_with_role_list_fields)
|
@marshal_with(account_with_role_list_fields)
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
members = TenantService.get_dataset_operator_members(current_user.current_tenant)
|
members = TenantService.get_dataset_operator_members(current_user.current_tenant)
|
||||||
return {"result": "success", "accounts": members}, 200
|
return {"result": "success", "accounts": members}, 200
|
||||||
|
|
||||||
@ -184,6 +209,10 @@ class SendOwnerTransferEmailApi(Resource):
|
|||||||
raise EmailSendIpLimitError()
|
raise EmailSendIpLimitError()
|
||||||
|
|
||||||
# check if the current user is the owner of the workspace
|
# check if the current user is the owner of the workspace
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
||||||
raise NotOwnerError()
|
raise NotOwnerError()
|
||||||
|
|
||||||
@ -198,7 +227,7 @@ class SendOwnerTransferEmailApi(Resource):
|
|||||||
account=current_user,
|
account=current_user,
|
||||||
email=email,
|
email=email,
|
||||||
language=language,
|
language=language,
|
||||||
workspace_name=current_user.current_tenant.name,
|
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"result": "success", "data": token}
|
return {"result": "success", "data": token}
|
||||||
@ -215,6 +244,10 @@ class OwnerTransferCheckApi(Resource):
|
|||||||
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
# check if the current user is the owner of the workspace
|
# check if the current user is the owner of the workspace
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
||||||
raise NotOwnerError()
|
raise NotOwnerError()
|
||||||
|
|
||||||
@ -256,6 +289,10 @@ class OwnerTransfer(Resource):
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# check if the current user is the owner of the workspace
|
# check if the current user is the owner of the workspace
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
if not TenantService.is_owner(current_user, current_user.current_tenant):
|
||||||
raise NotOwnerError()
|
raise NotOwnerError()
|
||||||
|
|
||||||
@ -274,9 +311,11 @@ class OwnerTransfer(Resource):
|
|||||||
member = db.session.get(Account, str(member_id))
|
member = db.session.get(Account, str(member_id))
|
||||||
if not member:
|
if not member:
|
||||||
abort(404)
|
abort(404)
|
||||||
else:
|
return # Never reached, but helps type checker
|
||||||
member_account = member
|
|
||||||
if not TenantService.is_member(member_account, current_user.current_tenant):
|
if not current_user.current_tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
|
if not TenantService.is_member(member, current_user.current_tenant):
|
||||||
raise MemberNotInTenantError()
|
raise MemberNotInTenantError()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -286,13 +325,13 @@ class OwnerTransfer(Resource):
|
|||||||
AccountService.send_new_owner_transfer_notify_email(
|
AccountService.send_new_owner_transfer_notify_email(
|
||||||
account=member,
|
account=member,
|
||||||
email=member.email,
|
email=member.email,
|
||||||
workspace_name=current_user.current_tenant.name,
|
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
|
||||||
)
|
)
|
||||||
|
|
||||||
AccountService.send_old_owner_transfer_notify_email(
|
AccountService.send_old_owner_transfer_notify_email(
|
||||||
account=current_user,
|
account=current_user,
|
||||||
email=current_user.email,
|
email=current_user.email,
|
||||||
workspace_name=current_user.current_tenant.name,
|
workspace_name=current_user.current_tenant.name if current_user.current_tenant else "",
|
||||||
new_owner_email=member.email,
|
new_owner_email=member.email,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from core.model_runtime.errors.validate import CredentialsValidateFailedError
|
|||||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||||
from libs.helper import StrLen, uuid_value
|
from libs.helper import StrLen, uuid_value
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
|
from models.account import Account
|
||||||
from services.billing_service import BillingService
|
from services.billing_service import BillingService
|
||||||
from services.model_provider_service import ModelProviderService
|
from services.model_provider_service import ModelProviderService
|
||||||
|
|
||||||
@ -21,6 +22,10 @@ class ModelProviderListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -45,6 +50,10 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = current_user.current_tenant_id
|
||||||
# if credential_id is not provided, return current used credential
|
# if credential_id is not provided, return current used credential
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -62,6 +71,8 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider: str):
|
def post(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
@ -72,6 +83,8 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
|
|
||||||
model_provider_service = ModelProviderService()
|
model_provider_service = ModelProviderService()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
try:
|
try:
|
||||||
model_provider_service.create_provider_credential(
|
model_provider_service.create_provider_credential(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
@ -88,6 +101,8 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def put(self, provider: str):
|
def put(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
@ -99,6 +114,8 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
|
|
||||||
model_provider_service = ModelProviderService()
|
model_provider_service = ModelProviderService()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
try:
|
try:
|
||||||
model_provider_service.update_provider_credential(
|
model_provider_service.update_provider_credential(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
@ -116,12 +133,16 @@ class ModelProviderCredentialApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def delete(self, provider: str):
|
def delete(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
|
parser.add_argument("credential_id", type=uuid_value, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
model_provider_service = ModelProviderService()
|
model_provider_service = ModelProviderService()
|
||||||
model_provider_service.remove_provider_credential(
|
model_provider_service.remove_provider_credential(
|
||||||
tenant_id=current_user.current_tenant_id, provider=provider, credential_id=args["credential_id"]
|
tenant_id=current_user.current_tenant_id, provider=provider, credential_id=args["credential_id"]
|
||||||
@ -135,12 +156,16 @@ class ModelProviderCredentialSwitchApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider: str):
|
def post(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credential_id", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("credential_id", type=str, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
service = ModelProviderService()
|
service = ModelProviderService()
|
||||||
service.switch_active_provider_credential(
|
service.switch_active_provider_credential(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
@ -155,10 +180,14 @@ class ModelProviderValidateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider: str):
|
def post(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
parser.add_argument("credentials", type=dict, required=True, nullable=False, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
model_provider_service = ModelProviderService()
|
model_provider_service = ModelProviderService()
|
||||||
@ -205,9 +234,13 @@ class PreferredProviderTypeUpdateApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self, provider: str):
|
def post(self, provider: str):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
if not current_user.is_admin_or_owner:
|
if not current_user.is_admin_or_owner:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant_id = current_user.current_tenant_id
|
tenant_id = current_user.current_tenant_id
|
||||||
|
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -236,7 +269,11 @@ class ModelProviderPaymentCheckoutUrlApi(Resource):
|
|||||||
def get(self, provider: str):
|
def get(self, provider: str):
|
||||||
if provider != "anthropic":
|
if provider != "anthropic":
|
||||||
raise ValueError(f"provider name {provider} is invalid")
|
raise ValueError(f"provider name {provider} is invalid")
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
BillingService.is_tenant_owner_or_admin(current_user)
|
BillingService.is_tenant_owner_or_admin(current_user)
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
data = BillingService.get_model_provider_payment_link(
|
data = BillingService.get_model_provider_payment_link(
|
||||||
provider_name=provider,
|
provider_name=provider,
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id,
|
||||||
|
|||||||
@ -25,7 +25,7 @@ from controllers.console.wraps import (
|
|||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import TimestampField
|
from libs.helper import TimestampField
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models.account import Tenant, TenantStatus
|
from models.account import Account, Tenant, TenantStatus
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
@ -70,6 +70,8 @@ class TenantListApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def get(self):
|
def get(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
tenants = TenantService.get_join_tenants(current_user)
|
tenants = TenantService.get_join_tenants(current_user)
|
||||||
tenant_dicts = []
|
tenant_dicts = []
|
||||||
|
|
||||||
@ -83,7 +85,7 @@ class TenantListApi(Resource):
|
|||||||
"status": tenant.status,
|
"status": tenant.status,
|
||||||
"created_at": tenant.created_at,
|
"created_at": tenant.created_at,
|
||||||
"plan": features.billing.subscription.plan if features.billing.enabled else "sandbox",
|
"plan": features.billing.subscription.plan if features.billing.enabled else "sandbox",
|
||||||
"current": tenant.id == current_user.current_tenant_id,
|
"current": tenant.id == current_user.current_tenant_id if current_user.current_tenant_id else False,
|
||||||
}
|
}
|
||||||
|
|
||||||
tenant_dicts.append(tenant_dict)
|
tenant_dicts.append(tenant_dict)
|
||||||
@ -125,7 +127,11 @@ class TenantApi(Resource):
|
|||||||
if request.path == "/info":
|
if request.path == "/info":
|
||||||
logger.warning("Deprecated URL /info was used.")
|
logger.warning("Deprecated URL /info was used.")
|
||||||
|
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
tenant = current_user.current_tenant
|
tenant = current_user.current_tenant
|
||||||
|
if not tenant:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
|
|
||||||
if tenant.status == TenantStatus.ARCHIVE:
|
if tenant.status == TenantStatus.ARCHIVE:
|
||||||
tenants = TenantService.get_join_tenants(current_user)
|
tenants = TenantService.get_join_tenants(current_user)
|
||||||
@ -137,6 +143,8 @@ class TenantApi(Resource):
|
|||||||
else:
|
else:
|
||||||
raise Unauthorized("workspace is archived")
|
raise Unauthorized("workspace is archived")
|
||||||
|
|
||||||
|
if not tenant:
|
||||||
|
raise ValueError("No tenant available")
|
||||||
return WorkspaceService.get_tenant_info(tenant), 200
|
return WorkspaceService.get_tenant_info(tenant), 200
|
||||||
|
|
||||||
|
|
||||||
@ -145,6 +153,8 @@ class SwitchWorkspaceApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("tenant_id", type=str, required=True, location="json")
|
parser.add_argument("tenant_id", type=str, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@ -168,11 +178,15 @@ class CustomConfigWorkspaceApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@cloud_edition_billing_resource_check("workspace_custom")
|
@cloud_edition_billing_resource_check("workspace_custom")
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("remove_webapp_brand", type=bool, location="json")
|
parser.add_argument("remove_webapp_brand", type=bool, location="json")
|
||||||
parser.add_argument("replace_webapp_logo", type=str, location="json")
|
parser.add_argument("replace_webapp_logo", type=str, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
|
tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
|
||||||
|
|
||||||
custom_config_dict = {
|
custom_config_dict = {
|
||||||
@ -194,6 +208,8 @@ class WebappLogoWorkspaceApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@cloud_edition_billing_resource_check("workspace_custom")
|
@cloud_edition_billing_resource_check("workspace_custom")
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
# check file
|
# check file
|
||||||
if "file" not in request.files:
|
if "file" not in request.files:
|
||||||
raise NoFileUploadedError()
|
raise NoFileUploadedError()
|
||||||
@ -232,10 +248,14 @@ class WorkspaceInfoApi(Resource):
|
|||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
# Change workspace name
|
# Change workspace name
|
||||||
def post(self):
|
def post(self):
|
||||||
|
if not isinstance(current_user, Account):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("name", type=str, required=True, location="json")
|
parser.add_argument("name", type=str, required=True, location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not current_user.current_tenant_id:
|
||||||
|
raise ValueError("No current tenant")
|
||||||
tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
|
tenant = db.get_or_404(Tenant, current_user.current_tenant_id)
|
||||||
tenant.name = args["name"]
|
tenant.name = args["name"]
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|||||||
@ -242,6 +242,19 @@ def email_password_login_enabled(view: Callable[P, R]):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def email_register_enabled(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
features = FeatureService.get_system_features()
|
||||||
|
if features.is_allow_register:
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
# otherwise, return 403
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def enable_change_email(view: Callable[P, R]):
|
def enable_change_email(view: Callable[P, R]):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args: P.args, **kwargs: P.kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
|
|||||||
@ -10,7 +10,6 @@ api = ExternalApi(
|
|||||||
version="1.0",
|
version="1.0",
|
||||||
title="Files API",
|
title="Files API",
|
||||||
description="API for file operations including upload and preview",
|
description="API for file operations including upload and preview",
|
||||||
doc="/docs", # Enable Swagger UI at /files/docs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
files_ns = Namespace("files", description="File operations", path="/")
|
files_ns = Namespace("files", description="File operations", path="/")
|
||||||
@ -18,3 +17,12 @@ files_ns = Namespace("files", description="File operations", path="/")
|
|||||||
from . import image_preview, tool_files, upload
|
from . import image_preview, tool_files, upload
|
||||||
|
|
||||||
api.add_namespace(files_ns)
|
api.add_namespace(files_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"api",
|
||||||
|
"bp",
|
||||||
|
"files_ns",
|
||||||
|
"image_preview",
|
||||||
|
"tool_files",
|
||||||
|
"upload",
|
||||||
|
]
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class PluginUploadFileApi(Resource):
|
|||||||
filename=filename,
|
filename=filename,
|
||||||
mimetype=mimetype,
|
mimetype=mimetype,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
user_id=user_id,
|
user_id=user.id,
|
||||||
timestamp=timestamp,
|
timestamp=timestamp,
|
||||||
nonce=nonce,
|
nonce=nonce,
|
||||||
sign=sign,
|
sign=sign,
|
||||||
|
|||||||
@ -10,14 +10,22 @@ api = ExternalApi(
|
|||||||
version="1.0",
|
version="1.0",
|
||||||
title="Inner API",
|
title="Inner API",
|
||||||
description="Internal APIs for enterprise features, billing, and plugin communication",
|
description="Internal APIs for enterprise features, billing, and plugin communication",
|
||||||
doc="/docs", # Enable Swagger UI at /inner/api/docs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create namespace
|
# Create namespace
|
||||||
inner_api_ns = Namespace("inner_api", description="Internal API operations", path="/")
|
inner_api_ns = Namespace("inner_api", description="Internal API operations", path="/")
|
||||||
|
|
||||||
from . import mail
|
from . import mail as _mail
|
||||||
from .plugin import plugin
|
from .plugin import plugin as _plugin
|
||||||
from .workspace import workspace
|
from .workspace import workspace as _workspace
|
||||||
|
|
||||||
api.add_namespace(inner_api_ns)
|
api.add_namespace(inner_api_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"_mail",
|
||||||
|
"_plugin",
|
||||||
|
"_workspace",
|
||||||
|
"api",
|
||||||
|
"bp",
|
||||||
|
"inner_api_ns",
|
||||||
|
]
|
||||||
|
|||||||
@ -37,9 +37,9 @@ from models.model import EndUser
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/llm")
|
@inner_api_ns.route("/invoke/llm")
|
||||||
class PluginInvokeLLMApi(Resource):
|
class PluginInvokeLLMApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeLLM)
|
@plugin_data(payload_type=RequestInvokeLLM)
|
||||||
@inner_api_ns.doc("plugin_invoke_llm")
|
@inner_api_ns.doc("plugin_invoke_llm")
|
||||||
@inner_api_ns.doc(description="Invoke LLM models through plugin interface")
|
@inner_api_ns.doc(description="Invoke LLM models through plugin interface")
|
||||||
@ -60,9 +60,9 @@ class PluginInvokeLLMApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/llm/structured-output")
|
@inner_api_ns.route("/invoke/llm/structured-output")
|
||||||
class PluginInvokeLLMWithStructuredOutputApi(Resource):
|
class PluginInvokeLLMWithStructuredOutputApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeLLMWithStructuredOutput)
|
@plugin_data(payload_type=RequestInvokeLLMWithStructuredOutput)
|
||||||
@inner_api_ns.doc("plugin_invoke_llm_structured")
|
@inner_api_ns.doc("plugin_invoke_llm_structured")
|
||||||
@inner_api_ns.doc(description="Invoke LLM models with structured output through plugin interface")
|
@inner_api_ns.doc(description="Invoke LLM models with structured output through plugin interface")
|
||||||
@ -85,9 +85,9 @@ class PluginInvokeLLMWithStructuredOutputApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/text-embedding")
|
@inner_api_ns.route("/invoke/text-embedding")
|
||||||
class PluginInvokeTextEmbeddingApi(Resource):
|
class PluginInvokeTextEmbeddingApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeTextEmbedding)
|
@plugin_data(payload_type=RequestInvokeTextEmbedding)
|
||||||
@inner_api_ns.doc("plugin_invoke_text_embedding")
|
@inner_api_ns.doc("plugin_invoke_text_embedding")
|
||||||
@inner_api_ns.doc(description="Invoke text embedding models through plugin interface")
|
@inner_api_ns.doc(description="Invoke text embedding models through plugin interface")
|
||||||
@ -115,9 +115,9 @@ class PluginInvokeTextEmbeddingApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/rerank")
|
@inner_api_ns.route("/invoke/rerank")
|
||||||
class PluginInvokeRerankApi(Resource):
|
class PluginInvokeRerankApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeRerank)
|
@plugin_data(payload_type=RequestInvokeRerank)
|
||||||
@inner_api_ns.doc("plugin_invoke_rerank")
|
@inner_api_ns.doc("plugin_invoke_rerank")
|
||||||
@inner_api_ns.doc(description="Invoke rerank models through plugin interface")
|
@inner_api_ns.doc(description="Invoke rerank models through plugin interface")
|
||||||
@ -141,9 +141,9 @@ class PluginInvokeRerankApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/tts")
|
@inner_api_ns.route("/invoke/tts")
|
||||||
class PluginInvokeTTSApi(Resource):
|
class PluginInvokeTTSApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeTTS)
|
@plugin_data(payload_type=RequestInvokeTTS)
|
||||||
@inner_api_ns.doc("plugin_invoke_tts")
|
@inner_api_ns.doc("plugin_invoke_tts")
|
||||||
@inner_api_ns.doc(description="Invoke text-to-speech models through plugin interface")
|
@inner_api_ns.doc(description="Invoke text-to-speech models through plugin interface")
|
||||||
@ -168,9 +168,9 @@ class PluginInvokeTTSApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/speech2text")
|
@inner_api_ns.route("/invoke/speech2text")
|
||||||
class PluginInvokeSpeech2TextApi(Resource):
|
class PluginInvokeSpeech2TextApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeSpeech2Text)
|
@plugin_data(payload_type=RequestInvokeSpeech2Text)
|
||||||
@inner_api_ns.doc("plugin_invoke_speech2text")
|
@inner_api_ns.doc("plugin_invoke_speech2text")
|
||||||
@inner_api_ns.doc(description="Invoke speech-to-text models through plugin interface")
|
@inner_api_ns.doc(description="Invoke speech-to-text models through plugin interface")
|
||||||
@ -194,9 +194,9 @@ class PluginInvokeSpeech2TextApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/moderation")
|
@inner_api_ns.route("/invoke/moderation")
|
||||||
class PluginInvokeModerationApi(Resource):
|
class PluginInvokeModerationApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeModeration)
|
@plugin_data(payload_type=RequestInvokeModeration)
|
||||||
@inner_api_ns.doc("plugin_invoke_moderation")
|
@inner_api_ns.doc("plugin_invoke_moderation")
|
||||||
@inner_api_ns.doc(description="Invoke moderation models through plugin interface")
|
@inner_api_ns.doc(description="Invoke moderation models through plugin interface")
|
||||||
@ -220,9 +220,9 @@ class PluginInvokeModerationApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/tool")
|
@inner_api_ns.route("/invoke/tool")
|
||||||
class PluginInvokeToolApi(Resource):
|
class PluginInvokeToolApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeTool)
|
@plugin_data(payload_type=RequestInvokeTool)
|
||||||
@inner_api_ns.doc("plugin_invoke_tool")
|
@inner_api_ns.doc("plugin_invoke_tool")
|
||||||
@inner_api_ns.doc(description="Invoke tools through plugin interface")
|
@inner_api_ns.doc(description="Invoke tools through plugin interface")
|
||||||
@ -252,9 +252,9 @@ class PluginInvokeToolApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/parameter-extractor")
|
@inner_api_ns.route("/invoke/parameter-extractor")
|
||||||
class PluginInvokeParameterExtractorNodeApi(Resource):
|
class PluginInvokeParameterExtractorNodeApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeParameterExtractorNode)
|
@plugin_data(payload_type=RequestInvokeParameterExtractorNode)
|
||||||
@inner_api_ns.doc("plugin_invoke_parameter_extractor")
|
@inner_api_ns.doc("plugin_invoke_parameter_extractor")
|
||||||
@inner_api_ns.doc(description="Invoke parameter extractor node through plugin interface")
|
@inner_api_ns.doc(description="Invoke parameter extractor node through plugin interface")
|
||||||
@ -285,9 +285,9 @@ class PluginInvokeParameterExtractorNodeApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/question-classifier")
|
@inner_api_ns.route("/invoke/question-classifier")
|
||||||
class PluginInvokeQuestionClassifierNodeApi(Resource):
|
class PluginInvokeQuestionClassifierNodeApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeQuestionClassifierNode)
|
@plugin_data(payload_type=RequestInvokeQuestionClassifierNode)
|
||||||
@inner_api_ns.doc("plugin_invoke_question_classifier")
|
@inner_api_ns.doc("plugin_invoke_question_classifier")
|
||||||
@inner_api_ns.doc(description="Invoke question classifier node through plugin interface")
|
@inner_api_ns.doc(description="Invoke question classifier node through plugin interface")
|
||||||
@ -318,9 +318,9 @@ class PluginInvokeQuestionClassifierNodeApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/app")
|
@inner_api_ns.route("/invoke/app")
|
||||||
class PluginInvokeAppApi(Resource):
|
class PluginInvokeAppApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeApp)
|
@plugin_data(payload_type=RequestInvokeApp)
|
||||||
@inner_api_ns.doc("plugin_invoke_app")
|
@inner_api_ns.doc("plugin_invoke_app")
|
||||||
@inner_api_ns.doc(description="Invoke application through plugin interface")
|
@inner_api_ns.doc(description="Invoke application through plugin interface")
|
||||||
@ -348,9 +348,9 @@ class PluginInvokeAppApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/encrypt")
|
@inner_api_ns.route("/invoke/encrypt")
|
||||||
class PluginInvokeEncryptApi(Resource):
|
class PluginInvokeEncryptApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeEncrypt)
|
@plugin_data(payload_type=RequestInvokeEncrypt)
|
||||||
@inner_api_ns.doc("plugin_invoke_encrypt")
|
@inner_api_ns.doc("plugin_invoke_encrypt")
|
||||||
@inner_api_ns.doc(description="Encrypt or decrypt data through plugin interface")
|
@inner_api_ns.doc(description="Encrypt or decrypt data through plugin interface")
|
||||||
@ -375,9 +375,9 @@ class PluginInvokeEncryptApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/invoke/summary")
|
@inner_api_ns.route("/invoke/summary")
|
||||||
class PluginInvokeSummaryApi(Resource):
|
class PluginInvokeSummaryApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestInvokeSummary)
|
@plugin_data(payload_type=RequestInvokeSummary)
|
||||||
@inner_api_ns.doc("plugin_invoke_summary")
|
@inner_api_ns.doc("plugin_invoke_summary")
|
||||||
@inner_api_ns.doc(description="Invoke summary functionality through plugin interface")
|
@inner_api_ns.doc(description="Invoke summary functionality through plugin interface")
|
||||||
@ -405,9 +405,9 @@ class PluginInvokeSummaryApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/upload/file/request")
|
@inner_api_ns.route("/upload/file/request")
|
||||||
class PluginUploadFileRequestApi(Resource):
|
class PluginUploadFileRequestApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestRequestUploadFile)
|
@plugin_data(payload_type=RequestRequestUploadFile)
|
||||||
@inner_api_ns.doc("plugin_upload_file_request")
|
@inner_api_ns.doc("plugin_upload_file_request")
|
||||||
@inner_api_ns.doc(description="Request signed URL for file upload through plugin interface")
|
@inner_api_ns.doc(description="Request signed URL for file upload through plugin interface")
|
||||||
@ -426,9 +426,9 @@ class PluginUploadFileRequestApi(Resource):
|
|||||||
|
|
||||||
@inner_api_ns.route("/fetch/app/info")
|
@inner_api_ns.route("/fetch/app/info")
|
||||||
class PluginFetchAppInfoApi(Resource):
|
class PluginFetchAppInfoApi(Resource):
|
||||||
|
@get_user_tenant
|
||||||
@setup_required
|
@setup_required
|
||||||
@plugin_inner_api_only
|
@plugin_inner_api_only
|
||||||
@get_user_tenant
|
|
||||||
@plugin_data(payload_type=RequestFetchAppInfo)
|
@plugin_data(payload_type=RequestFetchAppInfo)
|
||||||
@inner_api_ns.doc("plugin_fetch_app_info")
|
@inner_api_ns.doc("plugin_fetch_app_info")
|
||||||
@inner_api_ns.doc(description="Fetch application information through plugin interface")
|
@inner_api_ns.doc(description="Fetch application information through plugin interface")
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional
|
from typing import Optional, ParamSpec, TypeVar, cast
|
||||||
|
|
||||||
from flask import current_app, request
|
from flask import current_app, request
|
||||||
from flask_login import user_logged_in
|
from flask_login import user_logged_in
|
||||||
@ -8,11 +8,13 @@ from flask_restx import reqparse
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from core.file.constants import DEFAULT_SERVICE_API_USER_ID
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.login import _get_user
|
from libs.login import current_user
|
||||||
from models.account import Tenant
|
from models.account import Tenant
|
||||||
from models.model import EndUser
|
from models.model import DefaultEndUserSessionID, EndUser
|
||||||
|
|
||||||
|
P = ParamSpec("P")
|
||||||
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
||||||
@ -25,7 +27,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
|||||||
try:
|
try:
|
||||||
with Session(db.engine) as session:
|
with Session(db.engine) as session:
|
||||||
if not user_id:
|
if not user_id:
|
||||||
user_id = DEFAULT_SERVICE_API_USER_ID
|
user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID.value
|
||||||
|
|
||||||
user_model = (
|
user_model = (
|
||||||
session.query(EndUser)
|
session.query(EndUser)
|
||||||
@ -39,7 +41,7 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
|||||||
user_model = EndUser(
|
user_model = EndUser(
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
type="service_api",
|
type="service_api",
|
||||||
is_anonymous=user_id == DEFAULT_SERVICE_API_USER_ID,
|
is_anonymous=user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID.value,
|
||||||
session_id=user_id,
|
session_id=user_id,
|
||||||
)
|
)
|
||||||
session.add(user_model)
|
session.add(user_model)
|
||||||
@ -52,28 +54,25 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser:
|
|||||||
return user_model
|
return user_model
|
||||||
|
|
||||||
|
|
||||||
def get_user_tenant(view: Optional[Callable] = None):
|
def get_user_tenant(view: Optional[Callable[P, R]] = None):
|
||||||
def decorator(view_func):
|
def decorator(view_func: Callable[P, R]):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||||
# fetch json body
|
# fetch json body
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("tenant_id", type=str, required=True, location="json")
|
parser.add_argument("tenant_id", type=str, required=True, location="json")
|
||||||
parser.add_argument("user_id", type=str, required=True, location="json")
|
parser.add_argument("user_id", type=str, required=True, location="json")
|
||||||
|
|
||||||
kwargs = parser.parse_args()
|
p = parser.parse_args()
|
||||||
|
|
||||||
user_id = kwargs.get("user_id")
|
user_id = cast(str, p.get("user_id"))
|
||||||
tenant_id = kwargs.get("tenant_id")
|
tenant_id = cast(str, p.get("tenant_id"))
|
||||||
|
|
||||||
if not tenant_id:
|
if not tenant_id:
|
||||||
raise ValueError("tenant_id is required")
|
raise ValueError("tenant_id is required")
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
user_id = DEFAULT_SERVICE_API_USER_ID
|
user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID.value
|
||||||
|
|
||||||
del kwargs["tenant_id"]
|
|
||||||
del kwargs["user_id"]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tenant_model = (
|
tenant_model = (
|
||||||
@ -95,7 +94,7 @@ def get_user_tenant(view: Optional[Callable] = None):
|
|||||||
kwargs["user_model"] = user
|
kwargs["user_model"] = user
|
||||||
|
|
||||||
current_app.login_manager._update_request_context_with_user(user) # type: ignore
|
current_app.login_manager._update_request_context_with_user(user) # type: ignore
|
||||||
user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore
|
user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore
|
||||||
|
|
||||||
return view_func(*args, **kwargs)
|
return view_func(*args, **kwargs)
|
||||||
|
|
||||||
@ -107,9 +106,9 @@ def get_user_tenant(view: Optional[Callable] = None):
|
|||||||
return decorator(view)
|
return decorator(view)
|
||||||
|
|
||||||
|
|
||||||
def plugin_data(view: Optional[Callable] = None, *, payload_type: type[BaseModel]):
|
def plugin_data(view: Optional[Callable[P, R]] = None, *, payload_type: type[BaseModel]):
|
||||||
def decorator(view_func):
|
def decorator(view_func: Callable[P, R]):
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||||
try:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@ -46,9 +46,9 @@ def enterprise_inner_api_only(view: Callable[P, R]):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def enterprise_inner_api_user_auth(view):
|
def enterprise_inner_api_user_auth(view: Callable[P, R]):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
if not dify_config.INNER_API:
|
if not dify_config.INNER_API:
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ api = ExternalApi(
|
|||||||
version="1.0",
|
version="1.0",
|
||||||
title="MCP API",
|
title="MCP API",
|
||||||
description="API for Model Context Protocol operations",
|
description="API for Model Context Protocol operations",
|
||||||
doc="/docs", # Enable Swagger UI at /mcp/docs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
|
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
|
||||||
@ -18,3 +17,10 @@ mcp_ns = Namespace("mcp", description="MCP operations", path="/")
|
|||||||
from . import mcp
|
from . import mcp
|
||||||
|
|
||||||
api.add_namespace(mcp_ns)
|
api.add_namespace(mcp_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"api",
|
||||||
|
"bp",
|
||||||
|
"mcp",
|
||||||
|
"mcp_ns",
|
||||||
|
]
|
||||||
|
|||||||
@ -150,7 +150,7 @@ class MCPAppApi(Resource):
|
|||||||
def _get_user_input_form(self, app: App) -> list[VariableEntity]:
|
def _get_user_input_form(self, app: App) -> list[VariableEntity]:
|
||||||
"""Get and convert user input form"""
|
"""Get and convert user input form"""
|
||||||
# Get raw user input form based on app mode
|
# Get raw user input form based on app mode
|
||||||
if app.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
if app.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
if not app.workflow:
|
if not app.workflow:
|
||||||
raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable")
|
raise MCPRequestError(mcp_types.INVALID_REQUEST, "App is unavailable")
|
||||||
raw_user_input_form = app.workflow.user_input_form(to_old_structure=True)
|
raw_user_input_form = app.workflow.user_input_form(to_old_structure=True)
|
||||||
|
|||||||
@ -10,14 +10,50 @@ api = ExternalApi(
|
|||||||
version="1.0",
|
version="1.0",
|
||||||
title="Service API",
|
title="Service API",
|
||||||
description="API for application services",
|
description="API for application services",
|
||||||
doc="/docs", # Enable Swagger UI at /v1/docs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
service_api_ns = Namespace("service_api", description="Service operations", path="/")
|
service_api_ns = Namespace("service_api", description="Service operations", path="/")
|
||||||
|
|
||||||
from . import index
|
from . import index
|
||||||
from .app import annotation, app, audio, completion, conversation, file, file_preview, message, site, workflow
|
from .app import (
|
||||||
from .dataset import dataset, document, hit_testing, metadata, segment, upload_file
|
annotation,
|
||||||
|
app,
|
||||||
|
audio,
|
||||||
|
completion,
|
||||||
|
conversation,
|
||||||
|
file,
|
||||||
|
file_preview,
|
||||||
|
message,
|
||||||
|
site,
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
from .dataset import (
|
||||||
|
dataset,
|
||||||
|
document,
|
||||||
|
hit_testing,
|
||||||
|
metadata,
|
||||||
|
segment,
|
||||||
|
)
|
||||||
from .workspace import models
|
from .workspace import models
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"annotation",
|
||||||
|
"app",
|
||||||
|
"audio",
|
||||||
|
"completion",
|
||||||
|
"conversation",
|
||||||
|
"dataset",
|
||||||
|
"document",
|
||||||
|
"file",
|
||||||
|
"file_preview",
|
||||||
|
"hit_testing",
|
||||||
|
"index",
|
||||||
|
"message",
|
||||||
|
"metadata",
|
||||||
|
"models",
|
||||||
|
"segment",
|
||||||
|
"site",
|
||||||
|
"workflow",
|
||||||
|
]
|
||||||
|
|
||||||
api.add_namespace(service_api_ns)
|
api.add_namespace(service_api_ns)
|
||||||
|
|||||||
@ -165,7 +165,7 @@ class AnnotationUpdateDeleteApi(Resource):
|
|||||||
def put(self, app_model: App, annotation_id):
|
def put(self, app_model: App, annotation_id):
|
||||||
"""Update an existing annotation."""
|
"""Update an existing annotation."""
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
annotation_id = str(annotation_id)
|
annotation_id = str(annotation_id)
|
||||||
@ -189,7 +189,7 @@ class AnnotationUpdateDeleteApi(Resource):
|
|||||||
"""Delete an annotation."""
|
"""Delete an annotation."""
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
|
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
annotation_id = str(annotation_id)
|
annotation_id = str(annotation_id)
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class AppParameterApi(Resource):
|
|||||||
|
|
||||||
Returns the input form parameters and configuration for the application.
|
Returns the input form parameters and configuration for the application.
|
||||||
"""
|
"""
|
||||||
if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
workflow = app_model.workflow
|
workflow = app_model.workflow
|
||||||
if workflow is None:
|
if workflow is None:
|
||||||
raise AppUnavailableError()
|
raise AppUnavailableError()
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from flask_restx import Resource, reqparse
|
from flask_restx import Resource, reqparse
|
||||||
|
from flask_restx._http import HTTPStatus
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import BadRequest, NotFound
|
from werkzeug.exceptions import BadRequest, NotFound
|
||||||
@ -121,7 +122,7 @@ class ConversationDetailApi(Resource):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
|
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
|
||||||
@service_api_ns.marshal_with(build_conversation_delete_model(service_api_ns), code=204)
|
@service_api_ns.marshal_with(build_conversation_delete_model(service_api_ns), code=HTTPStatus.NO_CONTENT)
|
||||||
def delete(self, app_model: App, end_user: EndUser, c_id):
|
def delete(self, app_model: App, end_user: EndUser, c_id):
|
||||||
"""Delete a specific conversation."""
|
"""Delete a specific conversation."""
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
|
|||||||
@ -340,6 +340,9 @@ class DatasetApi(DatasetApiResource):
|
|||||||
else:
|
else:
|
||||||
data["embedding_available"] = True
|
data["embedding_available"] = True
|
||||||
|
|
||||||
|
# force update search method to keyword_search if indexing_technique is economic
|
||||||
|
data["retrieval_model_dict"]["search_method"] = "keyword_search"
|
||||||
|
|
||||||
if data.get("permission") == "partial_members":
|
if data.get("permission") == "partial_members":
|
||||||
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
part_users_list = DatasetPermissionService.get_dataset_partial_member_list(dataset_id_str)
|
||||||
data.update({"partial_member_list": part_users_list})
|
data.update({"partial_member_list": part_users_list})
|
||||||
@ -559,7 +562,7 @@ class DatasetTagsApi(DatasetApiResource):
|
|||||||
def post(self, _, dataset_id):
|
def post(self, _, dataset_id):
|
||||||
"""Add a knowledge type tag."""
|
"""Add a knowledge type tag."""
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not (current_user.is_editor or current_user.is_dataset_editor):
|
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
args = tag_create_parser.parse_args()
|
args = tag_create_parser.parse_args()
|
||||||
@ -583,7 +586,7 @@ class DatasetTagsApi(DatasetApiResource):
|
|||||||
@validate_dataset_token
|
@validate_dataset_token
|
||||||
def patch(self, _, dataset_id):
|
def patch(self, _, dataset_id):
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not (current_user.is_editor or current_user.is_dataset_editor):
|
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
args = tag_update_parser.parse_args()
|
args = tag_update_parser.parse_args()
|
||||||
@ -610,7 +613,7 @@ class DatasetTagsApi(DatasetApiResource):
|
|||||||
def delete(self, _, dataset_id):
|
def delete(self, _, dataset_id):
|
||||||
"""Delete a knowledge type tag."""
|
"""Delete a knowledge type tag."""
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not current_user.is_editor:
|
if not current_user.has_edit_permission:
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
args = tag_delete_parser.parse_args()
|
args = tag_delete_parser.parse_args()
|
||||||
TagService.delete_tag(args.get("tag_id"))
|
TagService.delete_tag(args.get("tag_id"))
|
||||||
@ -634,7 +637,7 @@ class DatasetTagBindingApi(DatasetApiResource):
|
|||||||
def post(self, _, dataset_id):
|
def post(self, _, dataset_id):
|
||||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not (current_user.is_editor or current_user.is_dataset_editor):
|
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
args = tag_binding_parser.parse_args()
|
args = tag_binding_parser.parse_args()
|
||||||
@ -660,7 +663,7 @@ class DatasetTagUnbindingApi(DatasetApiResource):
|
|||||||
def post(self, _, dataset_id):
|
def post(self, _, dataset_id):
|
||||||
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
# The role of the current user in the ta table must be admin, owner, editor, or dataset_operator
|
||||||
assert isinstance(current_user, Account)
|
assert isinstance(current_user, Account)
|
||||||
if not (current_user.is_editor or current_user.is_dataset_editor):
|
if not (current_user.has_edit_permission or current_user.is_dataset_editor):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
args = tag_unbinding_parser.parse_args()
|
args = tag_unbinding_parser.parse_args()
|
||||||
|
|||||||
@ -30,6 +30,7 @@ from extensions.ext_database import db
|
|||||||
from fields.document_fields import document_fields, document_status_fields
|
from fields.document_fields import document_fields, document_status_fields
|
||||||
from libs.login import current_user
|
from libs.login import current_user
|
||||||
from models.dataset import Dataset, Document, DocumentSegment
|
from models.dataset import Dataset, Document, DocumentSegment
|
||||||
|
from models.model import EndUser
|
||||||
from services.dataset_service import DatasetService, DocumentService
|
from services.dataset_service import DatasetService, DocumentService
|
||||||
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
|
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
@ -298,6 +299,9 @@ class DocumentAddByFileApi(DatasetApiResource):
|
|||||||
if not file.filename:
|
if not file.filename:
|
||||||
raise FilenameNotExistsError
|
raise FilenameNotExistsError
|
||||||
|
|
||||||
|
if not isinstance(current_user, EndUser):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
|
|
||||||
upload_file = FileService.upload_file(
|
upload_file = FileService.upload_file(
|
||||||
filename=file.filename,
|
filename=file.filename,
|
||||||
content=file.read(),
|
content=file.read(),
|
||||||
@ -387,6 +391,8 @@ class DocumentUpdateByFileApi(DatasetApiResource):
|
|||||||
raise FilenameNotExistsError
|
raise FilenameNotExistsError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(current_user, EndUser):
|
||||||
|
raise ValueError("Invalid user account")
|
||||||
upload_file = FileService.upload_file(
|
upload_file = FileService.upload_file(
|
||||||
filename=file.filename,
|
filename=file.filename,
|
||||||
content=file.read(),
|
content=file.read(),
|
||||||
|
|||||||
@ -174,7 +174,7 @@ class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
|
|||||||
MetadataService.enable_built_in_field(dataset)
|
MetadataService.enable_built_in_field(dataset)
|
||||||
elif action == "disable":
|
elif action == "disable":
|
||||||
MetadataService.disable_built_in_field(dataset)
|
MetadataService.disable_built_in_field(dataset)
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
@service_api_ns.route("/datasets/<uuid:dataset_id>/documents/metadata")
|
@service_api_ns.route("/datasets/<uuid:dataset_id>/documents/metadata")
|
||||||
@ -204,4 +204,4 @@ class DocumentMetadataEditServiceApi(DatasetApiResource):
|
|||||||
|
|
||||||
MetadataService.update_documents_metadata(dataset, metadata_args)
|
MetadataService.update_documents_metadata(dataset, metadata_args)
|
||||||
|
|
||||||
return 200
|
return {"result": "success"}, 200
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
from controllers.service_api import service_api_ns
|
|
||||||
from controllers.service_api.wraps import (
|
|
||||||
DatasetApiResource,
|
|
||||||
)
|
|
||||||
from core.file import helpers as file_helpers
|
|
||||||
from extensions.ext_database import db
|
|
||||||
from models.dataset import Dataset
|
|
||||||
from models.model import UploadFile
|
|
||||||
from services.dataset_service import DocumentService
|
|
||||||
|
|
||||||
|
|
||||||
@service_api_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/upload-file")
|
|
||||||
class UploadFileApi(DatasetApiResource):
|
|
||||||
@service_api_ns.doc("get_upload_file")
|
|
||||||
@service_api_ns.doc(description="Get upload file information and download URL")
|
|
||||||
@service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
|
||||||
@service_api_ns.doc(
|
|
||||||
responses={
|
|
||||||
200: "Upload file information retrieved successfully",
|
|
||||||
401: "Unauthorized - invalid API token",
|
|
||||||
404: "Dataset, document, or upload file not found",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def get(self, tenant_id, dataset_id, document_id):
|
|
||||||
"""Get upload file information and download URL.
|
|
||||||
|
|
||||||
Returns information about an uploaded file including its download URL.
|
|
||||||
"""
|
|
||||||
# check dataset
|
|
||||||
dataset_id = str(dataset_id)
|
|
||||||
tenant_id = str(tenant_id)
|
|
||||||
dataset = db.session.query(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).first()
|
|
||||||
if not dataset:
|
|
||||||
raise NotFound("Dataset not found.")
|
|
||||||
# check document
|
|
||||||
document_id = str(document_id)
|
|
||||||
document = DocumentService.get_document(dataset.id, document_id)
|
|
||||||
if not document:
|
|
||||||
raise NotFound("Document not found.")
|
|
||||||
# check upload file
|
|
||||||
if document.data_source_type != "upload_file":
|
|
||||||
raise ValueError(f"Document data source type ({document.data_source_type}) is not upload_file.")
|
|
||||||
data_source_info = document.data_source_info_dict
|
|
||||||
if data_source_info and "upload_file_id" in data_source_info:
|
|
||||||
file_id = data_source_info["upload_file_id"]
|
|
||||||
upload_file = db.session.query(UploadFile).where(UploadFile.id == file_id).first()
|
|
||||||
if not upload_file:
|
|
||||||
raise NotFound("UploadFile not found.")
|
|
||||||
else:
|
|
||||||
raise ValueError("Upload file id not found in document data source info.")
|
|
||||||
|
|
||||||
url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
|
|
||||||
return {
|
|
||||||
"id": upload_file.id,
|
|
||||||
"name": upload_file.name,
|
|
||||||
"size": upload_file.size,
|
|
||||||
"extension": upload_file.extension,
|
|
||||||
"url": url,
|
|
||||||
"download_url": f"{url}&as_attachment=true",
|
|
||||||
"mime_type": upload_file.mime_type,
|
|
||||||
"created_by": upload_file.created_by,
|
|
||||||
"created_at": upload_file.created_at.timestamp(),
|
|
||||||
}, 200
|
|
||||||
@ -19,7 +19,7 @@ class ModelProviderAvailableModelApi(Resource):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
@validate_dataset_token
|
@validate_dataset_token
|
||||||
def get(self, _, model_type):
|
def get(self, _, model_type: str):
|
||||||
"""Get available models by model type.
|
"""Get available models by model type.
|
||||||
|
|
||||||
Returns a list of available models for the specified model type.
|
Returns a list of available models for the specified model type.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from collections.abc import Callable
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import StrEnum, auto
|
from enum import StrEnum, auto
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Optional, ParamSpec, TypeVar
|
from typing import Concatenate, Optional, ParamSpec, TypeVar
|
||||||
|
|
||||||
from flask import current_app, request
|
from flask import current_app, request
|
||||||
from flask_login import user_logged_in
|
from flask_login import user_logged_in
|
||||||
@ -13,18 +13,18 @@ from sqlalchemy import select, update
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
|
from werkzeug.exceptions import Forbidden, NotFound, Unauthorized
|
||||||
|
|
||||||
from core.file.constants import DEFAULT_SERVICE_API_USER_ID
|
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from libs.datetime_utils import naive_utc_now
|
from libs.datetime_utils import naive_utc_now
|
||||||
from libs.login import _get_user
|
from libs.login import current_user
|
||||||
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
|
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
|
||||||
from models.dataset import Dataset, RateLimitLog
|
from models.dataset import Dataset, RateLimitLog
|
||||||
from models.model import ApiToken, App, EndUser
|
from models.model import ApiToken, App, DefaultEndUserSessionID, EndUser
|
||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
P = ParamSpec("P")
|
P = ParamSpec("P")
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class WhereisUserArg(StrEnum):
|
class WhereisUserArg(StrEnum):
|
||||||
@ -42,10 +42,10 @@ class FetchUserArg(BaseModel):
|
|||||||
required: bool = False
|
required: bool = False
|
||||||
|
|
||||||
|
|
||||||
def validate_app_token(view: Optional[Callable] = None, *, fetch_user_arg: Optional[FetchUserArg] = None):
|
def validate_app_token(view: Optional[Callable[P, R]] = None, *, fetch_user_arg: Optional[FetchUserArg] = None):
|
||||||
def decorator(view_func):
|
def decorator(view_func: Callable[P, R]):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def decorated_view(*args, **kwargs):
|
def decorated_view(*args: P.args, **kwargs: P.kwargs):
|
||||||
api_token = validate_and_get_api_token("app")
|
api_token = validate_and_get_api_token("app")
|
||||||
|
|
||||||
app_model = db.session.query(App).where(App.id == api_token.app_id).first()
|
app_model = db.session.query(App).where(App.id == api_token.app_id).first()
|
||||||
@ -189,10 +189,10 @@ def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
|
|||||||
return interceptor
|
return interceptor
|
||||||
|
|
||||||
|
|
||||||
def validate_dataset_token(view=None):
|
def validate_dataset_token(view: Optional[Callable[Concatenate[T, P], R]] = None):
|
||||||
def decorator(view):
|
def decorator(view: Callable[Concatenate[T, P], R]):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
api_token = validate_and_get_api_token("dataset")
|
api_token = validate_and_get_api_token("dataset")
|
||||||
tenant_account_join = (
|
tenant_account_join = (
|
||||||
db.session.query(Tenant, TenantAccountJoin)
|
db.session.query(Tenant, TenantAccountJoin)
|
||||||
@ -209,7 +209,7 @@ def validate_dataset_token(view=None):
|
|||||||
if account:
|
if account:
|
||||||
account.current_tenant = tenant
|
account.current_tenant = tenant
|
||||||
current_app.login_manager._update_request_context_with_user(account) # type: ignore
|
current_app.login_manager._update_request_context_with_user(account) # type: ignore
|
||||||
user_logged_in.send(current_app._get_current_object(), user=_get_user()) # type: ignore
|
user_logged_in.send(current_app._get_current_object(), user=current_user) # type: ignore
|
||||||
else:
|
else:
|
||||||
raise Unauthorized("Tenant owner account does not exist.")
|
raise Unauthorized("Tenant owner account does not exist.")
|
||||||
else:
|
else:
|
||||||
@ -272,7 +272,7 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
|
|||||||
Create or update session terminal based on user ID.
|
Create or update session terminal based on user ID.
|
||||||
"""
|
"""
|
||||||
if not user_id:
|
if not user_id:
|
||||||
user_id = DEFAULT_SERVICE_API_USER_ID
|
user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID.value
|
||||||
|
|
||||||
with Session(db.engine, expire_on_commit=False) as session:
|
with Session(db.engine, expire_on_commit=False) as session:
|
||||||
end_user = (
|
end_user = (
|
||||||
@ -291,7 +291,7 @@ def create_or_update_end_user_for_user_id(app_model: App, user_id: Optional[str]
|
|||||||
tenant_id=app_model.tenant_id,
|
tenant_id=app_model.tenant_id,
|
||||||
app_id=app_model.id,
|
app_id=app_model.id,
|
||||||
type="service_api",
|
type="service_api",
|
||||||
is_anonymous=user_id == DEFAULT_SERVICE_API_USER_ID,
|
is_anonymous=user_id == DefaultEndUserSessionID.DEFAULT_SESSION_ID.value,
|
||||||
session_id=user_id,
|
session_id=user_id,
|
||||||
)
|
)
|
||||||
session.add(end_user)
|
session.add(end_user)
|
||||||
|
|||||||
@ -10,7 +10,6 @@ api = ExternalApi(
|
|||||||
version="1.0",
|
version="1.0",
|
||||||
title="Web API",
|
title="Web API",
|
||||||
description="Public APIs for web applications including file uploads, chat interactions, and app management",
|
description="Public APIs for web applications including file uploads, chat interactions, and app management",
|
||||||
doc="/docs", # Enable Swagger UI at /api/docs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create namespace
|
# Create namespace
|
||||||
@ -34,3 +33,23 @@ from . import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
api.add_namespace(web_ns)
|
api.add_namespace(web_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"api",
|
||||||
|
"app",
|
||||||
|
"audio",
|
||||||
|
"bp",
|
||||||
|
"completion",
|
||||||
|
"conversation",
|
||||||
|
"feature",
|
||||||
|
"files",
|
||||||
|
"forgot_password",
|
||||||
|
"login",
|
||||||
|
"message",
|
||||||
|
"passport",
|
||||||
|
"remote_files",
|
||||||
|
"saved_message",
|
||||||
|
"site",
|
||||||
|
"web_ns",
|
||||||
|
"workflow",
|
||||||
|
]
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class AppParameterApi(WebApiResource):
|
|||||||
@marshal_with(fields.parameters_fields)
|
@marshal_with(fields.parameters_fields)
|
||||||
def get(self, app_model: App, end_user):
|
def get(self, app_model: App, end_user):
|
||||||
"""Retrieve app parameters."""
|
"""Retrieve app parameters."""
|
||||||
if app_model.mode in {AppMode.ADVANCED_CHAT.value, AppMode.WORKFLOW.value}:
|
if app_model.mode in {AppMode.ADVANCED_CHAT, AppMode.WORKFLOW}:
|
||||||
workflow = app_model.workflow
|
workflow = app_model.workflow
|
||||||
if workflow is None:
|
if workflow is None:
|
||||||
raise AppUnavailableError()
|
raise AppUnavailableError()
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from flask_restx import fields, marshal_with, reqparse
|
|||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import (
|
from controllers.web.error import (
|
||||||
AppUnavailableError,
|
AppUnavailableError,
|
||||||
AudioTooLargeError,
|
AudioTooLargeError,
|
||||||
@ -32,15 +32,16 @@ from services.errors.audio import (
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/audio-to-text")
|
||||||
class AudioApi(WebApiResource):
|
class AudioApi(WebApiResource):
|
||||||
audio_to_text_response_fields = {
|
audio_to_text_response_fields = {
|
||||||
"text": fields.String,
|
"text": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@marshal_with(audio_to_text_response_fields)
|
@marshal_with(audio_to_text_response_fields)
|
||||||
@api.doc("Audio to Text")
|
@web_ns.doc("Audio to Text")
|
||||||
@api.doc(description="Convert audio file to text using speech-to-text service.")
|
@web_ns.doc(description="Convert audio file to text using speech-to-text service.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -85,6 +86,7 @@ class AudioApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/text-to-audio")
|
||||||
class TextApi(WebApiResource):
|
class TextApi(WebApiResource):
|
||||||
text_to_audio_response_fields = {
|
text_to_audio_response_fields = {
|
||||||
"audio_url": fields.String,
|
"audio_url": fields.String,
|
||||||
@ -92,9 +94,9 @@ class TextApi(WebApiResource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@marshal_with(text_to_audio_response_fields)
|
@marshal_with(text_to_audio_response_fields)
|
||||||
@api.doc("Text to Audio")
|
@web_ns.doc("Text to Audio")
|
||||||
@api.doc(description="Convert text to audio using text-to-speech service.")
|
@web_ns.doc(description="Convert text to audio using text-to-speech service.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -145,7 +147,3 @@ class TextApi(WebApiResource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Failed to handle post request to TextApi")
|
logger.exception("Failed to handle post request to TextApi")
|
||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AudioApi, "/audio-to-text")
|
|
||||||
api.add_resource(TextApi, "/text-to-audio")
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from flask_restx import reqparse
|
|||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import (
|
from controllers.web.error import (
|
||||||
AppUnavailableError,
|
AppUnavailableError,
|
||||||
CompletionRequestError,
|
CompletionRequestError,
|
||||||
@ -35,10 +35,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# define completion api for user
|
# define completion api for user
|
||||||
|
@web_ns.route("/completion-messages")
|
||||||
class CompletionApi(WebApiResource):
|
class CompletionApi(WebApiResource):
|
||||||
@api.doc("Create Completion Message")
|
@web_ns.doc("Create Completion Message")
|
||||||
@api.doc(description="Create a completion message for text generation applications.")
|
@web_ns.doc(description="Create a completion message for text generation applications.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
params={
|
params={
|
||||||
"inputs": {"description": "Input variables for the completion", "type": "object", "required": True},
|
"inputs": {"description": "Input variables for the completion", "type": "object", "required": True},
|
||||||
"query": {"description": "Query text for completion", "type": "string", "required": False},
|
"query": {"description": "Query text for completion", "type": "string", "required": False},
|
||||||
@ -52,7 +53,7 @@ class CompletionApi(WebApiResource):
|
|||||||
"retriever_from": {"description": "Source of retriever", "type": "string", "required": False},
|
"retriever_from": {"description": "Source of retriever", "type": "string", "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -106,11 +107,12 @@ class CompletionApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/completion-messages/<string:task_id>/stop")
|
||||||
class CompletionStopApi(WebApiResource):
|
class CompletionStopApi(WebApiResource):
|
||||||
@api.doc("Stop Completion Message")
|
@web_ns.doc("Stop Completion Message")
|
||||||
@api.doc(description="Stop a running completion message task.")
|
@web_ns.doc(description="Stop a running completion message task.")
|
||||||
@api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}})
|
@web_ns.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}})
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -129,10 +131,11 @@ class CompletionStopApi(WebApiResource):
|
|||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/chat-messages")
|
||||||
class ChatApi(WebApiResource):
|
class ChatApi(WebApiResource):
|
||||||
@api.doc("Create Chat Message")
|
@web_ns.doc("Create Chat Message")
|
||||||
@api.doc(description="Create a chat message for conversational applications.")
|
@web_ns.doc(description="Create a chat message for conversational applications.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
params={
|
params={
|
||||||
"inputs": {"description": "Input variables for the chat", "type": "object", "required": True},
|
"inputs": {"description": "Input variables for the chat", "type": "object", "required": True},
|
||||||
"query": {"description": "User query/message", "type": "string", "required": True},
|
"query": {"description": "User query/message", "type": "string", "required": True},
|
||||||
@ -148,7 +151,7 @@ class ChatApi(WebApiResource):
|
|||||||
"retriever_from": {"description": "Source of retriever", "type": "string", "required": False},
|
"retriever_from": {"description": "Source of retriever", "type": "string", "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -207,11 +210,12 @@ class ChatApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/chat-messages/<string:task_id>/stop")
|
||||||
class ChatStopApi(WebApiResource):
|
class ChatStopApi(WebApiResource):
|
||||||
@api.doc("Stop Chat Message")
|
@web_ns.doc("Stop Chat Message")
|
||||||
@api.doc(description="Stop a running chat message task.")
|
@web_ns.doc(description="Stop a running chat message task.")
|
||||||
@api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}})
|
@web_ns.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}})
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -229,9 +233,3 @@ class ChatStopApi(WebApiResource):
|
|||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(CompletionApi, "/completion-messages")
|
|
||||||
api.add_resource(CompletionStopApi, "/completion-messages/<string:task_id>/stop")
|
|
||||||
api.add_resource(ChatApi, "/chat-messages")
|
|
||||||
api.add_resource(ChatStopApi, "/chat-messages/<string:task_id>/stop")
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from flask_restx.inputs import int_range
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import NotChatAppError
|
from controllers.web.error import NotChatAppError
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||||
@ -16,7 +16,44 @@ from services.errors.conversation import ConversationNotExistsError, LastConvers
|
|||||||
from services.web_conversation_service import WebConversationService
|
from services.web_conversation_service import WebConversationService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/conversations")
|
||||||
class ConversationListApi(WebApiResource):
|
class ConversationListApi(WebApiResource):
|
||||||
|
@web_ns.doc("Get Conversation List")
|
||||||
|
@web_ns.doc(description="Retrieve paginated list of conversations for a chat application.")
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"last_id": {"description": "Last conversation ID for pagination", "type": "string", "required": False},
|
||||||
|
"limit": {
|
||||||
|
"description": "Number of conversations to return (1-100)",
|
||||||
|
"type": "integer",
|
||||||
|
"required": False,
|
||||||
|
"default": 20,
|
||||||
|
},
|
||||||
|
"pinned": {
|
||||||
|
"description": "Filter by pinned status",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["true", "false"],
|
||||||
|
"required": False,
|
||||||
|
},
|
||||||
|
"sort_by": {
|
||||||
|
"description": "Sort order",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["created_at", "-created_at", "updated_at", "-updated_at"],
|
||||||
|
"required": False,
|
||||||
|
"default": "-updated_at",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Success",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "App Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(conversation_infinite_scroll_pagination_fields)
|
@marshal_with(conversation_infinite_scroll_pagination_fields)
|
||||||
def get(self, app_model, end_user):
|
def get(self, app_model, end_user):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -57,11 +94,25 @@ class ConversationListApi(WebApiResource):
|
|||||||
raise NotFound("Last Conversation Not Exists.")
|
raise NotFound("Last Conversation Not Exists.")
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/conversations/<uuid:c_id>")
|
||||||
class ConversationApi(WebApiResource):
|
class ConversationApi(WebApiResource):
|
||||||
delete_response_fields = {
|
delete_response_fields = {
|
||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Delete Conversation")
|
||||||
|
@web_ns.doc(description="Delete a specific conversation.")
|
||||||
|
@web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
204: "Conversation deleted successfully",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Conversation Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(delete_response_fields)
|
@marshal_with(delete_response_fields)
|
||||||
def delete(self, app_model, end_user, c_id):
|
def delete(self, app_model, end_user, c_id):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -76,7 +127,32 @@ class ConversationApi(WebApiResource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/conversations/<uuid:c_id>/name")
|
||||||
class ConversationRenameApi(WebApiResource):
|
class ConversationRenameApi(WebApiResource):
|
||||||
|
@web_ns.doc("Rename Conversation")
|
||||||
|
@web_ns.doc(description="Rename a specific conversation with a custom name or auto-generate one.")
|
||||||
|
@web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"name": {"description": "New conversation name", "type": "string", "required": False},
|
||||||
|
"auto_generate": {
|
||||||
|
"description": "Auto-generate conversation name",
|
||||||
|
"type": "boolean",
|
||||||
|
"required": False,
|
||||||
|
"default": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Conversation renamed successfully",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Conversation Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(simple_conversation_fields)
|
@marshal_with(simple_conversation_fields)
|
||||||
def post(self, app_model, end_user, c_id):
|
def post(self, app_model, end_user, c_id):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -96,11 +172,25 @@ class ConversationRenameApi(WebApiResource):
|
|||||||
raise NotFound("Conversation Not Exists.")
|
raise NotFound("Conversation Not Exists.")
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/conversations/<uuid:c_id>/pin")
|
||||||
class ConversationPinApi(WebApiResource):
|
class ConversationPinApi(WebApiResource):
|
||||||
pin_response_fields = {
|
pin_response_fields = {
|
||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Pin Conversation")
|
||||||
|
@web_ns.doc(description="Pin a specific conversation to keep it at the top of the list.")
|
||||||
|
@web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Conversation pinned successfully",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Conversation Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(pin_response_fields)
|
@marshal_with(pin_response_fields)
|
||||||
def patch(self, app_model, end_user, c_id):
|
def patch(self, app_model, end_user, c_id):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -117,11 +207,25 @@ class ConversationPinApi(WebApiResource):
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/conversations/<uuid:c_id>/unpin")
|
||||||
class ConversationUnPinApi(WebApiResource):
|
class ConversationUnPinApi(WebApiResource):
|
||||||
unpin_response_fields = {
|
unpin_response_fields = {
|
||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Unpin Conversation")
|
||||||
|
@web_ns.doc(description="Unpin a specific conversation to remove it from the top of the list.")
|
||||||
|
@web_ns.doc(params={"c_id": {"description": "Conversation UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Conversation unpinned successfully",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Conversation Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(unpin_response_fields)
|
@marshal_with(unpin_response_fields)
|
||||||
def patch(self, app_model, end_user, c_id):
|
def patch(self, app_model, end_user, c_id):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -132,10 +236,3 @@ class ConversationUnPinApi(WebApiResource):
|
|||||||
WebConversationService.unpin(app_model, conversation_id, end_user)
|
WebConversationService.unpin(app_model, conversation_id, end_user)
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ConversationRenameApi, "/conversations/<uuid:c_id>/name", endpoint="web_conversation_name")
|
|
||||||
api.add_resource(ConversationListApi, "/conversations")
|
|
||||||
api.add_resource(ConversationApi, "/conversations/<uuid:c_id>")
|
|
||||||
api.add_resource(ConversationPinApi, "/conversations/<uuid:c_id>/pin")
|
|
||||||
api.add_resource(ConversationUnPinApi, "/conversations/<uuid:c_id>/unpin")
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from flask_restx import fields, marshal_with, reqparse
|
|||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from werkzeug.exceptions import InternalServerError, NotFound
|
from werkzeug.exceptions import InternalServerError, NotFound
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import (
|
from controllers.web.error import (
|
||||||
AppMoreLikeThisDisabledError,
|
AppMoreLikeThisDisabledError,
|
||||||
AppSuggestedQuestionsAfterAnswerDisabledError,
|
AppSuggestedQuestionsAfterAnswerDisabledError,
|
||||||
@ -38,6 +38,7 @@ from services.message_service import MessageService
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/messages")
|
||||||
class MessageListApi(WebApiResource):
|
class MessageListApi(WebApiResource):
|
||||||
message_fields = {
|
message_fields = {
|
||||||
"id": fields.String,
|
"id": fields.String,
|
||||||
@ -62,6 +63,30 @@ class MessageListApi(WebApiResource):
|
|||||||
"data": fields.List(fields.Nested(message_fields)),
|
"data": fields.List(fields.Nested(message_fields)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Get Message List")
|
||||||
|
@web_ns.doc(description="Retrieve paginated list of messages from a conversation in a chat application.")
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"conversation_id": {"description": "Conversation UUID", "type": "string", "required": True},
|
||||||
|
"first_id": {"description": "First message ID for pagination", "type": "string", "required": False},
|
||||||
|
"limit": {
|
||||||
|
"description": "Number of messages to return (1-100)",
|
||||||
|
"type": "integer",
|
||||||
|
"required": False,
|
||||||
|
"default": 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Success",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Conversation Not Found or Not a Chat App",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(message_infinite_scroll_pagination_fields)
|
@marshal_with(message_infinite_scroll_pagination_fields)
|
||||||
def get(self, app_model, end_user):
|
def get(self, app_model, end_user):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -84,11 +109,36 @@ class MessageListApi(WebApiResource):
|
|||||||
raise NotFound("First Message Not Exists.")
|
raise NotFound("First Message Not Exists.")
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/messages/<uuid:message_id>/feedbacks")
|
||||||
class MessageFeedbackApi(WebApiResource):
|
class MessageFeedbackApi(WebApiResource):
|
||||||
feedback_response_fields = {
|
feedback_response_fields = {
|
||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Create Message Feedback")
|
||||||
|
@web_ns.doc(description="Submit feedback (like/dislike) for a specific message.")
|
||||||
|
@web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"rating": {
|
||||||
|
"description": "Feedback rating",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["like", "dislike"],
|
||||||
|
"required": False,
|
||||||
|
},
|
||||||
|
"content": {"description": "Feedback content/comment", "type": "string", "required": False},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Feedback submitted successfully",
|
||||||
|
400: "Bad Request",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Message Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(feedback_response_fields)
|
@marshal_with(feedback_response_fields)
|
||||||
def post(self, app_model, end_user, message_id):
|
def post(self, app_model, end_user, message_id):
|
||||||
message_id = str(message_id)
|
message_id = str(message_id)
|
||||||
@ -112,7 +162,31 @@ class MessageFeedbackApi(WebApiResource):
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/messages/<uuid:message_id>/more-like-this")
|
||||||
class MessageMoreLikeThisApi(WebApiResource):
|
class MessageMoreLikeThisApi(WebApiResource):
|
||||||
|
@web_ns.doc("Generate More Like This")
|
||||||
|
@web_ns.doc(description="Generate a new completion similar to an existing message (completion apps only).")
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"message_id": {"description": "Message UUID", "type": "string", "required": True},
|
||||||
|
"response_mode": {
|
||||||
|
"description": "Response mode",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["blocking", "streaming"],
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Success",
|
||||||
|
400: "Bad Request - Not a completion app or feature disabled",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Message Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
def get(self, app_model, end_user, message_id):
|
def get(self, app_model, end_user, message_id):
|
||||||
if app_model.mode != "completion":
|
if app_model.mode != "completion":
|
||||||
raise NotCompletionAppError()
|
raise NotCompletionAppError()
|
||||||
@ -156,11 +230,25 @@ class MessageMoreLikeThisApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/messages/<uuid:message_id>/suggested-questions")
|
||||||
class MessageSuggestedQuestionApi(WebApiResource):
|
class MessageSuggestedQuestionApi(WebApiResource):
|
||||||
suggested_questions_response_fields = {
|
suggested_questions_response_fields = {
|
||||||
"data": fields.List(fields.String),
|
"data": fields.List(fields.String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Get Suggested Questions")
|
||||||
|
@web_ns.doc(description="Get suggested follow-up questions after a message (chat apps only).")
|
||||||
|
@web_ns.doc(params={"message_id": {"description": "Message UUID", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Success",
|
||||||
|
400: "Bad Request - Not a chat app or feature disabled",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Message Not Found or Conversation Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(suggested_questions_response_fields)
|
@marshal_with(suggested_questions_response_fields)
|
||||||
def get(self, app_model, end_user, message_id):
|
def get(self, app_model, end_user, message_id):
|
||||||
app_mode = AppMode.value_of(app_model.mode)
|
app_mode = AppMode.value_of(app_model.mode)
|
||||||
@ -192,9 +280,3 @@ class MessageSuggestedQuestionApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
return {"data": questions}
|
return {"data": questions}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(MessageListApi, "/messages")
|
|
||||||
api.add_resource(MessageFeedbackApi, "/messages/<uuid:message_id>/feedbacks")
|
|
||||||
api.add_resource(MessageMoreLikeThisApi, "/messages/<uuid:message_id>/more-like-this")
|
|
||||||
api.add_resource(MessageSuggestedQuestionApi, "/messages/<uuid:message_id>/suggested-questions")
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from flask_restx import fields, marshal_with, reqparse
|
|||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import NotCompletionAppError
|
from controllers.web.error import NotCompletionAppError
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from fields.conversation_fields import message_file_fields
|
from fields.conversation_fields import message_file_fields
|
||||||
@ -23,6 +23,7 @@ message_fields = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/saved-messages")
|
||||||
class SavedMessageListApi(WebApiResource):
|
class SavedMessageListApi(WebApiResource):
|
||||||
saved_message_infinite_scroll_pagination_fields = {
|
saved_message_infinite_scroll_pagination_fields = {
|
||||||
"limit": fields.Integer,
|
"limit": fields.Integer,
|
||||||
@ -34,6 +35,29 @@ class SavedMessageListApi(WebApiResource):
|
|||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Get Saved Messages")
|
||||||
|
@web_ns.doc(description="Retrieve paginated list of saved messages for a completion application.")
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"last_id": {"description": "Last message ID for pagination", "type": "string", "required": False},
|
||||||
|
"limit": {
|
||||||
|
"description": "Number of messages to return (1-100)",
|
||||||
|
"type": "integer",
|
||||||
|
"required": False,
|
||||||
|
"default": 20,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Success",
|
||||||
|
400: "Bad Request - Not a completion app",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "App Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(saved_message_infinite_scroll_pagination_fields)
|
@marshal_with(saved_message_infinite_scroll_pagination_fields)
|
||||||
def get(self, app_model, end_user):
|
def get(self, app_model, end_user):
|
||||||
if app_model.mode != "completion":
|
if app_model.mode != "completion":
|
||||||
@ -46,6 +70,23 @@ class SavedMessageListApi(WebApiResource):
|
|||||||
|
|
||||||
return SavedMessageService.pagination_by_last_id(app_model, end_user, args["last_id"], args["limit"])
|
return SavedMessageService.pagination_by_last_id(app_model, end_user, args["last_id"], args["limit"])
|
||||||
|
|
||||||
|
@web_ns.doc("Save Message")
|
||||||
|
@web_ns.doc(description="Save a specific message for later reference.")
|
||||||
|
@web_ns.doc(
|
||||||
|
params={
|
||||||
|
"message_id": {"description": "Message UUID to save", "type": "string", "required": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
200: "Message saved successfully",
|
||||||
|
400: "Bad Request - Not a completion app",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Message Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(post_response_fields)
|
@marshal_with(post_response_fields)
|
||||||
def post(self, app_model, end_user):
|
def post(self, app_model, end_user):
|
||||||
if app_model.mode != "completion":
|
if app_model.mode != "completion":
|
||||||
@ -63,11 +104,25 @@ class SavedMessageListApi(WebApiResource):
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/saved-messages/<uuid:message_id>")
|
||||||
class SavedMessageApi(WebApiResource):
|
class SavedMessageApi(WebApiResource):
|
||||||
delete_response_fields = {
|
delete_response_fields = {
|
||||||
"result": fields.String,
|
"result": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@web_ns.doc("Delete Saved Message")
|
||||||
|
@web_ns.doc(description="Remove a message from saved messages.")
|
||||||
|
@web_ns.doc(params={"message_id": {"description": "Message UUID to delete", "type": "string", "required": True}})
|
||||||
|
@web_ns.doc(
|
||||||
|
responses={
|
||||||
|
204: "Message removed successfully",
|
||||||
|
400: "Bad Request - Not a completion app",
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
404: "Message Not Found",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
}
|
||||||
|
)
|
||||||
@marshal_with(delete_response_fields)
|
@marshal_with(delete_response_fields)
|
||||||
def delete(self, app_model, end_user, message_id):
|
def delete(self, app_model, end_user, message_id):
|
||||||
message_id = str(message_id)
|
message_id = str(message_id)
|
||||||
@ -78,7 +133,3 @@ class SavedMessageApi(WebApiResource):
|
|||||||
SavedMessageService.delete(app_model, end_user, message_id)
|
SavedMessageService.delete(app_model, end_user, message_id)
|
||||||
|
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(SavedMessageListApi, "/saved-messages")
|
|
||||||
api.add_resource(SavedMessageApi, "/saved-messages/<uuid:message_id>")
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from flask_restx import fields, marshal_with
|
|||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import AppIconUrlField
|
from libs.helper import AppIconUrlField
|
||||||
@ -11,6 +11,7 @@ from models.model import Site
|
|||||||
from services.feature_service import FeatureService
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/site")
|
||||||
class AppSiteApi(WebApiResource):
|
class AppSiteApi(WebApiResource):
|
||||||
"""Resource for app sites."""
|
"""Resource for app sites."""
|
||||||
|
|
||||||
@ -53,9 +54,9 @@ class AppSiteApi(WebApiResource):
|
|||||||
"custom_config": fields.Raw(attribute="custom_config"),
|
"custom_config": fields.Raw(attribute="custom_config"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@api.doc("Get App Site Info")
|
@web_ns.doc("Get App Site Info")
|
||||||
@api.doc(description="Retrieve app site information and configuration.")
|
@web_ns.doc(description="Retrieve app site information and configuration.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -82,9 +83,6 @@ class AppSiteApi(WebApiResource):
|
|||||||
return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo)
|
return AppSiteInfo(app_model.tenant, app_model, site, end_user.id, can_replace_logo)
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppSiteApi, "/site")
|
|
||||||
|
|
||||||
|
|
||||||
class AppSiteInfo:
|
class AppSiteInfo:
|
||||||
"""Class to store site information."""
|
"""Class to store site information."""
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import logging
|
|||||||
from flask_restx import reqparse
|
from flask_restx import reqparse
|
||||||
from werkzeug.exceptions import InternalServerError
|
from werkzeug.exceptions import InternalServerError
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import web_ns
|
||||||
from controllers.web.error import (
|
from controllers.web.error import (
|
||||||
CompletionRequestError,
|
CompletionRequestError,
|
||||||
NotWorkflowAppError,
|
NotWorkflowAppError,
|
||||||
@ -29,16 +29,17 @@ from services.errors.llm import InvokeRateLimitError
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/workflows/run")
|
||||||
class WorkflowRunApi(WebApiResource):
|
class WorkflowRunApi(WebApiResource):
|
||||||
@api.doc("Run Workflow")
|
@web_ns.doc("Run Workflow")
|
||||||
@api.doc(description="Execute a workflow with provided inputs and files.")
|
@web_ns.doc(description="Execute a workflow with provided inputs and files.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
params={
|
params={
|
||||||
"inputs": {"description": "Input variables for the workflow", "type": "object", "required": True},
|
"inputs": {"description": "Input variables for the workflow", "type": "object", "required": True},
|
||||||
"files": {"description": "Files to be processed by the workflow", "type": "array", "required": False},
|
"files": {"description": "Files to be processed by the workflow", "type": "array", "required": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -84,15 +85,16 @@ class WorkflowRunApi(WebApiResource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@web_ns.route("/workflows/tasks/<string:task_id>/stop")
|
||||||
class WorkflowTaskStopApi(WebApiResource):
|
class WorkflowTaskStopApi(WebApiResource):
|
||||||
@api.doc("Stop Workflow Task")
|
@web_ns.doc("Stop Workflow Task")
|
||||||
@api.doc(description="Stop a running workflow task.")
|
@web_ns.doc(description="Stop a running workflow task.")
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
params={
|
params={
|
||||||
"task_id": {"description": "Task ID to stop", "type": "string", "required": True},
|
"task_id": {"description": "Task ID to stop", "type": "string", "required": True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@api.doc(
|
@web_ns.doc(
|
||||||
responses={
|
responses={
|
||||||
200: "Success",
|
200: "Success",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@ -113,7 +115,3 @@ class WorkflowTaskStopApi(WebApiResource):
|
|||||||
AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id)
|
AppQueueManager.set_stop_flag(task_id, InvokeFrom.WEB_APP, end_user.id)
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(WorkflowRunApi, "/workflows/run")
|
|
||||||
api.add_resource(WorkflowTaskStopApi, "/workflows/tasks/<string:task_id>/stop")
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import ParamSpec, TypeVar
|
from typing import Concatenate, Optional, ParamSpec, TypeVar
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_restx import Resource
|
from flask_restx import Resource
|
||||||
@ -20,12 +21,11 @@ P = ParamSpec("P")
|
|||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
|
|
||||||
|
|
||||||
def validate_jwt_token(view=None):
|
def validate_jwt_token(view: Optional[Callable[Concatenate[App, EndUser, P], R]] = None):
|
||||||
def decorator(view):
|
def decorator(view: Callable[Concatenate[App, EndUser, P], R]):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args: P.args, **kwargs: P.kwargs):
|
||||||
app_model, end_user = decode_jwt_token()
|
app_model, end_user = decode_jwt_token()
|
||||||
|
|
||||||
return view(app_model, end_user, *args, **kwargs)
|
return view(app_model, end_user, *args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
import core.moderation.base
|
|
||||||
@ -72,6 +72,8 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
|||||||
function_call_state = True
|
function_call_state = True
|
||||||
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
||||||
final_answer = ""
|
final_answer = ""
|
||||||
|
prompt_messages: list = [] # Initialize prompt_messages
|
||||||
|
agent_thought_id = "" # Initialize agent_thought_id
|
||||||
|
|
||||||
def increase_usage(final_llm_usage_dict: dict[str, Optional[LLMUsage]], usage: LLMUsage):
|
def increase_usage(final_llm_usage_dict: dict[str, Optional[LLMUsage]], usage: LLMUsage):
|
||||||
if not final_llm_usage_dict["usage"]:
|
if not final_llm_usage_dict["usage"]:
|
||||||
|
|||||||
@ -54,6 +54,7 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
|||||||
function_call_state = True
|
function_call_state = True
|
||||||
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
llm_usage: dict[str, Optional[LLMUsage]] = {"usage": None}
|
||||||
final_answer = ""
|
final_answer = ""
|
||||||
|
prompt_messages: list = [] # Initialize prompt_messages
|
||||||
|
|
||||||
# get tracing instance
|
# get tracing instance
|
||||||
trace_manager = app_generate_entity.trace_manager
|
trace_manager = app_generate_entity.trace_manager
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import enum
|
from enum import StrEnum
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator
|
||||||
@ -26,25 +26,25 @@ class AgentStrategyProviderIdentity(ToolProviderIdentity):
|
|||||||
|
|
||||||
|
|
||||||
class AgentStrategyParameter(PluginParameter):
|
class AgentStrategyParameter(PluginParameter):
|
||||||
class AgentStrategyParameterType(enum.StrEnum):
|
class AgentStrategyParameterType(StrEnum):
|
||||||
"""
|
"""
|
||||||
Keep all the types from PluginParameterType
|
Keep all the types from PluginParameterType
|
||||||
"""
|
"""
|
||||||
|
|
||||||
STRING = CommonParameterType.STRING.value
|
STRING = CommonParameterType.STRING
|
||||||
NUMBER = CommonParameterType.NUMBER.value
|
NUMBER = CommonParameterType.NUMBER
|
||||||
BOOLEAN = CommonParameterType.BOOLEAN.value
|
BOOLEAN = CommonParameterType.BOOLEAN
|
||||||
SELECT = CommonParameterType.SELECT.value
|
SELECT = CommonParameterType.SELECT
|
||||||
SECRET_INPUT = CommonParameterType.SECRET_INPUT.value
|
SECRET_INPUT = CommonParameterType.SECRET_INPUT
|
||||||
FILE = CommonParameterType.FILE.value
|
FILE = CommonParameterType.FILE
|
||||||
FILES = CommonParameterType.FILES.value
|
FILES = CommonParameterType.FILES
|
||||||
APP_SELECTOR = CommonParameterType.APP_SELECTOR.value
|
APP_SELECTOR = CommonParameterType.APP_SELECTOR
|
||||||
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR.value
|
MODEL_SELECTOR = CommonParameterType.MODEL_SELECTOR
|
||||||
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR.value
|
TOOLS_SELECTOR = CommonParameterType.TOOLS_SELECTOR
|
||||||
ANY = CommonParameterType.ANY.value
|
ANY = CommonParameterType.ANY
|
||||||
|
|
||||||
# deprecated, should not use.
|
# deprecated, should not use.
|
||||||
SYSTEM_FILES = CommonParameterType.SYSTEM_FILES.value
|
SYSTEM_FILES = CommonParameterType.SYSTEM_FILES
|
||||||
|
|
||||||
def as_normal_type(self):
|
def as_normal_type(self):
|
||||||
return as_normal_type(self)
|
return as_normal_type(self)
|
||||||
@ -72,7 +72,7 @@ class AgentStrategyIdentity(ToolIdentity):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AgentFeature(enum.StrEnum):
|
class AgentFeature(StrEnum):
|
||||||
"""
|
"""
|
||||||
Agent Feature, used to describe the features of the agent strategy.
|
Agent Feature, used to describe the features of the agent strategy.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class SensitiveWordAvoidanceConfigManager:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_and_set_defaults(
|
def validate_and_set_defaults(
|
||||||
cls, tenant_id, config: dict, only_structure_validate: bool = False
|
cls, tenant_id: str, config: dict, only_structure_validate: bool = False
|
||||||
) -> tuple[dict, list[str]]:
|
) -> tuple[dict, list[str]]:
|
||||||
if not config.get("sensitive_word_avoidance"):
|
if not config.get("sensitive_word_avoidance"):
|
||||||
config["sensitive_word_avoidance"] = {"enabled": False}
|
config["sensitive_word_avoidance"] = {"enabled": False}
|
||||||
@ -38,7 +38,14 @@ class SensitiveWordAvoidanceConfigManager:
|
|||||||
|
|
||||||
if not only_structure_validate:
|
if not only_structure_validate:
|
||||||
typ = config["sensitive_word_avoidance"]["type"]
|
typ = config["sensitive_word_avoidance"]["type"]
|
||||||
sensitive_word_avoidance_config = config["sensitive_word_avoidance"]["config"]
|
if not isinstance(typ, str):
|
||||||
|
raise ValueError("sensitive_word_avoidance.type must be a string")
|
||||||
|
|
||||||
|
sensitive_word_avoidance_config = config["sensitive_word_avoidance"].get("config")
|
||||||
|
if sensitive_word_avoidance_config is None:
|
||||||
|
sensitive_word_avoidance_config = {}
|
||||||
|
if not isinstance(sensitive_word_avoidance_config, dict):
|
||||||
|
raise ValueError("sensitive_word_avoidance.config must be a dict")
|
||||||
|
|
||||||
ModerationFactory.validate_config(name=typ, tenant_id=tenant_id, config=sensitive_word_avoidance_config)
|
ModerationFactory.validate_config(name=typ, tenant_id=tenant_id, config=sensitive_word_avoidance_config)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user