mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 02:06:35 +08:00
Merge branch 'main' into feat/queue-based-graph-engine
This commit is contained in:
commit
b8ee1d4697
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/web"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
|
- package-ecosystem: "uv"
|
||||||
|
directory: "/api"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
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
|
||||||
|
|||||||
@ -54,90 +54,90 @@ api.add_resource(AppImportCheckDependenciesApi, "/apps/imports/<string:app_id>/c
|
|||||||
|
|
||||||
# Import other controllers
|
# Import other controllers
|
||||||
from . import (
|
from . import (
|
||||||
admin, # pyright: ignore[reportUnusedImport]
|
admin,
|
||||||
apikey, # pyright: ignore[reportUnusedImport]
|
apikey,
|
||||||
extension, # pyright: ignore[reportUnusedImport]
|
extension,
|
||||||
feature, # pyright: ignore[reportUnusedImport]
|
feature,
|
||||||
init_validate, # pyright: ignore[reportUnusedImport]
|
init_validate,
|
||||||
ping, # pyright: ignore[reportUnusedImport]
|
ping,
|
||||||
setup, # pyright: ignore[reportUnusedImport]
|
setup,
|
||||||
version, # pyright: ignore[reportUnusedImport]
|
version,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import app controllers
|
# Import app controllers
|
||||||
from .app import (
|
from .app import (
|
||||||
advanced_prompt_template, # pyright: ignore[reportUnusedImport]
|
advanced_prompt_template,
|
||||||
agent, # pyright: ignore[reportUnusedImport]
|
agent,
|
||||||
annotation, # pyright: ignore[reportUnusedImport]
|
annotation,
|
||||||
app, # pyright: ignore[reportUnusedImport]
|
app,
|
||||||
audio, # pyright: ignore[reportUnusedImport]
|
audio,
|
||||||
completion, # pyright: ignore[reportUnusedImport]
|
completion,
|
||||||
conversation, # pyright: ignore[reportUnusedImport]
|
conversation,
|
||||||
conversation_variables, # pyright: ignore[reportUnusedImport]
|
conversation_variables,
|
||||||
generator, # pyright: ignore[reportUnusedImport]
|
generator,
|
||||||
mcp_server, # pyright: ignore[reportUnusedImport]
|
mcp_server,
|
||||||
message, # pyright: ignore[reportUnusedImport]
|
message,
|
||||||
model_config, # pyright: ignore[reportUnusedImport]
|
model_config,
|
||||||
ops_trace, # pyright: ignore[reportUnusedImport]
|
ops_trace,
|
||||||
site, # pyright: ignore[reportUnusedImport]
|
site,
|
||||||
statistic, # pyright: ignore[reportUnusedImport]
|
statistic,
|
||||||
workflow, # pyright: ignore[reportUnusedImport]
|
workflow,
|
||||||
workflow_app_log, # pyright: ignore[reportUnusedImport]
|
workflow_app_log,
|
||||||
workflow_draft_variable, # pyright: ignore[reportUnusedImport]
|
workflow_draft_variable,
|
||||||
workflow_run, # pyright: ignore[reportUnusedImport]
|
workflow_run,
|
||||||
workflow_statistic, # pyright: ignore[reportUnusedImport]
|
workflow_statistic,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import auth controllers
|
# Import auth controllers
|
||||||
from .auth import (
|
from .auth import (
|
||||||
activate, # pyright: ignore[reportUnusedImport]
|
activate,
|
||||||
data_source_bearer_auth, # pyright: ignore[reportUnusedImport]
|
data_source_bearer_auth,
|
||||||
data_source_oauth, # pyright: ignore[reportUnusedImport]
|
data_source_oauth,
|
||||||
email_register, # pyright: ignore[reportUnusedImport]
|
email_register,
|
||||||
forgot_password, # pyright: ignore[reportUnusedImport]
|
forgot_password,
|
||||||
login, # pyright: ignore[reportUnusedImport]
|
login,
|
||||||
oauth, # pyright: ignore[reportUnusedImport]
|
oauth,
|
||||||
oauth_server, # pyright: ignore[reportUnusedImport]
|
oauth_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import billing controllers
|
# Import billing controllers
|
||||||
from .billing import billing, compliance # pyright: ignore[reportUnusedImport]
|
from .billing import billing, compliance
|
||||||
|
|
||||||
# Import datasets controllers
|
# Import datasets controllers
|
||||||
from .datasets import (
|
from .datasets import (
|
||||||
data_source, # pyright: ignore[reportUnusedImport]
|
data_source,
|
||||||
datasets, # pyright: ignore[reportUnusedImport]
|
datasets,
|
||||||
datasets_document, # pyright: ignore[reportUnusedImport]
|
datasets_document,
|
||||||
datasets_segments, # pyright: ignore[reportUnusedImport]
|
datasets_segments,
|
||||||
external, # pyright: ignore[reportUnusedImport]
|
external,
|
||||||
hit_testing, # pyright: ignore[reportUnusedImport]
|
hit_testing,
|
||||||
metadata, # pyright: ignore[reportUnusedImport]
|
metadata,
|
||||||
website, # pyright: ignore[reportUnusedImport]
|
website,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import explore controllers
|
# Import explore controllers
|
||||||
from .explore import (
|
from .explore import (
|
||||||
installed_app, # pyright: ignore[reportUnusedImport]
|
installed_app,
|
||||||
parameter, # pyright: ignore[reportUnusedImport]
|
parameter,
|
||||||
recommended_app, # pyright: ignore[reportUnusedImport]
|
recommended_app,
|
||||||
saved_message, # pyright: ignore[reportUnusedImport]
|
saved_message,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import tag controllers
|
# Import tag controllers
|
||||||
from .tag import tags # pyright: ignore[reportUnusedImport]
|
from .tag import tags
|
||||||
|
|
||||||
# Import workspace controllers
|
# Import workspace controllers
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
account, # pyright: ignore[reportUnusedImport]
|
account,
|
||||||
agent_providers, # pyright: ignore[reportUnusedImport]
|
agent_providers,
|
||||||
endpoint, # pyright: ignore[reportUnusedImport]
|
endpoint,
|
||||||
load_balancing_config, # pyright: ignore[reportUnusedImport]
|
load_balancing_config,
|
||||||
members, # pyright: ignore[reportUnusedImport]
|
members,
|
||||||
model_providers, # pyright: ignore[reportUnusedImport]
|
model_providers,
|
||||||
models, # pyright: ignore[reportUnusedImport]
|
models,
|
||||||
plugin, # pyright: ignore[reportUnusedImport]
|
plugin,
|
||||||
tool_providers, # pyright: ignore[reportUnusedImport]
|
tool_providers,
|
||||||
workspace, # pyright: ignore[reportUnusedImport]
|
workspace,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Explore Audio
|
# Explore Audio
|
||||||
@ -212,3 +212,70 @@ api.add_resource(
|
|||||||
)
|
)
|
||||||
|
|
||||||
api.add_namespace(console_ns)
|
api.add_namespace(console_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"account",
|
||||||
|
"activate",
|
||||||
|
"admin",
|
||||||
|
"advanced_prompt_template",
|
||||||
|
"agent",
|
||||||
|
"agent_providers",
|
||||||
|
"annotation",
|
||||||
|
"api",
|
||||||
|
"apikey",
|
||||||
|
"app",
|
||||||
|
"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",
|
||||||
|
]
|
||||||
|
|||||||
@ -4,13 +4,13 @@ from collections.abc import Sequence
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from flask import abort, request
|
from flask import abort, request
|
||||||
from flask_restx import Resource, inputs, marshal_with, reqparse
|
from flask_restx import Resource, fields, inputs, marshal_with, reqparse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||||
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
|
||||||
@ -58,7 +58,13 @@ def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence
|
|||||||
return file_objs
|
return file_objs
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft")
|
||||||
class DraftWorkflowApi(Resource):
|
class DraftWorkflowApi(Resource):
|
||||||
|
@api.doc("get_draft_workflow")
|
||||||
|
@api.doc(description="Get draft workflow for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Draft workflow retrieved successfully", workflow_fields)
|
||||||
|
@api.response(404, "Draft workflow not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -87,6 +93,23 @@ class DraftWorkflowApi(Resource):
|
|||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||||
|
@api.doc("sync_draft_workflow")
|
||||||
|
@api.doc(description="Sync draft workflow configuration")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"SyncDraftWorkflowRequest",
|
||||||
|
{
|
||||||
|
"graph": fields.Raw(required=True, description="Workflow graph configuration"),
|
||||||
|
"features": fields.Raw(required=True, description="Workflow features configuration"),
|
||||||
|
"hash": fields.String(description="Workflow hash for validation"),
|
||||||
|
"environment_variables": fields.List(fields.Raw, required=True, description="Environment variables"),
|
||||||
|
"conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Draft workflow synced successfully", workflow_fields)
|
||||||
|
@api.response(400, "Invalid workflow configuration")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
def post(self, app_model: App):
|
def post(self, app_model: App):
|
||||||
"""
|
"""
|
||||||
Sync draft workflow
|
Sync draft workflow
|
||||||
@ -160,7 +183,25 @@ class DraftWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
||||||
class AdvancedChatDraftWorkflowRunApi(Resource):
|
class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||||
|
@api.doc("run_advanced_chat_draft_workflow")
|
||||||
|
@api.doc(description="Run draft workflow for advanced chat application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"AdvancedChatWorkflowRunRequest",
|
||||||
|
{
|
||||||
|
"query": fields.String(required=True, description="User query"),
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
"files": fields.List(fields.Raw, description="File uploads"),
|
||||||
|
"conversation_id": fields.String(description="Conversation ID"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Workflow run started successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -209,7 +250,23 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run")
|
||||||
class AdvancedChatDraftRunIterationNodeApi(Resource):
|
class AdvancedChatDraftRunIterationNodeApi(Resource):
|
||||||
|
@api.doc("run_advanced_chat_draft_iteration_node")
|
||||||
|
@api.doc(description="Run draft workflow iteration node for advanced chat")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"IterationNodeRunRequest",
|
||||||
|
{
|
||||||
|
"task_id": fields.String(required=True, description="Task ID"),
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Iteration node run started successfully")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
|
@api.response(404, "Node not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -245,7 +302,23 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run")
|
||||||
class WorkflowDraftRunIterationNodeApi(Resource):
|
class WorkflowDraftRunIterationNodeApi(Resource):
|
||||||
|
@api.doc("run_workflow_draft_iteration_node")
|
||||||
|
@api.doc(description="Run draft workflow iteration node")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"WorkflowIterationNodeRunRequest",
|
||||||
|
{
|
||||||
|
"task_id": fields.String(required=True, description="Task ID"),
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Workflow iteration node run started successfully")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
|
@api.response(404, "Node not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -281,7 +354,23 @@ class WorkflowDraftRunIterationNodeApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run")
|
||||||
class AdvancedChatDraftRunLoopNodeApi(Resource):
|
class AdvancedChatDraftRunLoopNodeApi(Resource):
|
||||||
|
@api.doc("run_advanced_chat_draft_loop_node")
|
||||||
|
@api.doc(description="Run draft workflow loop node for advanced chat")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"LoopNodeRunRequest",
|
||||||
|
{
|
||||||
|
"task_id": fields.String(required=True, description="Task ID"),
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Loop node run started successfully")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
|
@api.response(404, "Node not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -318,7 +407,23 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run")
|
||||||
class WorkflowDraftRunLoopNodeApi(Resource):
|
class WorkflowDraftRunLoopNodeApi(Resource):
|
||||||
|
@api.doc("run_workflow_draft_loop_node")
|
||||||
|
@api.doc(description="Run draft workflow loop node")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"WorkflowLoopNodeRunRequest",
|
||||||
|
{
|
||||||
|
"task_id": fields.String(required=True, description="Task ID"),
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Workflow loop node run started successfully")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
|
@api.response(404, "Node not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -355,7 +460,22 @@ class WorkflowDraftRunLoopNodeApi(Resource):
|
|||||||
raise InternalServerError()
|
raise InternalServerError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/run")
|
||||||
class DraftWorkflowRunApi(Resource):
|
class DraftWorkflowRunApi(Resource):
|
||||||
|
@api.doc("run_draft_workflow")
|
||||||
|
@api.doc(description="Run draft workflow")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"DraftWorkflowRunRequest",
|
||||||
|
{
|
||||||
|
"inputs": fields.Raw(required=True, description="Input variables"),
|
||||||
|
"files": fields.List(fields.Raw, description="File uploads"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Draft workflow run started successfully")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -394,7 +514,14 @@ class DraftWorkflowRunApi(Resource):
|
|||||||
raise InvokeRateLimitHttpError(ex.description)
|
raise InvokeRateLimitHttpError(ex.description)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/tasks/<string:task_id>/stop")
|
||||||
class WorkflowTaskStopApi(Resource):
|
class WorkflowTaskStopApi(Resource):
|
||||||
|
@api.doc("stop_workflow_task")
|
||||||
|
@api.doc(description="Stop running workflow task")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "task_id": "Task ID"})
|
||||||
|
@api.response(200, "Task stopped successfully")
|
||||||
|
@api.response(404, "Task not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -420,7 +547,22 @@ class WorkflowTaskStopApi(Resource):
|
|||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run")
|
||||||
class DraftWorkflowNodeRunApi(Resource):
|
class DraftWorkflowNodeRunApi(Resource):
|
||||||
|
@api.doc("run_draft_workflow_node")
|
||||||
|
@api.doc(description="Run draft workflow node")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"DraftWorkflowNodeRunRequest",
|
||||||
|
{
|
||||||
|
"inputs": fields.Raw(description="Input variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Node run started successfully", workflow_run_node_execution_fields)
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
|
@api.response(404, "Node not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -468,7 +610,13 @@ class DraftWorkflowNodeRunApi(Resource):
|
|||||||
return workflow_node_execution
|
return workflow_node_execution
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/publish")
|
||||||
class PublishedWorkflowApi(Resource):
|
class PublishedWorkflowApi(Resource):
|
||||||
|
@api.doc("get_published_workflow")
|
||||||
|
@api.doc(description="Get published workflow for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Published workflow retrieved successfully", workflow_fields)
|
||||||
|
@api.response(404, "Published workflow not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -540,7 +688,12 @@ class PublishedWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/default-block-configs")
|
||||||
class DefaultBlockConfigsApi(Resource):
|
class DefaultBlockConfigsApi(Resource):
|
||||||
|
@api.doc("get_default_block_configs")
|
||||||
|
@api.doc(description="Get default block configurations for workflow")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Default block configurations retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -561,7 +714,13 @@ class DefaultBlockConfigsApi(Resource):
|
|||||||
return workflow_service.get_default_block_configs()
|
return workflow_service.get_default_block_configs()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/default-block-configs/<string:block_type>")
|
||||||
class DefaultBlockConfigApi(Resource):
|
class DefaultBlockConfigApi(Resource):
|
||||||
|
@api.doc("get_default_block_config")
|
||||||
|
@api.doc(description="Get default block configuration by type")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "block_type": "Block type"})
|
||||||
|
@api.response(200, "Default block configuration retrieved successfully")
|
||||||
|
@api.response(404, "Block type not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -594,7 +753,14 @@ class DefaultBlockConfigApi(Resource):
|
|||||||
return workflow_service.get_default_block_config(node_type=block_type, filters=filters)
|
return workflow_service.get_default_block_config(node_type=block_type, filters=filters)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/convert-to-workflow")
|
||||||
class ConvertToWorkflowApi(Resource):
|
class ConvertToWorkflowApi(Resource):
|
||||||
|
@api.doc("convert_to_workflow")
|
||||||
|
@api.doc(description="Convert application to workflow mode")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Application converted to workflow successfully")
|
||||||
|
@api.response(400, "Application cannot be converted")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -631,9 +797,14 @@ class ConvertToWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/config")
|
||||||
class WorkflowConfigApi(Resource):
|
class WorkflowConfigApi(Resource):
|
||||||
"""Resource for workflow configuration."""
|
"""Resource for workflow configuration."""
|
||||||
|
|
||||||
|
@api.doc("get_workflow_config")
|
||||||
|
@api.doc(description="Get workflow configuration")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Workflow configuration retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -644,7 +815,12 @@ class WorkflowConfigApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/published")
|
||||||
class PublishedAllWorkflowApi(Resource):
|
class PublishedAllWorkflowApi(Resource):
|
||||||
|
@api.doc("get_all_published_workflows")
|
||||||
|
@api.doc(description="Get all published workflows for an application")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Published workflows retrieved successfully", workflow_pagination_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -695,7 +871,23 @@ class PublishedAllWorkflowApi(Resource):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/<uuid:workflow_id>")
|
||||||
class WorkflowByIdApi(Resource):
|
class WorkflowByIdApi(Resource):
|
||||||
|
@api.doc("update_workflow_by_id")
|
||||||
|
@api.doc(description="Update workflow by ID")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "workflow_id": "Workflow ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateWorkflowRequest",
|
||||||
|
{
|
||||||
|
"environment_variables": fields.List(fields.Raw, description="Environment variables"),
|
||||||
|
"conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Workflow updated successfully", workflow_fields)
|
||||||
|
@api.response(404, "Workflow not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -786,7 +978,14 @@ class WorkflowByIdApi(Resource):
|
|||||||
return None, 204
|
return None, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/last-run")
|
||||||
class DraftWorkflowNodeLastRunApi(Resource):
|
class DraftWorkflowNodeLastRunApi(Resource):
|
||||||
|
@api.doc("get_draft_workflow_node_last_run")
|
||||||
|
@api.doc(description="Get last run result for draft workflow node")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.response(200, "Node last run retrieved successfully", workflow_run_node_execution_fields)
|
||||||
|
@api.response(404, "Node last run not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -805,73 +1004,3 @@ class DraftWorkflowNodeLastRunApi(Resource):
|
|||||||
if node_exec is None:
|
if node_exec is None:
|
||||||
raise NotFound("last run not found")
|
raise NotFound("last run not found")
|
||||||
return node_exec
|
return node_exec
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(
|
|
||||||
DraftWorkflowApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowConfigApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/config",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
AdvancedChatDraftWorkflowRunApi,
|
|
||||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
DraftWorkflowRunApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowTaskStopApi,
|
|
||||||
"/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
DraftWorkflowNodeRunApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
AdvancedChatDraftRunIterationNodeApi,
|
|
||||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowDraftRunIterationNodeApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
AdvancedChatDraftRunLoopNodeApi,
|
|
||||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowDraftRunLoopNodeApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
PublishedWorkflowApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/publish",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
PublishedAllWorkflowApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
DefaultBlockConfigsApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
DefaultBlockConfigApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
ConvertToWorkflowApi,
|
|
||||||
"/apps/<uuid:app_id>/convert-to-workflow",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowByIdApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/<string:workflow_id>",
|
|
||||||
)
|
|
||||||
api.add_resource(
|
|
||||||
DraftWorkflowNodeLastRunApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/last-run",
|
|
||||||
)
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from flask_restx import Resource, 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
|
||||||
|
|
||||||
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.workflow.enums import WorkflowExecutionStatus
|
from core.workflow.enums import WorkflowExecutionStatus
|
||||||
@ -15,7 +15,24 @@ from models.model import AppMode
|
|||||||
from services.workflow_app_service import WorkflowAppService
|
from services.workflow_app_service import WorkflowAppService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow-app-logs")
|
||||||
class WorkflowAppLogApi(Resource):
|
class WorkflowAppLogApi(Resource):
|
||||||
|
@api.doc("get_workflow_app_logs")
|
||||||
|
@api.doc(description="Get workflow application execution logs")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"keyword": "Search keyword for filtering logs",
|
||||||
|
"status": "Filter by execution status (succeeded, failed, stopped, partial-succeeded)",
|
||||||
|
"created_at__before": "Filter logs created before this timestamp",
|
||||||
|
"created_at__after": "Filter logs created after this timestamp",
|
||||||
|
"created_by_end_user_session_id": "Filter by end user session ID",
|
||||||
|
"created_by_account": "Filter by account",
|
||||||
|
"page": "Page number (1-99999)",
|
||||||
|
"limit": "Number of items per page (1-100)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(200, "Workflow app logs retrieved successfully", workflow_app_log_pagination_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -78,6 +95,3 @@ class WorkflowAppLogApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return workflow_app_log_pagination
|
return workflow_app_log_pagination
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(WorkflowAppLogApi, "/apps/<uuid:app_id>/workflow-app-logs")
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqpars
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
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.app.error import (
|
from controllers.console.app.error import (
|
||||||
DraftWorkflowNotExist,
|
DraftWorkflowNotExist,
|
||||||
)
|
)
|
||||||
@ -145,7 +145,13 @@ def _api_prerequisite(f):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables")
|
||||||
class WorkflowVariableCollectionApi(Resource):
|
class WorkflowVariableCollectionApi(Resource):
|
||||||
|
@api.doc("get_workflow_variables")
|
||||||
|
@api.doc(description="Get draft workflow variables")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"page": "Page number (1-100000)", "limit": "Number of items per page (1-100)"})
|
||||||
|
@api.response(200, "Workflow variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
||||||
def get(self, app_model: App):
|
def get(self, app_model: App):
|
||||||
@ -174,6 +180,9 @@ class WorkflowVariableCollectionApi(Resource):
|
|||||||
|
|
||||||
return workflow_vars
|
return workflow_vars
|
||||||
|
|
||||||
|
@api.doc("delete_workflow_variables")
|
||||||
|
@api.doc(description="Delete all draft workflow variables")
|
||||||
|
@api.response(204, "Workflow variables deleted successfully")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
def delete(self, app_model: App):
|
def delete(self, app_model: App):
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
draft_var_srv = WorkflowDraftVariableService(
|
||||||
@ -202,7 +211,12 @@ def validate_node_id(node_id: str) -> NoReturn | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
||||||
class NodeVariableCollectionApi(Resource):
|
class NodeVariableCollectionApi(Resource):
|
||||||
|
@api.doc("get_node_variables")
|
||||||
|
@api.doc(description="Get variables for a specific node")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||||
|
@api.response(200, "Node variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
def get(self, app_model: App, node_id: str):
|
def get(self, app_model: App, node_id: str):
|
||||||
@ -215,6 +229,9 @@ class NodeVariableCollectionApi(Resource):
|
|||||||
|
|
||||||
return node_vars
|
return node_vars
|
||||||
|
|
||||||
|
@api.doc("delete_node_variables")
|
||||||
|
@api.doc(description="Delete all variables for a specific node")
|
||||||
|
@api.response(204, "Node variables deleted successfully")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
def delete(self, app_model: App, node_id: str):
|
def delete(self, app_model: App, node_id: str):
|
||||||
validate_node_id(node_id)
|
validate_node_id(node_id)
|
||||||
@ -224,10 +241,16 @@ class NodeVariableCollectionApi(Resource):
|
|||||||
return Response("", 204)
|
return Response("", 204)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
||||||
class VariableApi(Resource):
|
class VariableApi(Resource):
|
||||||
_PATCH_NAME_FIELD = "name"
|
_PATCH_NAME_FIELD = "name"
|
||||||
_PATCH_VALUE_FIELD = "value"
|
_PATCH_VALUE_FIELD = "value"
|
||||||
|
|
||||||
|
@api.doc("get_variable")
|
||||||
|
@api.doc(description="Get a specific workflow variable")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "variable_id": "Variable ID"})
|
||||||
|
@api.response(200, "Variable retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||||
|
@api.response(404, "Variable not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||||
def get(self, app_model: App, variable_id: str):
|
def get(self, app_model: App, variable_id: str):
|
||||||
@ -241,6 +264,19 @@ class VariableApi(Resource):
|
|||||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||||
return variable
|
return variable
|
||||||
|
|
||||||
|
@api.doc("update_variable")
|
||||||
|
@api.doc(description="Update a workflow variable")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateVariableRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(description="Variable name"),
|
||||||
|
"value": fields.Raw(description="Variable value"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Variable updated successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||||
|
@api.response(404, "Variable not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||||
def patch(self, app_model: App, variable_id: str):
|
def patch(self, app_model: App, variable_id: str):
|
||||||
@ -303,6 +339,10 @@ class VariableApi(Resource):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return variable
|
return variable
|
||||||
|
|
||||||
|
@api.doc("delete_variable")
|
||||||
|
@api.doc(description="Delete a workflow variable")
|
||||||
|
@api.response(204, "Variable deleted successfully")
|
||||||
|
@api.response(404, "Variable not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
def delete(self, app_model: App, variable_id: str):
|
def delete(self, app_model: App, variable_id: str):
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
draft_var_srv = WorkflowDraftVariableService(
|
||||||
@ -318,7 +358,14 @@ class VariableApi(Resource):
|
|||||||
return Response("", 204)
|
return Response("", 204)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
||||||
class VariableResetApi(Resource):
|
class VariableResetApi(Resource):
|
||||||
|
@api.doc("reset_variable")
|
||||||
|
@api.doc(description="Reset a workflow variable to its default value")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "variable_id": "Variable ID"})
|
||||||
|
@api.response(200, "Variable reset successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||||
|
@api.response(204, "Variable reset (no content)")
|
||||||
|
@api.response(404, "Variable not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
def put(self, app_model: App, variable_id: str):
|
def put(self, app_model: App, variable_id: str):
|
||||||
draft_var_srv = WorkflowDraftVariableService(
|
draft_var_srv = WorkflowDraftVariableService(
|
||||||
@ -359,7 +406,13 @@ def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList:
|
|||||||
return draft_vars
|
return draft_vars
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
||||||
class ConversationVariableCollectionApi(Resource):
|
class ConversationVariableCollectionApi(Resource):
|
||||||
|
@api.doc("get_conversation_variables")
|
||||||
|
@api.doc(description="Get conversation variables for workflow")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Conversation variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
|
@api.response(404, "Draft workflow not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
def get(self, app_model: App):
|
def get(self, app_model: App):
|
||||||
@ -375,14 +428,25 @@ class ConversationVariableCollectionApi(Resource):
|
|||||||
return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID)
|
return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/system-variables")
|
||||||
class SystemVariableCollectionApi(Resource):
|
class SystemVariableCollectionApi(Resource):
|
||||||
|
@api.doc("get_system_variables")
|
||||||
|
@api.doc(description="Get system variables for workflow")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "System variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||||
def get(self, app_model: App):
|
def get(self, app_model: App):
|
||||||
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID)
|
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
||||||
class EnvironmentVariableCollectionApi(Resource):
|
class EnvironmentVariableCollectionApi(Resource):
|
||||||
|
@api.doc("get_environment_variables")
|
||||||
|
@api.doc(description="Get environment variables for workflow")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.response(200, "Environment variables retrieved successfully")
|
||||||
|
@api.response(404, "Draft workflow not found")
|
||||||
@_api_prerequisite
|
@_api_prerequisite
|
||||||
def get(self, app_model: App):
|
def get(self, app_model: App):
|
||||||
"""
|
"""
|
||||||
@ -414,16 +478,3 @@ class EnvironmentVariableCollectionApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {"items": env_vars_list}
|
return {"items": env_vars_list}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowVariableCollectionApi,
|
|
||||||
"/apps/<uuid:app_id>/workflows/draft/variables",
|
|
||||||
)
|
|
||||||
api.add_resource(NodeVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
|
||||||
api.add_resource(VariableApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
|
||||||
api.add_resource(VariableResetApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
|
||||||
|
|
||||||
api.add_resource(ConversationVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
|
||||||
api.add_resource(SystemVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/system-variables")
|
|
||||||
api.add_resource(EnvironmentVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from flask_login import current_user
|
|||||||
from flask_restx import Resource, marshal_with, reqparse
|
from flask_restx import Resource, marshal_with, reqparse
|
||||||
from flask_restx.inputs import int_range
|
from flask_restx.inputs import int_range
|
||||||
|
|
||||||
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 fields.workflow_run_fields import (
|
from fields.workflow_run_fields import (
|
||||||
@ -19,7 +19,13 @@ from models import Account, App, AppMode, EndUser
|
|||||||
from services.workflow_run_service import WorkflowRunService
|
from services.workflow_run_service import WorkflowRunService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflow-runs")
|
||||||
class AdvancedChatAppWorkflowRunListApi(Resource):
|
class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||||
|
@api.doc("get_advanced_chat_workflow_runs")
|
||||||
|
@api.doc(description="Get advanced chat workflow run list")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"last_id": "Last run ID for pagination", "limit": "Number of items per page (1-100)"})
|
||||||
|
@api.response(200, "Workflow runs retrieved successfully", advanced_chat_workflow_run_pagination_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -40,7 +46,13 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow-runs")
|
||||||
class WorkflowRunListApi(Resource):
|
class WorkflowRunListApi(Resource):
|
||||||
|
@api.doc("get_workflow_runs")
|
||||||
|
@api.doc(description="Get workflow run list")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"last_id": "Last run ID for pagination", "limit": "Number of items per page (1-100)"})
|
||||||
|
@api.response(200, "Workflow runs retrieved successfully", workflow_run_pagination_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -61,7 +73,13 @@ class WorkflowRunListApi(Resource):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>")
|
||||||
class WorkflowRunDetailApi(Resource):
|
class WorkflowRunDetailApi(Resource):
|
||||||
|
@api.doc("get_workflow_run_detail")
|
||||||
|
@api.doc(description="Get workflow run detail")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
|
||||||
|
@api.response(200, "Workflow run detail retrieved successfully", workflow_run_detail_fields)
|
||||||
|
@api.response(404, "Workflow run not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -79,7 +97,13 @@ class WorkflowRunDetailApi(Resource):
|
|||||||
return workflow_run
|
return workflow_run
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>/node-executions")
|
||||||
class WorkflowRunNodeExecutionListApi(Resource):
|
class WorkflowRunNodeExecutionListApi(Resource):
|
||||||
|
@api.doc("get_workflow_run_node_executions")
|
||||||
|
@api.doc(description="Get workflow run node execution list")
|
||||||
|
@api.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
|
||||||
|
@api.response(200, "Node executions retrieved successfully", workflow_run_node_execution_list_fields)
|
||||||
|
@api.response(404, "Workflow run not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -100,9 +124,3 @@ class WorkflowRunNodeExecutionListApi(Resource):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {"data": node_executions}
|
return {"data": node_executions}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AdvancedChatAppWorkflowRunListApi, "/apps/<uuid:app_id>/advanced-chat/workflow-runs")
|
|
||||||
api.add_resource(WorkflowRunListApi, "/apps/<uuid:app_id>/workflow-runs")
|
|
||||||
api.add_resource(WorkflowRunDetailApi, "/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>")
|
|
||||||
api.add_resource(WorkflowRunNodeExecutionListApi, "/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>/node-executions")
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ 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, 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 extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
@ -17,7 +17,13 @@ from models.enums import WorkflowRunTriggeredFrom
|
|||||||
from models.model import AppMode
|
from models.model import AppMode
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
|
||||||
class WorkflowDailyRunsStatistic(Resource):
|
class WorkflowDailyRunsStatistic(Resource):
|
||||||
|
@api.doc("get_workflow_daily_runs_statistic")
|
||||||
|
@api.doc(description="Get workflow daily runs statistics")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||||
|
@api.response(200, "Daily runs statistics retrieved successfully")
|
||||||
@get_app_model
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -79,7 +85,13 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
|
||||||
class WorkflowDailyTerminalsStatistic(Resource):
|
class WorkflowDailyTerminalsStatistic(Resource):
|
||||||
|
@api.doc("get_workflow_daily_terminals_statistic")
|
||||||
|
@api.doc(description="Get workflow daily terminals statistics")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||||
|
@api.response(200, "Daily terminals statistics retrieved successfully")
|
||||||
@get_app_model
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -141,7 +153,13 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/token-costs")
|
||||||
class WorkflowDailyTokenCostStatistic(Resource):
|
class WorkflowDailyTokenCostStatistic(Resource):
|
||||||
|
@api.doc("get_workflow_daily_token_cost_statistic")
|
||||||
|
@api.doc(description="Get workflow daily token cost statistics")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||||
|
@api.response(200, "Daily token cost statistics retrieved successfully")
|
||||||
@get_app_model
|
@get_app_model
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -208,7 +226,13 @@ WHERE
|
|||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/average-app-interactions")
|
||||||
class WorkflowAverageAppInteractionStatistic(Resource):
|
class WorkflowAverageAppInteractionStatistic(Resource):
|
||||||
|
@api.doc("get_workflow_average_app_interaction_statistic")
|
||||||
|
@api.doc(description="Get workflow average app interaction statistics")
|
||||||
|
@api.doc(params={"app_id": "Application ID"})
|
||||||
|
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||||
|
@api.response(200, "Average app interaction statistics retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -285,11 +309,3 @@ GROUP BY
|
|||||||
)
|
)
|
||||||
|
|
||||||
return jsonify({"data": response_data})
|
return jsonify({"data": response_data})
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(WorkflowDailyRunsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
|
|
||||||
api.add_resource(WorkflowDailyTerminalsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
|
|
||||||
api.add_resource(WorkflowDailyTokenCostStatistic, "/apps/<uuid:app_id>/workflow/statistics/token-costs")
|
|
||||||
api.add_resource(
|
|
||||||
WorkflowAverageAppInteractionStatistic, "/apps/<uuid:app_id>/workflow/statistics/average-app-interactions"
|
|
||||||
)
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import flask_restx
|
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, fields, marshal, marshal_with, reqparse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from controllers.console import api
|
from controllers.console import api, console_ns
|
||||||
from controllers.console.apikey import api_key_fields, api_key_list
|
from controllers.console.apikey import api_key_fields, api_key_list
|
||||||
from controllers.console.app.error import ProviderNotInitializeError
|
from controllers.console.app.error import ProviderNotInitializeError
|
||||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||||
@ -48,7 +48,21 @@ def _validate_description_length(description):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets")
|
||||||
class DatasetListApi(Resource):
|
class DatasetListApi(Resource):
|
||||||
|
@api.doc("get_datasets")
|
||||||
|
@api.doc(description="Get list of datasets")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"page": "Page number (default: 1)",
|
||||||
|
"limit": "Number of items per page (default: 20)",
|
||||||
|
"ids": "Filter by dataset IDs (list)",
|
||||||
|
"keyword": "Search keyword",
|
||||||
|
"tag_ids": "Filter by tag IDs (list)",
|
||||||
|
"include_all": "Include all datasets (default: false)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(200, "Datasets retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -100,6 +114,24 @@ class DatasetListApi(Resource):
|
|||||||
response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page}
|
response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page}
|
||||||
return response, 200
|
return response, 200
|
||||||
|
|
||||||
|
@api.doc("create_dataset")
|
||||||
|
@api.doc(description="Create a new dataset")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CreateDatasetRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(required=True, description="Dataset name (1-40 characters)"),
|
||||||
|
"description": fields.String(description="Dataset description (max 400 characters)"),
|
||||||
|
"indexing_technique": fields.String(description="Indexing technique"),
|
||||||
|
"permission": fields.String(description="Dataset permission"),
|
||||||
|
"provider": fields.String(description="Provider"),
|
||||||
|
"external_knowledge_api_id": fields.String(description="External knowledge API ID"),
|
||||||
|
"external_knowledge_id": fields.String(description="External knowledge ID"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "Dataset created successfully")
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -172,7 +204,14 @@ class DatasetListApi(Resource):
|
|||||||
return marshal(dataset, dataset_detail_fields), 201
|
return marshal(dataset, dataset_detail_fields), 201
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>")
|
||||||
class DatasetApi(Resource):
|
class DatasetApi(Resource):
|
||||||
|
@api.doc("get_dataset")
|
||||||
|
@api.doc(description="Get dataset details")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Dataset retrieved successfully", dataset_detail_fields)
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -215,6 +254,23 @@ class DatasetApi(Resource):
|
|||||||
|
|
||||||
return data, 200
|
return data, 200
|
||||||
|
|
||||||
|
@api.doc("update_dataset")
|
||||||
|
@api.doc(description="Update dataset details")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateDatasetRequest",
|
||||||
|
{
|
||||||
|
"name": fields.String(description="Dataset name"),
|
||||||
|
"description": fields.String(description="Dataset description"),
|
||||||
|
"permission": fields.String(description="Dataset permission"),
|
||||||
|
"indexing_technique": fields.String(description="Indexing technique"),
|
||||||
|
"external_retrieval_model": fields.Raw(description="External retrieval model settings"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Dataset updated successfully", dataset_detail_fields)
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -344,7 +400,12 @@ class DatasetApi(Resource):
|
|||||||
raise DatasetInUseError()
|
raise DatasetInUseError()
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/use-check")
|
||||||
class DatasetUseCheckApi(Resource):
|
class DatasetUseCheckApi(Resource):
|
||||||
|
@api.doc("check_dataset_use")
|
||||||
|
@api.doc(description="Check if dataset is in use")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Dataset use status retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -355,7 +416,12 @@ class DatasetUseCheckApi(Resource):
|
|||||||
return {"is_using": dataset_is_using}, 200
|
return {"is_using": dataset_is_using}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/queries")
|
||||||
class DatasetQueryApi(Resource):
|
class DatasetQueryApi(Resource):
|
||||||
|
@api.doc("get_dataset_queries")
|
||||||
|
@api.doc(description="Get dataset query history")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Query history retrieved successfully", dataset_query_detail_fields)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -385,7 +451,11 @@ class DatasetQueryApi(Resource):
|
|||||||
return response, 200
|
return response, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/indexing-estimate")
|
||||||
class DatasetIndexingEstimateApi(Resource):
|
class DatasetIndexingEstimateApi(Resource):
|
||||||
|
@api.doc("estimate_dataset_indexing")
|
||||||
|
@api.doc(description="Estimate dataset indexing cost")
|
||||||
|
@api.response(200, "Indexing estimate calculated successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -486,7 +556,12 @@ class DatasetIndexingEstimateApi(Resource):
|
|||||||
return response.model_dump(), 200
|
return response.model_dump(), 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/related-apps")
|
||||||
class DatasetRelatedAppListApi(Resource):
|
class DatasetRelatedAppListApi(Resource):
|
||||||
|
@api.doc("get_dataset_related_apps")
|
||||||
|
@api.doc(description="Get applications related to dataset")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Related apps retrieved successfully", related_app_list)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -513,7 +588,12 @@ class DatasetRelatedAppListApi(Resource):
|
|||||||
return {"data": related_apps, "total": len(related_apps)}, 200
|
return {"data": related_apps, "total": len(related_apps)}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/indexing-status")
|
||||||
class DatasetIndexingStatusApi(Resource):
|
class DatasetIndexingStatusApi(Resource):
|
||||||
|
@api.doc("get_dataset_indexing_status")
|
||||||
|
@api.doc(description="Get dataset indexing status")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Indexing status retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -560,11 +640,15 @@ class DatasetIndexingStatusApi(Resource):
|
|||||||
return data, 200
|
return data, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/api-keys")
|
||||||
class DatasetApiKeyApi(Resource):
|
class DatasetApiKeyApi(Resource):
|
||||||
max_keys = 10
|
max_keys = 10
|
||||||
token_prefix = "dataset-"
|
token_prefix = "dataset-"
|
||||||
resource_type = "dataset"
|
resource_type = "dataset"
|
||||||
|
|
||||||
|
@api.doc("get_dataset_api_keys")
|
||||||
|
@api.doc(description="Get dataset API keys")
|
||||||
|
@api.response(200, "API keys retrieved successfully", api_key_list)
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -609,9 +693,14 @@ class DatasetApiKeyApi(Resource):
|
|||||||
return api_token, 200
|
return api_token, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/api-keys/<uuid:api_key_id>")
|
||||||
class DatasetApiDeleteApi(Resource):
|
class DatasetApiDeleteApi(Resource):
|
||||||
resource_type = "dataset"
|
resource_type = "dataset"
|
||||||
|
|
||||||
|
@api.doc("delete_dataset_api_key")
|
||||||
|
@api.doc(description="Delete dataset API key")
|
||||||
|
@api.doc(params={"api_key_id": "API key ID"})
|
||||||
|
@api.response(204, "API key deleted successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -641,7 +730,11 @@ class DatasetApiDeleteApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/api-base-info")
|
||||||
class DatasetApiBaseUrlApi(Resource):
|
class DatasetApiBaseUrlApi(Resource):
|
||||||
|
@api.doc("get_dataset_api_base_info")
|
||||||
|
@api.doc(description="Get dataset API base information")
|
||||||
|
@api.response(200, "API base info retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -649,7 +742,11 @@ class DatasetApiBaseUrlApi(Resource):
|
|||||||
return {"api_base_url": (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"}
|
return {"api_base_url": (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/retrieval-setting")
|
||||||
class DatasetRetrievalSettingApi(Resource):
|
class DatasetRetrievalSettingApi(Resource):
|
||||||
|
@api.doc("get_dataset_retrieval_setting")
|
||||||
|
@api.doc(description="Get dataset retrieval settings")
|
||||||
|
@api.response(200, "Retrieval settings retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -700,7 +797,12 @@ class DatasetRetrievalSettingApi(Resource):
|
|||||||
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/retrieval-setting/<string:vector_type>")
|
||||||
class DatasetRetrievalSettingMockApi(Resource):
|
class DatasetRetrievalSettingMockApi(Resource):
|
||||||
|
@api.doc("get_dataset_retrieval_setting_mock")
|
||||||
|
@api.doc(description="Get mock dataset retrieval settings by vector type")
|
||||||
|
@api.doc(params={"vector_type": "Vector store type"})
|
||||||
|
@api.response(200, "Mock retrieval settings retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -749,7 +851,13 @@ class DatasetRetrievalSettingMockApi(Resource):
|
|||||||
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/error-docs")
|
||||||
class DatasetErrorDocs(Resource):
|
class DatasetErrorDocs(Resource):
|
||||||
|
@api.doc("get_dataset_error_docs")
|
||||||
|
@api.doc(description="Get dataset error documents")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Error documents retrieved successfully")
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -763,7 +871,14 @@ class DatasetErrorDocs(Resource):
|
|||||||
return {"data": [marshal(item, document_status_fields) for item in results], "total": len(results)}, 200
|
return {"data": [marshal(item, document_status_fields) for item in results], "total": len(results)}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/permission-part-users")
|
||||||
class DatasetPermissionUserListApi(Resource):
|
class DatasetPermissionUserListApi(Resource):
|
||||||
|
@api.doc("get_dataset_permission_users")
|
||||||
|
@api.doc(description="Get dataset permission user list")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Permission users retrieved successfully")
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -784,7 +899,13 @@ class DatasetPermissionUserListApi(Resource):
|
|||||||
}, 200
|
}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/auto-disable-logs")
|
||||||
class DatasetAutoDisableLogApi(Resource):
|
class DatasetAutoDisableLogApi(Resource):
|
||||||
|
@api.doc("get_dataset_auto_disable_logs")
|
||||||
|
@api.doc(description="Get dataset auto disable logs")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.response(200, "Auto disable logs retrieved successfully")
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -794,20 +915,3 @@ class DatasetAutoDisableLogApi(Resource):
|
|||||||
if dataset is None:
|
if dataset is None:
|
||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
return DatasetService.get_dataset_auto_disable_logs(dataset_id_str), 200
|
return DatasetService.get_dataset_auto_disable_logs(dataset_id_str), 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(DatasetListApi, "/datasets")
|
|
||||||
api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")
|
|
||||||
api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")
|
|
||||||
api.add_resource(DatasetQueryApi, "/datasets/<uuid:dataset_id>/queries")
|
|
||||||
api.add_resource(DatasetErrorDocs, "/datasets/<uuid:dataset_id>/error-docs")
|
|
||||||
api.add_resource(DatasetIndexingEstimateApi, "/datasets/indexing-estimate")
|
|
||||||
api.add_resource(DatasetRelatedAppListApi, "/datasets/<uuid:dataset_id>/related-apps")
|
|
||||||
api.add_resource(DatasetIndexingStatusApi, "/datasets/<uuid:dataset_id>/indexing-status")
|
|
||||||
api.add_resource(DatasetApiKeyApi, "/datasets/api-keys")
|
|
||||||
api.add_resource(DatasetApiDeleteApi, "/datasets/api-keys/<uuid:api_key_id>")
|
|
||||||
api.add_resource(DatasetApiBaseUrlApi, "/datasets/api-base-info")
|
|
||||||
api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")
|
|
||||||
api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")
|
|
||||||
api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")
|
|
||||||
api.add_resource(DatasetAutoDisableLogApi, "/datasets/<uuid:dataset_id>/auto-disable-logs")
|
|
||||||
|
|||||||
@ -5,12 +5,12 @@ from typing import Literal, 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, marshal, marshal_with, reqparse
|
from flask_restx import Resource, fields, marshal, marshal_with, reqparse
|
||||||
from sqlalchemy import asc, desc, select
|
from sqlalchemy import asc, desc, select
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, 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 (
|
||||||
ProviderModelCurrentlyNotSupportError,
|
ProviderModelCurrentlyNotSupportError,
|
||||||
ProviderNotInitializeError,
|
ProviderNotInitializeError,
|
||||||
@ -98,7 +98,12 @@ class DocumentResource(Resource):
|
|||||||
return documents
|
return documents
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/process-rule")
|
||||||
class GetProcessRuleApi(Resource):
|
class GetProcessRuleApi(Resource):
|
||||||
|
@api.doc("get_process_rule")
|
||||||
|
@api.doc(description="Get dataset document processing rules")
|
||||||
|
@api.doc(params={"document_id": "Document ID (optional)"})
|
||||||
|
@api.response(200, "Process rules retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -140,7 +145,21 @@ class GetProcessRuleApi(Resource):
|
|||||||
return {"mode": mode, "rules": rules, "limits": limits}
|
return {"mode": mode, "rules": rules, "limits": limits}
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents")
|
||||||
class DatasetDocumentListApi(Resource):
|
class DatasetDocumentListApi(Resource):
|
||||||
|
@api.doc("get_dataset_documents")
|
||||||
|
@api.doc(description="Get documents in a dataset")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"dataset_id": "Dataset ID",
|
||||||
|
"page": "Page number (default: 1)",
|
||||||
|
"limit": "Number of items per page (default: 20)",
|
||||||
|
"keyword": "Search keyword",
|
||||||
|
"sort": "Sort order (default: -created_at)",
|
||||||
|
"fetch": "Fetch full details (default: false)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(200, "Documents retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -324,7 +343,23 @@ class DatasetDocumentListApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/init")
|
||||||
class DatasetInitApi(Resource):
|
class DatasetInitApi(Resource):
|
||||||
|
@api.doc("init_dataset")
|
||||||
|
@api.doc(description="Initialize dataset with documents")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"DatasetInitRequest",
|
||||||
|
{
|
||||||
|
"upload_file_id": fields.String(required=True, description="Upload file ID"),
|
||||||
|
"indexing_technique": fields.String(description="Indexing technique"),
|
||||||
|
"process_rule": fields.Raw(description="Processing rules"),
|
||||||
|
"data_source": fields.Raw(description="Data source configuration"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "Dataset initialized successfully", dataset_and_document_fields)
|
||||||
|
@api.response(400, "Invalid request parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -394,7 +429,14 @@ class DatasetInitApi(Resource):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-estimate")
|
||||||
class DocumentIndexingEstimateApi(DocumentResource):
|
class DocumentIndexingEstimateApi(DocumentResource):
|
||||||
|
@api.doc("estimate_document_indexing")
|
||||||
|
@api.doc(description="Estimate document indexing cost")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||||
|
@api.response(200, "Indexing estimate calculated successfully")
|
||||||
|
@api.response(404, "Document not found")
|
||||||
|
@api.response(400, "Document already finished")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -593,7 +635,13 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-status")
|
||||||
class DocumentIndexingStatusApi(DocumentResource):
|
class DocumentIndexingStatusApi(DocumentResource):
|
||||||
|
@api.doc("get_document_indexing_status")
|
||||||
|
@api.doc(description="Get document indexing status")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||||
|
@api.response(200, "Indexing status retrieved successfully")
|
||||||
|
@api.response(404, "Document not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -635,9 +683,21 @@ class DocumentIndexingStatusApi(DocumentResource):
|
|||||||
return marshal(document_dict, document_status_fields)
|
return marshal(document_dict, document_status_fields)
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
||||||
class DocumentApi(DocumentResource):
|
class DocumentApi(DocumentResource):
|
||||||
METADATA_CHOICES = {"all", "only", "without"}
|
METADATA_CHOICES = {"all", "only", "without"}
|
||||||
|
|
||||||
|
@api.doc("get_document")
|
||||||
|
@api.doc(description="Get document details")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"dataset_id": "Dataset ID",
|
||||||
|
"document_id": "Document ID",
|
||||||
|
"metadata": "Metadata inclusion (all/only/without)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(200, "Document retrieved successfully")
|
||||||
|
@api.response(404, "Document not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -746,7 +806,16 @@ class DocumentApi(DocumentResource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/<string:action>")
|
||||||
class DocumentProcessingApi(DocumentResource):
|
class DocumentProcessingApi(DocumentResource):
|
||||||
|
@api.doc("update_document_processing")
|
||||||
|
@api.doc(description="Update document processing status (pause/resume)")
|
||||||
|
@api.doc(
|
||||||
|
params={"dataset_id": "Dataset ID", "document_id": "Document ID", "action": "Action to perform (pause/resume)"}
|
||||||
|
)
|
||||||
|
@api.response(200, "Processing status updated successfully")
|
||||||
|
@api.response(404, "Document not found")
|
||||||
|
@api.response(400, "Invalid action")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -781,7 +850,23 @@ class DocumentProcessingApi(DocumentResource):
|
|||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
||||||
class DocumentMetadataApi(DocumentResource):
|
class DocumentMetadataApi(DocumentResource):
|
||||||
|
@api.doc("update_document_metadata")
|
||||||
|
@api.doc(description="Update document metadata")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"UpdateDocumentMetadataRequest",
|
||||||
|
{
|
||||||
|
"doc_type": fields.String(description="Document type"),
|
||||||
|
"doc_metadata": fields.Raw(description="Document metadata"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Document metadata updated successfully")
|
||||||
|
@api.response(404, "Document not found")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -1015,26 +1100,3 @@ class WebsiteDocumentSyncApi(DocumentResource):
|
|||||||
DocumentService.sync_website_document(dataset_id, document)
|
DocumentService.sync_website_document(dataset_id, document)
|
||||||
|
|
||||||
return {"result": "success"}, 200
|
return {"result": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(GetProcessRuleApi, "/datasets/process-rule")
|
|
||||||
api.add_resource(DatasetDocumentListApi, "/datasets/<uuid:dataset_id>/documents")
|
|
||||||
api.add_resource(DatasetInitApi, "/datasets/init")
|
|
||||||
api.add_resource(
|
|
||||||
DocumentIndexingEstimateApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-estimate"
|
|
||||||
)
|
|
||||||
api.add_resource(DocumentBatchIndexingEstimateApi, "/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-estimate")
|
|
||||||
api.add_resource(DocumentBatchIndexingStatusApi, "/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-status")
|
|
||||||
api.add_resource(DocumentIndexingStatusApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-status")
|
|
||||||
api.add_resource(DocumentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
|
||||||
api.add_resource(
|
|
||||||
DocumentProcessingApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/<string:action>"
|
|
||||||
)
|
|
||||||
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
|
||||||
api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/status/<string:action>/batch")
|
|
||||||
api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
|
|
||||||
api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
|
|
||||||
api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")
|
|
||||||
api.add_resource(DocumentRenameApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/rename")
|
|
||||||
|
|
||||||
api.add_resource(WebsiteDocumentSyncApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/website-sync")
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
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, reqparse
|
from flask_restx import Resource, fields, marshal, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, 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.datasets.error import DatasetNameDuplicateError
|
from controllers.console.datasets.error import DatasetNameDuplicateError
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from fields.dataset_fields import dataset_detail_fields
|
from fields.dataset_fields import dataset_detail_fields
|
||||||
@ -21,7 +21,18 @@ def _validate_name(name):
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/external-knowledge-api")
|
||||||
class ExternalApiTemplateListApi(Resource):
|
class ExternalApiTemplateListApi(Resource):
|
||||||
|
@api.doc("get_external_api_templates")
|
||||||
|
@api.doc(description="Get external knowledge API templates")
|
||||||
|
@api.doc(
|
||||||
|
params={
|
||||||
|
"page": "Page number (default: 1)",
|
||||||
|
"limit": "Number of items per page (default: 20)",
|
||||||
|
"keyword": "Search keyword",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api.response(200, "External API templates retrieved successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -79,7 +90,13 @@ class ExternalApiTemplateListApi(Resource):
|
|||||||
return external_knowledge_api.to_dict(), 201
|
return external_knowledge_api.to_dict(), 201
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>")
|
||||||
class ExternalApiTemplateApi(Resource):
|
class ExternalApiTemplateApi(Resource):
|
||||||
|
@api.doc("get_external_api_template")
|
||||||
|
@api.doc(description="Get external knowledge API template details")
|
||||||
|
@api.doc(params={"external_knowledge_api_id": "External knowledge API ID"})
|
||||||
|
@api.response(200, "External API template retrieved successfully")
|
||||||
|
@api.response(404, "Template not found")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -138,7 +155,12 @@ class ExternalApiTemplateApi(Resource):
|
|||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>/use-check")
|
||||||
class ExternalApiUseCheckApi(Resource):
|
class ExternalApiUseCheckApi(Resource):
|
||||||
|
@api.doc("check_external_api_usage")
|
||||||
|
@api.doc(description="Check if external knowledge API is being used")
|
||||||
|
@api.doc(params={"external_knowledge_api_id": "External knowledge API ID"})
|
||||||
|
@api.response(200, "Usage check completed successfully")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -151,7 +173,24 @@ class ExternalApiUseCheckApi(Resource):
|
|||||||
return {"is_using": external_knowledge_api_is_using, "count": count}, 200
|
return {"is_using": external_knowledge_api_is_using, "count": count}, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/external")
|
||||||
class ExternalDatasetCreateApi(Resource):
|
class ExternalDatasetCreateApi(Resource):
|
||||||
|
@api.doc("create_external_dataset")
|
||||||
|
@api.doc(description="Create external knowledge dataset")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"CreateExternalDatasetRequest",
|
||||||
|
{
|
||||||
|
"external_knowledge_api_id": fields.String(required=True, description="External knowledge API ID"),
|
||||||
|
"external_knowledge_id": fields.String(required=True, description="External knowledge ID"),
|
||||||
|
"name": fields.String(required=True, description="Dataset name"),
|
||||||
|
"description": fields.String(description="Dataset description"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(201, "External dataset created successfully", dataset_detail_fields)
|
||||||
|
@api.response(400, "Invalid parameters")
|
||||||
|
@api.response(403, "Permission denied")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -191,7 +230,24 @@ class ExternalDatasetCreateApi(Resource):
|
|||||||
return marshal(dataset, dataset_detail_fields), 201
|
return marshal(dataset, dataset_detail_fields), 201
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/external-hit-testing")
|
||||||
class ExternalKnowledgeHitTestingApi(Resource):
|
class ExternalKnowledgeHitTestingApi(Resource):
|
||||||
|
@api.doc("test_external_knowledge_retrieval")
|
||||||
|
@api.doc(description="Test external knowledge retrieval for dataset")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"ExternalHitTestingRequest",
|
||||||
|
{
|
||||||
|
"query": fields.String(required=True, description="Query text for testing"),
|
||||||
|
"retrieval_model": fields.Raw(description="Retrieval model configuration"),
|
||||||
|
"external_retrieval_model": fields.Raw(description="External retrieval model configuration"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "External hit testing completed successfully")
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
|
@api.response(400, "Invalid parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -228,8 +284,22 @@ class ExternalKnowledgeHitTestingApi(Resource):
|
|||||||
raise InternalServerError(str(e))
|
raise InternalServerError(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/test/retrieval")
|
||||||
class BedrockRetrievalApi(Resource):
|
class BedrockRetrievalApi(Resource):
|
||||||
# this api is only for internal testing
|
# this api is only for internal testing
|
||||||
|
@api.doc("bedrock_retrieval_test")
|
||||||
|
@api.doc(description="Bedrock retrieval test (internal use only)")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"BedrockRetrievalTestRequest",
|
||||||
|
{
|
||||||
|
"retrieval_setting": fields.Raw(required=True, description="Retrieval settings"),
|
||||||
|
"query": fields.String(required=True, description="Query text"),
|
||||||
|
"knowledge_id": fields.String(required=True, description="Knowledge ID"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Bedrock retrieval test completed")
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("retrieval_setting", nullable=False, required=True, type=dict, location="json")
|
parser.add_argument("retrieval_setting", nullable=False, required=True, type=dict, location="json")
|
||||||
@ -247,12 +317,3 @@ class BedrockRetrievalApi(Resource):
|
|||||||
args["retrieval_setting"], args["query"], args["knowledge_id"]
|
args["retrieval_setting"], args["query"], args["knowledge_id"]
|
||||||
)
|
)
|
||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(ExternalKnowledgeHitTestingApi, "/datasets/<uuid:dataset_id>/external-hit-testing")
|
|
||||||
api.add_resource(ExternalDatasetCreateApi, "/datasets/external")
|
|
||||||
api.add_resource(ExternalApiTemplateListApi, "/datasets/external-knowledge-api")
|
|
||||||
api.add_resource(ExternalApiTemplateApi, "/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>")
|
|
||||||
api.add_resource(ExternalApiUseCheckApi, "/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>/use-check")
|
|
||||||
# this api is only for internal test
|
|
||||||
api.add_resource(BedrockRetrievalApi, "/test/retrieval")
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
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.datasets.hit_testing_base import DatasetsHitTestingBase
|
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
@ -10,7 +10,25 @@ from controllers.console.wraps import (
|
|||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/datasets/<uuid:dataset_id>/hit-testing")
|
||||||
class HitTestingApi(Resource, DatasetsHitTestingBase):
|
class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||||
|
@api.doc("test_dataset_retrieval")
|
||||||
|
@api.doc(description="Test dataset knowledge retrieval")
|
||||||
|
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"HitTestingRequest",
|
||||||
|
{
|
||||||
|
"query": fields.String(required=True, description="Query text for testing"),
|
||||||
|
"retrieval_model": fields.Raw(description="Retrieval model configuration"),
|
||||||
|
"top_k": fields.Integer(description="Number of top results to return"),
|
||||||
|
"score_threshold": fields.Float(description="Score threshold for filtering results"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Hit testing completed successfully")
|
||||||
|
@api.response(404, "Dataset not found")
|
||||||
|
@api.response(400, "Invalid parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -23,6 +41,3 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
|
|||||||
self.hit_testing_args_check(args)
|
self.hit_testing_args_check(args)
|
||||||
|
|
||||||
return self.perform_hit_testing(dataset, args)
|
return self.perform_hit_testing(dataset, args)
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(HitTestingApi, "/datasets/<uuid:dataset_id>/hit-testing")
|
|
||||||
|
|||||||
@ -1,13 +1,32 @@
|
|||||||
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.datasets.error import WebsiteCrawlError
|
from controllers.console.datasets.error import WebsiteCrawlError
|
||||||
from controllers.console.wraps import account_initialization_required, setup_required
|
from controllers.console.wraps import account_initialization_required, setup_required
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.website_service import WebsiteCrawlApiRequest, WebsiteCrawlStatusApiRequest, WebsiteService
|
from services.website_service import WebsiteCrawlApiRequest, WebsiteCrawlStatusApiRequest, WebsiteService
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/website/crawl")
|
||||||
class WebsiteCrawlApi(Resource):
|
class WebsiteCrawlApi(Resource):
|
||||||
|
@api.doc("crawl_website")
|
||||||
|
@api.doc(description="Crawl website content")
|
||||||
|
@api.expect(
|
||||||
|
api.model(
|
||||||
|
"WebsiteCrawlRequest",
|
||||||
|
{
|
||||||
|
"provider": fields.String(
|
||||||
|
required=True,
|
||||||
|
description="Crawl provider (firecrawl/watercrawl/jinareader)",
|
||||||
|
enum=["firecrawl", "watercrawl", "jinareader"],
|
||||||
|
),
|
||||||
|
"url": fields.String(required=True, description="URL to crawl"),
|
||||||
|
"options": fields.Raw(required=True, description="Crawl options"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@api.response(200, "Website crawl initiated successfully")
|
||||||
|
@api.response(400, "Invalid crawl parameters")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -39,7 +58,14 @@ class WebsiteCrawlApi(Resource):
|
|||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
|
@console_ns.route("/website/crawl/status/<string:job_id>")
|
||||||
class WebsiteCrawlStatusApi(Resource):
|
class WebsiteCrawlStatusApi(Resource):
|
||||||
|
@api.doc("get_crawl_status")
|
||||||
|
@api.doc(description="Get website crawl status")
|
||||||
|
@api.doc(params={"job_id": "Crawl job ID", "provider": "Crawl provider (firecrawl/watercrawl/jinareader)"})
|
||||||
|
@api.response(200, "Crawl status retrieved successfully")
|
||||||
|
@api.response(404, "Crawl job not found")
|
||||||
|
@api.response(400, "Invalid provider")
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@account_initialization_required
|
@account_initialization_required
|
||||||
@ -62,7 +88,3 @@ class WebsiteCrawlStatusApi(Resource):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise WebsiteCrawlError(str(e))
|
raise WebsiteCrawlError(str(e))
|
||||||
return result, 200
|
return result, 200
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(WebsiteCrawlApi, "/website/crawl")
|
|
||||||
api.add_resource(WebsiteCrawlStatusApi, "/website/crawl/status/<string:job_id>")
|
|
||||||
|
|||||||
@ -14,6 +14,15 @@ api = ExternalApi(
|
|||||||
|
|
||||||
files_ns = Namespace("files", description="File operations", path="/")
|
files_ns = Namespace("files", description="File operations", path="/")
|
||||||
|
|
||||||
from . import image_preview, tool_files, upload # pyright: ignore[reportUnusedImport]
|
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",
|
||||||
|
]
|
||||||
|
|||||||
@ -15,8 +15,17 @@ api = ExternalApi(
|
|||||||
# 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 as _mail # pyright: ignore[reportUnusedImport]
|
from . import mail as _mail
|
||||||
from .plugin import plugin as _plugin # pyright: ignore[reportUnusedImport]
|
from .plugin import plugin as _plugin
|
||||||
from .workspace import workspace as _workspace # pyright: ignore[reportUnusedImport]
|
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",
|
||||||
|
]
|
||||||
|
|||||||
@ -14,6 +14,13 @@ api = ExternalApi(
|
|||||||
|
|
||||||
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
|
mcp_ns = Namespace("mcp", description="MCP operations", path="/")
|
||||||
|
|
||||||
from . import mcp # pyright: ignore[reportUnusedImport]
|
from . import mcp
|
||||||
|
|
||||||
api.add_namespace(mcp_ns)
|
api.add_namespace(mcp_ns)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"api",
|
||||||
|
"bp",
|
||||||
|
"mcp",
|
||||||
|
"mcp_ns",
|
||||||
|
]
|
||||||
|
|||||||
@ -14,26 +14,46 @@ api = ExternalApi(
|
|||||||
|
|
||||||
service_api_ns = Namespace("service_api", description="Service operations", path="/")
|
service_api_ns = Namespace("service_api", description="Service operations", path="/")
|
||||||
|
|
||||||
from . import index # pyright: ignore[reportUnusedImport]
|
from . import index
|
||||||
from .app import (
|
from .app import (
|
||||||
annotation, # pyright: ignore[reportUnusedImport]
|
annotation,
|
||||||
app, # pyright: ignore[reportUnusedImport]
|
app,
|
||||||
audio, # pyright: ignore[reportUnusedImport]
|
audio,
|
||||||
completion, # pyright: ignore[reportUnusedImport]
|
completion,
|
||||||
conversation, # pyright: ignore[reportUnusedImport]
|
conversation,
|
||||||
file, # pyright: ignore[reportUnusedImport]
|
file,
|
||||||
file_preview, # pyright: ignore[reportUnusedImport]
|
file_preview,
|
||||||
message, # pyright: ignore[reportUnusedImport]
|
message,
|
||||||
site, # pyright: ignore[reportUnusedImport]
|
site,
|
||||||
workflow, # pyright: ignore[reportUnusedImport]
|
workflow,
|
||||||
)
|
)
|
||||||
from .dataset import (
|
from .dataset import (
|
||||||
dataset, # pyright: ignore[reportUnusedImport]
|
dataset,
|
||||||
document, # pyright: ignore[reportUnusedImport]
|
document,
|
||||||
hit_testing, # pyright: ignore[reportUnusedImport]
|
hit_testing,
|
||||||
metadata, # pyright: ignore[reportUnusedImport]
|
metadata,
|
||||||
segment, # pyright: ignore[reportUnusedImport]
|
segment,
|
||||||
)
|
)
|
||||||
from .workspace import models # pyright: ignore[reportUnusedImport]
|
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)
|
||||||
|
|||||||
@ -16,20 +16,40 @@ api = ExternalApi(
|
|||||||
web_ns = Namespace("web", description="Web application API operations", path="/")
|
web_ns = Namespace("web", description="Web application API operations", path="/")
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
app, # pyright: ignore[reportUnusedImport]
|
app,
|
||||||
audio, # pyright: ignore[reportUnusedImport]
|
audio,
|
||||||
completion, # pyright: ignore[reportUnusedImport]
|
completion,
|
||||||
conversation, # pyright: ignore[reportUnusedImport]
|
conversation,
|
||||||
feature, # pyright: ignore[reportUnusedImport]
|
feature,
|
||||||
files, # pyright: ignore[reportUnusedImport]
|
files,
|
||||||
forgot_password, # pyright: ignore[reportUnusedImport]
|
forgot_password,
|
||||||
login, # pyright: ignore[reportUnusedImport]
|
login,
|
||||||
message, # pyright: ignore[reportUnusedImport]
|
message,
|
||||||
passport, # pyright: ignore[reportUnusedImport]
|
passport,
|
||||||
remote_files, # pyright: ignore[reportUnusedImport]
|
remote_files,
|
||||||
saved_message, # pyright: ignore[reportUnusedImport]
|
saved_message,
|
||||||
site, # pyright: ignore[reportUnusedImport]
|
site,
|
||||||
workflow, # pyright: ignore[reportUnusedImport]
|
workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
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",
|
||||||
|
]
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class UnstructuredWordExtractor(BaseExtractor):
|
|||||||
unstructured_version = tuple(int(x) for x in __unstructured_version__.split("."))
|
unstructured_version = tuple(int(x) for x in __unstructured_version__.split("."))
|
||||||
# check the file extension
|
# check the file extension
|
||||||
try:
|
try:
|
||||||
import magic # noqa: F401 # pyright: ignore[reportUnusedImport]
|
import magic # noqa: F401
|
||||||
|
|
||||||
is_doc = detect_filetype(self._file_path) == FileType.DOC
|
is_doc = detect_filetype(self._file_path) == FileType.DOC
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@ -110,9 +110,7 @@ class TextSplitter(BaseDocumentTransformer, ABC):
|
|||||||
docs = []
|
docs = []
|
||||||
current_doc: list[str] = []
|
current_doc: list[str] = []
|
||||||
total = 0
|
total = 0
|
||||||
index = 0
|
for d, _len in zip(splits, lengths):
|
||||||
for d in splits:
|
|
||||||
_len = lengths[index]
|
|
||||||
if total + _len + (separator_len if len(current_doc) > 0 else 0) > self._chunk_size:
|
if total + _len + (separator_len if len(current_doc) > 0 else 0) > self._chunk_size:
|
||||||
if total > self._chunk_size:
|
if total > self._chunk_size:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@ -134,7 +132,6 @@ class TextSplitter(BaseDocumentTransformer, ABC):
|
|||||||
current_doc = current_doc[1:]
|
current_doc = current_doc[1:]
|
||||||
current_doc.append(d)
|
current_doc.append(d)
|
||||||
total += _len + (separator_len if len(current_doc) > 1 else 0)
|
total += _len + (separator_len if len(current_doc) > 1 else 0)
|
||||||
index += 1
|
|
||||||
doc = self._join_docs(current_doc, separator)
|
doc = self._join_docs(current_doc, separator)
|
||||||
if doc is not None:
|
if doc is not None:
|
||||||
docs.append(doc)
|
docs.append(doc)
|
||||||
|
|||||||
@ -79,7 +79,7 @@ dependencies = [
|
|||||||
"sqlalchemy~=2.0.29",
|
"sqlalchemy~=2.0.29",
|
||||||
"starlette==0.47.2",
|
"starlette==0.47.2",
|
||||||
"tiktoken~=0.9.0",
|
"tiktoken~=0.9.0",
|
||||||
"transformers~=4.53.0",
|
"transformers~=4.56.1",
|
||||||
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
|
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
|
||||||
"weave~=0.51.0",
|
"weave~=0.51.0",
|
||||||
"yarl~=1.18.3",
|
"yarl~=1.18.3",
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
import uuid
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -248,9 +250,15 @@ class TestWebAppAuthService:
|
|||||||
- Proper error handling for non-existent accounts
|
- Proper error handling for non-existent accounts
|
||||||
- Correct exception type and message
|
- Correct exception type and message
|
||||||
"""
|
"""
|
||||||
# Arrange: Use non-existent email
|
# Arrange: Generate a guaranteed non-existent email
|
||||||
fake = Faker()
|
# Use UUID and timestamp to ensure uniqueness
|
||||||
non_existent_email = fake.email()
|
unique_id = str(uuid.uuid4()).replace("-", "")
|
||||||
|
timestamp = str(int(time.time() * 1000000)) # microseconds
|
||||||
|
non_existent_email = f"nonexistent_{unique_id}_{timestamp}@test-domain-that-never-exists.invalid"
|
||||||
|
|
||||||
|
# Double-check this email doesn't exist in the database
|
||||||
|
existing_account = db_session_with_containers.query(Account).filter_by(email=non_existent_email).first()
|
||||||
|
assert existing_account is None, f"Test email {non_existent_email} already exists in database"
|
||||||
|
|
||||||
# Act & Assert: Verify proper error handling
|
# Act & Assert: Verify proper error handling
|
||||||
with pytest.raises(AccountNotFoundError):
|
with pytest.raises(AccountNotFoundError):
|
||||||
|
|||||||
@ -0,0 +1,727 @@
|
|||||||
|
"""
|
||||||
|
TestContainers-based integration tests for disable_segments_from_index_task.
|
||||||
|
|
||||||
|
This module provides comprehensive integration testing for the disable_segments_from_index_task
|
||||||
|
using TestContainers to ensure realistic database interactions and proper isolation.
|
||||||
|
The task is responsible for removing document segments from the search index when they are disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from models import Account, Dataset, DocumentSegment
|
||||||
|
from models import Document as DatasetDocument
|
||||||
|
from models.dataset import DatasetProcessRule
|
||||||
|
from tasks.disable_segments_from_index_task import disable_segments_from_index_task
|
||||||
|
|
||||||
|
|
||||||
|
class TestDisableSegmentsFromIndexTask:
|
||||||
|
"""
|
||||||
|
Comprehensive integration tests for disable_segments_from_index_task using testcontainers.
|
||||||
|
|
||||||
|
This test class covers all major functionality of the disable_segments_from_index_task:
|
||||||
|
- Successful segment disabling with proper index cleanup
|
||||||
|
- Error handling for various edge cases
|
||||||
|
- Database state validation after task execution
|
||||||
|
- Redis cache cleanup verification
|
||||||
|
- Index processor integration testing
|
||||||
|
|
||||||
|
All tests use the testcontainers infrastructure to ensure proper database isolation
|
||||||
|
and realistic testing environment with actual database interactions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _create_test_account(self, db_session_with_containers, fake=None):
|
||||||
|
"""
|
||||||
|
Helper method to create a test account with realistic data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
fake: Faker instance for generating test data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Account: Created test account instance
|
||||||
|
"""
|
||||||
|
fake = fake or Faker()
|
||||||
|
account = Account()
|
||||||
|
account.id = fake.uuid4()
|
||||||
|
account.email = fake.email()
|
||||||
|
account.name = fake.name()
|
||||||
|
account.avatar_url = fake.url()
|
||||||
|
account.tenant_id = fake.uuid4()
|
||||||
|
account.status = "active"
|
||||||
|
account.type = "normal"
|
||||||
|
account.role = "owner"
|
||||||
|
account.interface_language = "en-US"
|
||||||
|
account.created_at = fake.date_time_this_year()
|
||||||
|
account.updated_at = account.created_at
|
||||||
|
|
||||||
|
# Create a tenant for the account
|
||||||
|
from models.account import Tenant
|
||||||
|
|
||||||
|
tenant = Tenant()
|
||||||
|
tenant.id = account.tenant_id
|
||||||
|
tenant.name = f"Test Tenant {fake.company()}"
|
||||||
|
tenant.plan = "basic"
|
||||||
|
tenant.status = "active"
|
||||||
|
tenant.created_at = fake.date_time_this_year()
|
||||||
|
tenant.updated_at = tenant.created_at
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.add(tenant)
|
||||||
|
db.session.add(account)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Set the current tenant for the account
|
||||||
|
account.current_tenant = tenant
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
def _create_test_dataset(self, db_session_with_containers, account, fake=None):
|
||||||
|
"""
|
||||||
|
Helper method to create a test dataset with realistic data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
account: The account creating the dataset
|
||||||
|
fake: Faker instance for generating test data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dataset: Created test dataset instance
|
||||||
|
"""
|
||||||
|
fake = fake or Faker()
|
||||||
|
dataset = Dataset()
|
||||||
|
dataset.id = fake.uuid4()
|
||||||
|
dataset.tenant_id = account.tenant_id
|
||||||
|
dataset.name = f"Test Dataset {fake.word()}"
|
||||||
|
dataset.description = fake.text(max_nb_chars=200)
|
||||||
|
dataset.provider = "vendor"
|
||||||
|
dataset.permission = "only_me"
|
||||||
|
dataset.data_source_type = "upload_file"
|
||||||
|
dataset.indexing_technique = "high_quality"
|
||||||
|
dataset.created_by = account.id
|
||||||
|
dataset.updated_by = account.id
|
||||||
|
dataset.embedding_model = "text-embedding-ada-002"
|
||||||
|
dataset.embedding_model_provider = "openai"
|
||||||
|
dataset.built_in_field_enabled = False
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.add(dataset)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return dataset
|
||||||
|
|
||||||
|
def _create_test_document(self, db_session_with_containers, dataset, account, fake=None):
|
||||||
|
"""
|
||||||
|
Helper method to create a test document with realistic data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
dataset: The dataset containing the document
|
||||||
|
account: The account creating the document
|
||||||
|
fake: Faker instance for generating test data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DatasetDocument: Created test document instance
|
||||||
|
"""
|
||||||
|
fake = fake or Faker()
|
||||||
|
document = DatasetDocument()
|
||||||
|
document.id = fake.uuid4()
|
||||||
|
document.tenant_id = dataset.tenant_id
|
||||||
|
document.dataset_id = dataset.id
|
||||||
|
document.position = 1
|
||||||
|
document.data_source_type = "upload_file"
|
||||||
|
document.data_source_info = '{"upload_file_id": "test_file_id"}'
|
||||||
|
document.batch = fake.uuid4()
|
||||||
|
document.name = f"Test Document {fake.word()}.txt"
|
||||||
|
document.created_from = "upload_file"
|
||||||
|
document.created_by = account.id
|
||||||
|
document.created_api_request_id = fake.uuid4()
|
||||||
|
document.processing_started_at = fake.date_time_this_year()
|
||||||
|
document.file_id = fake.uuid4()
|
||||||
|
document.word_count = fake.random_int(min=100, max=1000)
|
||||||
|
document.parsing_completed_at = fake.date_time_this_year()
|
||||||
|
document.cleaning_completed_at = fake.date_time_this_year()
|
||||||
|
document.splitting_completed_at = fake.date_time_this_year()
|
||||||
|
document.tokens = fake.random_int(min=50, max=500)
|
||||||
|
document.indexing_started_at = fake.date_time_this_year()
|
||||||
|
document.indexing_completed_at = fake.date_time_this_year()
|
||||||
|
document.indexing_status = "completed"
|
||||||
|
document.enabled = True
|
||||||
|
document.archived = False
|
||||||
|
document.doc_form = "text_model" # Use text_model form for testing
|
||||||
|
document.doc_language = "en"
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.add(document)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
def _create_test_segments(self, db_session_with_containers, document, dataset, account, count=3, fake=None):
|
||||||
|
"""
|
||||||
|
Helper method to create test document segments with realistic data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
document: The document containing the segments
|
||||||
|
dataset: The dataset containing the document
|
||||||
|
account: The account creating the segments
|
||||||
|
count: Number of segments to create
|
||||||
|
fake: Faker instance for generating test data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[DocumentSegment]: Created test segment instances
|
||||||
|
"""
|
||||||
|
fake = fake or Faker()
|
||||||
|
segments = []
|
||||||
|
|
||||||
|
for i in range(count):
|
||||||
|
segment = DocumentSegment()
|
||||||
|
segment.id = fake.uuid4()
|
||||||
|
segment.tenant_id = dataset.tenant_id
|
||||||
|
segment.dataset_id = dataset.id
|
||||||
|
segment.document_id = document.id
|
||||||
|
segment.position = i + 1
|
||||||
|
segment.content = f"Test segment content {i + 1}: {fake.text(max_nb_chars=200)}"
|
||||||
|
segment.answer = f"Test answer {i + 1}" if i % 2 == 0 else None
|
||||||
|
segment.word_count = fake.random_int(min=10, max=100)
|
||||||
|
segment.tokens = fake.random_int(min=5, max=50)
|
||||||
|
segment.keywords = [fake.word() for _ in range(3)]
|
||||||
|
segment.index_node_id = f"node_{segment.id}"
|
||||||
|
segment.index_node_hash = fake.sha256()
|
||||||
|
segment.hit_count = 0
|
||||||
|
segment.enabled = True
|
||||||
|
segment.disabled_at = None
|
||||||
|
segment.disabled_by = None
|
||||||
|
segment.status = "completed"
|
||||||
|
segment.created_by = account.id
|
||||||
|
segment.updated_by = account.id
|
||||||
|
segment.indexing_at = fake.date_time_this_year()
|
||||||
|
segment.completed_at = fake.date_time_this_year()
|
||||||
|
segment.error = None
|
||||||
|
segment.stopped_at = None
|
||||||
|
|
||||||
|
segments.append(segment)
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
for segment in segments:
|
||||||
|
db.session.add(segment)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return segments
|
||||||
|
|
||||||
|
def _create_dataset_process_rule(self, db_session_with_containers, dataset, fake=None):
|
||||||
|
"""
|
||||||
|
Helper method to create a dataset process rule.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_session_with_containers: Database session from testcontainers infrastructure
|
||||||
|
dataset: The dataset for the process rule
|
||||||
|
fake: Faker instance for generating test data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DatasetProcessRule: Created process rule instance
|
||||||
|
"""
|
||||||
|
fake = fake or Faker()
|
||||||
|
process_rule = DatasetProcessRule()
|
||||||
|
process_rule.id = fake.uuid4()
|
||||||
|
process_rule.tenant_id = dataset.tenant_id
|
||||||
|
process_rule.dataset_id = dataset.id
|
||||||
|
process_rule.mode = "automatic"
|
||||||
|
process_rule.rules = (
|
||||||
|
"{"
|
||||||
|
'"mode": "automatic", '
|
||||||
|
'"rules": {'
|
||||||
|
'"pre_processing_rules": [], "segmentation": '
|
||||||
|
'{"separator": "\\n\\n", "max_tokens": 1000, "chunk_overlap": 50}}'
|
||||||
|
"}"
|
||||||
|
)
|
||||||
|
process_rule.created_by = dataset.created_by
|
||||||
|
process_rule.updated_by = dataset.updated_by
|
||||||
|
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.add(process_rule)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return process_rule
|
||||||
|
|
||||||
|
def test_disable_segments_success(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test successful disabling of segments from index.
|
||||||
|
|
||||||
|
This test verifies that the task can correctly disable segments from the index
|
||||||
|
when all conditions are met, including proper index cleanup and database state updates.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 3, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock the index processor to avoid external dependencies
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
|
||||||
|
# Verify index processor was called correctly
|
||||||
|
mock_factory.assert_called_once_with(document.doc_form)
|
||||||
|
mock_processor.clean.assert_called_once()
|
||||||
|
|
||||||
|
# Verify the call arguments (checking by attributes rather than object identity)
|
||||||
|
call_args = mock_processor.clean.call_args
|
||||||
|
assert call_args[0][0].id == dataset.id # First argument should be the dataset
|
||||||
|
assert call_args[0][1] == [
|
||||||
|
segment.index_node_id for segment in segments
|
||||||
|
] # Second argument should be node IDs
|
||||||
|
assert call_args[1]["with_keywords"] is True
|
||||||
|
assert call_args[1]["delete_child_chunks"] is False
|
||||||
|
|
||||||
|
# Verify Redis cache cleanup was called for each segment
|
||||||
|
assert mock_redis.delete.call_count == len(segments)
|
||||||
|
for segment in segments:
|
||||||
|
expected_key = f"segment_{segment.id}_indexing"
|
||||||
|
mock_redis.delete.assert_any_call(expected_key)
|
||||||
|
|
||||||
|
def test_disable_segments_dataset_not_found(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when dataset is not found.
|
||||||
|
|
||||||
|
This test ensures that the task correctly handles cases where the specified
|
||||||
|
dataset doesn't exist, logging appropriate messages and returning early.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
non_existent_dataset_id = fake.uuid4()
|
||||||
|
non_existent_document_id = fake.uuid4()
|
||||||
|
segment_ids = [fake.uuid4()]
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, non_existent_dataset_id, non_existent_document_id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Redis should not be called when dataset is not found
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
def test_disable_segments_document_not_found(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when document is not found.
|
||||||
|
|
||||||
|
This test ensures that the task correctly handles cases where the specified
|
||||||
|
document doesn't exist, logging appropriate messages and returning early.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
non_existent_document_id = fake.uuid4()
|
||||||
|
segment_ids = [fake.uuid4()]
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, non_existent_document_id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Redis should not be called when document is not found
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
def test_disable_segments_document_invalid_status(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when document has invalid status for disabling.
|
||||||
|
|
||||||
|
This test ensures that the task correctly handles cases where the document
|
||||||
|
is not enabled, archived, or not completed, preventing invalid operations.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 2, fake)
|
||||||
|
|
||||||
|
# Test case 1: Document not enabled
|
||||||
|
document.enabled = False
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Redis should not be called when document status is invalid
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
# Test case 2: Document archived
|
||||||
|
document.enabled = True
|
||||||
|
document.archived = True
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
# Test case 3: Document indexing not completed
|
||||||
|
document.enabled = True
|
||||||
|
document.archived = False
|
||||||
|
document.indexing_status = "indexing"
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
def test_disable_segments_no_segments_found(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when no segments are found for the given IDs.
|
||||||
|
|
||||||
|
This test ensures that the task correctly handles cases where the specified
|
||||||
|
segment IDs don't exist or don't match the dataset/document criteria.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
# Use non-existent segment IDs
|
||||||
|
non_existent_segment_ids = [fake.uuid4() for _ in range(3)]
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(non_existent_segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Redis should not be called when no segments are found
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
def test_disable_segments_index_processor_error(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when index processor encounters an error.
|
||||||
|
|
||||||
|
This test verifies that the task correctly handles index processor errors
|
||||||
|
by rolling back segment states and ensuring proper cleanup.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 2, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock the index processor to raise an exception
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_processor.clean.side_effect = Exception("Index processor error")
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
|
||||||
|
# Verify segments were rolled back to enabled state
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.refresh(segments[0])
|
||||||
|
db.session.refresh(segments[1])
|
||||||
|
|
||||||
|
# Check that segments are re-enabled after error
|
||||||
|
updated_segments = db.session.query(DocumentSegment).where(DocumentSegment.id.in_(segment_ids)).all()
|
||||||
|
|
||||||
|
for segment in updated_segments:
|
||||||
|
assert segment.enabled is True
|
||||||
|
assert segment.disabled_at is None
|
||||||
|
assert segment.disabled_by is None
|
||||||
|
|
||||||
|
# Verify Redis cache cleanup was still called
|
||||||
|
assert mock_redis.delete.call_count == len(segments)
|
||||||
|
|
||||||
|
def test_disable_segments_with_different_doc_forms(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test disabling segments with different document forms.
|
||||||
|
|
||||||
|
This test verifies that the task correctly handles different document forms
|
||||||
|
(paragraph, qa, parent_child) and initializes the appropriate index processor.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 2, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Test different document forms
|
||||||
|
doc_forms = ["text_model", "qa_model", "hierarchical_model"]
|
||||||
|
|
||||||
|
for doc_form in doc_forms:
|
||||||
|
# Update document form
|
||||||
|
document.doc_form = doc_form
|
||||||
|
from extensions.ext_database import db
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Mock the index processor factory
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
mock_factory.assert_called_with(doc_form)
|
||||||
|
|
||||||
|
def test_disable_segments_performance_timing(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test that the task properly measures and logs performance timing.
|
||||||
|
|
||||||
|
This test verifies that the task correctly measures execution time
|
||||||
|
and logs performance metrics for monitoring purposes.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 3, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock the index processor
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Mock time.perf_counter to control timing
|
||||||
|
with patch("tasks.disable_segments_from_index_task.time.perf_counter") as mock_perf_counter:
|
||||||
|
mock_perf_counter.side_effect = [1000.0, 1000.5] # 0.5 seconds execution time
|
||||||
|
|
||||||
|
# Mock logger to capture log messages
|
||||||
|
with patch("tasks.disable_segments_from_index_task.logger") as mock_logger:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
|
||||||
|
# Verify performance logging
|
||||||
|
mock_logger.info.assert_called()
|
||||||
|
log_calls = [call[0][0] for call in mock_logger.info.call_args_list]
|
||||||
|
performance_log = next((call for call in log_calls if "latency" in call), None)
|
||||||
|
assert performance_log is not None
|
||||||
|
assert "0.5" in performance_log # Should log the execution time
|
||||||
|
|
||||||
|
def test_disable_segments_redis_cache_cleanup(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test that Redis cache is properly cleaned up for all segments.
|
||||||
|
|
||||||
|
This test verifies that the task correctly removes indexing cache entries
|
||||||
|
from Redis for all processed segments, preventing stale cache issues.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 5, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock the index processor
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client to track delete calls
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
|
||||||
|
# Verify Redis delete was called for each segment
|
||||||
|
assert mock_redis.delete.call_count == len(segments)
|
||||||
|
|
||||||
|
# Verify correct cache keys were used
|
||||||
|
expected_keys = [f"segment_{segment.id}_indexing" for segment in segments]
|
||||||
|
actual_calls = [call[0][0] for call in mock_redis.delete.call_args_list]
|
||||||
|
|
||||||
|
for expected_key in expected_keys:
|
||||||
|
assert expected_key in actual_calls
|
||||||
|
|
||||||
|
def test_disable_segments_database_session_cleanup(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test that database session is properly closed after task execution.
|
||||||
|
|
||||||
|
This test verifies that the task correctly manages database sessions
|
||||||
|
and ensures proper cleanup to prevent connection leaks.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 2, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
segment_ids = [segment.id for segment in segments]
|
||||||
|
|
||||||
|
# Mock the index processor
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Mock db.session.close to verify it's called
|
||||||
|
with patch("tasks.disable_segments_from_index_task.db.session.close") as mock_close:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Verify session was closed
|
||||||
|
mock_close.assert_called()
|
||||||
|
|
||||||
|
def test_disable_segments_empty_segment_ids(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when empty segment IDs list is provided.
|
||||||
|
|
||||||
|
This test ensures that the task correctly handles edge cases where
|
||||||
|
an empty list of segment IDs is provided.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
empty_segment_ids = []
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(empty_segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
# Redis should not be called when no segments are provided
|
||||||
|
mock_redis.delete.assert_not_called()
|
||||||
|
|
||||||
|
def test_disable_segments_mixed_valid_invalid_ids(self, db_session_with_containers):
|
||||||
|
"""
|
||||||
|
Test handling when some segment IDs are valid and others are invalid.
|
||||||
|
|
||||||
|
This test verifies that the task correctly processes only the valid
|
||||||
|
segment IDs and ignores invalid ones.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
fake = Faker()
|
||||||
|
account = self._create_test_account(db_session_with_containers, fake)
|
||||||
|
dataset = self._create_test_dataset(db_session_with_containers, account, fake)
|
||||||
|
document = self._create_test_document(db_session_with_containers, dataset, account, fake)
|
||||||
|
segments = self._create_test_segments(db_session_with_containers, document, dataset, account, 2, fake)
|
||||||
|
self._create_dataset_process_rule(db_session_with_containers, dataset, fake)
|
||||||
|
|
||||||
|
# Mix valid and invalid segment IDs
|
||||||
|
valid_segment_ids = [segment.id for segment in segments]
|
||||||
|
invalid_segment_ids = [fake.uuid4() for _ in range(2)]
|
||||||
|
mixed_segment_ids = valid_segment_ids + invalid_segment_ids
|
||||||
|
|
||||||
|
# Mock the index processor
|
||||||
|
with patch("tasks.disable_segments_from_index_task.IndexProcessorFactory") as mock_factory:
|
||||||
|
mock_processor = MagicMock()
|
||||||
|
mock_factory.return_value.init_index_processor.return_value = mock_processor
|
||||||
|
|
||||||
|
# Mock Redis client
|
||||||
|
with patch("tasks.disable_segments_from_index_task.redis_client") as mock_redis:
|
||||||
|
mock_redis.delete.return_value = True
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = disable_segments_from_index_task(mixed_segment_ids, dataset.id, document.id)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is None # Task should complete without returning a value
|
||||||
|
|
||||||
|
# Verify index processor was called with only valid segment node IDs
|
||||||
|
expected_node_ids = [segment.index_node_id for segment in segments]
|
||||||
|
mock_processor.clean.assert_called_once()
|
||||||
|
|
||||||
|
# Verify the call arguments
|
||||||
|
call_args = mock_processor.clean.call_args
|
||||||
|
assert call_args[0][0].id == dataset.id # First argument should be the dataset
|
||||||
|
assert call_args[0][1] == expected_node_ids # Second argument should be node IDs
|
||||||
|
assert call_args[1]["with_keywords"] is True
|
||||||
|
assert call_args[1]["delete_child_chunks"] is False
|
||||||
|
|
||||||
|
# Verify Redis cleanup was called only for valid segments
|
||||||
|
assert mock_redis.delete.call_count == len(segments)
|
||||||
8
api/uv.lock
generated
8
api/uv.lock
generated
@ -1567,7 +1567,7 @@ requires-dist = [
|
|||||||
{ name = "sseclient-py", specifier = "~=1.8.0" },
|
{ name = "sseclient-py", specifier = "~=1.8.0" },
|
||||||
{ name = "starlette", specifier = "==0.47.2" },
|
{ name = "starlette", specifier = "==0.47.2" },
|
||||||
{ name = "tiktoken", specifier = "~=0.9.0" },
|
{ name = "tiktoken", specifier = "~=0.9.0" },
|
||||||
{ name = "transformers", specifier = "~=4.53.0" },
|
{ name = "transformers", specifier = "~=4.56.1" },
|
||||||
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
|
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
|
||||||
{ name = "weave", specifier = "~=0.51.0" },
|
{ name = "weave", specifier = "~=0.51.0" },
|
||||||
{ name = "webvtt-py", specifier = "~=0.5.1" },
|
{ name = "webvtt-py", specifier = "~=0.5.1" },
|
||||||
@ -6286,7 +6286,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "transformers"
|
name = "transformers"
|
||||||
version = "4.53.3"
|
version = "4.56.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
@ -6300,9 +6300,9 @@ dependencies = [
|
|||||||
{ name = "tokenizers" },
|
{ name = "tokenizers" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/5c/49182918b58eaa0b4c954fd0e37c79fc299e5643e69d70089d0b0eb0cd9b/transformers-4.53.3.tar.gz", hash = "sha256:b2eda1a261de79b78b97f7888fe2005fc0c3fabf5dad33d52cc02983f9f675d8", size = 9197478, upload-time = "2025-07-22T07:30:51.51Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/89/21/dc88ef3da1e49af07ed69386a11047a31dcf1aaf4ded3bc4b173fbf94116/transformers-4.56.1.tar.gz", hash = "sha256:0d88b1089a563996fc5f2c34502f10516cad3ea1aa89f179f522b54c8311fe74", size = 9855473, upload-time = "2025-09-04T20:47:13.14Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/b1/d7520cc5cb69c825599042eb3a7c986fa9baa8a8d2dea9acd78e152c81e2/transformers-4.53.3-py3-none-any.whl", hash = "sha256:5aba81c92095806b6baf12df35d756cf23b66c356975fb2a7fa9e536138d7c75", size = 10826382, upload-time = "2025-07-22T07:30:48.458Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/7c/283c3dd35e00e22a7803a0b2a65251347b745474a82399be058bde1c9f15/transformers-4.56.1-py3-none-any.whl", hash = "sha256:1697af6addfb6ddbce9618b763f4b52d5a756f6da4899ffd1b4febf58b779248", size = 11608197, upload-time = "2025-09-04T20:47:04.895Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from typing import Literal
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ class DifyClient:
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
|
|
||||||
def _send_request(self, method, endpoint, json=None, params=None, stream=False):
|
def _send_request(self, method: str, endpoint: str, json: dict | None = None, params: dict | None = None, stream: bool = False):
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {self.api_key}",
|
"Authorization": f"Bearer {self.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -31,15 +31,15 @@ class DifyClient:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def message_feedback(self, message_id, rating, user):
|
def message_feedback(self, message_id: str, rating: Literal["like", "dislike"], user: str):
|
||||||
data = {"rating": rating, "user": user}
|
data = {"rating": rating, "user": user}
|
||||||
return self._send_request("POST", f"/messages/{message_id}/feedbacks", data)
|
return self._send_request("POST", f"/messages/{message_id}/feedbacks", data)
|
||||||
|
|
||||||
def get_application_parameters(self, user):
|
def get_application_parameters(self, user: str):
|
||||||
params = {"user": user}
|
params = {"user": user}
|
||||||
return self._send_request("GET", "/parameters", params=params)
|
return self._send_request("GET", "/parameters", params=params)
|
||||||
|
|
||||||
def file_upload(self, user, files):
|
def file_upload(self, user: str, files: dict):
|
||||||
data = {"user": user}
|
data = {"user": user}
|
||||||
return self._send_request_with_files(
|
return self._send_request_with_files(
|
||||||
"POST", "/files/upload", data=data, files=files
|
"POST", "/files/upload", data=data, files=files
|
||||||
@ -49,13 +49,13 @@ class DifyClient:
|
|||||||
data = {"text": text, "user": user, "streaming": streaming}
|
data = {"text": text, "user": user, "streaming": streaming}
|
||||||
return self._send_request("POST", "/text-to-audio", json=data)
|
return self._send_request("POST", "/text-to-audio", json=data)
|
||||||
|
|
||||||
def get_meta(self, user):
|
def get_meta(self, user: str):
|
||||||
params = {"user": user}
|
params = {"user": user}
|
||||||
return self._send_request("GET", "/meta", params=params)
|
return self._send_request("GET", "/meta", params=params)
|
||||||
|
|
||||||
|
|
||||||
class CompletionClient(DifyClient):
|
class CompletionClient(DifyClient):
|
||||||
def create_completion_message(self, inputs, response_mode, user, files=None):
|
def create_completion_message(self, inputs: dict, response_mode: Literal["blocking", "streaming"], user: str, files: dict | None = None):
|
||||||
data = {
|
data = {
|
||||||
"inputs": inputs,
|
"inputs": inputs,
|
||||||
"response_mode": response_mode,
|
"response_mode": response_mode,
|
||||||
@ -76,7 +76,7 @@ class ChatClient(DifyClient):
|
|||||||
inputs: dict,
|
inputs: dict,
|
||||||
query: str,
|
query: str,
|
||||||
user: str,
|
user: str,
|
||||||
response_mode: str = "blocking",
|
response_mode: Literal["blocking", "streaming"] = "blocking",
|
||||||
conversation_id: str | None = None,
|
conversation_id: str | None = None,
|
||||||
files: dict | None = None,
|
files: dict | None = None,
|
||||||
):
|
):
|
||||||
@ -156,7 +156,7 @@ class ChatClient(DifyClient):
|
|||||||
|
|
||||||
class WorkflowClient(DifyClient):
|
class WorkflowClient(DifyClient):
|
||||||
def run(
|
def run(
|
||||||
self, inputs: dict, response_mode: str = "streaming", user: str = "abc-123"
|
self, inputs: dict, response_mode: Literal["blocking", "streaming"] = "streaming", user: str = "abc-123"
|
||||||
):
|
):
|
||||||
data = {"inputs": inputs, "response_mode": response_mode, "user": user}
|
data = {"inputs": inputs, "response_mode": response_mode, "user": user}
|
||||||
return self._send_request("POST", "/workflows/run", data)
|
return self._send_request("POST", "/workflows/run", data)
|
||||||
@ -172,7 +172,7 @@ class WorkflowClient(DifyClient):
|
|||||||
class KnowledgeBaseClient(DifyClient):
|
class KnowledgeBaseClient(DifyClient):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_key,
|
api_key: str,
|
||||||
base_url: str = "https://api.dify.ai/v1",
|
base_url: str = "https://api.dify.ai/v1",
|
||||||
dataset_id: str | None = None,
|
dataset_id: str | None = None,
|
||||||
):
|
):
|
||||||
@ -241,7 +241,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
return self._send_request("POST", url, json=data, **kwargs)
|
return self._send_request("POST", url, json=data, **kwargs)
|
||||||
|
|
||||||
def update_document_by_text(
|
def update_document_by_text(
|
||||||
self, document_id, name, text, extra_params: dict | None = None, **kwargs
|
self, document_id: str, name: str, text: str, extra_params: dict | None = None, **kwargs
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update a document by text.
|
Update a document by text.
|
||||||
@ -278,7 +278,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
return self._send_request("POST", url, json=data, **kwargs)
|
return self._send_request("POST", url, json=data, **kwargs)
|
||||||
|
|
||||||
def create_document_by_file(
|
def create_document_by_file(
|
||||||
self, file_path, original_document_id=None, extra_params: dict | None = None
|
self, file_path: str, original_document_id: str | None = None, extra_params: dict | None = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Create a document by file.
|
Create a document by file.
|
||||||
@ -320,7 +320,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_document_by_file(
|
def update_document_by_file(
|
||||||
self, document_id, file_path, extra_params: dict | None = None
|
self, document_id: str, file_path: str, extra_params: dict | None = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update a document by file.
|
Update a document by file.
|
||||||
@ -377,7 +377,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
url = f"/datasets/{self._get_dataset_id()}"
|
url = f"/datasets/{self._get_dataset_id()}"
|
||||||
return self._send_request("DELETE", url)
|
return self._send_request("DELETE", url)
|
||||||
|
|
||||||
def delete_document(self, document_id):
|
def delete_document(self, document_id: str):
|
||||||
"""
|
"""
|
||||||
Delete a document.
|
Delete a document.
|
||||||
|
|
||||||
@ -409,7 +409,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
url = f"/datasets/{self._get_dataset_id()}/documents"
|
url = f"/datasets/{self._get_dataset_id()}/documents"
|
||||||
return self._send_request("GET", url, params=params, **kwargs)
|
return self._send_request("GET", url, params=params, **kwargs)
|
||||||
|
|
||||||
def add_segments(self, document_id, segments, **kwargs):
|
def add_segments(self, document_id: str, segments: list[dict], **kwargs):
|
||||||
"""
|
"""
|
||||||
Add segments to a document.
|
Add segments to a document.
|
||||||
|
|
||||||
@ -423,7 +423,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
|
|
||||||
def query_segments(
|
def query_segments(
|
||||||
self,
|
self,
|
||||||
document_id,
|
document_id: str,
|
||||||
keyword: str | None = None,
|
keyword: str | None = None,
|
||||||
status: str | None = None,
|
status: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@ -445,7 +445,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
params.update(kwargs["params"])
|
params.update(kwargs["params"])
|
||||||
return self._send_request("GET", url, params=params, **kwargs)
|
return self._send_request("GET", url, params=params, **kwargs)
|
||||||
|
|
||||||
def delete_document_segment(self, document_id, segment_id):
|
def delete_document_segment(self, document_id: str, segment_id: str):
|
||||||
"""
|
"""
|
||||||
Delete a segment from a document.
|
Delete a segment from a document.
|
||||||
|
|
||||||
@ -456,7 +456,7 @@ class KnowledgeBaseClient(DifyClient):
|
|||||||
url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
|
url = f"/datasets/{self._get_dataset_id()}/documents/{document_id}/segments/{segment_id}"
|
||||||
return self._send_request("DELETE", url)
|
return self._send_request("DELETE", url)
|
||||||
|
|
||||||
def update_document_segment(self, document_id, segment_id, segment_data, **kwargs):
|
def update_document_segment(self, document_id: str, segment_id: str, segment_data: dict, **kwargs):
|
||||||
"""
|
"""
|
||||||
Update a segment in a document.
|
Update a segment in a document.
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export type IGetAutomaticResProps = {
|
|||||||
onFinished: (res: GenRes) => void
|
onFinished: (res: GenRes) => void
|
||||||
flowId?: string
|
flowId?: string
|
||||||
nodeId?: string
|
nodeId?: string
|
||||||
|
editorId?: string
|
||||||
currentPrompt?: string
|
currentPrompt?: string
|
||||||
isBasicMode?: boolean
|
isBasicMode?: boolean
|
||||||
}
|
}
|
||||||
@ -76,6 +77,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
flowId,
|
flowId,
|
||||||
nodeId,
|
nodeId,
|
||||||
|
editorId,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
isBasicMode,
|
isBasicMode,
|
||||||
onFinished,
|
onFinished,
|
||||||
@ -132,7 +134,8 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}`}`)
|
// eslint-disable-next-line sonarjs/no-nested-template-literals, sonarjs/no-nested-conditional
|
||||||
|
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}${editorId ? `-${editorId}` : ''}`}`)
|
||||||
const instruction = instructionFromSessionStorage || ''
|
const instruction = instructionFromSessionStorage || ''
|
||||||
const [ideaOutput, setIdeaOutput] = useState<string>('')
|
const [ideaOutput, setIdeaOutput] = useState<string>('')
|
||||||
|
|
||||||
@ -166,7 +169,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
|
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
|
||||||
const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
|
const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}${editorId ? `-${editorId}` : ''}`}`
|
||||||
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
|
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
|
||||||
storageKey,
|
storageKey,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -284,6 +284,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
value={cmdVal}
|
value={cmdVal}
|
||||||
onValueChange={setCmdVal}
|
onValueChange={setCmdVal}
|
||||||
disablePointerSelection
|
disablePointerSelection
|
||||||
|
loop
|
||||||
>
|
>
|
||||||
<div className='flex items-center gap-3 border-b border-divider-subtle bg-components-panel-bg-blur px-4 py-3'>
|
<div className='flex items-center gap-3 border-b border-divider-subtle bg-components-panel-bg-blur px-4 py-3'>
|
||||||
<RiSearchLine className='h-4 w-4 text-text-quaternary' />
|
<RiSearchLine className='h-4 w-4 text-text-quaternary' />
|
||||||
|
|||||||
@ -42,6 +42,7 @@ type Props = {
|
|||||||
headerClassName?: string
|
headerClassName?: string
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
nodeId?: string
|
nodeId?: string
|
||||||
|
editorId?: string
|
||||||
title: string | React.JSX.Element
|
title: string | React.JSX.Element
|
||||||
value: string
|
value: string
|
||||||
onChange: (value: string) => void
|
onChange: (value: string) => void
|
||||||
@ -85,6 +86,7 @@ const Editor: FC<Props> = ({
|
|||||||
headerClassName,
|
headerClassName,
|
||||||
instanceId,
|
instanceId,
|
||||||
nodeId,
|
nodeId,
|
||||||
|
editorId,
|
||||||
title,
|
title,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -163,6 +165,7 @@ const Editor: FC<Props> = ({
|
|||||||
{isSupportPromptGenerator && (
|
{isSupportPromptGenerator && (
|
||||||
<PromptGeneratorBtn
|
<PromptGeneratorBtn
|
||||||
nodeId={nodeId!}
|
nodeId={nodeId!}
|
||||||
|
editorId={editorId}
|
||||||
className='ml-[5px]'
|
className='ml-[5px]'
|
||||||
onGenerated={onGenerated}
|
onGenerated={onGenerated}
|
||||||
modelConfig={modelConfig}
|
modelConfig={modelConfig}
|
||||||
|
|||||||
@ -136,7 +136,8 @@ const ConfigPromptItem: FC<Props> = ({
|
|||||||
nodesOutputVars={availableVars}
|
nodesOutputVars={availableVars}
|
||||||
availableNodes={availableNodes}
|
availableNodes={availableNodes}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
isSupportPromptGenerator={payload.role === PromptRole.system}
|
editorId={id}
|
||||||
|
isSupportPromptGenerator
|
||||||
onGenerated={handleGenerated}
|
onGenerated={handleGenerated}
|
||||||
modelConfig={modelConfig}
|
modelConfig={modelConfig}
|
||||||
isSupportJinja
|
isSupportJinja
|
||||||
|
|||||||
@ -16,6 +16,7 @@ type Props = {
|
|||||||
onGenerated?: (prompt: string) => void
|
onGenerated?: (prompt: string) => void
|
||||||
modelConfig?: ModelConfig
|
modelConfig?: ModelConfig
|
||||||
nodeId: string
|
nodeId: string
|
||||||
|
editorId?: string
|
||||||
currentPrompt?: string
|
currentPrompt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ const PromptGeneratorBtn: FC<Props> = ({
|
|||||||
className,
|
className,
|
||||||
onGenerated,
|
onGenerated,
|
||||||
nodeId,
|
nodeId,
|
||||||
|
editorId,
|
||||||
currentPrompt,
|
currentPrompt,
|
||||||
}) => {
|
}) => {
|
||||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||||
@ -46,6 +48,7 @@ const PromptGeneratorBtn: FC<Props> = ({
|
|||||||
onFinished={handleAutomaticRes}
|
onFinished={handleAutomaticRes}
|
||||||
flowId={configsMap?.flowId || ''}
|
flowId={configsMap?.flowId || ''}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
|
editorId={editorId}
|
||||||
currentPrompt={currentPrompt}
|
currentPrompt={currentPrompt}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ const ValueContent = ({
|
|||||||
setValue(currentVar.value)
|
setValue(currentVar.value)
|
||||||
}
|
}
|
||||||
if (showJSONEditor)
|
if (showJSONEditor)
|
||||||
setJson(currentVar.value ? JSON.stringify(currentVar.value, null, 2) : '')
|
setJson(currentVar.value != null ? JSON.stringify(currentVar.value, null, 2) : '')
|
||||||
|
|
||||||
if (showFileEditor)
|
if (showFileEditor)
|
||||||
setFileValue(formatFileValue(currentVar))
|
setFileValue(formatFileValue(currentVar))
|
||||||
|
|||||||
@ -318,6 +318,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Keine übereinstimmenden Befehle gefunden',
|
noMatchingCommands: 'Keine übereinstimmenden Befehle gefunden',
|
||||||
tryDifferentSearch: 'Versuchen Sie es mit einem anderen Suchbegriff',
|
tryDifferentSearch: 'Versuchen Sie es mit einem anderen Suchbegriff',
|
||||||
slashHint: 'Geben Sie / ein, um alle verfügbaren Befehle anzuzeigen.',
|
slashHint: 'Geben Sie / ein, um alle verfügbaren Befehle anzuzeigen.',
|
||||||
|
tips: 'Drücken Sie ↑↓, um zu navigieren',
|
||||||
|
pressEscToClose: 'Drücken Sie ESC, um zu schließen',
|
||||||
|
startTyping: 'Beginnen Sie mit der Eingabe, um zu suchen',
|
||||||
|
selectToNavigate: 'Auswählen, um zu navigieren',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Prueba con un término de búsqueda diferente',
|
tryDifferentSearch: 'Prueba con un término de búsqueda diferente',
|
||||||
noMatchingCommands: 'No se encontraron comandos coincidentes',
|
noMatchingCommands: 'No se encontraron comandos coincidentes',
|
||||||
slashHint: 'Escribe / para ver todos los comandos disponibles',
|
slashHint: 'Escribe / para ver todos los comandos disponibles',
|
||||||
|
selectToNavigate: 'Seleccionar para navegar',
|
||||||
|
pressEscToClose: 'Presiona ESC para cerrar',
|
||||||
|
startTyping: 'Empieza a escribir para buscar',
|
||||||
|
tips: 'Presiona ↑↓ para navegar',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'هیچ دستوری منطبق یافت نشد',
|
noMatchingCommands: 'هیچ دستوری منطبق یافت نشد',
|
||||||
tryDifferentSearch: 'عبارت جستجوی دیگری را امتحان کنید',
|
tryDifferentSearch: 'عبارت جستجوی دیگری را امتحان کنید',
|
||||||
slashHint: 'برای مشاهده تمام دستورات موجود / را تایپ کنید',
|
slashHint: 'برای مشاهده تمام دستورات موجود / را تایپ کنید',
|
||||||
|
startTyping: 'برای جستجو شروع به تایپ کنید',
|
||||||
|
selectToNavigate: 'انتخاب کنید تا برای حرکت',
|
||||||
|
pressEscToClose: 'برای بستن ESC را فشار دهید',
|
||||||
|
tips: 'برای حرکت به بالا و پایین کلیدهای ↑ و ↓ را فشار دهید',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Aucune commande correspondante n’a été trouvée',
|
noMatchingCommands: 'Aucune commande correspondante n’a été trouvée',
|
||||||
tryDifferentSearch: 'Essayez un autre terme de recherche',
|
tryDifferentSearch: 'Essayez un autre terme de recherche',
|
||||||
slashHint: 'Tapez / pour voir toutes les commandes disponibles',
|
slashHint: 'Tapez / pour voir toutes les commandes disponibles',
|
||||||
|
pressEscToClose: 'Appuyez sur Échap pour fermer',
|
||||||
|
tips: 'Appuyez sur ↑↓ pour naviguer',
|
||||||
|
startTyping: 'Commencez à taper pour rechercher',
|
||||||
|
selectToNavigate: 'Sélectionnez pour naviguer',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'एक अलग खोज शब्द आजमाएँ',
|
tryDifferentSearch: 'एक अलग खोज शब्द आजमाएँ',
|
||||||
noMatchingCommands: 'कोई मिलती-जुलती कमांड्स नहीं मिलीं',
|
noMatchingCommands: 'कोई मिलती-जुलती कमांड्स नहीं मिलीं',
|
||||||
slashHint: 'सभी उपलब्ध कमांड देखने के लिए टाइप करें /',
|
slashHint: 'सभी उपलब्ध कमांड देखने के लिए टाइप करें /',
|
||||||
|
pressEscToClose: 'बंद करने के लिए ESC दबाएं',
|
||||||
|
startTyping: 'खोजने के लिए टाइप करना शुरू करें',
|
||||||
|
selectToNavigate: 'नेविगेट करने के लिए चुनें',
|
||||||
|
tips: 'नेविगेट करने के लिए ↑↓ दबाएँ',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -263,6 +263,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Coba istilah penelusuran lain',
|
tryDifferentSearch: 'Coba istilah penelusuran lain',
|
||||||
noMatchingCommands: 'Tidak ada perintah yang cocok ditemukan',
|
noMatchingCommands: 'Tidak ada perintah yang cocok ditemukan',
|
||||||
searchFailed: 'Pencarian gagal',
|
searchFailed: 'Pencarian gagal',
|
||||||
|
tips: 'Tekan ↑↓ untuk menavigasi',
|
||||||
|
startTyping: 'Mulai mengetik untuk mencari',
|
||||||
|
selectToNavigate: 'Pilih untuk menavigasi',
|
||||||
|
pressEscToClose: 'Tekan ESC untuk menutup',
|
||||||
},
|
},
|
||||||
createApp: 'BUAT APLIKASI',
|
createApp: 'BUAT APLIKASI',
|
||||||
accessControl: 'Kontrol Akses Aplikasi Web',
|
accessControl: 'Kontrol Akses Aplikasi Web',
|
||||||
|
|||||||
@ -322,6 +322,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Prova un termine di ricerca diverso',
|
tryDifferentSearch: 'Prova un termine di ricerca diverso',
|
||||||
noMatchingCommands: 'Nessun comando corrispondente trovato',
|
noMatchingCommands: 'Nessun comando corrispondente trovato',
|
||||||
slashHint: 'Digita / per vedere tutti i comandi disponibili',
|
slashHint: 'Digita / per vedere tutti i comandi disponibili',
|
||||||
|
selectToNavigate: 'Seleziona per navigare',
|
||||||
|
startTyping: 'Inizia a digitare per cercare',
|
||||||
|
tips: 'Premi ↑↓ per navigare',
|
||||||
|
pressEscToClose: 'Premi ESC per chiudere',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -336,6 +336,10 @@ const translation = {
|
|||||||
tryDifferentSearch: '다른 검색어 사용해 보기',
|
tryDifferentSearch: '다른 검색어 사용해 보기',
|
||||||
noMatchingCommands: '일치하는 명령을 찾을 수 없습니다.',
|
noMatchingCommands: '일치하는 명령을 찾을 수 없습니다.',
|
||||||
slashHint: '모든 사용 가능한 명령을 보려면 /를 입력하세요.',
|
slashHint: '모든 사용 가능한 명령을 보려면 /를 입력하세요.',
|
||||||
|
tips: '↑↓ 키를 눌러 탐색하세요',
|
||||||
|
pressEscToClose: 'ESC를 눌러 닫기',
|
||||||
|
selectToNavigate: '선택하여 탐색하기',
|
||||||
|
startTyping: '검색하려면 타이핑을 시작하세요',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -317,6 +317,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Nie znaleziono pasujących poleceń',
|
noMatchingCommands: 'Nie znaleziono pasujących poleceń',
|
||||||
tryDifferentSearch: 'Spróbuj użyć innego hasła',
|
tryDifferentSearch: 'Spróbuj użyć innego hasła',
|
||||||
slashHint: 'Wpisz / aby zobaczyć wszystkie dostępne polecenia',
|
slashHint: 'Wpisz / aby zobaczyć wszystkie dostępne polecenia',
|
||||||
|
selectToNavigate: 'Wybierz, aby nawigować',
|
||||||
|
tips: 'Naciśnij ↑↓, aby nawigować',
|
||||||
|
startTyping: 'Zacznij pisać, aby wyszukać',
|
||||||
|
pressEscToClose: 'Naciśnij ESC, aby zamknąć',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Nenhum comando correspondente encontrado',
|
noMatchingCommands: 'Nenhum comando correspondente encontrado',
|
||||||
tryDifferentSearch: 'Tente um termo de pesquisa diferente',
|
tryDifferentSearch: 'Tente um termo de pesquisa diferente',
|
||||||
slashHint: 'Digite / para ver todos os comandos disponíveis',
|
slashHint: 'Digite / para ver todos os comandos disponíveis',
|
||||||
|
tips: 'Pressione ↑↓ para navegar',
|
||||||
|
selectToNavigate: 'Selecione para navegar',
|
||||||
|
pressEscToClose: 'Pressione ESC para fechar',
|
||||||
|
startTyping: 'Comece a digitar para pesquisar',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Nu s-au găsit comenzi potrivite',
|
noMatchingCommands: 'Nu s-au găsit comenzi potrivite',
|
||||||
tryDifferentSearch: 'Încercați un alt termen de căutare',
|
tryDifferentSearch: 'Încercați un alt termen de căutare',
|
||||||
slashHint: 'Tastați / pentru a vedea toate comenzile disponibile',
|
slashHint: 'Tastați / pentru a vedea toate comenzile disponibile',
|
||||||
|
selectToNavigate: 'Selectați pentru a naviga',
|
||||||
|
startTyping: 'Începeți să tastați pentru a căuta',
|
||||||
|
tips: 'Apăsați ↑↓ pentru a naviga',
|
||||||
|
pressEscToClose: 'Apăsați ESC pentru a închide',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Соответствующие команды не найдены',
|
noMatchingCommands: 'Соответствующие команды не найдены',
|
||||||
tryDifferentSearch: 'Попробуйте использовать другой поисковый запрос',
|
tryDifferentSearch: 'Попробуйте использовать другой поисковый запрос',
|
||||||
slashHint: 'Введите / чтобы увидеть все доступные команды',
|
slashHint: 'Введите / чтобы увидеть все доступные команды',
|
||||||
|
startTyping: 'Начните вводить для поиска',
|
||||||
|
tips: 'Нажмите ↑↓ для навигации',
|
||||||
|
selectToNavigate: 'Выберите для навигации',
|
||||||
|
pressEscToClose: 'Нажмите ESC для закрытия',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Poskusite uporabiti drug iskalni izraz',
|
tryDifferentSearch: 'Poskusite uporabiti drug iskalni izraz',
|
||||||
noMatchingCommands: 'Ujemajoči se ukazi niso našli',
|
noMatchingCommands: 'Ujemajoči se ukazi niso našli',
|
||||||
slashHint: 'Vnesite / za ogled vseh razpoložljivih ukazov',
|
slashHint: 'Vnesite / za ogled vseh razpoložljivih ukazov',
|
||||||
|
startTyping: 'Začnite vnašati za iskanje',
|
||||||
|
pressEscToClose: 'Pritisnite ESC za zapiranje',
|
||||||
|
selectToNavigate: 'Izberite za navigacijo',
|
||||||
|
tips: 'Pritisnite ↑↓ za navigacijo',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -312,6 +312,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'ไม่พบคำสั่งที่ตรงกัน',
|
noMatchingCommands: 'ไม่พบคำสั่งที่ตรงกัน',
|
||||||
tryDifferentSearch: 'ลองใช้ข้อความค้นหาอื่น',
|
tryDifferentSearch: 'ลองใช้ข้อความค้นหาอื่น',
|
||||||
slashHint: 'พิมพ์ / เพื่อดูคำสั่งที่มีให้ทั้งหมด',
|
slashHint: 'พิมพ์ / เพื่อดูคำสั่งที่มีให้ทั้งหมด',
|
||||||
|
pressEscToClose: 'กด ESC เพื่อปิด',
|
||||||
|
selectToNavigate: 'เลือกเพื่อนำทาง',
|
||||||
|
startTyping: 'เริ่มพิมพ์เพื่อค้นหา',
|
||||||
|
tips: 'กด ↑↓ เพื่อเลื่อนดู',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -312,6 +312,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Farklı bir arama terimi deneyin',
|
tryDifferentSearch: 'Farklı bir arama terimi deneyin',
|
||||||
noMatchingCommands: 'Eşleşen komut bulunamadı',
|
noMatchingCommands: 'Eşleşen komut bulunamadı',
|
||||||
slashHint: 'Tüm mevcut komutları görmek için / yazın',
|
slashHint: 'Tüm mevcut komutları görmek için / yazın',
|
||||||
|
tips: 'Navigasyon için ↑↓ tuşlarına basın',
|
||||||
|
selectToNavigate: 'Gezinmek için seçin',
|
||||||
|
pressEscToClose: 'Kapatmak için ESC tuşuna basın',
|
||||||
|
startTyping: 'Arama yapmak için yazmaya başlayın',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
noMatchingCommands: 'Відповідних команд не знайдено',
|
noMatchingCommands: 'Відповідних команд не знайдено',
|
||||||
tryDifferentSearch: 'Спробуйте інший пошуковий термін',
|
tryDifferentSearch: 'Спробуйте інший пошуковий термін',
|
||||||
slashHint: 'Наберіть / , щоб побачити всі доступні команди',
|
slashHint: 'Наберіть / , щоб побачити всі доступні команди',
|
||||||
|
selectToNavigate: 'Виберіть, щоб перейти',
|
||||||
|
tips: 'Натисніть ↑↓ для навігації',
|
||||||
|
startTyping: 'Почніть вводити для пошуку',
|
||||||
|
pressEscToClose: 'Натисніть ESC, щоб закрити',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -316,6 +316,10 @@ const translation = {
|
|||||||
tryDifferentSearch: 'Thử một cụm từ tìm kiếm khác',
|
tryDifferentSearch: 'Thử một cụm từ tìm kiếm khác',
|
||||||
noMatchingCommands: 'Không tìm thấy lệnh phù hợp',
|
noMatchingCommands: 'Không tìm thấy lệnh phù hợp',
|
||||||
slashHint: 'Gõ / để xem tất cả các lệnh có sẵn',
|
slashHint: 'Gõ / để xem tất cả các lệnh có sẵn',
|
||||||
|
selectToNavigate: 'Chọn để điều hướng',
|
||||||
|
startTyping: 'Bắt đầu gõ để tìm kiếm',
|
||||||
|
pressEscToClose: 'Nhấn ESC để đóng',
|
||||||
|
tips: 'Nhấn ↑↓ để duyệt',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -315,6 +315,10 @@ const translation = {
|
|||||||
noMatchingCommands: '未找到匹配的命令',
|
noMatchingCommands: '未找到匹配的命令',
|
||||||
tryDifferentSearch: '嘗試其他搜尋字詞',
|
tryDifferentSearch: '嘗試其他搜尋字詞',
|
||||||
slashHint: '輸入 / 以查看所有可用的指令',
|
slashHint: '輸入 / 以查看所有可用的指令',
|
||||||
|
tips: '按 ↑ ↓ 鍵進行導航',
|
||||||
|
startTyping: '開始輸入以進行搜尋',
|
||||||
|
pressEscToClose: '按 ESC 鍵關閉',
|
||||||
|
selectToNavigate: '選擇以進行導航',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "dify-web",
|
"name": "dify-web",
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.15.1",
|
"packageManager": "pnpm@10.16.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=v22.11.0"
|
"node": ">=v22.11.0"
|
||||||
},
|
},
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"@lexical/list": "^0.30.0",
|
"@lexical/list": "^0.30.0",
|
||||||
"@lexical/react": "^0.30.0",
|
"@lexical/react": "^0.30.0",
|
||||||
"@lexical/selection": "^0.30.0",
|
"@lexical/selection": "^0.30.0",
|
||||||
"@lexical/text": "^0.30.0",
|
"@lexical/text": "^0.35.0",
|
||||||
"@lexical/utils": "^0.30.0",
|
"@lexical/utils": "^0.30.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@octokit/core": "^6.1.2",
|
"@octokit/core": "^6.1.2",
|
||||||
@ -139,7 +139,7 @@
|
|||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"scheduler": "^0.23.0",
|
"scheduler": "^0.26.0",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
@ -166,7 +166,7 @@
|
|||||||
"@happy-dom/jest-environment": "^17.4.4",
|
"@happy-dom/jest-environment": "^17.4.4",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@next/bundle-analyzer": "15.5.0",
|
"@next/bundle-analyzer": "15.5.3",
|
||||||
"@next/eslint-plugin-next": "15.5.0",
|
"@next/eslint-plugin-next": "15.5.0",
|
||||||
"@next/mdx": "15.5.0",
|
"@next/mdx": "15.5.0",
|
||||||
"@rgrove/parse-xml": "^4.1.0",
|
"@rgrove/parse-xml": "^4.1.0",
|
||||||
@ -220,7 +220,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"magicast": "^0.3.4",
|
"magicast": "^0.3.4",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"sass": "^1.80.3",
|
"sass": "^1.92.1",
|
||||||
"storybook": "8.5.0",
|
"storybook": "8.5.0",
|
||||||
"tailwindcss": "^3.4.14",
|
"tailwindcss": "^3.4.14",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
79
web/pnpm-lock.yaml
generated
79
web/pnpm-lock.yaml
generated
@ -91,8 +91,8 @@ importers:
|
|||||||
specifier: ^0.30.0
|
specifier: ^0.30.0
|
||||||
version: 0.30.0
|
version: 0.30.0
|
||||||
'@lexical/text':
|
'@lexical/text':
|
||||||
specifier: ^0.30.0
|
specifier: ^0.35.0
|
||||||
version: 0.30.0
|
version: 0.35.0
|
||||||
'@lexical/utils':
|
'@lexical/utils':
|
||||||
specifier: ^0.30.0
|
specifier: ^0.30.0
|
||||||
version: 0.30.0
|
version: 0.30.0
|
||||||
@ -230,10 +230,10 @@ importers:
|
|||||||
version: 0.6.4
|
version: 0.6.4
|
||||||
next:
|
next:
|
||||||
specifier: 15.5.0
|
specifier: 15.5.0
|
||||||
version: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2)
|
version: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)
|
||||||
next-pwa:
|
next-pwa:
|
||||||
specifier: ^5.6.0
|
specifier: ^5.6.0
|
||||||
version: 5.6.0(@babel/core@7.28.3)(@types/babel__core@7.20.5)(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2))(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
version: 5.6.0(@babel/core@7.28.3)(@types/babel__core@7.20.5)(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1))(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.3
|
specifier: ^0.4.3
|
||||||
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
@ -331,8 +331,8 @@ importers:
|
|||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
scheduler:
|
scheduler:
|
||||||
specifier: ^0.23.0
|
specifier: ^0.26.0
|
||||||
version: 0.23.2
|
version: 0.26.0
|
||||||
semver:
|
semver:
|
||||||
specifier: ^7.6.3
|
specifier: ^7.6.3
|
||||||
version: 7.7.2
|
version: 7.7.2
|
||||||
@ -359,7 +359,7 @@ importers:
|
|||||||
version: 7.0.10
|
version: 7.0.10
|
||||||
use-context-selector:
|
use-context-selector:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0(react@19.1.1)(scheduler@0.23.2)
|
version: 2.0.0(react@19.1.1)(scheduler@0.26.0)
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.0
|
version: 10.0.0
|
||||||
@ -407,8 +407,8 @@ importers:
|
|||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(@types/react@19.1.11)(react@19.1.1)
|
version: 3.1.0(@types/react@19.1.11)(react@19.1.1)
|
||||||
'@next/bundle-analyzer':
|
'@next/bundle-analyzer':
|
||||||
specifier: 15.5.0
|
specifier: 15.5.3
|
||||||
version: 15.5.0
|
version: 15.5.3
|
||||||
'@next/eslint-plugin-next':
|
'@next/eslint-plugin-next':
|
||||||
specifier: 15.5.0
|
specifier: 15.5.0
|
||||||
version: 15.5.0
|
version: 15.5.0
|
||||||
@ -438,7 +438,7 @@ importers:
|
|||||||
version: 8.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.5.0)
|
version: 8.5.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.5.0)
|
||||||
'@storybook/nextjs':
|
'@storybook/nextjs':
|
||||||
specifier: 8.5.0
|
specifier: 8.5.0
|
||||||
version: 8.5.0(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2)(storybook@8.5.0)(type-fest@2.19.0)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
version: 8.5.0(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)(storybook@8.5.0)(type-fest@2.19.0)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
'@storybook/react':
|
'@storybook/react':
|
||||||
specifier: 8.5.0
|
specifier: 8.5.0
|
||||||
version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.5.0)(typescript@5.8.3)
|
version: 8.5.0(@storybook/test@8.5.0(storybook@8.5.0))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@8.5.0)(typescript@5.8.3)
|
||||||
@ -569,8 +569,8 @@ importers:
|
|||||||
specifier: ^8.4.47
|
specifier: ^8.4.47
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
sass:
|
sass:
|
||||||
specifier: ^1.80.3
|
specifier: ^1.92.1
|
||||||
version: 1.89.2
|
version: 1.92.1
|
||||||
storybook:
|
storybook:
|
||||||
specifier: 8.5.0
|
specifier: 8.5.0
|
||||||
version: 8.5.0
|
version: 8.5.0
|
||||||
@ -2119,6 +2119,9 @@ packages:
|
|||||||
'@lexical/text@0.30.0':
|
'@lexical/text@0.30.0':
|
||||||
resolution: {integrity: sha512-P0ptriFwwP/hoDpz/MoBbzHxrFHqh0kCGzASWUdRZ1zrU0yPvJ9vV/UNMhyolH7xx+eAGI1Yl+m74NlpGmXqTg==}
|
resolution: {integrity: sha512-P0ptriFwwP/hoDpz/MoBbzHxrFHqh0kCGzASWUdRZ1zrU0yPvJ9vV/UNMhyolH7xx+eAGI1Yl+m74NlpGmXqTg==}
|
||||||
|
|
||||||
|
'@lexical/text@0.35.0':
|
||||||
|
resolution: {integrity: sha512-uaMh46BkysV8hK8wQwp5g/ByZW+2hPDt8ahAErxtf8NuzQem1FHG/f5RTchmFqqUDVHO3qLNTv4AehEGmXv8MA==}
|
||||||
|
|
||||||
'@lexical/utils@0.30.0':
|
'@lexical/utils@0.30.0':
|
||||||
resolution: {integrity: sha512-VJlAUhupCZmnbYYX3zMWovd4viu2guR01sAqKGbbOMbP+4rlaymixFbinvNPaRKDBloOARi+fpiveQFxnyr/Ew==}
|
resolution: {integrity: sha512-VJlAUhupCZmnbYYX3zMWovd4viu2guR01sAqKGbbOMbP+4rlaymixFbinvNPaRKDBloOARi+fpiveQFxnyr/Ew==}
|
||||||
|
|
||||||
@ -2164,8 +2167,8 @@ packages:
|
|||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||||
|
|
||||||
'@next/bundle-analyzer@15.5.0':
|
'@next/bundle-analyzer@15.5.3':
|
||||||
resolution: {integrity: sha512-6pSHKzl4lcxqgmdNGXeVShYBBJuJkgXIx8cP15XtdzPtFn/ZllDI8b/2OqtU0HDPcsq53yMq1hjUBze1GoSKXQ==}
|
resolution: {integrity: sha512-l2NxnWHP2gWHbomAlz/wFnN2jNCx/dpr7P/XWeOLhULiyKkXSac8O8SjxRO/8FNhr2l4JNtWVKk82Uya4cZYTw==}
|
||||||
|
|
||||||
'@next/env@15.5.0':
|
'@next/env@15.5.0':
|
||||||
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
|
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
|
||||||
@ -4811,6 +4814,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
detect-libc@2.1.0:
|
||||||
|
resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-newline@3.1.0:
|
detect-newline@3.1.0:
|
||||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -6354,6 +6361,9 @@ packages:
|
|||||||
lexical@0.30.0:
|
lexical@0.30.0:
|
||||||
resolution: {integrity: sha512-6gxYeXaJiAcreJD0whCofvO0MuJmnWoIgIl1w7L5FTigfhnEohuCx2SoI/oywzfzXE9gzZnyr3rVvZrMItPL8A==}
|
resolution: {integrity: sha512-6gxYeXaJiAcreJD0whCofvO0MuJmnWoIgIl1w7L5FTigfhnEohuCx2SoI/oywzfzXE9gzZnyr3rVvZrMItPL8A==}
|
||||||
|
|
||||||
|
lexical@0.35.0:
|
||||||
|
resolution: {integrity: sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==}
|
||||||
|
|
||||||
lib0@0.2.114:
|
lib0@0.2.114:
|
||||||
resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==}
|
resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@ -7858,8 +7868,8 @@ packages:
|
|||||||
webpack:
|
webpack:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
sass@1.89.2:
|
sass@1.92.1:
|
||||||
resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==}
|
resolution: {integrity: sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -10777,6 +10787,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lexical: 0.30.0
|
lexical: 0.30.0
|
||||||
|
|
||||||
|
'@lexical/text@0.35.0':
|
||||||
|
dependencies:
|
||||||
|
lexical: 0.35.0
|
||||||
|
|
||||||
'@lexical/utils@0.30.0':
|
'@lexical/utils@0.30.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lexical/list': 0.30.0
|
'@lexical/list': 0.30.0
|
||||||
@ -10793,7 +10807,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11':
|
'@mapbox/node-pre-gyp@1.0.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.0.4
|
detect-libc: 2.1.0
|
||||||
https-proxy-agent: 5.0.1
|
https-proxy-agent: 5.0.1
|
||||||
make-dir: 3.1.0
|
make-dir: 3.1.0
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
@ -10881,7 +10895,7 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.0
|
'@tybys/wasm-util': 0.10.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/bundle-analyzer@15.5.0':
|
'@next/bundle-analyzer@15.5.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack-bundle-analyzer: 4.10.1
|
webpack-bundle-analyzer: 4.10.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -11763,7 +11777,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
storybook: 8.5.0
|
storybook: 8.5.0
|
||||||
|
|
||||||
'@storybook/nextjs@8.5.0(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2)(storybook@8.5.0)(type-fest@2.19.0)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))':
|
'@storybook/nextjs@8.5.0(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)(storybook@8.5.0)(type-fest@2.19.0)(typescript@5.8.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.3
|
'@babel/core': 7.28.3
|
||||||
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3)
|
'@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.3)
|
||||||
@ -11789,7 +11803,7 @@ snapshots:
|
|||||||
find-up: 5.0.0
|
find-up: 5.0.0
|
||||||
image-size: 1.2.1
|
image-size: 1.2.1
|
||||||
loader-utils: 3.3.1
|
loader-utils: 3.3.1
|
||||||
next: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2)
|
next: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)
|
||||||
node-polyfill-webpack-plugin: 2.0.1(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
node-polyfill-webpack-plugin: 2.0.1(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
pnp-webpack-plugin: 1.7.0(typescript@5.8.3)
|
pnp-webpack-plugin: 1.7.0(typescript@5.8.3)
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@ -11798,7 +11812,7 @@ snapshots:
|
|||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
react-refresh: 0.14.2
|
react-refresh: 0.14.2
|
||||||
resolve-url-loader: 5.0.0
|
resolve-url-loader: 5.0.0
|
||||||
sass-loader: 14.2.1(sass@1.89.2)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
sass-loader: 14.2.1(sass@1.92.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
semver: 7.7.2
|
semver: 7.7.2
|
||||||
storybook: 8.5.0
|
storybook: 8.5.0
|
||||||
style-loader: 3.3.4(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
style-loader: 3.3.4(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
@ -13821,6 +13835,9 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.0.4: {}
|
detect-libc@2.0.4: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
@ -15865,6 +15882,8 @@ snapshots:
|
|||||||
|
|
||||||
lexical@0.30.0: {}
|
lexical@0.30.0: {}
|
||||||
|
|
||||||
|
lexical@0.35.0: {}
|
||||||
|
|
||||||
lib0@0.2.114:
|
lib0@0.2.114:
|
||||||
dependencies:
|
dependencies:
|
||||||
isomorphic.js: 0.2.5
|
isomorphic.js: 0.2.5
|
||||||
@ -16652,12 +16671,12 @@ snapshots:
|
|||||||
|
|
||||||
neo-async@2.6.2: {}
|
neo-async@2.6.2: {}
|
||||||
|
|
||||||
next-pwa@5.6.0(@babel/core@7.28.3)(@types/babel__core@7.20.5)(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2))(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)):
|
next-pwa@5.6.0(@babel/core@7.28.3)(@types/babel__core@7.20.5)(esbuild@0.25.0)(next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1))(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-loader: 8.4.1(@babel/core@7.28.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
babel-loader: 8.4.1(@babel/core@7.28.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
clean-webpack-plugin: 4.0.0(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
clean-webpack-plugin: 4.0.0(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
next: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2)
|
next: 15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1)
|
||||||
terser-webpack-plugin: 5.3.14(esbuild@0.25.0)(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
terser-webpack-plugin: 5.3.14(esbuild@0.25.0)(uglify-js@3.19.3)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3))
|
||||||
workbox-window: 6.6.0
|
workbox-window: 6.6.0
|
||||||
@ -16675,7 +16694,7 @@ snapshots:
|
|||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
react-dom: 19.1.1(react@19.1.1)
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
|
||||||
next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.89.2):
|
next@15.5.0(@babel/core@7.28.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(sass@1.92.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.5.0
|
'@next/env': 15.5.0
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@ -16693,7 +16712,7 @@ snapshots:
|
|||||||
'@next/swc-linux-x64-musl': 15.5.0
|
'@next/swc-linux-x64-musl': 15.5.0
|
||||||
'@next/swc-win32-arm64-msvc': 15.5.0
|
'@next/swc-win32-arm64-msvc': 15.5.0
|
||||||
'@next/swc-win32-x64-msvc': 15.5.0
|
'@next/swc-win32-x64-msvc': 15.5.0
|
||||||
sass: 1.89.2
|
sass: 1.92.1
|
||||||
sharp: 0.34.3
|
sharp: 0.34.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@ -17793,14 +17812,14 @@ snapshots:
|
|||||||
|
|
||||||
rw@1.3.3: {}
|
rw@1.3.3: {}
|
||||||
|
|
||||||
sass-loader@14.2.1(sass@1.89.2)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)):
|
sass-loader@14.2.1(sass@1.92.1)(webpack@5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
neo-async: 2.6.2
|
neo-async: 2.6.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
sass: 1.89.2
|
sass: 1.92.1
|
||||||
webpack: 5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)
|
webpack: 5.100.2(esbuild@0.25.0)(uglify-js@3.19.3)
|
||||||
|
|
||||||
sass@1.89.2:
|
sass@1.92.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
immutable: 5.1.3
|
immutable: 5.1.3
|
||||||
@ -18527,10 +18546,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.11
|
'@types/react': 19.1.11
|
||||||
|
|
||||||
use-context-selector@2.0.0(react@19.1.1)(scheduler@0.23.2):
|
use-context-selector@2.0.0(react@19.1.1)(scheduler@0.26.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.26.0
|
||||||
|
|
||||||
use-isomorphic-layout-effect@1.2.1(@types/react@19.1.11)(react@19.1.1):
|
use-isomorphic-layout-effect@1.2.1(@types/react@19.1.11)(react@19.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user