diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index b92d4c35a8..7460636824 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -7,7 +7,7 @@ cd web && pnpm install pipx install uv echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc -echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention\"" >> ~/.bashrc +echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention\"" >> ~/.bashrc echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev:inspect\"" >> ~/.bashrc echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 772ab8dd56..3946834e09 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -120,7 +120,6 @@ jobs: - name: ESLint autofix if: github.event_name != 'merge_group' && steps.web-changes.outputs.any_changed == 'true' run: | - cd web vp exec eslint --concurrency=2 --prune-suppressions --quiet || true - if: github.event_name != 'merge_group' diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index c32fc9d0cb..29f5b090f8 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -77,6 +77,8 @@ jobs: with: files: | web/** + e2e/** + sdks/nodejs-client/** packages/** package.json pnpm-lock.yaml @@ -95,14 +97,14 @@ jobs: id: eslint-cache-restore uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: web/.eslintcache - key: ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} + path: .eslintcache + key: ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}-${{ github.sha }} restore-keys: | - ${{ runner.os }}-web-eslint-${{ hashFiles('web/package.json', 'pnpm-lock.yaml', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- + ${{ runner.os }}-eslint-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'web/eslint.config.mjs', 'web/eslint.constants.mjs', 'web/plugins/eslint/**') }}- - name: Web style check if: steps.changed-files.outputs.any_changed == 'true' - working-directory: ./web + working-directory: . run: vp run lint:ci - name: Web tsslint @@ -112,7 +114,7 @@ jobs: - name: Web type check if: steps.changed-files.outputs.any_changed == 'true' - working-directory: ./web + working-directory: . run: vp run type-check - name: Web dead code check @@ -124,7 +126,7 @@ jobs: if: steps.changed-files.outputs.any_changed == 'true' && success() && steps.eslint-cache-restore.outputs.cache-hit != 'true' uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: - path: web/.eslintcache + path: .eslintcache key: ${{ steps.eslint-cache-restore.outputs.cache-primary-key }} superlinter: diff --git a/.gitignore b/.gitignore index 53dea88899..3493a7c756 100644 --- a/.gitignore +++ b/.gitignore @@ -203,6 +203,7 @@ sdks/python-client/dify_client.egg-info .vscode/* !.vscode/launch.json.template +!.vscode/settings.example.json !.vscode/README.md api/.vscode # vscode Code History Extension @@ -242,3 +243,5 @@ scripts/stress-test/reports/ # Code Agent Folder .qoder/* + +.eslintcache diff --git a/.vite-hooks/pre-commit b/.vite-hooks/pre-commit index 13bbd81cf6..d48381bce2 100755 --- a/.vite-hooks/pre-commit +++ b/.vite-hooks/pre-commit @@ -56,44 +56,9 @@ if $api_modified; then fi fi -if $web_modified; then - if $skip_web_checks; then - echo "Git operation in progress, skipping web checks" - exit 0 - fi - - echo "Running ESLint on web module" - - if git diff --cached --quiet -- 'web/**/*.ts' 'web/**/*.tsx'; then - web_ts_modified=false - else - ts_diff_status=$? - if [ $ts_diff_status -eq 1 ]; then - web_ts_modified=true - else - echo "Unable to determine staged TypeScript changes (git exit code: $ts_diff_status)." - exit $ts_diff_status - fi - fi - - cd ./web || exit 1 - vp staged - - if $web_ts_modified; then - echo "Running TypeScript type-check:tsgo" - if ! npm run type-check:tsgo; then - echo "Type check failed. Please run 'npm run type-check:tsgo' to fix the errors." - exit 1 - fi - else - echo "No staged TypeScript changes detected, skipping type-check:tsgo" - fi - - echo "Running knip" - if ! npm run knip; then - echo "Knip check failed. Please run 'npm run knip' to fix the errors." - exit 1 - fi - - cd ../ +if $skip_web_checks; then + echo "Git operation in progress, skipping web checks" + exit 0 fi + +vp staged diff --git a/.vscode/launch.json.template b/.vscode/launch.json.template index c3e2c50c52..2611b75c6c 100644 --- a/.vscode/launch.json.template +++ b/.vscode/launch.json.template @@ -2,21 +2,10 @@ "version": "0.2.0", "configurations": [ { - "name": "Python: Flask API", + "name": "Python: API (gevent)", "type": "debugpy", "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py", - "FLASK_ENV": "development" - }, - "args": [ - "run", - "--host=0.0.0.0", - "--port=5001", - "--no-debugger", - "--no-reload" - ], + "program": "${workspaceFolder}/api/app.py", "jinja": true, "justMyCode": true, "cwd": "${workspaceFolder}/api", diff --git a/web/.vscode/settings.example.json b/.vscode/settings.example.json similarity index 86% rename from web/.vscode/settings.example.json rename to .vscode/settings.example.json index 4b356f5b7a..7cdbc51a3b 100644 --- a/web/.vscode/settings.example.json +++ b/.vscode/settings.example.json @@ -1,12 +1,16 @@ { - // Disable the default formatter, use eslint instead - "prettier.enable": false, - "editor.formatOnSave": false, + "cucumber.features": [ + "e2e/features/**/*.feature", + ], + "cucumber.glue": [ + "e2e/features/**/*.ts", + ], + + "tailwindCSS.experimental.configFile": "web/app/styles/globals.css", // Auto fix "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", - "source.organizeImports": "never" }, // Silent the stylistic rules in your IDE, but still auto fix them diff --git a/api/.env.example b/api/.env.example index beb820e797..7455d4a0e9 100644 --- a/api/.env.example +++ b/api/.env.example @@ -33,6 +33,9 @@ TRIGGER_URL=http://localhost:5001 # The time in seconds after the signature is rejected FILES_ACCESS_TIMEOUT=300 +# Collaboration mode toggle +ENABLE_COLLABORATION_MODE=false + # Access token expiration time in minutes ACCESS_TOKEN_EXPIRE_MINUTES=60 diff --git a/api/.ruff.toml b/api/.ruff.toml index bd9684ef65..dd78024a02 100644 --- a/api/.ruff.toml +++ b/api/.ruff.toml @@ -106,3 +106,6 @@ msg = "Use Pydantic payload/query models instead of reqparse." [lint.flake8-tidy-imports.banned-api."flask_restx.reqparse.RequestParser"] msg = "Use Pydantic payload/query models instead of reqparse." + +[lint.isort] +known-first-party = ["graphon"] \ No newline at end of file diff --git a/api/.vscode/launch.json.example b/api/.vscode/launch.json.example index 6bdfa2c039..1001559176 100644 --- a/api/.vscode/launch.json.example +++ b/api/.vscode/launch.json.example @@ -3,29 +3,21 @@ "compounds": [ { "name": "Launch Flask and Celery", - "configurations": ["Python: Flask", "Python: Celery"] + "configurations": ["Python: API (gevent)", "Python: Celery"] } ], "configurations": [ { - "name": "Python: Flask", - "consoleName": "Flask", + "name": "Python: API (gevent)", + "consoleName": "API", "type": "debugpy", "request": "launch", "python": "${workspaceFolder}/.venv/bin/python", "cwd": "${workspaceFolder}", "envFile": ".env", - "module": "flask", + "program": "${workspaceFolder}/app.py", "justMyCode": true, - "jinja": true, - "env": { - "FLASK_APP": "app.py", - "GEVENT_SUPPORT": "True" - }, - "args": [ - "run", - "--port=5001" - ] + "jinja": true }, { "name": "Python: Celery", diff --git a/api/app.py b/api/app.py index c018c8a045..e53b037be5 100644 --- a/api/app.py +++ b/api/app.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import sys from typing import TYPE_CHECKING, cast @@ -9,17 +10,35 @@ if TYPE_CHECKING: celery: Celery +HOST = "0.0.0.0" +PORT = 5001 +logger = logging.getLogger(__name__) + + def is_db_command() -> bool: if len(sys.argv) > 1 and sys.argv[0].endswith("flask") and sys.argv[1] == "db": return True return False +def log_startup_banner(host: str, port: int) -> None: + debugger_attached = sys.gettrace() is not None + logger.info("Serving Dify API via gevent WebSocket server") + logger.info("Bound to http://%s:%s", host, port) + logger.info("Debugger attached: %s", "on" if debugger_attached else "off") + logger.info("Press CTRL+C to quit") + + # create app +flask_app = None +socketio_app = None + if is_db_command(): from app_factory import create_migrations_app app = create_migrations_app() + socketio_app = app + flask_app = app else: # Gunicorn and Celery handle monkey patching automatically in production by # specifying the `gevent` worker class. Manual monkey patching is not required here. @@ -30,8 +49,14 @@ else: from app_factory import create_app - app = create_app() + socketio_app, flask_app = create_app() + app = flask_app celery = cast("Celery", app.extensions["celery"]) if __name__ == "__main__": - app.run(host="0.0.0.0", port=5001) + from gevent import pywsgi + from geventwebsocket.handler import WebSocketHandler # type: ignore[reportMissingTypeStubs] + + log_startup_banner(HOST, PORT) + server = pywsgi.WSGIServer((HOST, PORT), socketio_app, handler_class=WebSocketHandler) + server.serve_forever() diff --git a/api/app_factory.py b/api/app_factory.py index 76838f9925..48e50ceae9 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -1,6 +1,7 @@ import logging import time +import socketio # type: ignore[reportMissingTypeStubs] from flask import request from opentelemetry.trace import get_current_span from opentelemetry.trace.span import INVALID_SPAN_ID, INVALID_TRACE_ID @@ -10,6 +11,7 @@ from contexts.wrapper import RecyclableContextVar from controllers.console.error import UnauthorizedAndForceLogout from core.logging.context import init_request_context from dify_app import DifyApp +from extensions.ext_socketio import sio from services.enterprise.enterprise_service import EnterpriseService from services.feature_service import LicenseStatus @@ -122,14 +124,18 @@ def create_flask_app_with_configs() -> DifyApp: return dify_app -def create_app() -> DifyApp: +def create_app() -> tuple[socketio.WSGIApp, DifyApp]: start_time = time.perf_counter() app = create_flask_app_with_configs() initialize_extensions(app) + + sio.app = app + socketio_app = socketio.WSGIApp(sio, app) + end_time = time.perf_counter() if dify_config.DEBUG: logger.info("Finished create_app (%s ms)", round((end_time - start_time) * 1000, 2)) - return app + return socketio_app, app def initialize_extensions(app: DifyApp): diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 78862c1384..68fb031bb6 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -1274,6 +1274,13 @@ class PositionConfig(BaseSettings): return {item.strip() for item in self.POSITION_TOOL_EXCLUDES.split(",") if item.strip() != ""} +class CollaborationConfig(BaseSettings): + ENABLE_COLLABORATION_MODE: bool = Field( + description="Whether to enable collaboration mode features across the workspace", + default=False, + ) + + class LoginConfig(BaseSettings): ENABLE_EMAIL_CODE_LOGIN: bool = Field( description="whether to enable email code login", @@ -1426,6 +1433,7 @@ class FeatureConfig( WorkflowConfig, WorkflowNodeExecutionConfig, WorkspaceConfig, + CollaborationConfig, LoginConfig, AccountConfig, SwaggerUIConfig, diff --git a/api/controllers/common/fields.py b/api/controllers/common/fields.py index 4fe3fc9062..8e665c1386 100644 --- a/api/controllers/common/fields.py +++ b/api/controllers/common/fields.py @@ -2,9 +2,9 @@ from __future__ import annotations from typing import Any -from graphon.file import helpers as file_helpers from pydantic import BaseModel, ConfigDict, computed_field +from graphon.file import helpers as file_helpers from models.model import IconType type JSONValue = str | int | float | bool | None | dict[str, Any] | list[Any] diff --git a/api/controllers/console/__init__.py b/api/controllers/console/__init__.py index e952e33465..0e9508d18a 100644 --- a/api/controllers/console/__init__.py +++ b/api/controllers/console/__init__.py @@ -65,6 +65,7 @@ from .app import ( statistic, workflow, workflow_app_log, + workflow_comment, workflow_draft_variable, workflow_run, workflow_statistic, @@ -119,6 +120,7 @@ from .explore import ( saved_message, trial, ) +from .socketio import workflow as socketio_workflow # pyright: ignore[reportUnusedImport] # Import snippet controllers from .snippets import snippet_workflow, snippet_workflow_draft_variable @@ -209,6 +211,7 @@ __all__ = [ "saved_message", "setup", "site", + "socketio_workflow", "snippet_workflow", "snippet_workflow_draft_variable", "snippets", @@ -222,6 +225,7 @@ __all__ = [ "website", "workflow", "workflow_app_log", + "workflow_comment", "workflow_draft_variable", "workflow_run", "workflow_statistic", diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 06810d7094..df5c94a867 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -5,7 +5,6 @@ from typing import Any, Literal from flask import request from flask_restx import Resource -from graphon.enums import WorkflowExecutionStatus from pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator from sqlalchemy import select from sqlalchemy.orm import Session @@ -30,6 +29,7 @@ from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.trigger.constants import TRIGGER_NODE_TYPES from extensions.ext_database import db from fields.base import ResponseModel +from graphon.enums import WorkflowExecutionStatus from libs.helper import build_icon_url from libs.login import current_account_with_tenant, login_required from models import App, DatasetPermissionEnum, Workflow diff --git a/api/controllers/console/app/audio.py b/api/controllers/console/app/audio.py index 78ddb904e1..91fbe4a85a 100644 --- a/api/controllers/console/app/audio.py +++ b/api/controllers/console/app/audio.py @@ -2,7 +2,6 @@ import logging from flask import request from flask_restx import Resource, fields -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from werkzeug.exceptions import InternalServerError @@ -23,6 +22,7 @@ from controllers.console.app.error import ( from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, setup_required from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from libs.login import login_required from models import App, AppMode from services.audio_service import AudioService diff --git a/api/controllers/console/app/completion.py b/api/controllers/console/app/completion.py index d83925d173..fe274e4c9a 100644 --- a/api/controllers/console/app/completion.py +++ b/api/controllers/console/app/completion.py @@ -3,7 +3,6 @@ from typing import Any, Literal from flask import request from flask_restx import Resource -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -27,6 +26,7 @@ from core.errors.error import ( QuotaExceededError, ) from core.helper.trace_id_helper import get_external_trace_id +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from libs.login import current_user, login_required diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index d329d22309..b2b1049f0c 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -2,20 +2,37 @@ from typing import Literal import sqlalchemy as sa from flask import abort, request -from flask_restx import Resource, fields, marshal_with +from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from sqlalchemy import func, or_ from sqlalchemy.orm import selectinload from werkzeug.exceptions import NotFound +from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db -from fields.raws import FilesContainedField +from fields.conversation_fields import ( + Conversation as ConversationResponse, +) +from fields.conversation_fields import ( + ConversationDetail as ConversationDetailResponse, +) +from fields.conversation_fields import ( + ConversationMessageDetail as ConversationMessageDetailResponse, +) +from fields.conversation_fields import ( + ConversationPagination as ConversationPaginationResponse, +) +from fields.conversation_fields import ( + ConversationWithSummaryPagination as ConversationWithSummaryPaginationResponse, +) +from fields.conversation_fields import ( + ResultResponse, +) from libs.datetime_utils import naive_utc_now, parse_time_range -from libs.helper import TimestampField from libs.login import current_account_with_tenant, login_required from models import Conversation, EndUser, Message, MessageAnnotation from models.model import AppMode @@ -62,267 +79,16 @@ console_ns.schema_model( ChatConversationQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), ) -# Register models for flask_restx to avoid dict type issues in Swagger -# Register in dependency order: base models first, then dependent models - -# Base models -simple_account_model = console_ns.model( - "SimpleAccount", - { - "id": fields.String, - "name": fields.String, - "email": fields.String, - }, -) - -feedback_stat_model = console_ns.model( - "FeedbackStat", - { - "like": fields.Integer, - "dislike": fields.Integer, - }, -) - -status_count_model = console_ns.model( - "StatusCount", - { - "success": fields.Integer, - "failed": fields.Integer, - "partial_success": fields.Integer, - "paused": fields.Integer, - }, -) - -message_file_model = console_ns.model( - "MessageFile", - { - "id": fields.String, - "filename": fields.String, - "type": fields.String, - "url": fields.String, - "mime_type": fields.String, - "size": fields.Integer, - "transfer_method": fields.String, - "belongs_to": fields.String(default="user"), - "upload_file_id": fields.String(default=None), - }, -) - -agent_thought_model = console_ns.model( - "AgentThought", - { - "id": fields.String, - "chain_id": fields.String, - "message_id": fields.String, - "position": fields.Integer, - "thought": fields.String, - "tool": fields.String, - "tool_labels": fields.Raw, - "tool_input": fields.String, - "created_at": TimestampField, - "observation": fields.String, - "files": fields.List(fields.String), - }, -) - -simple_model_config_model = console_ns.model( - "SimpleModelConfig", - { - "model": fields.Raw(attribute="model_dict"), - "pre_prompt": fields.String, - }, -) - -model_config_model = console_ns.model( - "ModelConfig", - { - "opening_statement": fields.String, - "suggested_questions": fields.Raw, - "model": fields.Raw, - "user_input_form": fields.Raw, - "pre_prompt": fields.String, - "agent_mode": fields.Raw, - }, -) - -# Models that depend on simple_account_model -feedback_model = console_ns.model( - "Feedback", - { - "rating": fields.String, - "content": fields.String, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_account": fields.Nested(simple_account_model, allow_null=True), - }, -) - -annotation_model = console_ns.model( - "Annotation", - { - "id": fields.String, - "question": fields.String, - "content": fields.String, - "account": fields.Nested(simple_account_model, allow_null=True), - "created_at": TimestampField, - }, -) - -annotation_hit_history_model = console_ns.model( - "AnnotationHitHistory", - { - "annotation_id": fields.String(attribute="id"), - "annotation_create_account": fields.Nested(simple_account_model, allow_null=True), - "created_at": TimestampField, - }, -) - - -class MessageTextField(fields.Raw): - def format(self, value): - return value[0]["text"] if value else "" - - -# Simple message detail model -simple_message_detail_model = console_ns.model( - "SimpleMessageDetail", - { - "inputs": FilesContainedField, - "query": fields.String, - "message": MessageTextField, - "answer": fields.String, - }, -) - -# Message detail model that depends on multiple models -message_detail_model = console_ns.model( - "MessageDetail", - { - "id": fields.String, - "conversation_id": fields.String, - "inputs": FilesContainedField, - "query": fields.String, - "message": fields.Raw, - "message_tokens": fields.Integer, - "answer": fields.String(attribute="re_sign_file_url_answer"), - "answer_tokens": fields.Integer, - "provider_response_latency": fields.Float, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_account_id": fields.String, - "feedbacks": fields.List(fields.Nested(feedback_model)), - "workflow_run_id": fields.String, - "annotation": fields.Nested(annotation_model, allow_null=True), - "annotation_hit_history": fields.Nested(annotation_hit_history_model, allow_null=True), - "created_at": TimestampField, - "agent_thoughts": fields.List(fields.Nested(agent_thought_model)), - "message_files": fields.List(fields.Nested(message_file_model)), - "metadata": fields.Raw(attribute="message_metadata_dict"), - "status": fields.String, - "error": fields.String, - "parent_message_id": fields.String, - }, -) - -# Conversation models -conversation_fields_model = console_ns.model( - "Conversation", - { - "id": fields.String, - "status": fields.String, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_end_user_session_id": fields.String(), - "from_account_id": fields.String, - "from_account_name": fields.String, - "read_at": TimestampField, - "created_at": TimestampField, - "updated_at": TimestampField, - "annotation": fields.Nested(annotation_model, allow_null=True), - "model_config": fields.Nested(simple_model_config_model), - "user_feedback_stats": fields.Nested(feedback_stat_model), - "admin_feedback_stats": fields.Nested(feedback_stat_model), - "message": fields.Nested(simple_message_detail_model, attribute="first_message"), - }, -) - -conversation_pagination_model = console_ns.model( - "ConversationPagination", - { - "page": fields.Integer, - "limit": fields.Integer(attribute="per_page"), - "total": fields.Integer, - "has_more": fields.Boolean(attribute="has_next"), - "data": fields.List(fields.Nested(conversation_fields_model), attribute="items"), - }, -) - -conversation_message_detail_model = console_ns.model( - "ConversationMessageDetail", - { - "id": fields.String, - "status": fields.String, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_account_id": fields.String, - "created_at": TimestampField, - "model_config": fields.Nested(model_config_model), - "message": fields.Nested(message_detail_model, attribute="first_message"), - }, -) - -conversation_with_summary_model = console_ns.model( - "ConversationWithSummary", - { - "id": fields.String, - "status": fields.String, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_end_user_session_id": fields.String, - "from_account_id": fields.String, - "from_account_name": fields.String, - "name": fields.String, - "summary": fields.String(attribute="summary_or_query"), - "read_at": TimestampField, - "created_at": TimestampField, - "updated_at": TimestampField, - "annotated": fields.Boolean, - "model_config": fields.Nested(simple_model_config_model), - "message_count": fields.Integer, - "user_feedback_stats": fields.Nested(feedback_stat_model), - "admin_feedback_stats": fields.Nested(feedback_stat_model), - "status_count": fields.Nested(status_count_model), - }, -) - -conversation_with_summary_pagination_model = console_ns.model( - "ConversationWithSummaryPagination", - { - "page": fields.Integer, - "limit": fields.Integer(attribute="per_page"), - "total": fields.Integer, - "has_more": fields.Boolean(attribute="has_next"), - "data": fields.List(fields.Nested(conversation_with_summary_model), attribute="items"), - }, -) - -conversation_detail_model = console_ns.model( - "ConversationDetail", - { - "id": fields.String, - "status": fields.String, - "from_source": fields.String, - "from_end_user_id": fields.String, - "from_account_id": fields.String, - "created_at": TimestampField, - "updated_at": TimestampField, - "annotated": fields.Boolean, - "introduction": fields.String, - "model_config": fields.Nested(model_config_model), - "message_count": fields.Integer, - "user_feedback_stats": fields.Nested(feedback_stat_model), - "admin_feedback_stats": fields.Nested(feedback_stat_model), - }, +register_schema_models( + console_ns, + CompletionConversationQuery, + ChatConversationQuery, + ConversationResponse, + ConversationPaginationResponse, + ConversationMessageDetailResponse, + ConversationWithSummaryPaginationResponse, + ConversationDetailResponse, + ResultResponse, ) @@ -332,13 +98,12 @@ class CompletionConversationApi(Resource): @console_ns.doc(description="Get completion conversations with pagination and filtering") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[CompletionConversationQuery.__name__]) - @console_ns.response(200, "Success", conversation_pagination_model) + @console_ns.response(200, "Success", console_ns.models[ConversationPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model(mode=AppMode.COMPLETION) - @marshal_with(conversation_pagination_model) @edit_permission_required def get(self, app_model): current_user, _ = current_account_with_tenant() @@ -394,7 +159,9 @@ class CompletionConversationApi(Resource): conversations = db.paginate(query, page=args.page, per_page=args.limit, error_out=False) - return conversations + return ConversationPaginationResponse.model_validate(conversations, from_attributes=True).model_dump( + mode="json" + ) @console_ns.route("/apps//completion-conversations/") @@ -402,19 +169,19 @@ class CompletionConversationDetailApi(Resource): @console_ns.doc("get_completion_conversation") @console_ns.doc(description="Get completion conversation details with messages") @console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"}) - @console_ns.response(200, "Success", conversation_message_detail_model) + @console_ns.response(200, "Success", console_ns.models[ConversationMessageDetailResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Conversation not found") @setup_required @login_required @account_initialization_required @get_app_model(mode=AppMode.COMPLETION) - @marshal_with(conversation_message_detail_model) @edit_permission_required def get(self, app_model, conversation_id): conversation_id = str(conversation_id) - - return _get_conversation(app_model, conversation_id) + return ConversationMessageDetailResponse.model_validate( + _get_conversation(app_model, conversation_id), from_attributes=True + ).model_dump(mode="json") @console_ns.doc("delete_completion_conversation") @console_ns.doc(description="Delete a completion conversation") @@ -436,7 +203,7 @@ class CompletionConversationDetailApi(Resource): except ConversationNotExistsError: raise NotFound("Conversation Not Exists.") - return {"result": "success"}, 204 + return ResultResponse(result="success").model_dump(mode="json"), 204 @console_ns.route("/apps//chat-conversations") @@ -445,13 +212,12 @@ class ChatConversationApi(Resource): @console_ns.doc(description="Get chat conversations with pagination, filtering and summary") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[ChatConversationQuery.__name__]) - @console_ns.response(200, "Success", conversation_with_summary_pagination_model) + @console_ns.response(200, "Success", console_ns.models[ConversationWithSummaryPaginationResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @setup_required @login_required @account_initialization_required @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) - @marshal_with(conversation_with_summary_pagination_model) @edit_permission_required def get(self, app_model): current_user, _ = current_account_with_tenant() @@ -546,7 +312,9 @@ class ChatConversationApi(Resource): conversations = db.paginate(query, page=args.page, per_page=args.limit, error_out=False) - return conversations + return ConversationWithSummaryPaginationResponse.model_validate(conversations, from_attributes=True).model_dump( + mode="json" + ) @console_ns.route("/apps//chat-conversations/") @@ -554,19 +322,19 @@ class ChatConversationDetailApi(Resource): @console_ns.doc("get_chat_conversation") @console_ns.doc(description="Get chat conversation details") @console_ns.doc(params={"app_id": "Application ID", "conversation_id": "Conversation ID"}) - @console_ns.response(200, "Success", conversation_detail_model) + @console_ns.response(200, "Success", console_ns.models[ConversationDetailResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Conversation not found") @setup_required @login_required @account_initialization_required @get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT]) - @marshal_with(conversation_detail_model) @edit_permission_required def get(self, app_model, conversation_id): conversation_id = str(conversation_id) - - return _get_conversation(app_model, conversation_id) + return ConversationDetailResponse.model_validate( + _get_conversation(app_model, conversation_id), from_attributes=True + ).model_dump(mode="json") @console_ns.doc("delete_chat_conversation") @console_ns.doc(description="Delete a chat conversation") @@ -588,7 +356,7 @@ class ChatConversationDetailApi(Resource): except ConversationNotExistsError: raise NotFound("Conversation Not Exists.") - return {"result": "success"}, 204 + return ResultResponse(result="success").model_dump(mode="json"), 204 def _get_conversation(app_model, conversation_id): diff --git a/api/controllers/console/app/generator.py b/api/controllers/console/app/generator.py index 7101d5df7b..c720a5e074 100644 --- a/api/controllers/console/app/generator.py +++ b/api/controllers/console/app/generator.py @@ -1,7 +1,6 @@ from collections.abc import Sequence from flask_restx import Resource -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from controllers.console import console_ns @@ -20,6 +19,7 @@ from core.helper.code_executor.python3.python3_code_provider import Python3CodeP from core.llm_generator.entities import RuleCodeGeneratePayload, RuleGeneratePayload, RuleStructuredOutputPayload from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db +from graphon.model_runtime.errors.invoke import InvokeError from libs.login import current_account_with_tenant, login_required from models import App from services.workflow_service import WorkflowService diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 5b1abc98dc..d517f695b8 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -18,12 +18,6 @@ from models.enums import AppMCPServerStatus from models.model import AppMCPServer -def _to_timestamp(value: datetime | int | None) -> int | None: - if isinstance(value, datetime): - return int(value.timestamp()) - return value - - class MCPServerCreatePayload(BaseModel): description: str | None = Field(default=None, description="Server description") parameters: dict[str, Any] = Field(..., description="Server parameters configuration") @@ -36,19 +30,25 @@ class MCPServerUpdatePayload(BaseModel): status: str | None = Field(default=None, description="Server status") +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value + + class AppMCPServerResponse(ResponseModel): id: str name: str server_code: str description: str - status: str + status: AppMCPServerStatus parameters: dict[str, Any] | list[Any] | str created_at: int | None = None updated_at: int | None = None @field_validator("parameters", mode="before") @classmethod - def _parse_json_string(cls, value: Any) -> Any: + def _normalize_parameters(cls, value: Any) -> Any: if isinstance(value, str): try: return json.loads(value) @@ -70,7 +70,9 @@ class AppMCPServerController(Resource): @console_ns.doc("get_app_mcp_server") @console_ns.doc(description="Get MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) - @console_ns.response(200, "Server configuration", console_ns.models[AppMCPServerResponse.__name__]) + @console_ns.response( + 200, "MCP server configuration retrieved successfully", console_ns.models[AppMCPServerResponse.__name__] + ) @login_required @account_initialization_required @setup_required @@ -85,7 +87,9 @@ class AppMCPServerController(Resource): @console_ns.doc(description="Create MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerCreatePayload.__name__]) - @console_ns.response(200, "Server created", console_ns.models[AppMCPServerResponse.__name__]) + @console_ns.response( + 201, "MCP server configuration created successfully", console_ns.models[AppMCPServerResponse.__name__] + ) @console_ns.response(403, "Insufficient permissions") @account_initialization_required @get_app_model @@ -111,13 +115,15 @@ class AppMCPServerController(Resource): ) db.session.add(server) db.session.commit() - return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json") + return AppMCPServerResponse.model_validate(server, from_attributes=True).model_dump(mode="json"), 201 @console_ns.doc("update_app_mcp_server") @console_ns.doc(description="Update MCP server configuration for an application") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.expect(console_ns.models[MCPServerUpdatePayload.__name__]) - @console_ns.response(200, "Server updated", console_ns.models[AppMCPServerResponse.__name__]) + @console_ns.response( + 200, "MCP server configuration updated successfully", console_ns.models[AppMCPServerResponse.__name__] + ) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @get_app_model @@ -154,7 +160,7 @@ class AppMCPServerRefreshController(Resource): @console_ns.doc("refresh_app_mcp_server") @console_ns.doc(description="Refresh MCP server configuration and regenerate server code") @console_ns.doc(params={"server_id": "Server ID"}) - @console_ns.response(200, "Server refreshed", console_ns.models[AppMCPServerResponse.__name__]) + @console_ns.response(200, "MCP server refreshed successfully", console_ns.models[AppMCPServerResponse.__name__]) @console_ns.response(403, "Insufficient permissions") @console_ns.response(404, "Server not found") @setup_required diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index daeed4627c..44e19b57db 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -4,7 +4,6 @@ from typing import Literal from flask import request from flask_restx import Resource -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from sqlalchemy import exists, func, select from werkzeug.exceptions import InternalServerError, NotFound @@ -40,6 +39,7 @@ from fields.conversation_fields import ( format_files_contained, to_timestamp, ) +from graphon.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value from libs.infinite_scroll_pagination import InfiniteScrollPagination from libs.login import current_account_with_tenant, login_required diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index a149960a23..1f25beeefc 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -5,10 +5,6 @@ from typing import Any, Literal from flask import abort, request from flask_restx import Resource, fields, marshal, marshal_with -from graphon.enums import NodeType -from graphon.file import File -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, ValidationError, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -39,7 +35,13 @@ from extensions.ext_database import db from extensions.ext_redis import redis_client from factories import file_factory, variable_factory from fields.member_fields import simple_account_fields +from fields.online_user_fields import online_user_list_fields from fields.workflow_fields import workflow_fields, workflow_pagination_fields +from graphon.enums import NodeType +from graphon.file import File +from graphon.file import helpers as file_helpers +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper from libs.datetime_utils import naive_utc_now from libs.helper import TimestampField, uuid_value @@ -47,6 +49,7 @@ from libs.login import current_account_with_tenant, login_required from models import App from models.model import AppMode from models.workflow import Workflow, WorkflowType +from repositories.workflow_collaboration_repository import WORKFLOW_ONLINE_USERS_PREFIX from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowHashNotEqualError, WorkflowNotFoundError from services.errors.llm import InvokeRateLimitError @@ -57,6 +60,7 @@ _file_access_controller = DatabaseFileAccessController() LISTENING_RETRY_IN = 2000 DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" RESTORE_SOURCE_WORKFLOW_MUST_BE_PUBLISHED_MESSAGE = "source workflow must be published" +MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS = 50 # Register models for flask_restx to avoid dict type issues in Swagger # Register in dependency order: base models first, then dependent models @@ -168,6 +172,14 @@ class WorkflowTypeConvertQuery(BaseModel): +class WorkflowFeaturesPayload(BaseModel): + features: dict[str, Any] = Field(..., description="Workflow feature configuration") + + +class WorkflowOnlineUsersQuery(BaseModel): + app_ids: str = Field(..., description="Comma-separated app IDs") + + class DraftWorkflowTriggerRunPayload(BaseModel): node_id: str @@ -192,6 +204,8 @@ reg(ConvertToWorkflowPayload) reg(WorkflowListQuery) reg(WorkflowUpdatePayload) reg(WorkflowTypeConvertQuery) +reg(WorkflowFeaturesPayload) +reg(WorkflowOnlineUsersQuery) reg(DraftWorkflowTriggerRunPayload) reg(DraftWorkflowTriggerRunAllPayload) @@ -998,6 +1012,32 @@ class ConvertToWorkflowApi(Resource): } +@console_ns.route("/apps//workflows/draft/features") +class WorkflowFeaturesApi(Resource): + """Update draft workflow features.""" + + @console_ns.expect(console_ns.models[WorkflowFeaturesPayload.__name__]) + @console_ns.doc("update_workflow_features") + @console_ns.doc(description="Update draft workflow features") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Workflow features updated successfully") + @setup_required + @login_required + @account_initialization_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + @edit_permission_required + def post(self, app_model: App): + current_user, _ = current_account_with_tenant() + + args = WorkflowFeaturesPayload.model_validate(console_ns.payload or {}) + features = args.features + + workflow_service = WorkflowService() + workflow_service.update_draft_workflow_features(app_model=app_model, features=features, account=current_user) + + return {"result": "success"} + + @console_ns.route("/apps//workflows") class PublishedAllWorkflowApi(Resource): @console_ns.expect(console_ns.models[WorkflowListQuery.__name__]) @@ -1452,3 +1492,62 @@ class DraftWorkflowTriggerRunAllApi(Resource): "status": "error", } ), 400 + + +@console_ns.route("/apps/workflows/online-users") +class WorkflowOnlineUsersApi(Resource): + @console_ns.expect(console_ns.models[WorkflowOnlineUsersQuery.__name__]) + @console_ns.doc("get_workflow_online_users") + @console_ns.doc(description="Get workflow online users") + @setup_required + @login_required + @account_initialization_required + @marshal_with(online_user_list_fields) + def get(self): + args = WorkflowOnlineUsersQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + + app_ids = list(dict.fromkeys(app_id.strip() for app_id in args.app_ids.split(",") if app_id.strip())) + if len(app_ids) > MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS: + raise BadRequest(f"Maximum {MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS} app_ids are allowed per request.") + + if not app_ids: + return {"data": []} + + _, current_tenant_id = current_account_with_tenant() + workflow_service = WorkflowService() + accessible_app_ids = workflow_service.get_accessible_app_ids(app_ids, current_tenant_id) + + results = [] + for app_id in app_ids: + if app_id not in accessible_app_ids: + continue + + users_json = redis_client.hgetall(f"{WORKFLOW_ONLINE_USERS_PREFIX}{app_id}") + + users = [] + for _, user_info_json in users_json.items(): + try: + user_info = json.loads(user_info_json) + except Exception: + continue + + if not isinstance(user_info, dict): + continue + + avatar = user_info.get("avatar") + if isinstance(avatar, str) and avatar and not avatar.startswith(("http://", "https://")): + try: + user_info["avatar"] = file_helpers.get_signed_file_url(avatar) + except Exception as exc: + logger.warning( + "Failed to sign workflow online user avatar; using original value. " + "app_id=%s avatar=%s error=%s", + app_id, + avatar, + exc, + ) + + users.append(user_info) + results.append({"app_id": app_id, "users": users}) + + return {"data": results} diff --git a/api/controllers/console/app/workflow_app_log.py b/api/controllers/console/app/workflow_app_log.py index 6b402898e8..4b39590235 100644 --- a/api/controllers/console/app/workflow_app_log.py +++ b/api/controllers/console/app/workflow_app_log.py @@ -4,7 +4,6 @@ from typing import Any from dateutil.parser import isoparse from flask import request from flask_restx import Resource -from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker @@ -16,6 +15,7 @@ from extensions.ext_database import db from fields.base import ResponseModel from fields.end_user_fields import SimpleEndUser from fields.member_fields import SimpleAccount +from graphon.enums import WorkflowExecutionStatus from libs.login import login_required from models import App from models.model import AppMode diff --git a/api/controllers/console/app/workflow_comment.py b/api/controllers/console/app/workflow_comment.py new file mode 100644 index 0000000000..e7c3e982a6 --- /dev/null +++ b/api/controllers/console/app/workflow_comment.py @@ -0,0 +1,335 @@ +import logging + +from flask_restx import Resource, marshal_with +from pydantic import BaseModel, Field, TypeAdapter + +from controllers.common.schema import register_schema_models +from controllers.console import console_ns +from controllers.console.app.wraps import get_app_model +from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required +from fields.member_fields import AccountWithRole +from fields.workflow_comment_fields import ( + workflow_comment_basic_fields, + workflow_comment_create_fields, + workflow_comment_detail_fields, + workflow_comment_reply_create_fields, + workflow_comment_reply_update_fields, + workflow_comment_resolve_fields, + workflow_comment_update_fields, +) +from libs.login import current_user, login_required +from models import App +from services.account_service import TenantService +from services.workflow_comment_service import WorkflowCommentService + +logger = logging.getLogger(__name__) +DEFAULT_REF_TEMPLATE_SWAGGER_2_0 = "#/definitions/{model}" + + +class WorkflowCommentCreatePayload(BaseModel): + content: str = Field(..., description="Comment content") + position_x: float = Field(..., description="Comment X position") + position_y: float = Field(..., description="Comment Y position") + mentioned_user_ids: list[str] = Field(default_factory=list, description="Mentioned user IDs") + + +class WorkflowCommentUpdatePayload(BaseModel): + content: str = Field(..., description="Comment content") + position_x: float | None = Field(default=None, description="Comment X position") + position_y: float | None = Field(default=None, description="Comment Y position") + mentioned_user_ids: list[str] | None = Field( + default=None, + description="Mentioned user IDs. Omit to keep existing mentions.", + ) + + +class WorkflowCommentReplyPayload(BaseModel): + content: str = Field(..., description="Reply content") + mentioned_user_ids: list[str] = Field(default_factory=list, description="Mentioned user IDs") + + +class WorkflowCommentMentionUsersPayload(BaseModel): + users: list[AccountWithRole] + + +for model in ( + WorkflowCommentCreatePayload, + WorkflowCommentUpdatePayload, + WorkflowCommentReplyPayload, +): + console_ns.schema_model(model.__name__, model.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0)) +register_schema_models(console_ns, AccountWithRole, WorkflowCommentMentionUsersPayload) + +workflow_comment_basic_model = console_ns.model("WorkflowCommentBasic", workflow_comment_basic_fields) +workflow_comment_detail_model = console_ns.model("WorkflowCommentDetail", workflow_comment_detail_fields) +workflow_comment_create_model = console_ns.model("WorkflowCommentCreate", workflow_comment_create_fields) +workflow_comment_update_model = console_ns.model("WorkflowCommentUpdate", workflow_comment_update_fields) +workflow_comment_resolve_model = console_ns.model("WorkflowCommentResolve", workflow_comment_resolve_fields) +workflow_comment_reply_create_model = console_ns.model( + "WorkflowCommentReplyCreate", workflow_comment_reply_create_fields +) +workflow_comment_reply_update_model = console_ns.model( + "WorkflowCommentReplyUpdate", workflow_comment_reply_update_fields +) + + +@console_ns.route("/apps//workflow/comments") +class WorkflowCommentListApi(Resource): + """API for listing and creating workflow comments.""" + + @console_ns.doc("list_workflow_comments") + @console_ns.doc(description="Get all comments for a workflow") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Comments retrieved successfully", workflow_comment_basic_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_basic_model, envelope="data") + def get(self, app_model: App): + """Get all comments for a workflow.""" + comments = WorkflowCommentService.get_comments(tenant_id=current_user.current_tenant_id, app_id=app_model.id) + + return comments + + @console_ns.doc("create_workflow_comment") + @console_ns.doc(description="Create a new workflow comment") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.expect(console_ns.models[WorkflowCommentCreatePayload.__name__]) + @console_ns.response(201, "Comment created successfully", workflow_comment_create_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_create_model) + @edit_permission_required + def post(self, app_model: App): + """Create a new workflow comment.""" + payload = WorkflowCommentCreatePayload.model_validate(console_ns.payload or {}) + + result = WorkflowCommentService.create_comment( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + created_by=current_user.id, + content=payload.content, + position_x=payload.position_x, + position_y=payload.position_y, + mentioned_user_ids=payload.mentioned_user_ids, + ) + + return result, 201 + + +@console_ns.route("/apps//workflow/comments/") +class WorkflowCommentDetailApi(Resource): + """API for managing individual workflow comments.""" + + @console_ns.doc("get_workflow_comment") + @console_ns.doc(description="Get a specific workflow comment") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) + @console_ns.response(200, "Comment retrieved successfully", workflow_comment_detail_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_detail_model) + def get(self, app_model: App, comment_id: str): + """Get a specific workflow comment.""" + comment = WorkflowCommentService.get_comment( + tenant_id=current_user.current_tenant_id, app_id=app_model.id, comment_id=comment_id + ) + + return comment + + @console_ns.doc("update_workflow_comment") + @console_ns.doc(description="Update a workflow comment") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) + @console_ns.expect(console_ns.models[WorkflowCommentUpdatePayload.__name__]) + @console_ns.response(200, "Comment updated successfully", workflow_comment_update_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_update_model) + @edit_permission_required + def put(self, app_model: App, comment_id: str): + """Update a workflow comment.""" + payload = WorkflowCommentUpdatePayload.model_validate(console_ns.payload or {}) + + result = WorkflowCommentService.update_comment( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + comment_id=comment_id, + user_id=current_user.id, + content=payload.content, + position_x=payload.position_x, + position_y=payload.position_y, + mentioned_user_ids=payload.mentioned_user_ids, + ) + + return result + + @console_ns.doc("delete_workflow_comment") + @console_ns.doc(description="Delete a workflow comment") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) + @console_ns.response(204, "Comment deleted successfully") + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @edit_permission_required + def delete(self, app_model: App, comment_id: str): + """Delete a workflow comment.""" + WorkflowCommentService.delete_comment( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + comment_id=comment_id, + user_id=current_user.id, + ) + + return {"result": "success"}, 204 + + +@console_ns.route("/apps//workflow/comments//resolve") +class WorkflowCommentResolveApi(Resource): + """API for resolving and reopening workflow comments.""" + + @console_ns.doc("resolve_workflow_comment") + @console_ns.doc(description="Resolve a workflow comment") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) + @console_ns.response(200, "Comment resolved successfully", workflow_comment_resolve_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_resolve_model) + @edit_permission_required + def post(self, app_model: App, comment_id: str): + """Resolve a workflow comment.""" + comment = WorkflowCommentService.resolve_comment( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + comment_id=comment_id, + user_id=current_user.id, + ) + + return comment + + +@console_ns.route("/apps//workflow/comments//replies") +class WorkflowCommentReplyApi(Resource): + """API for managing comment replies.""" + + @console_ns.doc("create_workflow_comment_reply") + @console_ns.doc(description="Add a reply to a workflow comment") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID"}) + @console_ns.expect(console_ns.models[WorkflowCommentReplyPayload.__name__]) + @console_ns.response(201, "Reply created successfully", workflow_comment_reply_create_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_reply_create_model) + @edit_permission_required + def post(self, app_model: App, comment_id: str): + """Add a reply to a workflow comment.""" + # Validate comment access first + WorkflowCommentService.validate_comment_access( + comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id + ) + + payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) + + result = WorkflowCommentService.create_reply( + comment_id=comment_id, + content=payload.content, + created_by=current_user.id, + mentioned_user_ids=payload.mentioned_user_ids, + ) + + return result, 201 + + +@console_ns.route("/apps//workflow/comments//replies/") +class WorkflowCommentReplyDetailApi(Resource): + """API for managing individual comment replies.""" + + @console_ns.doc("update_workflow_comment_reply") + @console_ns.doc(description="Update a comment reply") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID", "reply_id": "Reply ID"}) + @console_ns.expect(console_ns.models[WorkflowCommentReplyPayload.__name__]) + @console_ns.response(200, "Reply updated successfully", workflow_comment_reply_update_model) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @marshal_with(workflow_comment_reply_update_model) + @edit_permission_required + def put(self, app_model: App, comment_id: str, reply_id: str): + """Update a comment reply.""" + # Validate comment access first + WorkflowCommentService.validate_comment_access( + comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id + ) + + payload = WorkflowCommentReplyPayload.model_validate(console_ns.payload or {}) + + reply = WorkflowCommentService.update_reply( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + comment_id=comment_id, + reply_id=reply_id, + user_id=current_user.id, + content=payload.content, + mentioned_user_ids=payload.mentioned_user_ids, + ) + + return reply + + @console_ns.doc("delete_workflow_comment_reply") + @console_ns.doc(description="Delete a comment reply") + @console_ns.doc(params={"app_id": "Application ID", "comment_id": "Comment ID", "reply_id": "Reply ID"}) + @console_ns.response(204, "Reply deleted successfully") + @login_required + @setup_required + @account_initialization_required + @get_app_model() + @edit_permission_required + def delete(self, app_model: App, comment_id: str, reply_id: str): + """Delete a comment reply.""" + # Validate comment access first + WorkflowCommentService.validate_comment_access( + comment_id=comment_id, tenant_id=current_user.current_tenant_id, app_id=app_model.id + ) + + WorkflowCommentService.delete_reply( + tenant_id=current_user.current_tenant_id, + app_id=app_model.id, + comment_id=comment_id, + reply_id=reply_id, + user_id=current_user.id, + ) + + return {"result": "success"}, 204 + + +@console_ns.route("/apps//workflow/comments/mention-users") +class WorkflowCommentMentionUsersApi(Resource): + """API for getting mentionable users for workflow comments.""" + + @console_ns.doc("workflow_comment_mention_users") + @console_ns.doc(description="Get all users in current tenant for mentions") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response( + 200, "Mentionable users retrieved successfully", console_ns.models[WorkflowCommentMentionUsersPayload.__name__] + ) + @login_required + @setup_required + @account_initialization_required + @get_app_model() + def get(self, app_model: App): + """Get all users in current tenant for mentions.""" + members = TenantService.get_tenant_members(current_user.current_tenant) + users = TypeAdapter(list[AccountWithRole]).validate_python(members, from_attributes=True) + response = WorkflowCommentMentionUsersPayload(users=users) + return response.model_dump(mode="json"), 200 diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 657e794ac4..f6319573e0 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -5,10 +5,6 @@ from typing import Any, TypedDict from flask import Response, request from flask_restx import Resource, fields, marshal, marshal_with -from graphon.file import helpers as file_helpers -from graphon.variables.segment_group import SegmentGroup -from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment -from graphon.variables.types import SegmentType from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker @@ -22,8 +18,13 @@ from controllers.web.error import InvalidArgumentError, NotFoundError from core.app.file_access import DatabaseFileAccessController from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from extensions.ext_database import db +from factories import variable_factory from factories.file_factory import build_from_mapping, build_from_mappings from factories.variable_factory import build_segment_with_type +from graphon.file import helpers as file_helpers +from graphon.variables.segment_group import SegmentGroup +from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment +from graphon.variables.types import SegmentType from libs.login import current_user, login_required from models import App, AppMode from models.workflow import WorkflowDraftVariable @@ -45,6 +46,16 @@ class WorkflowDraftVariableUpdatePayload(BaseModel): value: Any | None = Field(default=None, description="Variable value") +class ConversationVariableUpdatePayload(BaseModel): + conversation_variables: list[dict[str, Any]] = Field( + ..., description="Conversation variables for the draft workflow" + ) + + +class EnvironmentVariableUpdatePayload(BaseModel): + environment_variables: list[dict[str, Any]] = Field(..., description="Environment variables for the draft workflow") + + console_ns.schema_model( WorkflowDraftVariableListQuery.__name__, WorkflowDraftVariableListQuery.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), @@ -53,6 +64,14 @@ console_ns.schema_model( WorkflowDraftVariableUpdatePayload.__name__, WorkflowDraftVariableUpdatePayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), ) +console_ns.schema_model( + ConversationVariableUpdatePayload.__name__, + ConversationVariableUpdatePayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), +) +console_ns.schema_model( + EnvironmentVariableUpdatePayload.__name__, + EnvironmentVariableUpdatePayload.model_json_schema(ref_template=DEFAULT_REF_TEMPLATE_SWAGGER_2_0), +) def _convert_values_to_json_serializable_object(value: Segment): @@ -510,6 +529,34 @@ class ConversationVariableCollectionApi(Resource): db.session.commit() return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID) + @console_ns.expect(console_ns.models[ConversationVariableUpdatePayload.__name__]) + @console_ns.doc("update_conversation_variables") + @console_ns.doc(description="Update conversation variables for workflow draft") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Conversation variables updated successfully") + @setup_required + @login_required + @account_initialization_required + @edit_permission_required + @get_app_model(mode=AppMode.ADVANCED_CHAT) + def post(self, app_model: App): + payload = ConversationVariableUpdatePayload.model_validate(console_ns.payload or {}) + + workflow_service = WorkflowService() + + conversation_variables_list = payload.conversation_variables + conversation_variables = [ + variable_factory.build_conversation_variable_from_mapping(obj) for obj in conversation_variables_list + ] + + workflow_service.update_draft_workflow_conversation_variables( + app_model=app_model, + account=current_user, + conversation_variables=conversation_variables, + ) + + return {"result": "success"} + @console_ns.route("/apps//workflows/draft/system-variables") class SystemVariableCollectionApi(Resource): @@ -561,3 +608,31 @@ class EnvironmentVariableCollectionApi(Resource): ) return {"items": env_vars_list} + + @console_ns.expect(console_ns.models[EnvironmentVariableUpdatePayload.__name__]) + @console_ns.doc("update_environment_variables") + @console_ns.doc(description="Update environment variables for workflow draft") + @console_ns.doc(params={"app_id": "Application ID"}) + @console_ns.response(200, "Environment variables updated successfully") + @setup_required + @login_required + @account_initialization_required + @edit_permission_required + @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) + def post(self, app_model: App): + payload = EnvironmentVariableUpdatePayload.model_validate(console_ns.payload or {}) + + workflow_service = WorkflowService() + + environment_variables_list = payload.environment_variables + environment_variables = [ + variable_factory.build_environment_variable_from_mapping(obj) for obj in environment_variables_list + ] + + workflow_service.update_draft_workflow_environment_variables( + app_model=app_model, + account=current_user, + environment_variables=environment_variables, + ) + + return {"result": "success"} diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index a1a075be71..6748d95d6b 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -3,8 +3,6 @@ from typing import Literal, TypedDict, cast from flask import request from flask_restx import Resource, fields, marshal_with -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus from pydantic import BaseModel, Field, field_validator from sqlalchemy import select from sqlalchemy.orm import sessionmaker @@ -28,6 +26,8 @@ from fields.workflow_run_fields import ( workflow_run_node_execution_list_fields, workflow_run_pagination_fields, ) +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus from libs.archive_storage import ArchiveStorageNotConfiguredError, get_archive_storage from libs.custom_inputs import time_duration from libs.helper import uuid_value diff --git a/api/controllers/console/auth/oauth_server.py b/api/controllers/console/auth/oauth_server.py index b55cda4244..727428c8e7 100644 --- a/api/controllers/console/auth/oauth_server.py +++ b/api/controllers/console/auth/oauth_server.py @@ -5,11 +5,11 @@ from typing import Concatenate from flask import jsonify, request from flask.typing import ResponseReturnValue from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel from werkzeug.exceptions import BadRequest, NotFound from controllers.console.wraps import account_initialization_required, setup_required +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models import Account from models.model import OAuthProviderApp diff --git a/api/controllers/console/datasets/datasets.py b/api/controllers/console/datasets/datasets.py index 14ca27acbd..0b493d2c71 100644 --- a/api/controllers/console/datasets/datasets.py +++ b/api/controllers/console/datasets/datasets.py @@ -4,7 +4,6 @@ from urllib.parse import quote from flask import Response, request from flask_restx import Resource, fields, marshal, marshal_with -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field, field_validator from sqlalchemy import func, select from sqlalchemy.orm import Session @@ -54,6 +53,7 @@ from fields.dataset_fields import ( weighted_score_fields, ) from fields.document_fields import document_status_fields +from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_account_with_tenant, login_required from models import ApiToken, Dataset, Document, DocumentSegment, EvaluationRun, EvaluationTargetType, UploadFile from models.dataset import DatasetPermission, DatasetPermissionEnum diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index 98d4ad9412..3372a967d9 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -3,20 +3,19 @@ import logging from argparse import ArgumentTypeError from collections.abc import Sequence from contextlib import ExitStack +from datetime import datetime from typing import Any, Literal, cast import sqlalchemy as sa from flask import request, send_file -from flask_restx import Resource, fields, marshal, marshal_with -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from pydantic import BaseModel, Field +from flask_restx import Resource, marshal +from pydantic import BaseModel, Field, field_validator from sqlalchemy import asc, desc, func, select from werkzeug.exceptions import Forbidden, NotFound import services from controllers.common.controller_schemas import DocumentBatchDownloadZipPayload -from controllers.common.schema import get_or_create_model, register_schema_models +from controllers.common.schema import register_schema_models from controllers.console import console_ns from core.errors.error import ( LLMBadRequestError, @@ -31,14 +30,14 @@ from core.rag.extractor.entity.datasource_type import DatasourceType from core.rag.extractor.entity.extract_setting import ExtractSetting, NotionInfo, WebsiteInfo from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db -from fields.dataset_fields import dataset_fields +from fields.base import ResponseModel from fields.document_fields import ( - dataset_and_document_fields, document_fields, - document_metadata_fields, document_status_fields, document_with_segments_fields, ) +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from libs.datetime_utils import naive_utc_now from libs.login import current_account_with_tenant, login_required from models import DatasetProcessRule, Document, DocumentSegment, UploadFile @@ -72,27 +71,100 @@ from ..wraps import ( logger = logging.getLogger(__name__) -# Register models for flask_restx to avoid dict type issues in Swagger -dataset_model = get_or_create_model("Dataset", dataset_fields) +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value -document_metadata_model = get_or_create_model("DocumentMetadata", document_metadata_fields) -document_fields_copy = document_fields.copy() -document_fields_copy["doc_metadata"] = fields.List( - fields.Nested(document_metadata_model), attribute="doc_metadata_details" -) -document_model = get_or_create_model("Document", document_fields_copy) +def _normalize_enum(value: Any) -> Any: + if isinstance(value, str) or value is None: + return value + return getattr(value, "value", value) -document_with_segments_fields_copy = document_with_segments_fields.copy() -document_with_segments_fields_copy["doc_metadata"] = fields.List( - fields.Nested(document_metadata_model), attribute="doc_metadata_details" -) -document_with_segments_model = get_or_create_model("DocumentWithSegments", document_with_segments_fields_copy) -dataset_and_document_fields_copy = dataset_and_document_fields.copy() -dataset_and_document_fields_copy["dataset"] = fields.Nested(dataset_model) -dataset_and_document_fields_copy["documents"] = fields.List(fields.Nested(document_model)) -dataset_and_document_model = get_or_create_model("DatasetAndDocument", dataset_and_document_fields_copy) +class DatasetResponse(ResponseModel): + id: str + name: str + description: str | None = None + permission: str | None = None + data_source_type: str | None = None + indexing_technique: str | None = None + created_by: str | None = None + created_at: int | None = None + + @field_validator("data_source_type", "indexing_technique", mode="before") + @classmethod + def _normalize_enum_fields(cls, value: Any) -> Any: + return _normalize_enum(value) + + @field_validator("created_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class DocumentMetadataResponse(ResponseModel): + id: str + name: str + type: str + value: str | None = None + + +class DocumentResponse(ResponseModel): + id: str + position: int | None = None + data_source_type: str | None = None + data_source_info: Any = Field(default=None, validation_alias="data_source_info_dict") + data_source_detail_dict: Any = None + dataset_process_rule_id: str | None = None + name: str + created_from: str | None = None + created_by: str | None = None + created_at: int | None = None + tokens: int | None = None + indexing_status: str | None = None + error: str | None = None + enabled: bool | None = None + disabled_at: int | None = None + disabled_by: str | None = None + archived: bool | None = None + display_status: str | None = None + word_count: int | None = None + hit_count: int | None = None + doc_form: str | None = None + doc_metadata: list[DocumentMetadataResponse] = Field(default_factory=list, validation_alias="doc_metadata_details") + summary_index_status: str | None = None + need_summary: bool | None = None + + @field_validator("data_source_type", "indexing_status", "display_status", "doc_form", mode="before") + @classmethod + def _normalize_enum_fields(cls, value: Any) -> Any: + return _normalize_enum(value) + + @field_validator("doc_metadata", mode="before") + @classmethod + def _normalize_doc_metadata(cls, value: Any) -> list[Any]: + if value is None: + return [] + return value + + @field_validator("created_at", "disabled_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class DocumentWithSegmentsResponse(DocumentResponse): + process_rule_dict: Any = None + completed_segments: int | None = None + total_segments: int | None = None + + +class DatasetAndDocumentResponse(ResponseModel): + dataset: DatasetResponse + documents: list[DocumentResponse] + batch: str class DocumentRetryPayload(BaseModel): @@ -107,6 +179,11 @@ class GenerateSummaryPayload(BaseModel): document_list: list[str] +class DocumentMetadataUpdatePayload(BaseModel): + doc_type: str | None = None + doc_metadata: Any = None + + class DocumentDatasetListParam(BaseModel): page: int = Field(1, title="Page", description="Page number.") limit: int = Field(20, title="Limit", description="Page size.") @@ -124,7 +201,13 @@ register_schema_models( DocumentRetryPayload, DocumentRenamePayload, GenerateSummaryPayload, + DocumentMetadataUpdatePayload, DocumentBatchDownloadZipPayload, + DatasetResponse, + DocumentMetadataResponse, + DocumentResponse, + DocumentWithSegmentsResponse, + DatasetAndDocumentResponse, ) @@ -357,10 +440,10 @@ class DatasetDocumentListApi(Resource): @setup_required @login_required @account_initialization_required - @marshal_with(dataset_and_document_model) @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") @console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) + @console_ns.response(200, "Documents created successfully", console_ns.models[DatasetAndDocumentResponse.__name__]) def post(self, dataset_id): current_user, _ = current_account_with_tenant() dataset_id = str(dataset_id) @@ -398,7 +481,9 @@ class DatasetDocumentListApi(Resource): except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() - return {"dataset": dataset, "documents": documents, "batch": batch} + return DatasetAndDocumentResponse.model_validate( + {"dataset": dataset, "documents": documents, "batch": batch}, from_attributes=True + ).model_dump(mode="json") @setup_required @login_required @@ -426,12 +511,13 @@ class DatasetInitApi(Resource): @console_ns.doc("init_dataset") @console_ns.doc(description="Initialize dataset with documents") @console_ns.expect(console_ns.models[KnowledgeConfig.__name__]) - @console_ns.response(201, "Dataset initialized successfully", dataset_and_document_model) + @console_ns.response( + 201, "Dataset initialized successfully", console_ns.models[DatasetAndDocumentResponse.__name__] + ) @console_ns.response(400, "Invalid request parameters") @setup_required @login_required @account_initialization_required - @marshal_with(dataset_and_document_model) @cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_rate_limit_check("knowledge") def post(self): @@ -479,9 +565,9 @@ class DatasetInitApi(Resource): except ModelCurrentlyNotSupportError: raise ProviderModelCurrentlyNotSupportError() - response = {"dataset": dataset, "documents": documents, "batch": batch} - - return response + return DatasetAndDocumentResponse.model_validate( + {"dataset": dataset, "documents": documents, "batch": batch}, from_attributes=True + ).model_dump(mode="json") @console_ns.route("/datasets//documents//indexing-estimate") @@ -988,15 +1074,7 @@ class DocumentMetadataApi(DocumentResource): @console_ns.doc("update_document_metadata") @console_ns.doc(description="Update document metadata") @console_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) - @console_ns.expect( - console_ns.model( - "UpdateDocumentMetadataRequest", - { - "doc_type": fields.String(description="Document type"), - "doc_metadata": fields.Raw(description="Document metadata"), - }, - ) - ) + @console_ns.expect(console_ns.models[DocumentMetadataUpdatePayload.__name__]) @console_ns.response(200, "Document metadata updated successfully") @console_ns.response(404, "Document not found") @console_ns.response(403, "Permission denied") @@ -1009,10 +1087,10 @@ class DocumentMetadataApi(DocumentResource): document_id = str(document_id) document = self.get_document(dataset_id, document_id) - req_data = request.get_json() + req_data = DocumentMetadataUpdatePayload.model_validate(request.get_json() or {}) - doc_type = req_data.get("doc_type") - doc_metadata = req_data.get("doc_metadata") + doc_type = req_data.doc_type + doc_metadata = req_data.doc_metadata # The role of the current user in the ta table must be admin, owner, dataset_operator, or editor if not current_user.is_dataset_editor: @@ -1194,7 +1272,7 @@ class DocumentRenameApi(DocumentResource): @setup_required @login_required @account_initialization_required - @marshal_with(document_model) + @console_ns.response(200, "Document renamed successfully", console_ns.models[DocumentResponse.__name__]) @console_ns.expect(console_ns.models[DocumentRenamePayload.__name__]) def post(self, dataset_id, document_id): # The role of the current user in the ta table must be admin, owner, editor, or dataset_operator @@ -1212,7 +1290,7 @@ class DocumentRenameApi(DocumentResource): except services.errors.document.DocumentIndexingError: raise DocumentIndexingError("Cannot delete document during indexing.") - return document + return DocumentResponse.model_validate(document, from_attributes=True).model_dump(mode="json") @console_ns.route("/datasets//documents//website-sync") diff --git a/api/controllers/console/datasets/datasets_segments.py b/api/controllers/console/datasets/datasets_segments.py index 354c299bef..2647bb1f5a 100644 --- a/api/controllers/console/datasets/datasets_segments.py +++ b/api/controllers/console/datasets/datasets_segments.py @@ -2,7 +2,6 @@ import uuid from flask import request from flask_restx import Resource, marshal -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import String, cast, func, or_, select from sqlalchemy.dialects.postgresql import JSONB @@ -32,6 +31,7 @@ from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db from extensions.ext_redis import redis_client from fields.segment_fields import child_chunk_fields, segment_fields +from graphon.model_runtime.entities.model_entities import ModelType from libs.helper import escape_like_pattern from libs.login import current_account_with_tenant, login_required from models.dataset import ChildChunk, DocumentSegment diff --git a/api/controllers/console/datasets/hit_testing_base.py b/api/controllers/console/datasets/hit_testing_base.py index 8fb3699849..699fa599c8 100644 --- a/api/controllers/console/datasets/hit_testing_base.py +++ b/api/controllers/console/datasets/hit_testing_base.py @@ -2,7 +2,6 @@ import logging from typing import Any from flask_restx import marshal -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field from werkzeug.exceptions import Forbidden, InternalServerError, NotFound @@ -21,6 +20,7 @@ from core.errors.error import ( QuotaExceededError, ) from fields.hit_testing_fields import hit_testing_record_fields +from graphon.model_runtime.errors.invoke import InvokeError from libs.login import current_user from models.account import Account from services.dataset_service import DatasetService diff --git a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py index bdf83b991e..fd0a8b33bc 100644 --- a/api/controllers/console/datasets/rag_pipeline/datasource_auth.py +++ b/api/controllers/console/datasets/rag_pipeline/datasource_auth.py @@ -2,8 +2,6 @@ from typing import Any from flask import make_response, redirect, request from flask_restx import Resource -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from werkzeug.exceptions import Forbidden, NotFound @@ -12,6 +10,8 @@ from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required from core.plugin.impl.oauth import OAuthHandler +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models.provider_ids import DatasourceProviderID from services.datasource_provider_service import DatasourceProviderService diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index 3549f9542d..b31d73f27d 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -4,7 +4,6 @@ from typing import Any, NoReturn from flask import Response, request from flask_restx import Resource, marshal, marshal_with -from graphon.variables.types import SegmentType from pydantic import BaseModel, Field from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden @@ -28,6 +27,7 @@ from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTE from extensions.ext_database import db from factories.file_factory import build_from_mapping, build_from_mappings from factories.variable_factory import build_segment_with_type +from graphon.variables.types import SegmentType from libs.login import current_user, login_required from models import Account from models.dataset import Pipeline diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index a8077d9eb0..ee146e8287 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -4,7 +4,6 @@ from typing import Any, Literal, cast from flask import abort, request from flask_restx import Resource, marshal_with # type: ignore -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, ValidationError from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -41,6 +40,7 @@ from core.app.apps.pipeline.pipeline_generator import PipelineGenerator from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db from factories import variable_factory +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs import helper from libs.helper import TimestampField, UUIDStrOrEmpty from libs.login import current_account_with_tenant, current_user, login_required diff --git a/api/controllers/console/explore/audio.py b/api/controllers/console/explore/audio.py index a37077af42..ab660d9dc3 100644 --- a/api/controllers/console/explore/audio.py +++ b/api/controllers/console/explore/audio.py @@ -1,7 +1,6 @@ import logging from flask import request -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError import services @@ -20,6 +19,7 @@ from controllers.console.app.error import ( ) from controllers.console.explore.wraps import InstalledAppResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from services.audio_service import AudioService from services.errors.audio import ( AudioTooLargeServiceError, diff --git a/api/controllers/console/explore/completion.py b/api/controllers/console/explore/completion.py index eacd7332fe..ccdccceaa6 100644 --- a/api/controllers/console/explore/completion.py +++ b/api/controllers/console/explore/completion.py @@ -2,7 +2,6 @@ import logging from typing import Any, Literal from uuid import UUID -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -26,6 +25,7 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_database import db +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.datetime_utils import naive_utc_now from libs.login import current_user diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index 7dbb7220f4..2d9a997fbf 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -4,7 +4,6 @@ from typing import Any from flask import request from flask_restx import Resource -from graphon.file import helpers as file_helpers from pydantic import BaseModel, Field, computed_field, field_validator from sqlalchemy import and_, select from werkzeug.exceptions import BadRequest, Forbidden, NotFound @@ -15,6 +14,7 @@ from controllers.console.explore.wraps import InstalledAppResource from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check from extensions.ext_database import db from fields.base import ResponseModel +from graphon.file import helpers as file_helpers from libs.datetime_utils import naive_utc_now from libs.login import current_account_with_tenant, login_required from models import App, InstalledApp, RecommendedApp diff --git a/api/controllers/console/explore/message.py b/api/controllers/console/explore/message.py index 64d55d7ca3..209667d1d0 100644 --- a/api/controllers/console/explore/message.py +++ b/api/controllers/console/explore/message.py @@ -2,7 +2,6 @@ import logging from typing import Literal from flask import request -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound @@ -25,6 +24,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from fields.conversation_fields import ResultResponse from fields.message_fields import MessageInfiniteScrollPagination, MessageListItem, SuggestedQuestionsResponse +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.login import current_account_with_tenant from models.enums import FeedbackRating diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index 0a3595454a..1456301a24 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -3,8 +3,6 @@ from typing import Any, Literal, cast from flask import request from flask_restx import Resource, fields, marshal, marshal_with -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel from sqlalchemy import select from werkzeug.exceptions import Forbidden, InternalServerError, NotFound @@ -61,6 +59,8 @@ from fields.workflow_fields import ( workflow_fields, workflow_partial_fields, ) +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from libs.login import current_user diff --git a/api/controllers/console/explore/workflow.py b/api/controllers/console/explore/workflow.py index da88de6776..438cce4fd8 100644 --- a/api/controllers/console/explore/workflow.py +++ b/api/controllers/console/explore/workflow.py @@ -1,7 +1,5 @@ import logging -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError from controllers.common.controller_schemas import WorkflowRunPayload @@ -23,6 +21,8 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_redis import redis_client +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.login import current_account_with_tenant from models.model import AppMode, InstalledApp diff --git a/api/controllers/console/remote_files.py b/api/controllers/console/remote_files.py index 551c86fd82..2a46d2250a 100644 --- a/api/controllers/console/remote_files.py +++ b/api/controllers/console/remote_files.py @@ -2,7 +2,6 @@ import urllib.parse import httpx from flask_restx import Resource -from graphon.file import helpers as file_helpers from pydantic import BaseModel, Field import services @@ -16,6 +15,7 @@ from controllers.console import console_ns from core.helper import ssrf_proxy from extensions.ext_database import db from fields.file_fields import FileWithSignedUrl, RemoteFileInfo +from graphon.file import helpers as file_helpers from libs.login import current_account_with_tenant, login_required from services.file_service import FileService diff --git a/api/controllers/console/socketio/__init__.py b/api/controllers/console/socketio/__init__.py new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/api/controllers/console/socketio/__init__.py @@ -0,0 +1 @@ + diff --git a/api/controllers/console/socketio/workflow.py b/api/controllers/console/socketio/workflow.py new file mode 100644 index 0000000000..b4f03593fd --- /dev/null +++ b/api/controllers/console/socketio/workflow.py @@ -0,0 +1,108 @@ +import logging +from collections.abc import Callable +from typing import cast + +from flask import Request as FlaskRequest + +from extensions.ext_socketio import sio +from libs.passport import PassportService +from libs.token import extract_access_token +from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository +from services.account_service import AccountService +from services.workflow_collaboration_service import WorkflowCollaborationService + +repository = WorkflowCollaborationRepository() +collaboration_service = WorkflowCollaborationService(repository, sio) + + +def _sio_on(event: str) -> Callable[[Callable[..., object]], Callable[..., object]]: + return cast(Callable[[Callable[..., object]], Callable[..., object]], sio.on(event)) + + +@_sio_on("connect") +def socket_connect(sid, environ, auth): + """ + WebSocket connect event, do authentication here. + """ + try: + request_environ = FlaskRequest(environ) + token = extract_access_token(request_environ) + except Exception: + logging.exception("Failed to extract token") + token = None + + if not token: + logging.warning("Socket connect rejected: missing token (sid=%s)", sid) + return False + + try: + decoded = PassportService().verify(token) + user_id = decoded.get("user_id") + if not user_id: + logging.warning("Socket connect rejected: missing user_id (sid=%s)", sid) + return False + + with sio.app.app_context(): + user = AccountService.load_logged_in_account(account_id=user_id) + if not user: + logging.warning("Socket connect rejected: user not found (user_id=%s, sid=%s)", user_id, sid) + return False + if not user.has_edit_permission: + logging.warning("Socket connect rejected: no edit permission (user_id=%s, sid=%s)", user_id, sid) + return False + + collaboration_service.save_socket_identity(sid, user) + return True + + except Exception: + logging.exception("Socket authentication failed") + return False + + +@_sio_on("user_connect") +def handle_user_connect(sid, data): + """ + Handle user connect event. Each session (tab) is treated as an independent collaborator. + """ + workflow_id = data.get("workflow_id") + if not workflow_id: + return {"msg": "workflow_id is required"}, 400 + + result = collaboration_service.authorize_and_join_workflow_room(workflow_id, sid) + if not result: + return {"msg": "unauthorized"}, 401 + + user_id, is_leader = result + return {"msg": "connected", "user_id": user_id, "sid": sid, "isLeader": is_leader} + + +@_sio_on("disconnect") +def handle_disconnect(sid): + """ + Handle session disconnect event. Remove the specific session from online users. + """ + collaboration_service.disconnect_session(sid) + + +@_sio_on("collaboration_event") +def handle_collaboration_event(sid, data): + """ + Handle general collaboration events, include: + 1. mouse_move + 2. vars_and_features_update + 3. sync_request (ask leader to update graph) + 4. app_state_update + 5. mcp_server_update + 6. workflow_update + 7. comments_update + 8. node_panel_presence + """ + return collaboration_service.relay_collaboration_event(sid, data) + + +@_sio_on("graph_event") +def handle_graph_event(sid, data): + """ + Handle graph events - simple broadcast relay. + """ + return collaboration_service.relay_graph_event(sid, data) diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py index 39b84d3869..614bf03ea5 100644 --- a/api/controllers/console/tag/tags.py +++ b/api/controllers/console/tag/tags.py @@ -1,13 +1,14 @@ from typing import Literal from flask import request -from flask_restx import Namespace, Resource, fields, marshal_with -from pydantic import BaseModel, Field +from flask_restx import Resource +from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required +from fields.base import ResponseModel from libs.login import current_account_with_tenant, login_required from models.enums import TagType from services.tag_service import ( @@ -18,17 +19,6 @@ from services.tag_service import ( UpdateTagPayload, ) -dataset_tag_fields = { - "id": fields.String, - "name": fields.String, - "type": fields.String, - "binding_count": fields.String, -} - - -def build_dataset_tag_fields(api_or_ns: Namespace): - return api_or_ns.model("DataSetTag", dataset_tag_fields) - class TagBasePayload(BaseModel): name: str = Field(description="Tag name", min_length=1, max_length=50) @@ -52,12 +42,36 @@ class TagListQueryParam(BaseModel): keyword: str | None = Field(None, description="Search keyword") +class TagResponse(ResponseModel): + id: str + name: str + type: str | None = None + binding_count: str | None = None + + @field_validator("type", mode="before") + @classmethod + def normalize_type(cls, value: TagType | str | None) -> str | None: + if value is None: + return None + if isinstance(value, TagType): + return value.value + return value + + @field_validator("binding_count", mode="before") + @classmethod + def normalize_binding_count(cls, value: int | str | None) -> str | None: + if value is None: + return None + return str(value) + + register_schema_models( console_ns, TagBasePayload, TagBindingPayload, TagBindingRemovePayload, TagListQueryParam, + TagResponse, ) @@ -69,14 +83,18 @@ class TagListApi(Resource): @console_ns.doc( params={"type": 'Tag type filter. Can be "knowledge" or "app".', "keyword": "Search keyword for tag name."} ) - @marshal_with(dataset_tag_fields) + @console_ns.doc(responses={200: ("Success", [console_ns.models[TagResponse.__name__]])}) def get(self): _, current_tenant_id = current_account_with_tenant() raw_args = request.args.to_dict() param = TagListQueryParam.model_validate(raw_args) tags = TagService.get_tags(param.type, current_tenant_id, param.keyword) - return tags, 200 + serialized_tags = [ + TagResponse.model_validate(tag, from_attributes=True).model_dump(mode="json") for tag in tags + ] + + return serialized_tags, 200 @console_ns.expect(console_ns.models[TagBasePayload.__name__]) @setup_required @@ -91,7 +109,9 @@ class TagListApi(Resource): payload = TagBasePayload.model_validate(console_ns.payload or {}) tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type)) - response = {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} + response = TagResponse.model_validate( + {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} + ).model_dump(mode="json") return response, 200 @@ -114,7 +134,9 @@ class TagUpdateDeleteApi(Resource): binding_count = TagService.get_tag_binding_count(tag_id) - response = {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": binding_count} + response = TagResponse.model_validate( + {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": binding_count} + ).model_dump(mode="json") return response, 200 diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index 582c38052e..44404005b2 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -39,6 +39,7 @@ from controllers.console.wraps import ( from extensions.ext_database import db from fields.base import ResponseModel from fields.member_fields import Account as AccountResponse +from graphon.file import helpers as file_helpers from libs.datetime_utils import naive_utc_now from libs.helper import EmailStr, extract_remote_ip, timezone from libs.login import current_account_with_tenant, login_required @@ -75,6 +76,10 @@ class AccountAvatarPayload(BaseModel): avatar: str +class AccountAvatarQuery(BaseModel): + avatar: str = Field(..., description="Avatar file ID") + + class AccountInterfaceLanguagePayload(BaseModel): interface_language: str @@ -160,6 +165,7 @@ def reg(cls: type[BaseModel]): reg(AccountInitPayload) reg(AccountNamePayload) reg(AccountAvatarPayload) +reg(AccountAvatarQuery) reg(AccountInterfaceLanguagePayload) reg(AccountInterfaceThemePayload) reg(AccountTimezonePayload) @@ -309,6 +315,18 @@ class AccountNameApi(Resource): @console_ns.route("/account/avatar") class AccountAvatarApi(Resource): + @console_ns.expect(console_ns.models[AccountAvatarQuery.__name__]) + @console_ns.doc("get_account_avatar") + @console_ns.doc(description="Get account avatar url") + @setup_required + @login_required + @account_initialization_required + def get(self): + args = AccountAvatarQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + + avatar_url = file_helpers.get_signed_file_url(args.avatar) + return {"avatar_url": avatar_url} + @console_ns.expect(console_ns.models[AccountAvatarPayload.__name__]) @setup_required @login_required diff --git a/api/controllers/console/workspace/agent_providers.py b/api/controllers/console/workspace/agent_providers.py index 3fdcbc4710..764f488755 100644 --- a/api/controllers/console/workspace/agent_providers.py +++ b/api/controllers/console/workspace/agent_providers.py @@ -1,8 +1,8 @@ from flask_restx import Resource, fields -from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from services.agent_service import AgentService diff --git a/api/controllers/console/workspace/endpoint.py b/api/controllers/console/workspace/endpoint.py index b6b9deb1f9..f45b72f390 100644 --- a/api/controllers/console/workspace/endpoint.py +++ b/api/controllers/console/workspace/endpoint.py @@ -2,13 +2,13 @@ from typing import Any from flask import request from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required from core.plugin.impl.exc import PluginPermissionDeniedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from services.plugin.endpoint_service import EndpointService diff --git a/api/controllers/console/workspace/load_balancing_config.py b/api/controllers/console/workspace/load_balancing_config.py index e4cfca9fa4..2a6f37aec8 100644 --- a/api/controllers/console/workspace/load_balancing_config.py +++ b/api/controllers/console/workspace/load_balancing_config.py @@ -1,12 +1,12 @@ from flask_restx import Resource -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, setup_required +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from libs.login import current_account_with_tenant, login_required from models import TenantAccountRole from services.model_load_balancing_service import ModelLoadBalancingService diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index cbb9677309..4b10561fdb 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -3,13 +3,13 @@ from typing import Any, Literal from flask import request, send_file from flask_restx import Resource -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import uuid_value from libs.login import current_account_with_tenant, login_required from services.billing_service import BillingService diff --git a/api/controllers/console/workspace/models.py b/api/controllers/console/workspace/models.py index f8f95304f0..b2d07ff8f9 100644 --- a/api/controllers/console/workspace/models.py +++ b/api/controllers/console/workspace/models.py @@ -3,14 +3,14 @@ from typing import Any, cast from flask import request from flask_restx import Resource -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from controllers.common.schema import register_enum_models, register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import uuid_value from libs.login import current_account_with_tenant, login_required from services.model_load_balancing_service import ModelLoadBalancingService diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index aa674a63b3..b3e344ccea 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -4,7 +4,6 @@ from typing import Any, Literal from flask import request, send_file from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field from werkzeug.datastructures import FileStorage from werkzeug.exceptions import Forbidden @@ -15,6 +14,7 @@ from controllers.console import console_ns from controllers.console.workspace import plugin_permission_required from controllers.console.wraps import account_initialization_required, is_admin_or_owner_required, setup_required from core.plugin.impl.exc import PluginDaemonClientSideError +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_account_with_tenant, login_required from models.account import TenantPluginAutoUpgradeStrategy, TenantPluginPermission from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index c9956501e2..471594f349 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -5,7 +5,6 @@ from urllib.parse import urlparse from flask import make_response, redirect, request, send_file from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, HttpUrl, field_validator, model_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import Forbidden @@ -28,6 +27,7 @@ from core.plugin.entities.plugin_daemon import CredentialType from core.plugin.impl.oauth import OAuthHandler from core.tools.entities.tool_entities import ApiProviderSchemaType, WorkflowToolParameterConfiguration from extensions.ext_database import db +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import alphanumeric, uuid_value from libs.login import current_account_with_tenant, login_required from models.provider_ids import ToolProviderID diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index 7a28a09861..d11b66244f 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -3,7 +3,6 @@ from typing import Any from flask import make_response, redirect, request from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, model_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, Forbidden @@ -16,6 +15,7 @@ from core.plugin.impl.oauth import OAuthHandler from core.trigger.entities.entities import SubscriptionBuilderUpdater from core.trigger.trigger_manager import TriggerManager from extensions.ext_database import db +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.login import current_user, login_required from models.account import Account from models.provider_ids import TriggerProviderID diff --git a/api/controllers/inner_api/plugin/plugin.py b/api/controllers/inner_api/plugin/plugin.py index 83c8fa02fe..72cab3de73 100644 --- a/api/controllers/inner_api/plugin/plugin.py +++ b/api/controllers/inner_api/plugin/plugin.py @@ -1,5 +1,4 @@ from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.console.wraps import setup_required from controllers.inner_api import inner_api_ns @@ -30,6 +29,7 @@ from core.plugin.entities.request import ( ) from core.tools.entities.tool_entities import ToolProviderType from core.tools.signature import get_signed_file_url_for_plugin +from graphon.model_runtime.utils.encoders import jsonable_encoder from libs.helper import length_prefixed_response from models import Account, Tenant from models.model import EndUser diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index a5846e2815..2f309262cb 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -20,10 +20,13 @@ class TenantUserPayload(BaseModel): def get_user(tenant_id: str, user_id: str | None) -> EndUser: """ - Get current user + Get current user. NOTE: user_id is not trusted, it could be maliciously set to any value. - As a result, it could only be considered as an end user id. + As a result, it could only be considered as an end user id. Even when a + concrete end-user ID is supplied, lookups must stay tenant-scoped so one + tenant cannot bind another tenant's user record into the plugin request + context. """ if not user_id: user_id = DefaultEndUserSessionID.DEFAULT_SESSION_ID @@ -42,7 +45,14 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser: .limit(1) ) else: - user_model = session.get(EndUser, user_id) + user_model = session.scalar( + select(EndUser) + .where( + EndUser.id == user_id, + EndUser.tenant_id == tenant_id, + ) + .limit(1) + ) if not user_model: user_model = EndUser( diff --git a/api/controllers/mcp/mcp.py b/api/controllers/mcp/mcp.py index 8066f198bb..f652bbc581 100644 --- a/api/controllers/mcp/mcp.py +++ b/api/controllers/mcp/mcp.py @@ -2,7 +2,6 @@ from typing import Any, Union from flask import Response from flask_restx import Resource -from graphon.variables.input_entities import VariableEntity, VariableEntityType from pydantic import BaseModel, Field, ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -12,6 +11,7 @@ from controllers.mcp import mcp_ns from core.mcp import types as mcp_types from core.mcp.server.streamable_http import handle_mcp_request from extensions.ext_database import db +from graphon.variables.input_entities import VariableEntity, VariableEntityType from libs import helper from models.enums import AppMCPServerStatus from models.model import App, AppMCPServer, AppMode, EndUser diff --git a/api/controllers/service_api/app/audio.py b/api/controllers/service_api/app/audio.py index 907dd1b06d..e818573b8f 100644 --- a/api/controllers/service_api/app/audio.py +++ b/api/controllers/service_api/app/audio.py @@ -2,7 +2,6 @@ import logging from flask import request from flask_restx import Resource -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError import services @@ -22,6 +21,7 @@ from controllers.service_api.app.error import ( ) from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from models.model import App, EndUser from services.audio_service import AudioService from services.errors.audio import ( diff --git a/api/controllers/service_api/app/completion.py b/api/controllers/service_api/app/completion.py index 3142e5118e..31f2797d66 100644 --- a/api/controllers/service_api/app/completion.py +++ b/api/controllers/service_api/app/completion.py @@ -4,7 +4,6 @@ from uuid import UUID from flask import request from flask_restx import Resource -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import BadRequest, InternalServerError, NotFound @@ -29,6 +28,7 @@ from core.errors.error import ( QuotaExceededError, ) from core.helper.trace_id_helper import get_external_trace_id +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import UUIDStrOrEmpty from models.model import App, AppMode, EndUser diff --git a/api/controllers/service_api/app/conversation.py b/api/controllers/service_api/app/conversation.py index 50851aea08..c4353ca7b8 100644 --- a/api/controllers/service_api/app/conversation.py +++ b/api/controllers/service_api/app/conversation.py @@ -3,7 +3,6 @@ from typing import Any, Literal from flask import request from flask_restx import Resource -from graphon.variables.types import SegmentType from pydantic import BaseModel, Field, TypeAdapter, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, NotFound @@ -22,6 +21,7 @@ from fields.conversation_fields import ( ConversationInfiniteScrollPagination, SimpleConversation, ) +from graphon.variables.types import SegmentType from libs.helper import UUIDStrOrEmpty from models.model import App, AppMode, EndUser from services.conversation_service import ConversationService diff --git a/api/controllers/service_api/app/workflow.py b/api/controllers/service_api/app/workflow.py index d5544ff473..cc763fa89c 100644 --- a/api/controllers/service_api/app/workflow.py +++ b/api/controllers/service_api/app/workflow.py @@ -6,9 +6,6 @@ from typing import Literal from dateutil.parser import isoparse from flask import request from flask_restx import Resource, fields -from graphon.enums import WorkflowExecutionStatus -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from sqlalchemy.orm import sessionmaker from werkzeug.exceptions import BadRequest, InternalServerError, NotFound @@ -38,6 +35,9 @@ from extensions.ext_redis import redis_client from fields.base import ResponseModel from fields.end_user_fields import SimpleEndUser from fields.member_fields import SimpleAccount +from graphon.enums import WorkflowExecutionStatus +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.model import App, AppMode, EndUser from models.workflow import WorkflowRun diff --git a/api/controllers/service_api/dataset/dataset.py b/api/controllers/service_api/dataset/dataset.py index fd954be6b1..76519cad0a 100644 --- a/api/controllers/service_api/dataset/dataset.py +++ b/api/controllers/service_api/dataset/dataset.py @@ -2,7 +2,6 @@ from typing import Any, Literal, cast from flask import request from flask_restx import marshal -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field, TypeAdapter, field_validator from werkzeug.exceptions import Forbidden, NotFound @@ -19,6 +18,7 @@ from core.plugin.impl.model_runtime_factory import create_plugin_provider_manage from core.rag.index_processor.constant.index_type import IndexTechniqueType from fields.dataset_fields import dataset_detail_fields from fields.tag_fields import DataSetTag +from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_user from models.account import Account from models.dataset import DatasetPermissionEnum diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index 971b63577c..5992fa7410 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -2,7 +2,6 @@ from typing import Any from flask import request from flask_restx import marshal -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import select from werkzeug.exceptions import NotFound @@ -23,6 +22,7 @@ from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexTechniqueType from extensions.ext_database import db from fields.segment_fields import child_chunk_fields, segment_fields +from graphon.model_runtime.entities.model_entities import ModelType from libs.login import current_account_with_tenant from models.dataset import Dataset from services.dataset_service import DatasetService, DocumentService, SegmentService diff --git a/api/controllers/service_api/workspace/models.py b/api/controllers/service_api/workspace/models.py index c0a6cb0a76..5ac65fc4e6 100644 --- a/api/controllers/service_api/workspace/models.py +++ b/api/controllers/service_api/workspace/models.py @@ -1,9 +1,9 @@ from flask_login import current_user from flask_restx import Resource -from graphon.model_runtime.utils.encoders import jsonable_encoder from controllers.service_api import service_api_ns from controllers.service_api.wraps import validate_dataset_token +from graphon.model_runtime.utils.encoders import jsonable_encoder from services.model_provider_service import ModelProviderService diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py index 0ef4471018..3ad595f1f4 100644 --- a/api/controllers/web/audio.py +++ b/api/controllers/web/audio.py @@ -2,7 +2,6 @@ import logging from flask import request from flask_restx import fields, marshal_with -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import field_validator from werkzeug.exceptions import InternalServerError @@ -22,6 +21,7 @@ from controllers.web.error import ( ) from controllers.web.wraps import WebApiResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from libs.helper import uuid_value from models.model import App from services.audio_service import AudioService diff --git a/api/controllers/web/completion.py b/api/controllers/web/completion.py index e37f9af5f0..0528184d79 100644 --- a/api/controllers/web/completion.py +++ b/api/controllers/web/completion.py @@ -1,7 +1,6 @@ import logging from typing import Any, Literal -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import InternalServerError, NotFound @@ -26,6 +25,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from libs.helper import uuid_value from models.model import AppMode diff --git a/api/controllers/web/message.py b/api/controllers/web/message.py index 39afdd843f..07ecf8035b 100644 --- a/api/controllers/web/message.py +++ b/api/controllers/web/message.py @@ -2,7 +2,6 @@ import logging from typing import Literal from flask import request -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import BaseModel, Field, TypeAdapter from werkzeug.exceptions import InternalServerError, NotFound @@ -24,6 +23,7 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from fields.conversation_fields import ResultResponse from fields.message_fields import SuggestedQuestionsResponse, WebMessageInfiniteScrollPagination, WebMessageListItem +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.enums import FeedbackRating from models.model import AppMode diff --git a/api/controllers/web/remote_files.py b/api/controllers/web/remote_files.py index 38aeccc642..fe31e9d4ac 100644 --- a/api/controllers/web/remote_files.py +++ b/api/controllers/web/remote_files.py @@ -1,7 +1,6 @@ import urllib.parse import httpx -from graphon.file import helpers as file_helpers from pydantic import BaseModel, Field, HttpUrl import services @@ -14,6 +13,7 @@ from controllers.common.errors import ( from core.helper import ssrf_proxy from extensions.ext_database import db from fields.file_fields import FileWithSignedUrl, RemoteFileInfo +from graphon.file import helpers as file_helpers from services.file_service import FileService from ..common.schema import register_schema_models diff --git a/api/controllers/web/workflow.py b/api/controllers/web/workflow.py index 796e090976..98211193a0 100644 --- a/api/controllers/web/workflow.py +++ b/api/controllers/web/workflow.py @@ -1,7 +1,5 @@ import logging -from graphon.graph_engine.manager import GraphEngineManager -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError from controllers.common.controller_schemas import WorkflowRunPayload @@ -24,6 +22,8 @@ from core.errors.error import ( QuotaExceededError, ) from extensions.ext_redis import redis_client +from graphon.graph_engine.manager import GraphEngineManager +from graphon.model_runtime.errors.invoke import InvokeError from libs import helper from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService diff --git a/api/core/agent/base_agent_runner.py b/api/core/agent/base_agent_runner.py index 06c746990d..790602ef5d 100644 --- a/api/core/agent/base_agent_runner.py +++ b/api/core/agent/base_agent_runner.py @@ -4,20 +4,6 @@ import uuid from decimal import Decimal from typing import Union, cast -from graphon.file import file_manager -from graphon.model_runtime.entities import ( - AssistantPromptMessage, - LLMUsage, - PromptMessage, - PromptMessageTool, - SystemPromptMessage, - TextPromptMessageContent, - ToolPromptMessage, - UserPromptMessage, -) -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes -from graphon.model_runtime.entities.model_entities import ModelFeature -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from sqlalchemy import func, select from core.agent.entities import AgentEntity, AgentToolEntity @@ -43,6 +29,20 @@ from core.tools.tool_manager import ToolManager from core.tools.utils.dataset_retriever_tool import DatasetRetrieverTool from extensions.ext_database import db from factories import file_factory +from graphon.file import file_manager +from graphon.model_runtime.entities import ( + AssistantPromptMessage, + LLMUsage, + PromptMessage, + PromptMessageTool, + SystemPromptMessage, + TextPromptMessageContent, + ToolPromptMessage, + UserPromptMessage, +) +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes +from graphon.model_runtime.entities.model_entities import ModelFeature +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from models.enums import CreatorUserRole from models.model import Conversation, Message, MessageAgentThought, MessageFile diff --git a/api/core/agent/cot_agent_runner.py b/api/core/agent/cot_agent_runner.py index f07ac64498..0bc93ad34d 100644 --- a/api/core/agent/cot_agent_runner.py +++ b/api/core/agent/cot_agent_runner.py @@ -4,15 +4,6 @@ from abc import ABC, abstractmethod from collections.abc import Generator, Mapping, Sequence from typing import Any, TypedDict -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - PromptMessage, - PromptMessageTool, - ToolPromptMessage, - UserPromptMessage, -) - from core.agent.base_agent_runner import BaseAgentRunner from core.agent.entities import AgentScratchpadUnit from core.agent.errors import AgentMaxIterationError @@ -24,6 +15,14 @@ from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransfo from core.tools.__base.tool import Tool from core.tools.entities.tool_entities import ToolInvokeMeta from core.tools.tool_engine import ToolEngine +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + PromptMessage, + PromptMessageTool, + ToolPromptMessage, + UserPromptMessage, +) from models.model import Message logger = logging.getLogger(__name__) diff --git a/api/core/agent/cot_chat_agent_runner.py b/api/core/agent/cot_chat_agent_runner.py index 2b2e26987e..a2186be100 100644 --- a/api/core/agent/cot_chat_agent_runner.py +++ b/api/core/agent/cot_chat_agent_runner.py @@ -1,5 +1,6 @@ import json +from core.agent.cot_agent_runner import CotAgentRunner from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -11,8 +12,6 @@ from graphon.model_runtime.entities import ( from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes from graphon.model_runtime.utils.encoders import jsonable_encoder -from core.agent.cot_agent_runner import CotAgentRunner - class CotChatAgentRunner(CotAgentRunner): def _organize_system_prompt(self) -> SystemPromptMessage: diff --git a/api/core/agent/cot_completion_agent_runner.py b/api/core/agent/cot_completion_agent_runner.py index d4c52a8eb1..51a30998ae 100644 --- a/api/core/agent/cot_completion_agent_runner.py +++ b/api/core/agent/cot_completion_agent_runner.py @@ -1,5 +1,6 @@ import json +from core.agent.cot_agent_runner import CotAgentRunner from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, PromptMessage, @@ -8,8 +9,6 @@ from graphon.model_runtime.entities.message_entities import ( ) from graphon.model_runtime.utils.encoders import jsonable_encoder -from core.agent.cot_agent_runner import CotAgentRunner - class CotCompletionAgentRunner(CotAgentRunner): def _organize_instruction_prompt(self) -> str: diff --git a/api/core/agent/fc_agent_runner.py b/api/core/agent/fc_agent_runner.py index fdffde85d0..d38d24d1e7 100644 --- a/api/core/agent/fc_agent_runner.py +++ b/api/core/agent/fc_agent_runner.py @@ -4,6 +4,13 @@ from collections.abc import Generator from copy import deepcopy from typing import Any, Union +from core.agent.base_agent_runner import BaseAgentRunner +from core.agent.errors import AgentMaxIterationError +from core.app.apps.base_app_queue_manager import PublishFrom +from core.app.entities.queue_entities import QueueAgentThoughtEvent, QueueMessageEndEvent, QueueMessageFileEvent +from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform +from core.tools.entities.tool_entities import ToolInvokeMeta +from core.tools.tool_engine import ToolEngine from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -19,14 +26,6 @@ from graphon.model_runtime.entities import ( UserPromptMessage, ) from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes - -from core.agent.base_agent_runner import BaseAgentRunner -from core.agent.errors import AgentMaxIterationError -from core.app.apps.base_app_queue_manager import PublishFrom -from core.app.entities.queue_entities import QueueAgentThoughtEvent, QueueMessageEndEvent, QueueMessageFileEvent -from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform -from core.tools.entities.tool_entities import ToolInvokeMeta -from core.tools.tool_engine import ToolEngine from models.model import Message logger = logging.getLogger(__name__) diff --git a/api/core/agent/output_parser/cot_output_parser.py b/api/core/agent/output_parser/cot_output_parser.py index 8cccd2be6d..f341ca5a0b 100644 --- a/api/core/agent/output_parser/cot_output_parser.py +++ b/api/core/agent/output_parser/cot_output_parser.py @@ -3,9 +3,8 @@ import re from collections.abc import Generator from typing import Any, Union -from graphon.model_runtime.entities.llm_entities import LLMResultChunk - from core.agent.entities import AgentScratchpadUnit +from graphon.model_runtime.entities.llm_entities import LLMResultChunk class CotAgentOutputParser: diff --git a/api/core/app/app_config/easy_ui_based_app/model_config/converter.py b/api/core/app/app_config/easy_ui_based_app/model_config/converter.py index b7dd55632e..dbd7527fc6 100644 --- a/api/core/app/app_config/easy_ui_based_app/model_config/converter.py +++ b/api/core/app/app_config/easy_ui_based_app/model_config/converter.py @@ -1,14 +1,13 @@ from typing import cast -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel - from core.app.app_config.entities import EasyUIBasedAppConfig from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.entities.model_entities import ModelStatus from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel class ModelConfigConverter: diff --git a/api/core/app/app_config/easy_ui_based_app/model_config/manager.py b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py index 9d980e5ca3..02498c23e1 100644 --- a/api/core/app/app_config/easy_ui_based_app/model_config/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/model_config/manager.py @@ -1,10 +1,9 @@ from collections.abc import Mapping from typing import Any -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType - from core.app.app_config.entities import ModelConfigEntity from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType from models.model import AppModelConfigDict from models.provider_ids import ModelProviderID diff --git a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py index 57c6d1c496..4c07445df3 100644 --- a/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/prompt_template/manager.py @@ -1,7 +1,5 @@ from typing import Any -from graphon.model_runtime.entities.message_entities import PromptMessageRole - from core.app.app_config.entities import ( AdvancedChatMessageEntity, AdvancedChatPromptTemplateEntity, @@ -9,6 +7,7 @@ from core.app.app_config.entities import ( PromptTemplateEntity, ) from core.prompt.simple_prompt_transform import ModelMode +from graphon.model_runtime.entities.message_entities import PromptMessageRole from models.model import AppMode, AppModelConfigDict diff --git a/api/core/app/app_config/easy_ui_based_app/variables/manager.py b/api/core/app/app_config/easy_ui_based_app/variables/manager.py index c89e1b3c3d..ddb500cccf 100644 --- a/api/core/app/app_config/easy_ui_based_app/variables/manager.py +++ b/api/core/app/app_config/easy_ui_based_app/variables/manager.py @@ -1,10 +1,9 @@ import re from typing import Any, cast -from graphon.variables.input_entities import VariableEntity, VariableEntityType - from core.app.app_config.entities import ExternalDataVariableEntity from core.external_data_tool.factory import ExternalDataToolFactory +from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import AppModelConfigDict _ALLOWED_VARIABLE_ENTITY_TYPE = frozenset( diff --git a/api/core/app/app_config/entities.py b/api/core/app/app_config/entities.py index 819aca864c..53563dc5da 100644 --- a/api/core/app/app_config/entities.py +++ b/api/core/app/app_config/entities.py @@ -1,14 +1,14 @@ from enum import StrEnum, auto from typing import Any, Literal -from graphon.file import FileUploadConfig -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.message_entities import PromptMessageRole -from graphon.variables.input_entities import VariableEntity as WorkflowVariableEntity from pydantic import BaseModel, Field from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict from core.rag.entities import MetadataFilteringCondition +from graphon.file import FileUploadConfig +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.message_entities import PromptMessageRole +from graphon.variables.input_entities import VariableEntity as WorkflowVariableEntity from models.model import AppMode diff --git a/api/core/app/app_config/features/file_upload/manager.py b/api/core/app/app_config/features/file_upload/manager.py index 959c3868b4..8f20ef2ff9 100644 --- a/api/core/app/app_config/features/file_upload/manager.py +++ b/api/core/app/app_config/features/file_upload/manager.py @@ -1,9 +1,8 @@ from collections.abc import Mapping from typing import Any -from graphon.file import FileUploadConfig - from constants import DEFAULT_FILE_NUMBER_LIMITS +from graphon.file import FileUploadConfig class FileUploadConfigManager: diff --git a/api/core/app/app_config/workflow_ui_based_app/variables/manager.py b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py index 62e0c31d1a..13ace32fd6 100644 --- a/api/core/app/app_config/workflow_ui_based_app/variables/manager.py +++ b/api/core/app/app_config/workflow_ui_based_app/variables/manager.py @@ -1,8 +1,7 @@ import re -from graphon.variables.input_entities import VariableEntity - from core.app.app_config.entities import RagPipelineVariableEntity +from graphon.variables.input_entities import VariableEntity from models.workflow import Workflow diff --git a/api/core/app/apps/advanced_chat/app_generator.py b/api/core/app/apps/advanced_chat/app_generator.py index 985ded0f74..9e64b471cb 100644 --- a/api/core/app/apps/advanced_chat/app_generator.py +++ b/api/core/app/apps/advanced_chat/app_generator.py @@ -18,11 +18,6 @@ from constants import UUID_NIL if TYPE_CHECKING: from controllers.console.app.workflow import LoopNodeRunPayload -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.runtime import GraphRuntimeState -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader - from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner @@ -48,6 +43,10 @@ from core.repositories import DifyCoreRepositoryFactory from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.runtime import GraphRuntimeState +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models import Account, App, Conversation, EndUser, Message, Workflow, WorkflowNodeExecutionTriggeredFrom from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/app/apps/advanced_chat/app_runner.py b/api/core/app/apps/advanced_chat/app_runner.py index 7b4cb98bd4..4e57b4dedc 100644 --- a/api/core/app/apps/advanced_chat/app_runner.py +++ b/api/core/app/apps/advanced_chat/app_runner.py @@ -3,12 +3,6 @@ import time from collections.abc import Mapping, Sequence from typing import Any, cast -from graphon.enums import WorkflowType -from graphon.graph_engine.command_channels import RedisChannel -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader -from graphon.variables.variables import Variable from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -43,6 +37,12 @@ from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span +from graphon.enums import WorkflowType +from graphon.graph_engine.command_channels import RedisChannel +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader +from graphon.variables.variables import Variable from models import Workflow from models.model import App, Conversation, Message, MessageAnnotation from models.workflow import ConversationVariable diff --git a/api/core/app/apps/advanced_chat/generate_task_pipeline.py b/api/core/app/apps/advanced_chat/generate_task_pipeline.py index 0ce9ddce9e..78b582bdf5 100644 --- a/api/core/app/apps/advanced_chat/generate_task_pipeline.py +++ b/api/core/app/apps/advanced_chat/generate_task_pipeline.py @@ -9,12 +9,6 @@ from datetime import datetime from threading import Thread from typing import Any, Union -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes import BuiltinNodeTypes -from graphon.runtime import GraphRuntimeState from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -77,6 +71,12 @@ from core.repositories.human_input_repository import HumanInputFormRepositoryImp from core.workflow.file_reference import resolve_file_record_id from core.workflow.system_variables import build_system_variables from extensions.ext_database import db +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes import BuiltinNodeTypes +from graphon.runtime import GraphRuntimeState from libs.datetime_utils import naive_utc_now from models import Account, Conversation, EndUser, Message, MessageFile from models.enums import CreatorUserRole, MessageFileBelongsTo, MessageStatus diff --git a/api/core/app/apps/agent_chat/app_generator.py b/api/core/app/apps/agent_chat/app_generator.py index 5872f6b264..5cdc477028 100644 --- a/api/core/app/apps/agent_chat/app_generator.py +++ b/api/core/app/apps/agent_chat/app_generator.py @@ -6,7 +6,6 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, current_app -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from configs import dify_config @@ -24,6 +23,7 @@ from core.app.entities.app_invoke_entities import AgentChatAppGenerateEntity, In from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from libs.flask_utils import preserve_flask_contexts from models import Account, App, EndUser from services.conversation_service import ConversationService diff --git a/api/core/app/apps/agent_chat/app_runner.py b/api/core/app/apps/agent_chat/app_runner.py index a20d3f3c38..09ddce327e 100644 --- a/api/core/app/apps/agent_chat/app_runner.py +++ b/api/core/app/apps/agent_chat/app_runner.py @@ -1,9 +1,6 @@ import logging from typing import cast -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from sqlalchemy import select from core.agent.cot_chat_agent_runner import CotChatAgentRunner @@ -19,6 +16,9 @@ from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.moderation.base import ModerationError from extensions.ext_database import db +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from models.model import App, Conversation, Message logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/base_app_generate_response_converter.py b/api/core/app/apps/base_app_generate_response_converter.py index 406d07927e..d5edfaeb25 100644 --- a/api/core/app/apps/base_app_generate_response_converter.py +++ b/api/core/app/apps/base_app_generate_response_converter.py @@ -3,11 +3,10 @@ from abc import ABC, abstractmethod from collections.abc import Generator, Mapping from typing import Any, Union -from graphon.model_runtime.errors.invoke import InvokeError - from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.task_entities import AppBlockingResponse, AppStreamResponse from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 7eccd59d17..8e8ccf2b90 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -2,9 +2,6 @@ from collections.abc import Generator, Mapping, Sequence from contextlib import AbstractContextManager, nullcontext from typing import TYPE_CHECKING, Any, Union, final -from graphon.enums import NodeType -from graphon.file import File, FileUploadConfig -from graphon.variables.input_entities import VariableEntityType from sqlalchemy.orm import Session from core.app.apps.draft_variable_saver import ( @@ -16,6 +13,9 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import DatabaseFileAccessController, FileAccessScope, bind_file_access_scope from extensions.ext_database import db from factories import file_factory +from graphon.enums import NodeType +from graphon.file import File, FileUploadConfig +from graphon.variables.input_entities import VariableEntityType from libs.orjson import orjson_dumps from models import Account, EndUser from services.workflow_draft_variable_service import DraftVariableSaver as DraftVariableSaverImpl diff --git a/api/core/app/apps/base_app_queue_manager.py b/api/core/app/apps/base_app_queue_manager.py index 20bf81aeec..d1771452c5 100644 --- a/api/core/app/apps/base_app_queue_manager.py +++ b/api/core/app/apps/base_app_queue_manager.py @@ -7,7 +7,6 @@ from enum import IntEnum, auto from typing import Any from cachetools import TTLCache, cachedmethod -from graphon.runtime import GraphRuntimeState from redis.exceptions import RedisError from sqlalchemy.orm import DeclarativeMeta @@ -22,6 +21,7 @@ from core.app.entities.queue_entities import ( WorkflowQueueMessage, ) from extensions.ext_redis import redis_client +from graphon.runtime import GraphRuntimeState logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/base_app_runner.py b/api/core/app/apps/base_app_runner.py index 4aebc0cb30..1251b397e2 100644 --- a/api/core/app/apps/base_app_runner.py +++ b/api/core/app/apps/base_app_runner.py @@ -5,17 +5,6 @@ from collections.abc import Generator, Mapping, Sequence from mimetypes import guess_extension from typing import TYPE_CHECKING, Any, Union -from graphon.file import FileTransferMethod, FileType -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - ImagePromptMessageContent, - PromptMessage, - TextPromptMessageContent, -) -from graphon.model_runtime.entities.model_entities import ModelPropertyKey -from graphon.model_runtime.errors.invoke import InvokeBadRequestError - from core.app.app_config.entities import ExternalDataVariableEntity, PromptTemplateEntity from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom from core.app.entities.app_invoke_entities import ( @@ -41,6 +30,16 @@ from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, Comp from core.prompt.simple_prompt_transform import ModelMode, SimplePromptTransform from core.tools.tool_file_manager import ToolFileManager from extensions.ext_database import db +from graphon.file import FileTransferMethod, FileType +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + ImagePromptMessageContent, + PromptMessage, + TextPromptMessageContent, +) +from graphon.model_runtime.entities.model_entities import ModelPropertyKey +from graphon.model_runtime.errors.invoke import InvokeBadRequestError from models.enums import CreatorUserRole, MessageFileBelongsTo from models.model import App, AppMode, Message, MessageAnnotation, MessageFile diff --git a/api/core/app/apps/chat/app_generator.py b/api/core/app/apps/chat/app_generator.py index 891dcece73..58afefe296 100644 --- a/api/core/app/apps/chat/app_generator.py +++ b/api/core/app/apps/chat/app_generator.py @@ -6,7 +6,6 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, copy_current_request_context, current_app -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from configs import dify_config @@ -24,6 +23,7 @@ from core.app.entities.app_invoke_entities import ChatAppGenerateEntity, InvokeF from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from models import Account from models.model import App, EndUser from services.conversation_service import ConversationService diff --git a/api/core/app/apps/chat/app_runner.py b/api/core/app/apps/chat/app_runner.py index 050f763e95..077c5239f3 100644 --- a/api/core/app/apps/chat/app_runner.py +++ b/api/core/app/apps/chat/app_runner.py @@ -1,8 +1,6 @@ import logging from typing import cast -from graphon.file import File -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom @@ -18,6 +16,8 @@ from core.model_manager import ModelInstance from core.moderation.base import ModerationError from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from extensions.ext_database import db +from graphon.file import File +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.model import App, Conversation, Message logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/common/graph_runtime_state_support.py b/api/core/app/apps/common/graph_runtime_state_support.py index ab277857fe..2a90fbdad0 100644 --- a/api/core/app/apps/common/graph_runtime_state_support.py +++ b/api/core/app/apps/common/graph_runtime_state_support.py @@ -4,9 +4,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -from graphon.runtime import GraphRuntimeState - from core.workflow.system_variables import SystemVariableKey, get_system_text +from graphon.runtime import GraphRuntimeState if TYPE_CHECKING: from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline diff --git a/api/core/app/apps/common/workflow_response_converter.py b/api/core/app/apps/common/workflow_response_converter.py index a515531616..bd685d5189 100644 --- a/api/core/app/apps/common/workflow_response_converter.py +++ b/api/core/app/apps/common/workflow_response_converter.py @@ -6,19 +6,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, NewType, TypedDict, Union -from graphon.entities import WorkflowStartReason -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import ( - BuiltinNodeTypes, - WorkflowExecutionStatus, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) -from graphon.file import FILE_MODEL_IDENTITY, File -from graphon.runtime import GraphRuntimeState -from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment -from graphon.variables.variables import Variable -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import select from sqlalchemy.orm import Session @@ -68,6 +55,19 @@ from core.workflow.human_input_forms import load_form_tokens_by_form_id from core.workflow.system_variables import SystemVariableKey, system_variables_to_mapping from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db +from graphon.entities import WorkflowStartReason +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import ( + BuiltinNodeTypes, + WorkflowExecutionStatus, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from graphon.file import FILE_MODEL_IDENTITY, File +from graphon.runtime import GraphRuntimeState +from graphon.variables.segments import ArrayFileSegment, FileSegment, Segment +from graphon.variables.variables import Variable +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.human_input import HumanInputForm diff --git a/api/core/app/apps/completion/app_generator.py b/api/core/app/apps/completion/app_generator.py index 61339b316a..423bfdac51 100644 --- a/api/core/app/apps/completion/app_generator.py +++ b/api/core/app/apps/completion/app_generator.py @@ -6,7 +6,6 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, overload from flask import Flask, copy_current_request_context, current_app -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from sqlalchemy import select @@ -24,6 +23,7 @@ from core.app.entities.app_invoke_entities import CompletionAppGenerateEntity, I from core.ops.ops_trace_manager import TraceQueueManager from extensions.ext_database import db from factories import file_factory +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from models import Account, App, EndUser, Message from services.errors.app import MoreLikeThisDisabledError from services.errors.message import MessageNotExistsError diff --git a/api/core/app/apps/completion/app_runner.py b/api/core/app/apps/completion/app_runner.py index b216f7cf7b..6bb1ecdcb1 100644 --- a/api/core/app/apps/completion/app_runner.py +++ b/api/core/app/apps/completion/app_runner.py @@ -1,8 +1,6 @@ import logging from typing import cast -from graphon.file import File -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager @@ -16,6 +14,8 @@ from core.model_manager import ModelInstance from core.moderation.base import ModerationError from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from extensions.ext_database import db +from graphon.file import File +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.model import App, Message logger = logging.getLogger(__name__) diff --git a/api/core/app/apps/pipeline/pipeline_generator.py b/api/core/app/apps/pipeline/pipeline_generator.py index 83c74b86e5..4b2f17189b 100644 --- a/api/core/app/apps/pipeline/pipeline_generator.py +++ b/api/core/app/apps/pipeline/pipeline_generator.py @@ -10,8 +10,6 @@ from collections.abc import Generator, Mapping from typing import Any, Literal, cast, overload from flask import Flask, current_app -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -43,6 +41,8 @@ from core.repositories.factory import ( WorkflowNodeExecutionRepository, ) from extensions.ext_database import db +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models import Account, EndUser, Workflow, WorkflowNodeExecutionTriggeredFrom from models.dataset import Document, DocumentPipelineExecutionLog, Pipeline diff --git a/api/core/app/apps/pipeline/pipeline_runner.py b/api/core/app/apps/pipeline/pipeline_runner.py index 36daaf09e9..2ee0ae27eb 100644 --- a/api/core/app/apps/pipeline/pipeline_runner.py +++ b/api/core/app/apps/pipeline/pipeline_runner.py @@ -2,12 +2,6 @@ import logging import time from typing import cast -from graphon.enums import WorkflowType -from graphon.graph import Graph -from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader -from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput from sqlalchemy import select from core.app.apps.base_app_queue_manager import AppQueueManager @@ -26,6 +20,12 @@ from core.workflow.system_variables import build_bootstrap_variables, build_syst from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_database import db +from graphon.enums import WorkflowType +from graphon.graph import Graph +from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader +from graphon.variables.variables import RAGPipelineVariable, RAGPipelineVariableInput from models.dataset import Document, Pipeline from models.model import EndUser from models.workflow import Workflow diff --git a/api/core/app/apps/workflow/app_generator.py b/api/core/app/apps/workflow/app_generator.py index ba070ffa94..5a1e7e117f 100644 --- a/api/core/app/apps/workflow/app_generator.py +++ b/api/core/app/apps/workflow/app_generator.py @@ -8,10 +8,6 @@ from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal, overload from flask import Flask, current_app -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError -from graphon.runtime import GraphRuntimeState -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from pydantic import ValidationError from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -38,6 +34,10 @@ from core.repositories import DifyCoreRepositoryFactory from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository from extensions.ext_database import db from factories import file_factory +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError +from graphon.runtime import GraphRuntimeState +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader from libs.flask_utils import preserve_flask_contexts from models.account import Account from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/app/apps/workflow/app_runner.py b/api/core/app/apps/workflow/app_runner.py index 2cb8088971..cfb9208486 100644 --- a/api/core/app/apps/workflow/app_runner.py +++ b/api/core/app/apps/workflow/app_runner.py @@ -3,12 +3,6 @@ import time from collections.abc import Sequence from typing import cast -from graphon.enums import WorkflowType -from graphon.graph_engine.command_channels import RedisChannel -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import VariableLoader - from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.app_config_manager import WorkflowAppConfig from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner @@ -21,6 +15,11 @@ from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add from core.workflow.workflow_entry import WorkflowEntry from extensions.ext_redis import redis_client from extensions.otel import WorkflowAppRunnerHandler, trace_span +from graphon.enums import WorkflowType +from graphon.graph_engine.command_channels import RedisChannel +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import VariableLoader from libs.datetime_utils import naive_utc_now from models.workflow import Workflow diff --git a/api/core/app/apps/workflow/generate_task_pipeline.py b/api/core/app/apps/workflow/generate_task_pipeline.py index 96387133b1..15645add57 100644 --- a/api/core/app/apps/workflow/generate_task_pipeline.py +++ b/api/core/app/apps/workflow/generate_task_pipeline.py @@ -4,9 +4,6 @@ from collections.abc import Callable, Generator from contextlib import contextmanager from typing import Union -from graphon.entities import WorkflowStartReason -from graphon.enums import WorkflowExecutionStatus -from graphon.runtime import GraphRuntimeState from sqlalchemy.orm import Session, sessionmaker from constants.tts_auto_play_timeout import TTS_AUTO_PLAY_TIMEOUT, TTS_AUTO_PLAY_YIELD_CPU_TIME @@ -61,6 +58,9 @@ from core.base.tts import AppGeneratorTTSPublisher, AudioTrunk from core.ops.ops_trace_manager import TraceQueueManager from core.workflow.system_variables import build_system_variables from extensions.ext_database import db +from graphon.entities import WorkflowStartReason +from graphon.enums import WorkflowExecutionStatus +from graphon.runtime import GraphRuntimeState from models import Account from models.enums import CreatorUserRole from models.model import EndUser diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index 437432611d..047b54c86c 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -3,39 +3,6 @@ import time from collections.abc import Mapping, Sequence from typing import Any, cast -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph import Graph -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import ( - GraphEngineEvent, - GraphRunAbortedEvent, - GraphRunFailedEvent, - GraphRunPartialSucceededEvent, - GraphRunPausedEvent, - GraphRunStartedEvent, - GraphRunSucceededEvent, - NodeRunAgentLogEvent, - NodeRunExceptionEvent, - NodeRunFailedEvent, - NodeRunHumanInputFormFilledEvent, - NodeRunHumanInputFormTimeoutEvent, - NodeRunIterationFailedEvent, - NodeRunIterationNextEvent, - NodeRunIterationStartedEvent, - NodeRunIterationSucceededEvent, - NodeRunLoopFailedEvent, - NodeRunLoopNextEvent, - NodeRunLoopStartedEvent, - NodeRunLoopSucceededEvent, - NodeRunRetrieverResourceEvent, - NodeRunRetryEvent, - NodeRunStartedEvent, - NodeRunStreamChunkEvent, - NodeRunSucceededEvent, -) -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from pydantic import ValidationError from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom @@ -82,6 +49,39 @@ from core.workflow.system_variables import ( from core.workflow.variable_pool_initializer import add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph import Graph +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import ( + GraphEngineEvent, + GraphRunAbortedEvent, + GraphRunFailedEvent, + GraphRunPartialSucceededEvent, + GraphRunPausedEvent, + GraphRunStartedEvent, + GraphRunSucceededEvent, + NodeRunAgentLogEvent, + NodeRunExceptionEvent, + NodeRunFailedEvent, + NodeRunHumanInputFormFilledEvent, + NodeRunHumanInputFormTimeoutEvent, + NodeRunIterationFailedEvent, + NodeRunIterationNextEvent, + NodeRunIterationStartedEvent, + NodeRunIterationSucceededEvent, + NodeRunLoopFailedEvent, + NodeRunLoopNextEvent, + NodeRunLoopStartedEvent, + NodeRunLoopSucceededEvent, + NodeRunRetrieverResourceEvent, + NodeRunRetryEvent, + NodeRunStartedEvent, + NodeRunStreamChunkEvent, + NodeRunSucceededEvent, +) +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from models.workflow import Workflow from tasks.mail_human_input_delivery_task import dispatch_human_input_email_task diff --git a/api/core/app/entities/app_invoke_entities.py b/api/core/app/entities/app_invoke_entities.py index a3fb7b4c5d..09992f4bbf 100644 --- a/api/core/app/entities/app_invoke_entities.py +++ b/api/core/app/entities/app_invoke_entities.py @@ -2,13 +2,13 @@ from collections.abc import Mapping, Sequence from enum import StrEnum from typing import TYPE_CHECKING, Any -from graphon.file import File, FileUploadConfig -from graphon.model_runtime.entities.model_entities import AIModelEntity from pydantic import BaseModel, ConfigDict, Field, ValidationInfo, field_validator from constants import UUID_NIL from core.app.app_config.entities import EasyUIBasedAppConfig, WorkflowUIBasedAppConfig from core.entities.provider_configuration import ProviderModelBundle +from graphon.file import File, FileUploadConfig +from graphon.model_runtime.entities.model_entities import AIModelEntity if TYPE_CHECKING: from core.ops.ops_trace_manager import TraceQueueManager diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 482f995d8e..221b7fb058 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -3,14 +3,14 @@ from datetime import datetime from enum import StrEnum, auto from typing import Any -from graphon.entities import WorkflowStartReason -from graphon.entities.pause_reason import PauseReason -from graphon.enums import NodeType, WorkflowNodeExecutionMetadataKey -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk from pydantic import BaseModel, ConfigDict, Field from core.app.entities.agent_strategy import AgentStrategyInfo from core.rag.entities import RetrievalSourceMetadata +from graphon.entities import WorkflowStartReason +from graphon.entities.pause_reason import PauseReason +from graphon.enums import NodeType, WorkflowNodeExecutionMetadataKey +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk class QueueEvent(StrEnum): diff --git a/api/core/app/entities/task_entities.py b/api/core/app/entities/task_entities.py index 88faf235d1..6e4ca69cf0 100644 --- a/api/core/app/entities/task_entities.py +++ b/api/core/app/entities/task_entities.py @@ -2,14 +2,14 @@ from collections.abc import Mapping, Sequence from enum import StrEnum from typing import Any -from graphon.entities import WorkflowStartReason -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.nodes.human_input.entities import FormInput, UserAction from pydantic import BaseModel, ConfigDict, Field from core.app.entities.agent_strategy import AgentStrategyInfo from core.rag.entities import RetrievalSourceMetadata +from graphon.entities import WorkflowStartReason +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.nodes.human_input.entities import FormInput, UserAction class AnnotationReplyAccount(BaseModel): diff --git a/api/core/app/features/hosting_moderation/hosting_moderation.py b/api/core/app/features/hosting_moderation/hosting_moderation.py index d2d2fea4fb..d59f5125e3 100644 --- a/api/core/app/features/hosting_moderation/hosting_moderation.py +++ b/api/core/app/features/hosting_moderation/hosting_moderation.py @@ -1,9 +1,8 @@ import logging -from graphon.model_runtime.entities.message_entities import PromptMessage - from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity from core.helper import moderation +from graphon.model_runtime.entities.message_entities import PromptMessage logger = logging.getLogger(__name__) diff --git a/api/core/app/layers/conversation_variable_persist_layer.py b/api/core/app/layers/conversation_variable_persist_layer.py index e09869f5f8..d5e6b04a4a 100644 --- a/api/core/app/layers/conversation_variable_persist_layer.py +++ b/api/core/app/layers/conversation_variable_persist_layer.py @@ -9,11 +9,10 @@ scope updates that matter to chat applications. import logging -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphEngineEvent, NodeRunVariableUpdatedEvent - from core.workflow.system_variables import SystemVariableKey, get_system_text from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphEngineEvent, NodeRunVariableUpdatedEvent from services.conversation_variable_updater import ConversationVariableUpdater logger = logging.getLogger(__name__) diff --git a/api/core/app/layers/pause_state_persist_layer.py b/api/core/app/layers/pause_state_persist_layer.py index c027f42788..9811f9f830 100644 --- a/api/core/app/layers/pause_state_persist_layer.py +++ b/api/core/app/layers/pause_state_persist_layer.py @@ -1,14 +1,14 @@ from dataclasses import dataclass from typing import Annotated, Literal, Self -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent from pydantic import BaseModel, Field from sqlalchemy import Engine from sqlalchemy.orm import Session, sessionmaker from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity from core.workflow.system_variables import SystemVariableKey, get_system_text +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphEngineEvent, GraphRunPausedEvent from models.model import AppMode from repositories.api_workflow_run_repository import APIWorkflowRunRepository from repositories.factory import DifyAPIRepositoryFactory diff --git a/api/core/app/layers/timeslice_layer.py b/api/core/app/layers/timeslice_layer.py index 8c8daf8712..bb9fc1b6fa 100644 --- a/api/core/app/layers/timeslice_layer.py +++ b/api/core/app/layers/timeslice_layer.py @@ -3,10 +3,10 @@ import uuid from typing import ClassVar from apscheduler.schedulers.background import BackgroundScheduler # type: ignore + from graphon.graph_engine.entities.commands import CommandType, GraphEngineCommand from graphon.graph_engine.layers import GraphEngineLayer from graphon.graph_events import GraphEngineEvent - from services.workflow.entities import WorkflowScheduleCFSPlanEntity from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand diff --git a/api/core/app/layers/trigger_post_layer.py b/api/core/app/layers/trigger_post_layer.py index 77c7bec67e..b60fe82ffe 100644 --- a/api/core/app/layers/trigger_post_layer.py +++ b/api/core/app/layers/trigger_post_layer.py @@ -2,12 +2,12 @@ import logging from datetime import UTC, datetime from typing import Any, ClassVar -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent, GraphRunPausedEvent, GraphRunSucceededEvent from pydantic import TypeAdapter from core.db.session_factory import session_factory from core.workflow.system_variables import SystemVariableKey, get_system_text +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent, GraphRunPausedEvent, GraphRunSucceededEvent from models.enums import WorkflowTriggerStatus from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity diff --git a/api/core/app/llm/model_access.py b/api/core/app/llm/model_access.py index 278d0cb30b..c49c4eb0ac 100644 --- a/api/core/app/llm/model_access.py +++ b/api/core/app/llm/model_access.py @@ -2,16 +2,15 @@ from __future__ import annotations from typing import Any -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.nodes.llm.entities import ModelConfig -from graphon.nodes.llm.exc import LLMModeRequiredError, ModelNotExistError -from graphon.nodes.llm.protocols import CredentialsProvider - from core.app.entities.app_invoke_entities import DifyRunContext, ModelConfigWithCredentialsEntity from core.errors.error import ProviderTokenNotInitError from core.model_manager import ModelInstance, ModelManager from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager from core.provider_manager import ProviderManager +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.nodes.llm.entities import ModelConfig +from graphon.nodes.llm.exc import LLMModeRequiredError, ModelNotExistError +from graphon.nodes.llm.protocols import CredentialsProvider class DifyCredentialsProvider: diff --git a/api/core/app/llm/quota.py b/api/core/app/llm/quota.py index 0bb10190c4..b6039e1e4e 100644 --- a/api/core/app/llm/quota.py +++ b/api/core/app/llm/quota.py @@ -1,4 +1,3 @@ -from graphon.model_runtime.entities.llm_entities import LLMUsage from sqlalchemy import update from sqlalchemy.orm import sessionmaker @@ -8,6 +7,7 @@ from core.entities.provider_entities import ProviderQuotaType, QuotaUnit from core.errors.error import QuotaExceededError from core.model_manager import ModelInstance from extensions.ext_database import db +from graphon.model_runtime.entities.llm_entities import LLMUsage from libs.datetime_utils import naive_utc_now from models.provider import Provider, ProviderType from models.provider_ids import ModelProviderID diff --git a/api/core/app/task_pipeline/based_generate_task_pipeline.py b/api/core/app/task_pipeline/based_generate_task_pipeline.py index 10b9c36d3e..9e688589db 100644 --- a/api/core/app/task_pipeline/based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/based_generate_task_pipeline.py @@ -1,7 +1,6 @@ import logging import time -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from sqlalchemy import select from sqlalchemy.orm import Session @@ -18,6 +17,7 @@ from core.app.entities.task_entities import ( ) from core.errors.error import QuotaExceededError from core.moderation.output_moderation import ModerationRule, OutputModeration +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from models.enums import MessageStatus from models.model import Message diff --git a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py index 6bb177fe02..dfe6133cb6 100644 --- a/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py +++ b/api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py @@ -4,13 +4,6 @@ from collections.abc import Generator from threading import Thread from typing import Any, cast -from graphon.file import FileTransferMethod -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - TextPromptMessageContent, -) -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -60,6 +53,13 @@ from core.prompt.utils.prompt_message_util import PromptMessageUtil from core.prompt.utils.prompt_template_parser import PromptTemplateParser from events.message_event import message_was_created from extensions.ext_database import db +from graphon.file import FileTransferMethod +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + TextPromptMessageContent, +) +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from libs.datetime_utils import naive_utc_now from models.model import AppMode, Conversation, Message, MessageAgentThought, MessageFile, UploadFile diff --git a/api/core/app/task_pipeline/message_file_utils.py b/api/core/app/task_pipeline/message_file_utils.py index 77310baf74..1dd713821f 100644 --- a/api/core/app/task_pipeline/message_file_utils.py +++ b/api/core/app/task_pipeline/message_file_utils.py @@ -1,9 +1,8 @@ from typing import TypedDict +from core.tools.signature import sign_tool_file from graphon.file import FileTransferMethod from graphon.file import helpers as file_helpers - -from core.tools.signature import sign_tool_file from models.model import MessageFile, UploadFile MAX_TOOL_FILE_EXTENSION_LENGTH = 10 diff --git a/api/core/app/workflow/file_runtime.py b/api/core/app/workflow/file_runtime.py index 8604235ef2..68e5e5f0c8 100644 --- a/api/core/app/workflow/file_runtime.py +++ b/api/core/app/workflow/file_runtime.py @@ -9,10 +9,6 @@ import urllib.parse from collections.abc import Generator from typing import TYPE_CHECKING, Literal -from graphon.file import FileTransferMethod -from graphon.file.protocols import HttpResponseProtocol, WorkflowFileRuntimeProtocol -from graphon.file.runtime import set_workflow_file_runtime - from configs import dify_config from core.app.file_access import DatabaseFileAccessController, FileAccessControllerProtocol from core.db.session_factory import session_factory @@ -20,6 +16,9 @@ from core.helper.ssrf_proxy import ssrf_proxy from core.tools.signature import sign_tool_file from core.workflow.file_reference import parse_file_reference from extensions.ext_storage import storage +from graphon.file import FileTransferMethod +from graphon.file.protocols import HttpResponseProtocol, WorkflowFileRuntimeProtocol +from graphon.file.runtime import set_workflow_file_runtime if TYPE_CHECKING: from graphon.file import File diff --git a/api/core/app/workflow/layers/llm_quota.py b/api/core/app/workflow/layers/llm_quota.py index c577ce0754..4a7918032e 100644 --- a/api/core/app/workflow/layers/llm_quota.py +++ b/api/core/app/workflow/layers/llm_quota.py @@ -7,17 +7,16 @@ This layer centralizes model-quota deduction outside node implementations. import logging from typing import TYPE_CHECKING, cast, final, override +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.app.llm import deduct_llm_quota, ensure_llm_quota_available +from core.errors.error import QuotaExceededError +from core.model_manager import ModelInstance from graphon.enums import BuiltinNodeTypes from graphon.graph_engine.entities.commands import AbortCommand, CommandType from graphon.graph_engine.layers import GraphEngineLayer from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, NodeRunSucceededEvent from graphon.nodes.base.node import Node -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.app.llm import deduct_llm_quota, ensure_llm_quota_available -from core.errors.error import QuotaExceededError -from core.model_manager import ModelInstance - if TYPE_CHECKING: from graphon.nodes.llm.node import LLMNode from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode diff --git a/api/core/app/workflow/layers/observability.py b/api/core/app/workflow/layers/observability.py index 99e8015c0b..8b5a5b9d7f 100644 --- a/api/core/app/workflow/layers/observability.py +++ b/api/core/app/workflow/layers/observability.py @@ -12,10 +12,6 @@ from contextvars import Token from dataclasses import dataclass from typing import cast, final, override -from graphon.enums import BuiltinNodeTypes, NodeType -from graphon.graph_engine.layers import GraphEngineLayer -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node from opentelemetry import context as context_api from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer, set_span_in_context @@ -28,6 +24,10 @@ from extensions.otel.parser import ( ToolNodeOTelParser, ) from extensions.otel.runtime import is_instrument_flag_enabled +from graphon.enums import BuiltinNodeTypes, NodeType +from graphon.graph_engine.layers import GraphEngineLayer +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node logger = logging.getLogger(__name__) diff --git a/api/core/app/workflow/layers/persistence.py b/api/core/app/workflow/layers/persistence.py index ada065a943..87f005a250 100644 --- a/api/core/app/workflow/layers/persistence.py +++ b/api/core/app/workflow/layers/persistence.py @@ -14,6 +14,13 @@ from dataclasses import dataclass from datetime import datetime from typing import Any, Union +from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity +from core.ops.entities.trace_entity import TraceTaskName +from core.ops.ops_trace_manager import TraceQueueManager, TraceTask +from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository +from core.workflow.system_variables import SystemVariableKey +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run from graphon.entities import WorkflowExecution, WorkflowNodeExecution from graphon.enums import ( WorkflowExecutionStatus, @@ -38,14 +45,6 @@ from graphon.graph_events import ( NodeRunSucceededEvent, ) from graphon.node_events import NodeRunResult - -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, WorkflowAppGenerateEntity -from core.ops.entities.trace_entity import TraceTaskName -from core.ops.ops_trace_manager import TraceQueueManager, TraceTask -from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository -from core.workflow.system_variables import SystemVariableKey -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID -from core.workflow.workflow_run_outputs import project_node_outputs_for_workflow_run from libs.datetime_utils import naive_utc_now diff --git a/api/core/base/tts/app_generator_tts_publisher.py b/api/core/base/tts/app_generator_tts_publisher.py index 3d8a7a54f3..9e3c187210 100644 --- a/api/core/base/tts/app_generator_tts_publisher.py +++ b/api/core/base/tts/app_generator_tts_publisher.py @@ -6,9 +6,6 @@ import re import threading from collections.abc import Iterable -from graphon.model_runtime.entities.message_entities import TextPromptMessageContent -from graphon.model_runtime.entities.model_entities import ModelType - from core.app.entities.queue_entities import ( MessageQueueMessage, QueueAgentMessageEvent, @@ -18,6 +15,8 @@ from core.app.entities.queue_entities import ( WorkflowQueueMessage, ) from core.model_manager import ModelInstance, ModelManager +from graphon.model_runtime.entities.message_entities import TextPromptMessageContent +from graphon.model_runtime.entities.model_entities import ModelType class AudioTrunk: diff --git a/api/core/datasource/datasource_manager.py b/api/core/datasource/datasource_manager.py index a5297fa33a..dc831e5cac 100644 --- a/api/core/datasource/datasource_manager.py +++ b/api/core/datasource/datasource_manager.py @@ -3,9 +3,6 @@ from collections.abc import Generator from threading import Lock from typing import Any, cast -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.file import File, FileTransferMethod, FileType, get_file_type_by_mime_type -from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent from sqlalchemy import select import contexts @@ -31,6 +28,9 @@ from core.plugin.impl.datasource import PluginDatasourceManager from core.workflow.file_reference import build_file_reference from core.workflow.nodes.datasource.entities import DatasourceParameter, OnlineDriveDownloadFileParam from factories import file_factory +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.file import File, FileTransferMethod, FileType, get_file_type_by_mime_type +from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent from models.model import UploadFile from models.tools import ToolFile from services.datasource_provider_service import DatasourceProviderService diff --git a/api/core/datasource/entities/api_entities.py b/api/core/datasource/entities/api_entities.py index 9c22d5e67c..352e6bfd49 100644 --- a/api/core/datasource/entities/api_entities.py +++ b/api/core/datasource/entities/api_entities.py @@ -1,10 +1,10 @@ from typing import Any, Literal, TypedDict -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from core.datasource.entities.datasource_entities import DatasourceParameter from core.tools.entities.common_entities import I18nObject, I18nObjectDict +from graphon.model_runtime.utils.encoders import jsonable_encoder class DatasourceApiEntity(BaseModel): diff --git a/api/core/datasource/utils/message_transformer.py b/api/core/datasource/utils/message_transformer.py index c012e128f4..6a3f9e684a 100644 --- a/api/core/datasource/utils/message_transformer.py +++ b/api/core/datasource/utils/message_transformer.py @@ -2,11 +2,10 @@ import logging from collections.abc import Generator from mimetypes import guess_extension, guess_type -from graphon.file import File, FileTransferMethod, FileType - from core.datasource.entities.datasource_entities import DatasourceMessage from core.tools.tool_file_manager import ToolFileManager from core.workflow.file_reference import parse_file_reference +from graphon.file import File, FileTransferMethod, FileType from models.tools import ToolFile logger = logging.getLogger(__name__) diff --git a/api/core/entities/execution_extra_content.py b/api/core/entities/execution_extra_content.py index d304c982cd..04ae193396 100644 --- a/api/core/entities/execution_extra_content.py +++ b/api/core/entities/execution_extra_content.py @@ -3,9 +3,9 @@ from __future__ import annotations from collections.abc import Mapping, Sequence from typing import Any, TypeAlias -from graphon.nodes.human_input.entities import FormInput, UserAction from pydantic import BaseModel, ConfigDict, Field +from graphon.nodes.human_input.entities import FormInput, UserAction from models.execution_extra_content import ExecutionContentType diff --git a/api/core/entities/mcp_provider.py b/api/core/entities/mcp_provider.py index a440829b46..bfa4f56915 100644 --- a/api/core/entities/mcp_provider.py +++ b/api/core/entities/mcp_provider.py @@ -6,7 +6,6 @@ from enum import StrEnum from typing import TYPE_CHECKING, Any from urllib.parse import urlparse -from graphon.file import helpers as file_helpers from pydantic import BaseModel from configs import dify_config @@ -16,6 +15,7 @@ from core.helper.provider_cache import NoOpProviderCredentialCache from core.mcp.types import OAuthClientInformation, OAuthClientMetadata, OAuthTokens from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType +from graphon.file import helpers as file_helpers if TYPE_CHECKING: from models.tools import MCPToolProvider diff --git a/api/core/entities/model_entities.py b/api/core/entities/model_entities.py index 84d95c38c6..e99a131500 100644 --- a/api/core/entities/model_entities.py +++ b/api/core/entities/model_entities.py @@ -1,10 +1,11 @@ from collections.abc import Sequence from enum import StrEnum, auto +from pydantic import BaseModel, ConfigDict + from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import ModelType, ProviderModel from graphon.model_runtime.entities.provider_entities import ProviderEntity -from pydantic import BaseModel, ConfigDict class ModelStatus(StrEnum): diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index d07f6f913a..1ab66cceee 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -8,16 +8,6 @@ from collections.abc import Iterator, Sequence from json import JSONDecodeError from typing import Any -from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FormType, - ProviderEntity, -) -from graphon.model_runtime.model_providers.__base.ai_model import AIModel -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory -from graphon.model_runtime.runtime import ModelRuntime from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, model_validator from sqlalchemy import func, select from sqlalchemy.orm import Session @@ -34,6 +24,16 @@ from core.entities.provider_entities import ( from core.helper import encrypter from core.helper.model_provider_cache import ProviderCredentialsCache, ProviderCredentialsCacheType from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FormType, + ProviderEntity, +) +from graphon.model_runtime.model_providers.__base.ai_model import AIModel +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory +from graphon.model_runtime.runtime import ModelRuntime from libs.datetime_utils import naive_utc_now from models.engine import db from models.enums import CredentialSourceType diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 95431c0e01..72b29c2277 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -3,7 +3,6 @@ from __future__ import annotations from enum import StrEnum, auto from typing import Any, Union -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, ConfigDict, Field from core.entities.parameter_entities import ( @@ -13,6 +12,7 @@ from core.entities.parameter_entities import ( ToolSelectorScope, ) from core.tools.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType class ProviderQuotaType(StrEnum): diff --git a/api/core/helper/code_executor/code_executor.py b/api/core/helper/code_executor/code_executor.py index 35bfcfb6a5..951e065b2c 100644 --- a/api/core/helper/code_executor/code_executor.py +++ b/api/core/helper/code_executor/code_executor.py @@ -4,7 +4,6 @@ from threading import Lock from typing import Any import httpx -from graphon.nodes.code.entities import CodeLanguage from pydantic import BaseModel from yarl import URL @@ -14,6 +13,7 @@ from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTr from core.helper.code_executor.python3.python3_transformer import Python3TemplateTransformer from core.helper.code_executor.template_transformer import TemplateTransformer from core.helper.http_client_pooling import get_pooled_http_client +from graphon.nodes.code.entities import CodeLanguage logger = logging.getLogger(__name__) code_execution_endpoint_url = URL(str(dify_config.CODE_EXECUTION_ENDPOINT)) diff --git a/api/core/helper/moderation.py b/api/core/helper/moderation.py index a1e782a094..dc37a36943 100644 --- a/api/core/helper/moderation.py +++ b/api/core/helper/moderation.py @@ -2,14 +2,13 @@ import logging import secrets from typing import cast -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeBadRequestError -from graphon.model_runtime.model_providers.__base.moderation_model import ModerationModel - from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.entities import DEFAULT_PLUGIN_ID from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory from extensions.ext_hosting_provider import hosting_configuration +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeBadRequestError +from graphon.model_runtime.model_providers.__base.moderation_model import ModerationModel from models.provider import ProviderType logger = logging.getLogger(__name__) diff --git a/api/core/hosting_configuration.py b/api/core/hosting_configuration.py index f8f56e12d2..8bcb899b23 100644 --- a/api/core/hosting_configuration.py +++ b/api/core/hosting_configuration.py @@ -1,12 +1,12 @@ from typing import Any from flask import Flask -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel from configs import dify_config from core.entities import DEFAULT_PLUGIN_ID from core.entities.provider_entities import ProviderQuotaType, QuotaUnit, RestrictModel +from graphon.model_runtime.entities.model_entities import ModelType class HostingQuota(BaseModel): diff --git a/api/core/indexing_runner.py b/api/core/indexing_runner.py index 8d0a8b99b4..b6e33396d1 100644 --- a/api/core/indexing_runner.py +++ b/api/core/indexing_runner.py @@ -9,7 +9,6 @@ from collections.abc import Mapping from typing import Any from flask import Flask, current_app -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import delete, func, select, update from sqlalchemy.orm.exc import ObjectDeletedError @@ -35,6 +34,7 @@ from core.tools.utils.web_reader_tool import get_image_upload_file_ids from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.ext_storage import storage +from graphon.model_runtime.entities.model_entities import ModelType from libs import helper from libs.datetime_utils import naive_utc_now from models import Account diff --git a/api/core/llm_generator/llm_generator.py b/api/core/llm_generator/llm_generator.py index c43c0274cd..348526b0ef 100644 --- a/api/core/llm_generator/llm_generator.py +++ b/api/core/llm_generator/llm_generator.py @@ -5,11 +5,6 @@ from collections.abc import Sequence from typing import Any, Protocol, TypedDict, cast import json_repair -from graphon.enums import WorkflowNodeExecutionMetadataKey -from graphon.model_runtime.entities.llm_entities import LLMResult -from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from sqlalchemy import select from core.app.app_config.entities import ModelConfig @@ -35,6 +30,11 @@ from core.ops.utils import measure_time from core.prompt.utils.prompt_template_parser import PromptTemplateParser from extensions.ext_database import db from extensions.ext_storage import storage +from graphon.enums import WorkflowNodeExecutionMetadataKey +from graphon.model_runtime.entities.llm_entities import LLMResult +from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from models import App, Message, WorkflowNodeExecutionModel from models.workflow import Workflow diff --git a/api/core/llm_generator/output_parser/structured_output.py b/api/core/llm_generator/output_parser/structured_output.py index a8ad7c9179..d2e375626f 100644 --- a/api/core/llm_generator/output_parser/structured_output.py +++ b/api/core/llm_generator/output_parser/structured_output.py @@ -5,6 +5,11 @@ from enum import StrEnum from typing import Any, Literal, cast, overload import json_repair +from pydantic import TypeAdapter, ValidationError + +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT +from core.model_manager import ModelInstance from graphon.model_runtime.callbacks.base_callback import Callback from graphon.model_runtime.entities.llm_entities import ( LLMResult, @@ -21,11 +26,6 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, ) from graphon.model_runtime.entities.model_entities import AIModelEntity, ParameterRule -from pydantic import TypeAdapter, ValidationError - -from core.llm_generator.output_parser.errors import OutputParserError -from core.llm_generator.prompts import STRUCTURED_OUTPUT_PROMPT -from core.model_manager import ModelInstance class ResponseFormat(StrEnum): diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 72171d1536..884610ca82 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -3,12 +3,11 @@ import logging from collections.abc import Mapping from typing import Any, NotRequired, TypedDict, cast -from graphon.variables.input_entities import VariableEntity, VariableEntityType - from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.mcp import types as mcp_types +from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser from services.app_generate_service import AppGenerateService diff --git a/api/core/mcp/utils.py b/api/core/mcp/utils.py index 7e35044176..7b5a7635f1 100644 --- a/api/core/mcp/utils.py +++ b/api/core/mcp/utils.py @@ -4,11 +4,11 @@ from contextlib import AbstractContextManager import httpx import httpx_sse -from graphon.model_runtime.utils.encoders import jsonable_encoder from httpx_sse import connect_sse from configs import dify_config from core.mcp.types import ErrorData, JSONRPCError +from graphon.model_runtime.utils.encoders import jsonable_encoder HTTP_REQUEST_NODE_SSL_VERIFY = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY diff --git a/api/core/memory/token_buffer_memory.py b/api/core/memory/token_buffer_memory.py index 5809d6f74a..d840ee213c 100644 --- a/api/core/memory/token_buffer_memory.py +++ b/api/core/memory/token_buffer_memory.py @@ -1,5 +1,14 @@ from collections.abc import Sequence +from sqlalchemy import select +from sqlalchemy.orm import sessionmaker + +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager +from core.app.file_access import DatabaseFileAccessController +from core.model_manager import ModelInstance +from core.prompt.utils.extract_thread_messages import extract_thread_messages +from extensions.ext_database import db +from factories import file_factory from graphon.file import file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -10,15 +19,6 @@ from graphon.model_runtime.entities import ( UserPromptMessage, ) from graphon.model_runtime.entities.message_entities import PromptMessageContentUnionTypes -from sqlalchemy import select -from sqlalchemy.orm import sessionmaker - -from core.app.app_config.features.file_upload.manager import FileUploadConfigManager -from core.app.file_access import DatabaseFileAccessController -from core.model_manager import ModelInstance -from core.prompt.utils.extract_thread_messages import extract_thread_messages -from extensions.ext_database import db -from factories import file_factory from models.model import AppMode, Conversation, Message, MessageFile from models.workflow import Workflow from repositories.api_workflow_run_repository import APIWorkflowRunRepository diff --git a/api/core/model_manager.py b/api/core/model_manager.py index 36beb55d7f..d8d8dfedd8 100644 --- a/api/core/model_manager.py +++ b/api/core/model_manager.py @@ -2,6 +2,15 @@ import logging from collections.abc import Callable, Generator, Iterable, Mapping, Sequence from typing import IO, Any, Literal, Optional, Union, cast, overload +from configs import dify_config +from core.entities import PluginCredentialType +from core.entities.embedding_type import EmbeddingInputType +from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle +from core.entities.provider_entities import ModelLoadBalancingConfiguration +from core.errors.error import ProviderTokenNotInitError +from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager +from core.provider_manager import ProviderManager +from extensions.ext_redis import redis_client from graphon.model_runtime.callbacks.base_callback import Callback from graphon.model_runtime.entities.llm_entities import LLMResult from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool @@ -15,16 +24,6 @@ from graphon.model_runtime.model_providers.__base.rerank_model import RerankMode from graphon.model_runtime.model_providers.__base.speech2text_model import Speech2TextModel from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from graphon.model_runtime.model_providers.__base.tts_model import TTSModel - -from configs import dify_config -from core.entities import PluginCredentialType -from core.entities.embedding_type import EmbeddingInputType -from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle -from core.entities.provider_entities import ModelLoadBalancingConfiguration -from core.errors.error import ProviderTokenNotInitError -from core.plugin.impl.model_runtime_factory import create_plugin_provider_manager -from core.provider_manager import ProviderManager -from extensions.ext_redis import redis_client from models.provider import ProviderType logger = logging.getLogger(__name__) diff --git a/api/core/moderation/openai_moderation/openai_moderation.py b/api/core/moderation/openai_moderation/openai_moderation.py index 732803b332..6e6e94502c 100644 --- a/api/core/moderation/openai_moderation/openai_moderation.py +++ b/api/core/moderation/openai_moderation/openai_moderation.py @@ -1,9 +1,8 @@ from typing import Any -from graphon.model_runtime.entities.model_entities import ModelType - from core.model_manager import ModelManager from core.moderation.base import Moderation, ModerationAction, ModerationInputsResult, ModerationOutputsResult +from graphon.model_runtime.entities.model_entities import ModelType class OpenAIModeration(Moderation): diff --git a/api/core/ops/aliyun_trace/aliyun_trace.py b/api/core/ops/aliyun_trace/aliyun_trace.py index 70aaf2a07b..76e81242f4 100644 --- a/api/core/ops/aliyun_trace/aliyun_trace.py +++ b/api/core/ops/aliyun_trace/aliyun_trace.py @@ -1,8 +1,6 @@ import logging from collections.abc import Sequence -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from opentelemetry.trace import SpanKind from sqlalchemy.orm import sessionmaker @@ -60,6 +58,8 @@ from core.ops.entities.trace_entity import ( ) from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/core/ops/aliyun_trace/utils.py b/api/core/ops/aliyun_trace/utils.py index aa35ac74c2..2e02a186cc 100644 --- a/api/core/ops/aliyun_trace/utils.py +++ b/api/core/ops/aliyun_trace/utils.py @@ -2,8 +2,6 @@ import json from collections.abc import Mapping from typing import Any, TypedDict -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionStatus from opentelemetry.trace import Link, Status, StatusCode from core.ops.aliyun_trace.entities.semconv import ( @@ -17,6 +15,8 @@ from core.ops.aliyun_trace.entities.semconv import ( ) from core.rag.models.document import Document from extensions.ext_database import db +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionStatus from models import EndUser # Constants diff --git a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py index dd5edde630..78516e1a22 100644 --- a/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py +++ b/api/core/ops/arize_phoenix_trace/arize_phoenix_trace.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta from typing import Any, Union, cast from urllib.parse import urlparse -from graphon.enums import WorkflowNodeExecutionStatus from openinference.semconv.trace import ( MessageAttributes, OpenInferenceMimeTypeValues, @@ -41,6 +40,7 @@ from core.ops.entities.trace_entity import ( from core.ops.utils import JSON_DICT_ADAPTER from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.enums import WorkflowNodeExecutionStatus from models.model import EndUser, MessageFile from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/core/ops/langfuse_trace/langfuse_trace.py b/api/core/ops/langfuse_trace/langfuse_trace.py index d53aa84aed..7eacc2be46 100644 --- a/api/core/ops/langfuse_trace/langfuse_trace.py +++ b/api/core/ops/langfuse_trace/langfuse_trace.py @@ -3,7 +3,6 @@ import os import uuid from datetime import UTC, datetime, timedelta -from graphon.enums import BuiltinNodeTypes from langfuse import Langfuse from langfuse.api import ( CreateGenerationBody, @@ -40,6 +39,7 @@ from core.ops.langfuse_trace.entities.langfuse_trace_entity import ( from core.ops.utils import filter_none_values from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.enums import BuiltinNodeTypes from models import EndUser, WorkflowNodeExecutionTriggeredFrom from models.enums import MessageStatus diff --git a/api/core/ops/langsmith_trace/langsmith_trace.py b/api/core/ops/langsmith_trace/langsmith_trace.py index 490c64af84..d960038f15 100644 --- a/api/core/ops/langsmith_trace/langsmith_trace.py +++ b/api/core/ops/langsmith_trace/langsmith_trace.py @@ -4,7 +4,6 @@ import uuid from datetime import datetime, timedelta from typing import cast -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from langsmith import Client from langsmith.schemas import RunBase from sqlalchemy.orm import sessionmaker @@ -30,6 +29,7 @@ from core.ops.langsmith_trace.entities.langsmith_trace_entity import ( from core.ops.utils import filter_none_values, generate_dotted_order from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/core/ops/mlflow_trace/mlflow_trace.py b/api/core/ops/mlflow_trace/mlflow_trace.py index c070a937be..87fcaeabcc 100644 --- a/api/core/ops/mlflow_trace/mlflow_trace.py +++ b/api/core/ops/mlflow_trace/mlflow_trace.py @@ -4,7 +4,6 @@ from datetime import datetime, timedelta from typing import Any, cast import mlflow -from graphon.enums import BuiltinNodeTypes from mlflow.entities import Document, Span, SpanEvent, SpanStatusCode, SpanType from mlflow.tracing.constant import SpanAttributeKey, TokenUsageKey, TraceMetadataKey from mlflow.tracing.fluent import start_span_no_context, update_current_trace @@ -26,6 +25,7 @@ from core.ops.entities.trace_entity import ( ) from core.ops.utils import JSON_DICT_ADAPTER from extensions.ext_database import db +from graphon.enums import BuiltinNodeTypes from models import EndUser from models.workflow import WorkflowNodeExecutionModel diff --git a/api/core/ops/opik_trace/opik_trace.py b/api/core/ops/opik_trace/opik_trace.py index e0c7b9bfe5..672efe45bd 100644 --- a/api/core/ops/opik_trace/opik_trace.py +++ b/api/core/ops/opik_trace/opik_trace.py @@ -5,7 +5,6 @@ import uuid from datetime import datetime, timedelta from typing import Any, cast -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from opik import Opik, Trace from opik.id_helpers import uuid4_to_uuid7 from sqlalchemy.orm import sessionmaker @@ -25,6 +24,7 @@ from core.ops.entities.trace_entity import ( ) from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/core/ops/tencent_trace/span_builder.py b/api/core/ops/tencent_trace/span_builder.py index f79095d966..36878dc58f 100644 --- a/api/core/ops/tencent_trace/span_builder.py +++ b/api/core/ops/tencent_trace/span_builder.py @@ -6,8 +6,6 @@ import json import logging from datetime import datetime -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from opentelemetry.trace import Status, StatusCode from core.ops.entities.trace_entity import ( @@ -43,6 +41,8 @@ from core.ops.tencent_trace.entities.semconv import ( from core.ops.tencent_trace.entities.tencent_trace_entity import SpanData from core.ops.tencent_trace.utils import TencentTraceUtils from core.rag.models.document import Document +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus logger = logging.getLogger(__name__) diff --git a/api/core/ops/tencent_trace/tencent_trace.py b/api/core/ops/tencent_trace/tencent_trace.py index 84f54d8a5a..d681b9da80 100644 --- a/api/core/ops/tencent_trace/tencent_trace.py +++ b/api/core/ops/tencent_trace/tencent_trace.py @@ -4,10 +4,6 @@ Tencent APM tracing implementation with separated concerns import logging -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, -) -from graphon.nodes import BuiltinNodeTypes from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -29,6 +25,10 @@ from core.ops.tencent_trace.span_builder import TencentSpanBuilder from core.ops.tencent_trace.utils import TencentTraceUtils from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository from extensions.ext_database import db +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, +) +from graphon.nodes import BuiltinNodeTypes from models import Account, App, TenantAccountJoin, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/core/ops/weave_trace/weave_trace.py b/api/core/ops/weave_trace/weave_trace.py index 8d9ba4694d..f79544f1c7 100644 --- a/api/core/ops/weave_trace/weave_trace.py +++ b/api/core/ops/weave_trace/weave_trace.py @@ -6,7 +6,6 @@ from typing import Any, cast import wandb import weave -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from sqlalchemy.orm import sessionmaker from weave.trace_server.trace_server_interface import ( CallEndReq, @@ -33,6 +32,7 @@ from core.ops.entities.trace_entity import ( from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom logger = logging.getLogger(__name__) diff --git a/api/core/plugin/backwards_invocation/model.py b/api/core/plugin/backwards_invocation/model.py index a4b24ff849..c92438960a 100644 --- a/api/core/plugin/backwards_invocation/model.py +++ b/api/core/plugin/backwards_invocation/model.py @@ -3,20 +3,6 @@ from binascii import hexlify, unhexlify from collections.abc import Generator from typing import Any -from graphon.model_runtime.entities.llm_entities import ( - LLMResult, - LLMResultChunk, - LLMResultChunkDelta, - LLMResultChunkWithStructuredOutput, - LLMResultWithStructuredOutput, -) -from graphon.model_runtime.entities.message_entities import ( - PromptMessage, - SystemPromptMessage, - UserPromptMessage, -) -from graphon.model_runtime.entities.model_entities import ModelType - from core.app.llm import deduct_llm_quota from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from core.model_manager import ModelManager @@ -33,6 +19,19 @@ from core.plugin.entities.request import ( ) from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils +from graphon.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultChunkWithStructuredOutput, + LLMResultWithStructuredOutput, +) +from graphon.model_runtime.entities.message_entities import ( + PromptMessage, + SystemPromptMessage, + UserPromptMessage, +) +from graphon.model_runtime.entities.model_entities import ModelType from models.account import Tenant diff --git a/api/core/plugin/backwards_invocation/node.py b/api/core/plugin/backwards_invocation/node.py index 9478997494..9550e49992 100644 --- a/api/core/plugin/backwards_invocation/node.py +++ b/api/core/plugin/backwards_invocation/node.py @@ -1,3 +1,4 @@ +from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from graphon.enums import BuiltinNodeTypes from graphon.nodes.llm.entities import ModelConfig as LLMModelConfig from graphon.nodes.parameter_extractor.entities import ( @@ -8,8 +9,6 @@ from graphon.nodes.question_classifier.entities import ( ClassConfig, QuestionClassifierNodeData, ) - -from core.plugin.backwards_invocation.base import BaseBackwardsInvocation from services.workflow_service import WorkflowService diff --git a/api/core/plugin/entities/marketplace.py b/api/core/plugin/entities/marketplace.py index fd2094228a..03398873e3 100644 --- a/api/core/plugin/entities/marketplace.py +++ b/api/core/plugin/entities/marketplace.py @@ -1,12 +1,12 @@ from typing import Any -from graphon.model_runtime.entities.provider_entities import ProviderEntity from pydantic import BaseModel, Field, computed_field, model_validator from core.plugin.entities.endpoint import EndpointProviderDeclaration from core.plugin.entities.plugin import PluginResourceRequirements from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntity +from graphon.model_runtime.entities.provider_entities import ProviderEntity class MarketplacePluginDeclaration(BaseModel): diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index 4d28032a57..89e0e8881c 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -3,7 +3,6 @@ from collections.abc import Mapping from enum import StrEnum, auto from typing import Any -from graphon.model_runtime.entities.provider_entities import ProviderEntity from packaging.version import InvalidVersion, Version from pydantic import BaseModel, Field, field_validator, model_validator @@ -14,6 +13,7 @@ from core.plugin.entities.endpoint import EndpointProviderDeclaration from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntity from core.trigger.entities.entities import TriggerProviderEntity +from graphon.model_runtime.entities.provider_entities import ProviderEntity class PluginInstallationSource(StrEnum): diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index e0ddb746c7..257638ad77 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -6,8 +6,6 @@ from datetime import datetime from enum import StrEnum from typing import Any -from graphon.model_runtime.entities.model_entities import AIModelEntity -from graphon.model_runtime.entities.provider_entities import ProviderEntity from pydantic import BaseModel, ConfigDict, Field from core.agent.plugin_entities import AgentProviderEntityWithPlugin @@ -18,6 +16,8 @@ from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin from core.trigger.entities.entities import TriggerProviderEntity +from graphon.model_runtime.entities.model_entities import AIModelEntity +from graphon.model_runtime.entities.provider_entities import ProviderEntity class PluginDaemonBasicResponse[T: BaseModel | dict | list | bool | str](BaseModel): diff --git a/api/core/plugin/entities/request.py b/api/core/plugin/entities/request.py index 4a85952dcd..1474883204 100644 --- a/api/core/plugin/entities/request.py +++ b/api/core/plugin/entities/request.py @@ -4,6 +4,10 @@ from collections.abc import Mapping from typing import Any, Literal from flask import Response +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from core.entities.provider_entities import BasicProviderConfig +from core.plugin.utils.http_parser import deserialize_response from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, PromptMessage, @@ -21,10 +25,6 @@ from graphon.nodes.parameter_extractor.entities import ( from graphon.nodes.question_classifier.entities import ( ClassConfig, ) -from pydantic import BaseModel, ConfigDict, Field, field_validator - -from core.entities.provider_entities import BasicProviderConfig -from core.plugin.utils.http_parser import deserialize_response class InvokeCredentials(BaseModel): diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 7f36560b49..9ee8469892 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -5,14 +5,6 @@ from collections.abc import Callable, Generator from typing import Any, cast import httpx -from graphon.model_runtime.errors.invoke import ( - InvokeAuthorizationError, - InvokeBadRequestError, - InvokeConnectionError, - InvokeRateLimitError, - InvokeServerUnavailableError, -) -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from yarl import URL @@ -37,6 +29,14 @@ from core.trigger.errors import ( TriggerPluginInvokeError, TriggerProviderCredentialValidationError, ) +from graphon.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError plugin_daemon_inner_api_baseurl = URL(str(dify_config.PLUGIN_DAEMON_URL)) _plugin_daemon_timeout_config = cast( diff --git a/api/core/plugin/impl/model.py b/api/core/plugin/impl/model.py index 703af63f7c..47608bdfa6 100644 --- a/api/core/plugin/impl/model.py +++ b/api/core/plugin/impl/model.py @@ -2,13 +2,6 @@ import binascii from collections.abc import Generator, Sequence from typing import IO, Any -from graphon.model_runtime.entities.llm_entities import LLMResultChunk -from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool -from graphon.model_runtime.entities.model_entities import AIModelEntity -from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult -from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult -from graphon.model_runtime.utils.encoders import jsonable_encoder - from core.plugin.entities.plugin_daemon import ( PluginBasicBooleanResponse, PluginDaemonInnerError, @@ -20,6 +13,12 @@ from core.plugin.entities.plugin_daemon import ( PluginVoicesResponse, ) from core.plugin.impl.base import BasePluginClient +from graphon.model_runtime.entities.llm_entities import LLMResultChunk +from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool +from graphon.model_runtime.entities.model_entities import AIModelEntity +from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult +from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult +from graphon.model_runtime.utils.encoders import jsonable_encoder class PluginModelClient(BasePluginClient): diff --git a/api/core/plugin/impl/model_runtime.py b/api/core/plugin/impl/model_runtime.py index 22c846b6de..e3fba4ef3a 100644 --- a/api/core/plugin/impl/model_runtime.py +++ b/api/core/plugin/impl/model_runtime.py @@ -6,13 +6,6 @@ from collections.abc import Generator, Iterable, Sequence from threading import Lock from typing import IO, Any, Union -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk -from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool -from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType -from graphon.model_runtime.entities.provider_entities import ProviderEntity -from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult -from graphon.model_runtime.entities.text_embedding_entities import EmbeddingInputType, EmbeddingResult -from graphon.model_runtime.runtime import ModelRuntime from pydantic import ValidationError from redis import RedisError @@ -21,6 +14,13 @@ from core.plugin.entities.plugin_daemon import PluginModelProviderEntity from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.model import PluginModelClient from extensions.ext_redis import redis_client +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk +from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageTool +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType +from graphon.model_runtime.entities.provider_entities import ProviderEntity +from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult +from graphon.model_runtime.entities.text_embedding_entities import EmbeddingInputType, EmbeddingResult +from graphon.model_runtime.runtime import ModelRuntime from models.provider_ids import ModelProviderID logger = logging.getLogger(__name__) diff --git a/api/core/plugin/impl/model_runtime_factory.py b/api/core/plugin/impl/model_runtime_factory.py index 4b29a6fc56..35abd2ae8c 100644 --- a/api/core/plugin/impl/model_runtime_factory.py +++ b/api/core/plugin/impl/model_runtime_factory.py @@ -2,9 +2,8 @@ from __future__ import annotations from typing import TYPE_CHECKING -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory - from core.plugin.impl.model import PluginModelClient +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory if TYPE_CHECKING: from core.model_manager import ModelManager diff --git a/api/core/plugin/utils/converter.py b/api/core/plugin/utils/converter.py index 90350f8400..12d8e282b2 100644 --- a/api/core/plugin/utils/converter.py +++ b/api/core/plugin/utils/converter.py @@ -1,8 +1,7 @@ from typing import Any -from graphon.file import File - from core.tools.entities.tool_entities import ToolSelector +from graphon.file import File def convert_parameters_to_plugin_format(parameters: dict[str, Any]) -> dict[str, Any]: diff --git a/api/core/prompt/advanced_prompt_transform.py b/api/core/prompt/advanced_prompt_transform.py index 19b5e9223a..24e05ef865 100644 --- a/api/core/prompt/advanced_prompt_transform.py +++ b/api/core/prompt/advanced_prompt_transform.py @@ -1,6 +1,13 @@ from collections.abc import Mapping, Sequence from typing import cast +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.helper.code_executor.jinja2.jinja2_formatter import Jinja2Formatter +from core.memory.token_buffer_memory import TokenBufferMemory +from core.model_manager import ModelInstance +from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig +from core.prompt.prompt_transform import PromptTransform +from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import File, file_manager from graphon.model_runtime.entities import ( AssistantPromptMessage, @@ -13,14 +20,6 @@ from graphon.model_runtime.entities import ( from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent, PromptMessageContentUnionTypes from graphon.runtime import VariablePool -from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.helper.code_executor.jinja2.jinja2_formatter import Jinja2Formatter -from core.memory.token_buffer_memory import TokenBufferMemory -from core.model_manager import ModelInstance -from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig -from core.prompt.prompt_transform import PromptTransform -from core.prompt.utils.prompt_template_parser import PromptTemplateParser - class AdvancedPromptTransform(PromptTransform): """ diff --git a/api/core/prompt/agent_history_prompt_transform.py b/api/core/prompt/agent_history_prompt_transform.py index 9be70199b7..8f1d51f08a 100644 --- a/api/core/prompt/agent_history_prompt_transform.py +++ b/api/core/prompt/agent_history_prompt_transform.py @@ -1,17 +1,16 @@ from typing import cast -from graphon.model_runtime.entities.message_entities import ( - PromptMessage, - SystemPromptMessage, - UserPromptMessage, -) -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel - from core.app.entities.app_invoke_entities import ( ModelConfigWithCredentialsEntity, ) from core.memory.token_buffer_memory import TokenBufferMemory from core.prompt.prompt_transform import PromptTransform +from graphon.model_runtime.entities.message_entities import ( + PromptMessage, + SystemPromptMessage, + UserPromptMessage, +) +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel class AgentHistoryPromptTransform(PromptTransform): diff --git a/api/core/prompt/prompt_transform.py b/api/core/prompt/prompt_transform.py index 4539ae9f11..6ff2f44cdc 100644 --- a/api/core/prompt/prompt_transform.py +++ b/api/core/prompt/prompt_transform.py @@ -1,12 +1,11 @@ from typing import Any -from graphon.model_runtime.entities.message_entities import PromptMessage -from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey - from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.memory.token_buffer_memory import TokenBufferMemory from core.model_manager import ModelInstance from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from graphon.model_runtime.entities.message_entities import PromptMessage +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelPropertyKey class PromptTransform: diff --git a/api/core/prompt/simple_prompt_transform.py b/api/core/prompt/simple_prompt_transform.py index dc8391a6a5..1665bdeb52 100644 --- a/api/core/prompt/simple_prompt_transform.py +++ b/api/core/prompt/simple_prompt_transform.py @@ -4,6 +4,12 @@ from collections.abc import Mapping, Sequence from enum import StrEnum, auto from typing import TYPE_CHECKING, Any, TypedDict, cast +from core.app.app_config.entities import PromptTemplateEntity +from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity +from core.memory.token_buffer_memory import TokenBufferMemory +from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.prompt.prompt_transform import PromptTransform +from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import file_manager from graphon.model_runtime.entities.message_entities import ( ImagePromptMessageContent, @@ -13,13 +19,6 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, UserPromptMessage, ) - -from core.app.app_config.entities import PromptTemplateEntity -from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity -from core.memory.token_buffer_memory import TokenBufferMemory -from core.prompt.entities.advanced_prompt_entities import MemoryConfig -from core.prompt.prompt_transform import PromptTransform -from core.prompt.utils.prompt_template_parser import PromptTemplateParser from models.model import AppMode if TYPE_CHECKING: diff --git a/api/core/prompt/utils/prompt_message_util.py b/api/core/prompt/utils/prompt_message_util.py index dbda749925..ba76eb0c4e 100644 --- a/api/core/prompt/utils/prompt_message_util.py +++ b/api/core/prompt/utils/prompt_message_util.py @@ -1,6 +1,7 @@ from collections.abc import Sequence from typing import Any, cast +from core.prompt.simple_prompt_transform import ModelMode from graphon.model_runtime.entities import ( AssistantPromptMessage, AudioPromptMessageContent, @@ -11,8 +12,6 @@ from graphon.model_runtime.entities import ( TextPromptMessageContent, ) -from core.prompt.simple_prompt_transform import ModelMode - class PromptMessageUtil: @staticmethod diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 39ef31632e..c3bbe8fc09 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -6,14 +6,6 @@ from collections.abc import Sequence from json import JSONDecodeError from typing import TYPE_CHECKING, Any -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FormType, - ProviderEntity, -) -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.exc import IntegrityError @@ -41,6 +33,14 @@ from core.helper.position_helper import is_filtered from extensions import ext_hosting_provider from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FormType, + ProviderEntity, +) +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from models.provider import ( LoadBalancingModelConfig, Provider, diff --git a/api/core/rag/data_post_processor/data_post_processor.py b/api/core/rag/data_post_processor/data_post_processor.py index 9ce91f52ff..ca530748ed 100644 --- a/api/core/rag/data_post_processor/data_post_processor.py +++ b/api/core/rag/data_post_processor/data_post_processor.py @@ -1,8 +1,5 @@ from typing import TypedDict -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError - from core.model_manager import ModelInstance, ModelManager from core.rag.data_post_processor.reorder import ReorderRunner from core.rag.index_processor.constant.query_type import QueryType @@ -11,6 +8,8 @@ from core.rag.rerank.entity.weight import KeywordSetting, VectorSetting, Weights from core.rag.rerank.rerank_base import BaseRerankRunner from core.rag.rerank.rerank_factory import RerankRunnerFactory from core.rag.rerank.rerank_type import RerankMode +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError class RerankingModelDict(TypedDict): diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index f978e072f3..7e71d67ec0 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -4,7 +4,6 @@ from concurrent.futures import ThreadPoolExecutor from typing import Any, NotRequired, TypedDict from flask import Flask, current_app -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import select from sqlalchemy.orm import Session, load_only @@ -24,6 +23,7 @@ from core.rag.rerank.rerank_type import RerankMode from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.signature import sign_upload_file from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ( ChildChunk, Dataset, @@ -195,6 +195,23 @@ class RetrievalService: ) return all_documents + @classmethod + def _filter_documents_by_vector_score_threshold( + cls, documents: list[Document], score_threshold: float | None + ) -> list[Document]: + """Keep documents whose stored retrieval score meets the threshold. + + Used when hybrid search skips early vector thresholding but no rerank + runner applies a threshold afterward (same rule as ``calculate_vector_score``). + """ + if score_threshold is None: + return documents + return [ + document + for document in documents + if document.metadata and document.metadata.get("score", 0) >= score_threshold + ] + @classmethod def _deduplicate_documents(cls, documents: list[Document]) -> list[Document]: """Deduplicate documents in O(n) while preserving first-seen order. @@ -294,13 +311,20 @@ class RetrievalService: vector = Vector(dataset=dataset) documents = [] + # Hybrid search merges keyword / full-text / vector hits and then reranks + # (weighted fusion or reranking model). Applying the user score threshold at + # vector retrieval time uses embedding similarity, which is not comparable to + # reranked or fused scores and incorrectly drops high-quality chunks (#35233). + embedding_score_threshold = ( + 0.0 if retrieval_method == RetrievalMethod.HYBRID_SEARCH else score_threshold + ) if query_type == QueryType.TEXT_QUERY: documents.extend( vector.search_by_vector( query, search_type="similarity_score_threshold", top_k=top_k, - score_threshold=score_threshold, + score_threshold=embedding_score_threshold, filter={"group_id": [dataset.id]}, document_ids_filter=document_ids_filter, ) @@ -312,7 +336,7 @@ class RetrievalService: vector.search_by_file( file_id=query, top_k=top_k, - score_threshold=score_threshold, + score_threshold=embedding_score_threshold, filter={"group_id": [dataset.id]}, document_ids_filter=document_ids_filter, ) @@ -844,6 +868,10 @@ class RetrievalService: top_n=top_k, query_type=QueryType.TEXT_QUERY if query else QueryType.IMAGE_QUERY, ) + if not data_post_processor.rerank_runner and score_threshold: + all_documents_item = self._filter_documents_by_vector_score_threshold( + all_documents_item, score_threshold + ) all_documents.extend(all_documents_item) diff --git a/api/core/rag/datasource/vdb/vector_factory.py b/api/core/rag/datasource/vdb/vector_factory.py index dddd5fc994..59d7f3c3c4 100644 --- a/api/core/rag/datasource/vdb/vector_factory.py +++ b/api/core/rag/datasource/vdb/vector_factory.py @@ -4,7 +4,6 @@ import time from abc import ABC, abstractmethod from typing import Any -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import select from configs import dify_config @@ -19,6 +18,7 @@ from core.rag.models.document import Document from extensions.ext_database import db from extensions.ext_redis import redis_client from extensions.ext_storage import storage +from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, Whitelist from models.model import UploadFile diff --git a/api/core/rag/docstore/dataset_docstore.py b/api/core/rag/docstore/dataset_docstore.py index 8e9ebdd17a..f4699f6869 100644 --- a/api/core/rag/docstore/dataset_docstore.py +++ b/api/core/rag/docstore/dataset_docstore.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Sequence from typing import Any -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import delete, func, select from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import AttachmentDocument, Document from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import ChildChunk, Dataset, DocumentSegment, SegmentAttachmentBinding diff --git a/api/core/rag/embedding/cached_embedding.py b/api/core/rag/embedding/cached_embedding.py index 9f1c73ec88..4926f44f16 100644 --- a/api/core/rag/embedding/cached_embedding.py +++ b/api/core/rag/embedding/cached_embedding.py @@ -4,8 +4,6 @@ import pickle from typing import Any, cast import numpy as np -from graphon.model_runtime.entities.model_entities import ModelPropertyKey -from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from sqlalchemy import select from sqlalchemy.exc import IntegrityError @@ -15,6 +13,8 @@ from core.model_manager import ModelInstance from core.rag.embedding.embedding_base import Embeddings from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.model_runtime.entities.model_entities import ModelPropertyKey +from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from libs import helper from models.dataset import Embedding diff --git a/api/core/rag/index_processor/processor/paragraph_index_processor.py b/api/core/rag/index_processor/processor/paragraph_index_processor.py index a487c49053..f8242efe31 100644 --- a/api/core/rag/index_processor/processor/paragraph_index_processor.py +++ b/api/core/rag/index_processor/processor/paragraph_index_processor.py @@ -7,16 +7,6 @@ from typing import Any, TypedDict, cast logger = logging.getLogger(__name__) -from graphon.file import File, FileTransferMethod, FileType, file_manager -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - ImagePromptMessageContent, - PromptMessage, - PromptMessageContentUnionTypes, - TextPromptMessageContent, - UserPromptMessage, -) -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from sqlalchemy import select from core.app.file_access import DatabaseFileAccessController @@ -43,6 +33,16 @@ from core.tools.utils.text_processing_utils import remove_leading_symbols from core.workflow.file_reference import build_file_reference from extensions.ext_database import db from factories.file_factory import build_from_mapping +from graphon.file import File, FileTransferMethod, FileType, file_manager +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + ImagePromptMessageContent, + PromptMessage, + PromptMessageContentUnionTypes, + TextPromptMessageContent, + UserPromptMessage, +) +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from libs import helper from models import UploadFile from models.account import Account diff --git a/api/core/rag/models/document.py b/api/core/rag/models/document.py index 087736d0b0..4ebf095904 100644 --- a/api/core/rag/models/document.py +++ b/api/core/rag/models/document.py @@ -2,9 +2,10 @@ from abc import ABC, abstractmethod from collections.abc import Sequence from typing import Any -from graphon.file import File from pydantic import BaseModel, Field +from graphon.file import File + class ChildDocument(BaseModel): """Class for storing a piece of text and associated metadata.""" diff --git a/api/core/rag/rerank/rerank_model.py b/api/core/rag/rerank/rerank_model.py index a8d37845a5..bce08f998f 100644 --- a/api/core/rag/rerank/rerank_model.py +++ b/api/core/rag/rerank/rerank_model.py @@ -1,8 +1,5 @@ import base64 -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult - from core.model_manager import ModelInstance, ModelManager from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.query_type import QueryType @@ -10,6 +7,8 @@ from core.rag.models.document import Document from core.rag.rerank.rerank_base import BaseRerankRunner from extensions.ext_database import db from extensions.ext_storage import storage +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.rerank_entities import MultimodalRerankInput, RerankResult from models.model import UploadFile diff --git a/api/core/rag/rerank/weight_rerank.py b/api/core/rag/rerank/weight_rerank.py index 49123e13d0..d0732b269a 100644 --- a/api/core/rag/rerank/weight_rerank.py +++ b/api/core/rag/rerank/weight_rerank.py @@ -2,7 +2,6 @@ import math from collections import Counter import numpy as np -from graphon.model_runtime.entities.model_entities import ModelType from core.model_manager import ModelManager from core.rag.datasource.keyword.jieba.jieba_keyword_table_handler import JiebaKeywordTableHandler @@ -12,6 +11,7 @@ from core.rag.index_processor.constant.query_type import QueryType from core.rag.models.document import Document from core.rag.rerank.entity.weight import VectorSetting, Weights from core.rag.rerank.rerank_base import BaseRerankRunner +from graphon.model_runtime.entities.model_entities import ModelType class WeightRerankRunner(BaseRerankRunner): diff --git a/api/core/rag/retrieval/dataset_retrieval.py b/api/core/rag/retrieval/dataset_retrieval.py index 8ebc840b99..1453fe020b 100644 --- a/api/core/rag/retrieval/dataset_retrieval.py +++ b/api/core/rag/retrieval/dataset_retrieval.py @@ -9,11 +9,6 @@ from collections.abc import Generator, Mapping from typing import Any, Union, cast from flask import Flask, current_app -from graphon.file import File, FileTransferMethod, FileType -from graphon.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from sqlalchemy import and_, func, literal, or_, select, update from sqlalchemy.orm import sessionmaker @@ -69,6 +64,11 @@ from core.workflow.nodes.knowledge_retrieval.retrieval import ( ) from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.file import File, FileTransferMethod, FileType +from graphon.model_runtime.entities.llm_entities import LLMMode, LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from libs.helper import parse_uuid_str_or_none from libs.json_in_md_parser import parse_and_check_json_markdown from models import UploadFile diff --git a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py index dce7b6226c..e617a9660e 100644 --- a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py +++ b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py @@ -1,10 +1,9 @@ from typing import Union -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import PromptMessageTool, SystemPromptMessage, UserPromptMessage - from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.model_manager import ModelInstance +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import PromptMessageTool, SystemPromptMessage, UserPromptMessage class FunctionCallMultiDatasetRouter: diff --git a/api/core/rag/retrieval/router/multi_dataset_react_route.py b/api/core/rag/retrieval/router/multi_dataset_react_route.py index 9b223075d8..21a9d04f7f 100644 --- a/api/core/rag/retrieval/router/multi_dataset_react_route.py +++ b/api/core/rag/retrieval/router/multi_dataset_react_route.py @@ -1,10 +1,6 @@ from collections.abc import Generator, Sequence from typing import Any, Union -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool -from graphon.model_runtime.entities.model_entities import ModelType - from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.app.llm import deduct_llm_quota from core.model_manager import ModelInstance, ModelManager @@ -12,6 +8,9 @@ from core.prompt.advanced_prompt_transform import AdvancedPromptTransform from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate from core.rag.retrieval.output_parser.react_output import ReactAction from core.rag.retrieval.output_parser.structured_chat import StructuredChatOutputParser +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import PromptMessage, PromptMessageRole, PromptMessageTool +from graphon.model_runtime.entities.model_entities import ModelType PREFIX = """Respond to the human as helpfully and accurately as possible. You have access to the following tools:""" diff --git a/api/core/rag/splitter/fixed_text_splitter.py b/api/core/rag/splitter/fixed_text_splitter.py index 3383c7f3bd..2581c354dd 100644 --- a/api/core/rag/splitter/fixed_text_splitter.py +++ b/api/core/rag/splitter/fixed_text_splitter.py @@ -7,10 +7,9 @@ import re from collections.abc import Collection from typing import Any, Literal -from graphon.model_runtime.model_providers.__base.tokenizers.gpt2_tokenizer import GPT2Tokenizer - from core.model_manager import ModelInstance from core.rag.splitter.text_splitter import RecursiveCharacterTextSplitter +from graphon.model_runtime.model_providers.__base.tokenizers.gpt2_tokenizer import GPT2Tokenizer class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter): diff --git a/api/core/repositories/celery_workflow_execution_repository.py b/api/core/repositories/celery_workflow_execution_repository.py index b07c63fdf0..e87d1cd6b2 100644 --- a/api/core/repositories/celery_workflow_execution_repository.py +++ b/api/core/repositories/celery_workflow_execution_repository.py @@ -7,11 +7,11 @@ providing improved performance by offloading database operations to background w import logging -from graphon.entities import WorkflowExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository +from graphon.entities import WorkflowExecution from libs.helper import extract_tenant_id from models import Account, CreatorUserRole, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/repositories/celery_workflow_node_execution_repository.py b/api/core/repositories/celery_workflow_node_execution_repository.py index cdb3af01a8..2451563317 100644 --- a/api/core/repositories/celery_workflow_node_execution_repository.py +++ b/api/core/repositories/celery_workflow_node_execution_repository.py @@ -8,7 +8,6 @@ providing improved performance by offloading database operations to background w import logging from collections.abc import Sequence -from graphon.entities import WorkflowNodeExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker @@ -16,6 +15,7 @@ from core.repositories.factory import ( OrderConfig, WorkflowNodeExecutionRepository, ) +from graphon.entities import WorkflowNodeExecution from libs.helper import extract_tenant_id from models import Account, CreatorUserRole, EndUser from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/core/repositories/factory.py b/api/core/repositories/factory.py index ce3ad15759..4e83e70799 100644 --- a/api/core/repositories/factory.py +++ b/api/core/repositories/factory.py @@ -9,11 +9,11 @@ from collections.abc import Sequence from dataclasses import dataclass from typing import Literal, Protocol -from graphon.entities import WorkflowExecution, WorkflowNodeExecution from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from configs import dify_config +from graphon.entities import WorkflowExecution, WorkflowNodeExecution from libs.module_loading import import_string from models import Account, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/core/repositories/human_input_repository.py b/api/core/repositories/human_input_repository.py index 72d9394149..02625e242f 100644 --- a/api/core/repositories/human_input_repository.py +++ b/api/core/repositories/human_input_repository.py @@ -4,8 +4,6 @@ from collections.abc import Mapping, Sequence from datetime import datetime from typing import Any, Protocol -from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from sqlalchemy import select from sqlalchemy.orm import Session, selectinload @@ -19,6 +17,8 @@ from core.workflow.human_input_compat import ( InteractiveSurfaceDeliveryMethod, is_human_input_webapp_enabled, ) +from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 from models.account import Account, TenantAccountJoin diff --git a/api/core/repositories/sqlalchemy_workflow_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_execution_repository.py index d74cc8f231..6be3902317 100644 --- a/api/core/repositories/sqlalchemy_workflow_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_execution_repository.py @@ -5,13 +5,13 @@ SQLAlchemy implementation of the WorkflowExecutionRepository. import json import logging -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus, WorkflowType -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus, WorkflowType +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from models import ( Account, diff --git a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py index 13e885672a..b036687bc9 100644 --- a/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_node_execution_repository.py @@ -10,10 +10,6 @@ from concurrent.futures import ThreadPoolExecutor from typing import Any import psycopg2.errors -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import UnaryExpression, asc, desc, select from sqlalchemy.engine import Engine from sqlalchemy.exc import IntegrityError @@ -23,6 +19,10 @@ from tenacity import before_sleep_log, retry, retry_if_exception, stop_after_att from configs import dify_config from core.repositories.factory import OrderConfig, WorkflowNodeExecutionRepository from extensions.ext_storage import storage +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from libs.uuid_utils import uuidv7 from models import ( diff --git a/api/core/tools/builtin_tool/providers/audio/tools/asr.py b/api/core/tools/builtin_tool/providers/audio/tools/asr.py index e539074303..95660ab93b 100644 --- a/api/core/tools/builtin_tool/providers/audio/tools/asr.py +++ b/api/core/tools/builtin_tool/providers/audio/tools/asr.py @@ -2,15 +2,14 @@ import io from collections.abc import Generator from typing import Any -from graphon.file import FileType -from graphon.file.file_manager import download -from graphon.model_runtime.entities.model_entities import ModelType - from core.model_manager import ModelManager from core.plugin.entities.parameters import PluginParameterOption from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter +from graphon.file import FileType +from graphon.file.file_manager import download +from graphon.model_runtime.entities.model_entities import ModelType from services.model_provider_service import ModelProviderService diff --git a/api/core/tools/builtin_tool/providers/audio/tools/tts.py b/api/core/tools/builtin_tool/providers/audio/tools/tts.py index f49c669fe0..ac3820f1ab 100644 --- a/api/core/tools/builtin_tool/providers/audio/tools/tts.py +++ b/api/core/tools/builtin_tool/providers/audio/tools/tts.py @@ -2,13 +2,12 @@ import io from collections.abc import Generator from typing import Any -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType - from core.model_manager import ModelManager from core.plugin.entities.parameters import PluginParameterOption from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType from services.model_provider_service import ModelProviderService diff --git a/api/core/tools/builtin_tool/tool.py b/api/core/tools/builtin_tool/tool.py index 14af63a962..d41503e1e6 100644 --- a/api/core/tools/builtin_tool/tool.py +++ b/api/core/tools/builtin_tool/tool.py @@ -1,12 +1,11 @@ from __future__ import annotations -from graphon.model_runtime.entities.llm_entities import LLMResult -from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage - from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ToolProviderType from core.tools.utils.model_invocation_utils import ModelInvocationUtils +from graphon.model_runtime.entities.llm_entities import LLMResult +from graphon.model_runtime.entities.message_entities import PromptMessage, SystemPromptMessage, UserPromptMessage _SUMMARY_PROMPT = """You are a professional language researcher, you are interested in the language and you can quickly aimed at the main point of an webpage and reproduce it in your own words but diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 0a2c37c563..168e5f4493 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -6,7 +6,6 @@ from typing import Any, Union from urllib.parse import urlencode import httpx -from graphon.file.file_manager import download from core.helper import ssrf_proxy from core.tools.__base.tool import Tool @@ -14,6 +13,7 @@ from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_bundle import ApiToolBundle from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType from core.tools.errors import ToolInvokeError, ToolParameterValidationError, ToolProviderCredentialValidationError +from graphon.file.file_manager import download API_TOOL_DEFAULT_TIMEOUT = ( int(getenv("API_TOOL_DEFAULT_CONNECT_TIMEOUT", "10")), diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index 410ec72baf..42a88c0003 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -2,7 +2,6 @@ from collections.abc import Mapping from datetime import datetime from typing import Any, Literal -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, Field, field_validator from core.entities.mcp_provider import MCPAuthentication, MCPConfiguration @@ -10,6 +9,7 @@ from core.plugin.entities.plugin_daemon import CredentialType from core.tools.__base.tool import ToolParameter from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderType +from graphon.model_runtime.utils.encoders import jsonable_encoder class ToolApiEntity(BaseModel): diff --git a/api/core/tools/errors.py b/api/core/tools/errors.py index 4c3efd6ff9..2b26832b44 100644 --- a/api/core/tools/errors.py +++ b/api/core/tools/errors.py @@ -38,6 +38,17 @@ class ToolCredentialPolicyViolationError(ValueError): pass +class ApiToolProviderNotFoundError(ValueError): + error_code = "api_tool_provider_not_found" + provider_name: str + tenant_id: str + + def __init__(self, provider_name: str, tenant_id: str): + self.provider_name = provider_name + self.tenant_id = tenant_id + super().__init__(f"api provider {provider_name} does not exist") + + class WorkflowToolHumanInputNotSupportedError(BaseHTTPException): error_code = "workflow_tool_human_input_not_supported" description = "Workflow with Human Input nodes cannot be published as a workflow tool." diff --git a/api/core/tools/mcp_tool/tool.py b/api/core/tools/mcp_tool/tool.py index f6d09472b3..00fc8a8282 100644 --- a/api/core/tools/mcp_tool/tool.py +++ b/api/core/tools/mcp_tool/tool.py @@ -6,8 +6,6 @@ import logging from collections.abc import Generator, Mapping from typing import Any, cast -from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata - from core.mcp.auth_client import MCPClientWithAuthRetry from core.mcp.error import MCPConnectionError from core.mcp.types import ( @@ -23,6 +21,7 @@ from core.tools.__base.tool import Tool from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.tool_entities import ToolEntity, ToolInvokeMessage, ToolProviderType from core.tools.errors import ToolInvokeError +from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata logger = logging.getLogger(__name__) diff --git a/api/core/tools/tool_engine.py b/api/core/tools/tool_engine.py index d060fa8b49..3caacb8706 100644 --- a/api/core/tools/tool_engine.py +++ b/api/core/tools/tool_engine.py @@ -7,7 +7,6 @@ from datetime import UTC, datetime from mimetypes import guess_type from typing import Any, Union, cast -from graphon.file import FileTransferMethod, FileType from yarl import URL from core.app.entities.app_invoke_entities import InvokeFrom @@ -33,6 +32,7 @@ from core.tools.errors import ( from core.tools.utils.message_transformer import ToolFileMessageTransformer, safe_json_value from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db +from graphon.file import FileTransferMethod, FileType from models.enums import CreatorUserRole, MessageFileBelongsTo from models.model import Message, MessageFile diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py index d8674b3af9..b3424cd9a5 100644 --- a/api/core/tools/tool_file_manager.py +++ b/api/core/tools/tool_file_manager.py @@ -9,7 +9,6 @@ from mimetypes import guess_extension, guess_type from uuid import uuid4 import httpx -from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type from sqlalchemy import select from configs import dify_config @@ -17,6 +16,7 @@ from core.db.session_factory import session_factory from core.helper import ssrf_proxy from core.workflow.file_reference import build_file_reference from extensions.ext_storage import storage +from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type from models.model import MessageFile from models.tools import ToolFile diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index be13d40f3e..f4588904d3 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -8,7 +8,6 @@ from threading import Lock from typing import TYPE_CHECKING, Any, Literal, Protocol, cast import sqlalchemy as sa -from graphon.runtime import VariablePool from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.orm import Session @@ -29,14 +28,13 @@ from core.tools.plugin_tool.tool import PluginTool from core.tools.utils.uuid_utils import is_valid_uuid from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from extensions.ext_database import db +from graphon.runtime import VariablePool from models.provider_ids import ToolProviderID from services.tools.mcp_tools_manage_service import MCPToolManageService if TYPE_CHECKING: pass -from graphon.model_runtime.utils.encoders import jsonable_encoder - from core.agent.entities import AgentToolEntity from core.app.entities.app_invoke_entities import InvokeFrom from core.helper.module_import_helper import load_single_subclass_from_source @@ -62,6 +60,7 @@ from core.tools.tool_label_manager import ToolLabelManager from core.tools.utils.configuration import ToolParameterConfigurationManager from core.tools.utils.encryption import create_provider_encrypter, create_tool_provider_encrypter from core.tools.workflow_as_tool.tool import WorkflowTool +from graphon.model_runtime.utils.encoders import jsonable_encoder from models.tools import ApiToolProvider, BuiltinToolProvider, WorkflowToolProvider from services.tools.tools_transform_service import ToolTransformService diff --git a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py index 03e3c5918d..b6890b2611 100644 --- a/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py +++ b/api/core/tools/utils/dataset_retriever/dataset_multi_retriever_tool.py @@ -1,7 +1,6 @@ import threading from flask import Flask, current_app -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import BaseModel, Field from sqlalchemy import select @@ -15,6 +14,7 @@ from core.rag.rerank.rerank_model import RerankModelRunner from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.tools.utils.dataset_retriever.dataset_retriever_base_tool import DatasetRetrieverBaseTool from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, Document, DocumentSegment default_retrieval_model: DefaultRetrievalModelDict = { diff --git a/api/core/tools/utils/message_transformer.py b/api/core/tools/utils/message_transformer.py index 81c85bc90d..79d0c114d4 100644 --- a/api/core/tools/utils/message_transformer.py +++ b/api/core/tools/utils/message_transformer.py @@ -9,11 +9,11 @@ from uuid import UUID import numpy as np import pytz -from graphon.file import File, FileTransferMethod, FileType from core.tools.entities.tool_entities import ToolInvokeMessage from core.tools.tool_file_manager import ToolFileManager from core.workflow.file_reference import parse_file_reference +from graphon.file import File, FileTransferMethod, FileType from libs.login import current_user from models import Account diff --git a/api/core/tools/utils/model_invocation_utils.py b/api/core/tools/utils/model_invocation_utils.py index 8d6f83dc07..9e1d41cb39 100644 --- a/api/core/tools/utils/model_invocation_utils.py +++ b/api/core/tools/utils/model_invocation_utils.py @@ -8,6 +8,9 @@ import json from decimal import Decimal from typing import cast +from core.model_manager import ModelManager +from core.tools.entities.tool_entities import ToolProviderType +from extensions.ext_database import db from graphon.model_runtime.entities.llm_entities import LLMResult from graphon.model_runtime.entities.message_entities import PromptMessage from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType @@ -20,10 +23,6 @@ from graphon.model_runtime.errors.invoke import ( ) from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from graphon.model_runtime.utils.encoders import jsonable_encoder - -from core.model_manager import ModelManager -from core.tools.entities.tool_entities import ToolProviderType -from extensions.ext_database import db from models.tools import ToolModelInvoke diff --git a/api/core/tools/utils/workflow_configuration_sync.py b/api/core/tools/utils/workflow_configuration_sync.py index 2159eb8638..45718cadb6 100644 --- a/api/core/tools/utils/workflow_configuration_sync.py +++ b/api/core/tools/utils/workflow_configuration_sync.py @@ -1,13 +1,12 @@ from collections.abc import Mapping, Sequence from typing import Any +from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration +from core.tools.errors import WorkflowToolHumanInputNotSupportedError from graphon.enums import BuiltinNodeTypes from graphon.nodes.base.entities import OutputVariableEntity from graphon.variables.input_entities import VariableEntity -from core.tools.entities.tool_entities import WorkflowToolParameterConfiguration -from core.tools.errors import WorkflowToolHumanInputNotSupportedError - class WorkflowToolConfigurationUtils: @classmethod diff --git a/api/core/tools/workflow_as_tool/provider.py b/api/core/tools/workflow_as_tool/provider.py index a01004448a..5905fd919e 100644 --- a/api/core/tools/workflow_as_tool/provider.py +++ b/api/core/tools/workflow_as_tool/provider.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Mapping -from graphon.variables.input_entities import VariableEntity, VariableEntityType from pydantic import Field from sqlalchemy import select from sqlalchemy.orm import Session @@ -25,6 +24,7 @@ from core.tools.entities.tool_entities import ( from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db +from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.account import Account from models.model import App, AppMode from models.tools import WorkflowToolProvider diff --git a/api/core/tools/workflow_as_tool/tool.py b/api/core/tools/workflow_as_tool/tool.py index 7c4f8ee03a..52ab605963 100644 --- a/api/core/tools/workflow_as_tool/tool.py +++ b/api/core/tools/workflow_as_tool/tool.py @@ -5,8 +5,6 @@ import logging from collections.abc import Generator, Mapping, Sequence from typing import Any, cast -from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod -from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from sqlalchemy import select from core.app.file_access import DatabaseFileAccessController @@ -22,6 +20,8 @@ from core.tools.entities.tool_entities import ( from core.tools.errors import ToolInvokeError from core.workflow.file_reference import resolve_file_record_id from factories.file_factory import build_from_mapping +from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod +from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata from models import Account, Tenant from models.model import App, EndUser from models.utils.file_input_compat import build_file_from_stored_mapping diff --git a/api/core/trigger/debug/event_selectors.py b/api/core/trigger/debug/event_selectors.py index 61d1cd8540..24c1271488 100644 --- a/api/core/trigger/debug/event_selectors.py +++ b/api/core/trigger/debug/event_selectors.py @@ -8,7 +8,6 @@ from collections.abc import Mapping from datetime import datetime from typing import Any -from graphon.entities.graph_config import NodeConfigDict from pydantic import BaseModel from core.plugin.entities.request import TriggerInvokeEventResponse @@ -28,6 +27,7 @@ from core.trigger.debug.events import ( from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData from core.workflow.nodes.trigger_schedule.entities import ScheduleConfig from extensions.ext_redis import redis_client +from graphon.entities.graph_config import NodeConfigDict from libs.datetime_utils import ensure_naive_utc, naive_utc_now from libs.schedule_utils import calculate_next_run_at from models.model import App diff --git a/api/core/workflow/human_input_compat.py b/api/core/workflow/human_input_compat.py index c95516a240..75a0a0c202 100644 --- a/api/core/workflow/human_input_compat.py +++ b/api/core/workflow/human_input_compat.py @@ -14,12 +14,13 @@ from typing import Annotated, Any, ClassVar, Literal import bleach import markdown +from markdown.extensions.tables import TableExtension +from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter + from graphon.enums import BuiltinNodeTypes from graphon.nodes.base.variable_template_parser import VariableTemplateParser from graphon.runtime import VariablePool from graphon.variables.consts import SELECTORS_LENGTH -from markdown.extensions.tables import TableExtension -from pydantic import AliasChoices, BaseModel, ConfigDict, Field, TypeAdapter class DeliveryMethodType(enum.StrEnum): diff --git a/api/core/workflow/node_factory.py b/api/core/workflow/node_factory.py index b04ac7da3d..351da3444f 100644 --- a/api/core/workflow/node_factory.py +++ b/api/core/workflow/node_factory.py @@ -5,22 +5,6 @@ from dataclasses import dataclass from functools import lru_cache from typing import TYPE_CHECKING, Any, cast, final, override -from graphon.entities.base_node_data import BaseNodeData -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter -from graphon.enums import BuiltinNodeTypes, NodeType -from graphon.file.file_manager import file_manager -from graphon.graph.graph import NodeFactory -from graphon.model_runtime.memory import PromptMessageMemory -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel -from graphon.nodes.base.node import Node -from graphon.nodes.code.code_node import WorkflowCodeExecutor -from graphon.nodes.code.entities import CodeLanguage -from graphon.nodes.code.limits import CodeNodeLimits -from graphon.nodes.document_extractor import UnstructuredApiConfig -from graphon.nodes.http_request import build_http_request_config -from graphon.nodes.llm.entities import LLMNodeData -from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData -from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData from sqlalchemy import select from sqlalchemy.orm import Session @@ -56,6 +40,22 @@ from core.workflow.nodes.agent.runtime_support import AgentRuntimeSupport from core.workflow.system_variables import SystemVariableKey, get_system_text, system_variable_selector from core.workflow.template_rendering import CodeExecutorJinja2TemplateRenderer from extensions.ext_database import db +from graphon.entities.base_node_data import BaseNodeData +from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter +from graphon.enums import BuiltinNodeTypes, NodeType +from graphon.file.file_manager import file_manager +from graphon.graph.graph import NodeFactory +from graphon.model_runtime.memory import PromptMessageMemory +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel +from graphon.nodes.base.node import Node +from graphon.nodes.code.code_node import WorkflowCodeExecutor +from graphon.nodes.code.entities import CodeLanguage +from graphon.nodes.code.limits import CodeNodeLimits +from graphon.nodes.document_extractor import UnstructuredApiConfig +from graphon.nodes.http_request import build_http_request_config +from graphon.nodes.llm.entities import LLMNodeData +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData +from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData from models.model import Conversation if TYPE_CHECKING: diff --git a/api/core/workflow/node_runtime.py b/api/core/workflow/node_runtime.py index 19cb3a7b0a..2e632e56f0 100644 --- a/api/core/workflow/node_runtime.py +++ b/api/core/workflow/node_runtime.py @@ -4,6 +4,32 @@ from collections.abc import Callable, Generator, Mapping, Sequence from dataclasses import dataclass from typing import TYPE_CHECKING, Any, cast +from sqlalchemy import select +from sqlalchemy.orm import Session + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.app.file_access import DatabaseFileAccessController +from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output +from core.model_manager import ModelInstance +from core.plugin.impl.exc import PluginDaemonClientSideError, PluginInvokeError +from core.plugin.impl.plugin import PluginInstaller +from core.prompt.utils.prompt_message_util import PromptMessageUtil +from core.repositories.human_input_repository import ( + FormCreateParams, + HumanInputFormRepository, + HumanInputFormRepositoryImpl, +) +from core.tools.entities.tool_entities import ToolProviderType as CoreToolProviderType +from core.tools.errors import ToolInvokeError +from core.tools.tool_engine import ToolEngine +from core.tools.tool_file_manager import ToolFileManager +from core.tools.tool_manager import ToolManager +from core.tools.utils.message_transformer import ToolFileMessageTransformer +from core.workflow.file_reference import build_file_reference +from extensions.ext_database import db +from factories import file_factory from graphon.file import FileTransferMethod, FileType from graphon.model_runtime.entities import LLMMode from graphon.model_runtime.entities.llm_entities import ( @@ -34,32 +60,6 @@ from graphon.nodes.tool_runtime_entities import ( ToolRuntimeMessage, ToolRuntimeParameter, ) -from sqlalchemy import select -from sqlalchemy.orm import Session - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.app.file_access import DatabaseFileAccessController -from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler -from core.llm_generator.output_parser.errors import OutputParserError -from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output -from core.model_manager import ModelInstance -from core.plugin.impl.exc import PluginDaemonClientSideError, PluginInvokeError -from core.plugin.impl.plugin import PluginInstaller -from core.prompt.utils.prompt_message_util import PromptMessageUtil -from core.repositories.human_input_repository import ( - FormCreateParams, - HumanInputFormRepository, - HumanInputFormRepositoryImpl, -) -from core.tools.entities.tool_entities import ToolProviderType as CoreToolProviderType -from core.tools.errors import ToolInvokeError -from core.tools.tool_engine import ToolEngine -from core.tools.tool_file_manager import ToolFileManager -from core.tools.tool_manager import ToolManager -from core.tools.utils.message_transformer import ToolFileMessageTransformer -from core.workflow.file_reference import build_file_reference -from extensions.ext_database import db -from factories import file_factory from models.dataset import SegmentAttachmentBinding from models.model import UploadFile from services.tools.builtin_tools_manage_service import BuiltinToolManageService @@ -76,13 +76,12 @@ from .human_input_compat import ( from .system_variables import SystemVariableKey, get_system_text if TYPE_CHECKING: + from core.tools.__base.tool import Tool + from core.tools.entities.tool_entities import ToolInvokeMessage as CoreToolInvokeMessage from graphon.file import File from graphon.nodes.llm.file_saver import LLMFileSaver from graphon.nodes.tool.entities import ToolNodeData - from core.tools.__base.tool import Tool - from core.tools.entities.tool_entities import ToolInvokeMessage as CoreToolInvokeMessage - _file_access_controller = DatabaseFileAccessController() diff --git a/api/core/workflow/nodes/agent/agent_node.py b/api/core/workflow/nodes/agent/agent_node.py index bfd5536e4a..7b000101b0 100644 --- a/api/core/workflow/nodes/agent/agent_node.py +++ b/api/core/workflow/nodes/agent/agent_node.py @@ -3,15 +3,14 @@ from __future__ import annotations from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.workflow.system_variables import SystemVariableKey, get_system_text from graphon.entities.graph_config import NodeConfigDict from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.node_events import NodeEventBase, NodeRunResult, StreamCompletedEvent from graphon.nodes.base.node import Node from graphon.nodes.base.variable_template_parser import VariableTemplateParser -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.workflow.system_variables import SystemVariableKey, get_system_text - from .entities import AgentNodeData from .exceptions import ( AgentInvocationError, diff --git a/api/core/workflow/nodes/agent/entities.py b/api/core/workflow/nodes/agent/entities.py index c52aad150b..51452c29a3 100644 --- a/api/core/workflow/nodes/agent/entities.py +++ b/api/core/workflow/nodes/agent/entities.py @@ -1,12 +1,12 @@ from enum import IntEnum, StrEnum, auto from typing import Any, Literal, Union -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import BuiltinNodeTypes, NodeType from pydantic import BaseModel from core.prompt.entities.advanced_prompt_entities import MemoryConfig from core.tools.entities.tool_entities import ToolSelector +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import BuiltinNodeTypes, NodeType class AgentNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/agent/message_transformer.py b/api/core/workflow/nodes/agent/message_transformer.py index db74590ed7..f44681377d 100644 --- a/api/core/workflow/nodes/agent/message_transformer.py +++ b/api/core/workflow/nodes/agent/message_transformer.py @@ -3,6 +3,14 @@ from __future__ import annotations from collections.abc import Generator, Mapping from typing import Any, cast +from sqlalchemy import select +from sqlalchemy.orm import Session + +from core.app.file_access import DatabaseFileAccessController +from core.tools.entities.tool_entities import ToolInvokeMessage +from core.tools.utils.message_transformer import ToolFileMessageTransformer +from extensions.ext_database import db +from factories import file_factory from graphon.enums import BuiltinNodeTypes, NodeType, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod, get_file_type_by_mime_type from graphon.model_runtime.entities.llm_entities import LLMUsage, LLMUsageMetadata @@ -15,14 +23,6 @@ from graphon.node_events import ( StreamCompletedEvent, ) from graphon.variables.segments import ArrayFileSegment -from sqlalchemy import select -from sqlalchemy.orm import Session - -from core.app.file_access import DatabaseFileAccessController -from core.tools.entities.tool_entities import ToolInvokeMessage -from core.tools.utils.message_transformer import ToolFileMessageTransformer -from extensions.ext_database import db -from factories import file_factory from models import ToolFile from services.tools.builtin_tools_manage_service import BuiltinToolManageService diff --git a/api/core/workflow/nodes/agent/runtime_support.py b/api/core/workflow/nodes/agent/runtime_support.py index be50edbc4d..a872774c98 100644 --- a/api/core/workflow/nodes/agent/runtime_support.py +++ b/api/core/workflow/nodes/agent/runtime_support.py @@ -4,8 +4,6 @@ import json from collections.abc import Sequence from typing import Any, cast -from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType -from graphon.runtime import VariablePool from packaging.version import Version from pydantic import ValidationError from sqlalchemy import select @@ -21,6 +19,8 @@ from core.tools.entities.tool_entities import ToolIdentity, ToolParameter, ToolP from core.tools.tool_manager import ToolManager from core.workflow.system_variables import SystemVariableKey, get_system_text from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType +from graphon.runtime import VariablePool from models.model import Conversation from .entities import AgentNodeData, AgentOldVersionModelFeatures, ParamsAutoGenerated diff --git a/api/core/workflow/nodes/datasource/datasource_node.py b/api/core/workflow/nodes/datasource/datasource_node.py index d9247b2593..e4f6b3b470 100644 --- a/api/core/workflow/nodes/datasource/datasource_node.py +++ b/api/core/workflow/nodes/datasource/datasource_node.py @@ -1,6 +1,12 @@ from collections.abc import Generator, Mapping, Sequence from typing import TYPE_CHECKING, Any +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.datasource.datasource_manager import DatasourceManager +from core.datasource.entities.datasource_entities import DatasourceProviderType +from core.plugin.impl.exc import PluginDaemonClientSideError +from core.workflow.file_reference import resolve_file_record_id +from core.workflow.system_variables import SystemVariableKey, get_system_segment from graphon.entities.graph_config import NodeConfigDict from graphon.enums import ( BuiltinNodeTypes, @@ -12,13 +18,6 @@ from graphon.node_events import NodeRunResult, StreamCompletedEvent from graphon.nodes.base.node import Node from graphon.nodes.base.variable_template_parser import VariableTemplateParser -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.datasource.datasource_manager import DatasourceManager -from core.datasource.entities.datasource_entities import DatasourceProviderType -from core.plugin.impl.exc import PluginDaemonClientSideError -from core.workflow.file_reference import resolve_file_record_id -from core.workflow.system_variables import SystemVariableKey, get_system_segment - from .entities import DatasourceNodeData, DatasourceParameter, OnlineDriveDownloadFileParam from .exc import DatasourceNodeError diff --git a/api/core/workflow/nodes/datasource/entities.py b/api/core/workflow/nodes/datasource/entities.py index cad32f8d5b..28966f2392 100644 --- a/api/core/workflow/nodes/datasource/entities.py +++ b/api/core/workflow/nodes/datasource/entities.py @@ -1,9 +1,10 @@ from typing import Any, Literal, Union +from pydantic import BaseModel, field_validator +from pydantic_core.core_schema import ValidationInfo + from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType -from pydantic import BaseModel, field_validator -from pydantic_core.core_schema import ValidationInfo class DatasourceEntity(BaseModel): diff --git a/api/core/workflow/nodes/knowledge_index/entities.py b/api/core/workflow/nodes/knowledge_index/entities.py index 04a10f9257..260881e49c 100644 --- a/api/core/workflow/nodes/knowledge_index/entities.py +++ b/api/core/workflow/nodes/knowledge_index/entities.py @@ -1,13 +1,13 @@ from typing import Union -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType from pydantic import BaseModel from core.rag.entities import RerankingModelConfig, WeightedScoreConfig from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType class RetrievalSetting(BaseModel): diff --git a/api/core/workflow/nodes/knowledge_index/knowledge_index_node.py b/api/core/workflow/nodes/knowledge_index/knowledge_index_node.py index bb72fe3881..d5cab05dbe 100644 --- a/api/core/workflow/nodes/knowledge_index/knowledge_index_node.py +++ b/api/core/workflow/nodes/knowledge_index/knowledge_index_node.py @@ -2,17 +2,16 @@ import logging from collections.abc import Mapping from typing import TYPE_CHECKING, Any -from graphon.entities.graph_config import NodeConfigDict -from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus -from graphon.node_events import NodeRunResult -from graphon.nodes.base.node import Node -from graphon.nodes.base.template import Template - from core.rag.index_processor.index_processor import IndexProcessor from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.rag.summary_index.summary_index import SummaryIndex from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE from core.workflow.system_variables import SystemVariableKey, get_system_segment, get_system_text +from graphon.entities.graph_config import NodeConfigDict +from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus +from graphon.node_events import NodeRunResult +from graphon.nodes.base.node import Node +from graphon.nodes.base.template import Template from .entities import KnowledgeIndexNodeData from .exc import ( diff --git a/api/core/workflow/nodes/knowledge_retrieval/entities.py b/api/core/workflow/nodes/knowledge_retrieval/entities.py index 460ec693ce..3825f526a2 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/entities.py +++ b/api/core/workflow/nodes/knowledge_retrieval/entities.py @@ -1,11 +1,11 @@ from typing import Literal -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import BuiltinNodeTypes, NodeType -from graphon.nodes.llm.entities import ModelConfig, VisionConfig from pydantic import BaseModel, Field from core.rag.entities import Condition, MetadataFilteringCondition, RerankingModelConfig, WeightedScoreConfig +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import BuiltinNodeTypes, NodeType +from graphon.nodes.llm.entities import ModelConfig, VisionConfig __all__ = ["Condition"] diff --git a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py index 13624b27b3..47ad14b499 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py @@ -8,6 +8,11 @@ import logging from collections.abc import Mapping, Sequence from typing import TYPE_CHECKING, Any, Literal +from core.app.app_config.entities import DatasetRetrieveConfigEntity +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext +from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict +from core.rag.retrieval.dataset_retrieval import DatasetRetrieval +from core.workflow.file_reference import parse_file_reference from graphon.entities import GraphInitParams from graphon.entities.graph_config import NodeConfigDict from graphon.enums import ( @@ -27,12 +32,6 @@ from graphon.variables import ( ) from graphon.variables.segments import ArrayObjectSegment -from core.app.app_config.entities import DatasetRetrieveConfigEntity -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext -from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval -from core.workflow.file_reference import parse_file_reference - from .entities import ( Condition, KnowledgeRetrievalNodeData, diff --git a/api/core/workflow/nodes/knowledge_retrieval/retrieval.py b/api/core/workflow/nodes/knowledge_retrieval/retrieval.py index 39e2008a2c..ea45dcf5c2 100644 --- a/api/core/workflow/nodes/knowledge_retrieval/retrieval.py +++ b/api/core/workflow/nodes/knowledge_retrieval/retrieval.py @@ -1,10 +1,10 @@ from typing import Any, Literal, Protocol -from graphon.model_runtime.entities import LLMUsage -from graphon.nodes.llm.entities import ModelConfig from pydantic import BaseModel, Field from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict +from graphon.model_runtime.entities import LLMUsage +from graphon.nodes.llm.entities import ModelConfig from .entities import MetadataFilteringCondition diff --git a/api/core/workflow/nodes/trigger_plugin/entities.py b/api/core/workflow/nodes/trigger_plugin/entities.py index bf5be2379a..23ed2cd408 100644 --- a/api/core/workflow/nodes/trigger_plugin/entities.py +++ b/api/core/workflow/nodes/trigger_plugin/entities.py @@ -1,12 +1,12 @@ from collections.abc import Mapping from typing import Any, Literal, Union -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType from pydantic import BaseModel, Field, ValidationInfo, field_validator from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE from core.trigger.entities.entities import EventParameter +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType from .exc import TriggerEventParameterError diff --git a/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py b/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py index e50de11bb9..c848a86255 100644 --- a/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py +++ b/api/core/workflow/nodes/trigger_plugin/trigger_event_node.py @@ -1,13 +1,12 @@ from collections.abc import Mapping from typing import Any +from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID from graphon.enums import NodeExecutionType, WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.node_events import NodeRunResult from graphon.nodes.base.node import Node -from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID - from .entities import TriggerEventNodeData diff --git a/api/core/workflow/nodes/trigger_schedule/entities.py b/api/core/workflow/nodes/trigger_schedule/entities.py index 04f1f7e6bb..683c8d420f 100644 --- a/api/core/workflow/nodes/trigger_schedule/entities.py +++ b/api/core/workflow/nodes/trigger_schedule/entities.py @@ -1,10 +1,10 @@ from typing import Any, Literal, Union -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType from pydantic import BaseModel, Field from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType class TriggerScheduleNodeData(BaseNodeData): diff --git a/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py b/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py index a9753ab387..b46cc76a6e 100644 --- a/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py +++ b/api/core/workflow/nodes/trigger_schedule/trigger_schedule_node.py @@ -1,11 +1,10 @@ from collections.abc import Mapping -from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus -from graphon.node_events import NodeRunResult -from graphon.nodes.base.node import Node - from core.trigger.constants import TRIGGER_SCHEDULE_NODE_TYPE from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus +from graphon.node_events import NodeRunResult +from graphon.nodes.base.node import Node from .entities import TriggerScheduleNodeData diff --git a/api/core/workflow/nodes/trigger_webhook/entities.py b/api/core/workflow/nodes/trigger_webhook/entities.py index a30f877e4b..b261039448 100644 --- a/api/core/workflow/nodes/trigger_webhook/entities.py +++ b/api/core/workflow/nodes/trigger_webhook/entities.py @@ -1,12 +1,12 @@ from collections.abc import Sequence from enum import StrEnum -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import NodeType -from graphon.variables.types import SegmentType from pydantic import BaseModel, Field, field_validator from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import NodeType +from graphon.variables.types import SegmentType _WEBHOOK_HEADER_ALLOWED_TYPES: frozenset[SegmentType] = frozenset((SegmentType.STRING,)) diff --git a/api/core/workflow/nodes/trigger_webhook/node.py b/api/core/workflow/nodes/trigger_webhook/node.py index d942a718cc..13c4f05bfd 100644 --- a/api/core/workflow/nodes/trigger_webhook/node.py +++ b/api/core/workflow/nodes/trigger_webhook/node.py @@ -2,6 +2,10 @@ import logging from collections.abc import Mapping from typing import Any +from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE +from core.workflow.file_reference import resolve_file_record_id +from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from factories.variable_factory import build_segment_with_type from graphon.enums import NodeExecutionType, WorkflowNodeExecutionStatus from graphon.file import FileTransferMethod from graphon.node_events import NodeRunResult @@ -10,11 +14,6 @@ from graphon.nodes.protocols import FileReferenceFactoryProtocol from graphon.variables.types import SegmentType from graphon.variables.variables import FileVariable -from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE -from core.workflow.file_reference import resolve_file_record_id -from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID -from factories.variable_factory import build_segment_with_type - from .entities import ContentType, WebhookData logger = logging.getLogger(__name__) diff --git a/api/core/workflow/template_rendering.py b/api/core/workflow/template_rendering.py index d51cfadd09..b4ffb37549 100644 --- a/api/core/workflow/template_rendering.py +++ b/api/core/workflow/template_rendering.py @@ -3,11 +3,10 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor from graphon.nodes.code.entities import CodeLanguage from graphon.template_rendering import Jinja2TemplateRenderer, TemplateRenderError -from core.helper.code_executor.code_executor import CodeExecutionError, CodeExecutor - class CodeExecutorJinja2TemplateRenderer(Jinja2TemplateRenderer): """Sandbox-backed Jinja2 renderer for workflow-owned node composition.""" diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index f0a5fbb400..4e2f603e5b 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -3,20 +3,6 @@ import time from collections.abc import Generator, Mapping, Sequence from typing import Any, TypedDict -from graphon.entities import GraphInitParams -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.errors import WorkflowNodeRunFailedError -from graphon.file import File -from graphon.graph import Graph -from graphon.graph_engine import GraphEngine, GraphEngineConfig -from graphon.graph_engine.command_channels import CommandChannel, InMemoryChannel -from graphon.graph_engine.layers import DebugLoggingLayer, ExecutionLimitsLayer -from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, GraphRunFailedEvent -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.base.node import Node -from graphon.runtime import ChildGraphNotFoundError, GraphRuntimeState, VariablePool -from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool - from configs import dify_config from context import capture_current_context from core.app.apps.exc import GenerateTaskStoppedError @@ -40,6 +26,19 @@ from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add from core.workflow.variable_prefixes import ENVIRONMENT_VARIABLE_NODE_ID from extensions.otel.runtime import is_instrument_flag_enabled from factories import file_factory +from graphon.entities import GraphInitParams +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.errors import WorkflowNodeRunFailedError +from graphon.file import File +from graphon.graph import Graph +from graphon.graph_engine import GraphEngine, GraphEngineConfig +from graphon.graph_engine.command_channels import CommandChannel, InMemoryChannel +from graphon.graph_engine.layers import DebugLoggingLayer, ExecutionLimitsLayer +from graphon.graph_events import GraphEngineEvent, GraphNodeEventBase, GraphRunFailedEvent +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.base.node import Node +from graphon.runtime import ChildGraphNotFoundError, GraphRuntimeState, VariablePool +from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader, load_into_variable_pool from models.workflow import Workflow logger = logging.getLogger(__name__) diff --git a/api/docker/entrypoint.sh b/api/docker/entrypoint.sh index 6b904b7d0d..fc118df5bc 100755 --- a/api/docker/entrypoint.sh +++ b/api/docker/entrypoint.sh @@ -35,10 +35,10 @@ if [[ "${MODE}" == "worker" ]]; then if [[ -z "${CELERY_QUEUES}" ]]; then if [[ "${EDITION}" == "CLOUD" ]]; then # Cloud edition: separate queues for dataset and trigger tasks - DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow_professional,workflow_team,workflow_sandbox,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution" + DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow_professional,workflow_team,workflow_sandbox,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution" else # Community edition (SELF_HOSTED): dataset, pipeline and workflow have separate queues - DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor,retention,workflow_based_app_execution" + DEFAULT_QUEUES="api_token,dataset,dataset_summary,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_publisher,trigger_refresh_executor,retention,workflow_based_app_execution" fi else DEFAULT_QUEUES="${CELERY_QUEUES}" @@ -119,14 +119,16 @@ elif [[ "${MODE}" == "job" ]]; then else if [[ "${DEBUG}" == "true" ]]; then - exec flask run --host=${DIFY_BIND_ADDRESS:-0.0.0.0} --port=${DIFY_PORT:-5001} --debug + export HOST=${DIFY_BIND_ADDRESS:-0.0.0.0} + export PORT=${DIFY_PORT:-5001} + exec python -m app else exec gunicorn \ --bind "${DIFY_BIND_ADDRESS:-0.0.0.0}:${DIFY_PORT:-5001}" \ --workers ${SERVER_WORKER_AMOUNT:-1} \ - --worker-class ${SERVER_WORKER_CLASS:-gevent} \ + --worker-class ${SERVER_WORKER_CLASS:-geventwebsocket.gunicorn.workers.GeventWebSocketWorker} \ --worker-connections ${SERVER_WORKER_CONNECTIONS:-10} \ --timeout ${GUNICORN_TIMEOUT:-200} \ - app:app + app:socketio_app fi fi diff --git a/api/enterprise/telemetry/draft_trace.py b/api/enterprise/telemetry/draft_trace.py index 5a8d0ee6f4..dff558988c 100644 --- a/api/enterprise/telemetry/draft_trace.py +++ b/api/enterprise/telemetry/draft_trace.py @@ -3,10 +3,9 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from graphon.enums import WorkflowNodeExecutionMetadataKey - from core.telemetry import TelemetryContext, TelemetryEvent, TraceTaskName from core.telemetry import emit as telemetry_emit +from graphon.enums import WorkflowNodeExecutionMetadataKey from models.workflow import WorkflowNodeExecutionModel diff --git a/api/events/event_handlers/create_document_index.py b/api/events/event_handlers/create_document_index.py index b7e7a6e60f..0c535a1c5b 100644 --- a/api/events/event_handlers/create_document_index.py +++ b/api/events/event_handlers/create_document_index.py @@ -6,9 +6,9 @@ import click from sqlalchemy import select from werkzeug.exceptions import NotFound +from core.db.session_factory import session_factory from core.indexing_runner import DocumentIsPausedError, IndexingRunner from events.document_index_event import document_index_created -from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.dataset import Document from models.enums import IndexingStatus @@ -22,24 +22,25 @@ def handle(sender, **kwargs): document_ids = kwargs.get("document_ids", []) documents = [] start_at = time.perf_counter() - for document_id in document_ids: - logger.info(click.style(f"Start process document: {document_id}", fg="green")) + with session_factory.create_session() as session: + for document_id in document_ids: + logger.info(click.style(f"Start process document: {document_id}", fg="green")) - document = db.session.scalar( - select(Document).where( - Document.id == document_id, - Document.dataset_id == dataset_id, + document = session.scalar( + select(Document).where( + Document.id == document_id, + Document.dataset_id == dataset_id, + ) ) - ) - if not document: - raise NotFound("Document not found") + if not document: + raise NotFound("Document not found") - document.indexing_status = IndexingStatus.PARSING - document.processing_started_at = naive_utc_now() - documents.append(document) - db.session.add(document) - db.session.commit() + document.indexing_status = IndexingStatus.PARSING + document.processing_started_at = naive_utc_now() + documents.append(document) + session.add(document) + session.commit() with contextlib.suppress(Exception): try: diff --git a/api/events/event_handlers/create_installed_app_when_app_created.py b/api/events/event_handlers/create_installed_app_when_app_created.py index 57412cc4ad..38e102d5fd 100644 --- a/api/events/event_handlers/create_installed_app_when_app_created.py +++ b/api/events/event_handlers/create_installed_app_when_app_created.py @@ -1,5 +1,5 @@ +from core.db.session_factory import session_factory from events.app_event import app_was_created -from extensions.ext_database import db from models.model import InstalledApp @@ -12,5 +12,6 @@ def handle(sender, **kwargs): app_id=app.id, app_owner_tenant_id=app.tenant_id, ) - db.session.add(installed_app) - db.session.commit() + with session_factory.create_session() as session: + session.add(installed_app) + session.commit() diff --git a/api/events/event_handlers/create_site_record_when_app_created.py b/api/events/event_handlers/create_site_record_when_app_created.py index 84be592b1a..5e2a456dce 100644 --- a/api/events/event_handlers/create_site_record_when_app_created.py +++ b/api/events/event_handlers/create_site_record_when_app_created.py @@ -1,5 +1,5 @@ +from core.db.session_factory import session_factory from events.app_event import app_was_created -from extensions.ext_database import db from models.enums import CustomizeTokenStrategy from models.model import Site @@ -22,6 +22,6 @@ def handle(sender, **kwargs): created_by=app.created_by, updated_by=app.updated_by, ) - - db.session.add(site) - db.session.commit() + with session_factory.create_session() as session: + session.add(site) + session.commit() diff --git a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py index 7bd8e88231..ba9758175f 100644 --- a/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py +++ b/api/events/event_handlers/delete_tool_parameters_cache_when_sync_draft_workflow.py @@ -1,12 +1,11 @@ import logging -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.tool.entities import ToolEntity - from core.tools.entities.tool_entities import ToolProviderType from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_draft_workflow_was_synced +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.tool.entities import ToolEntity logger = logging.getLogger(__name__) diff --git a/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py b/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py index 86b5b2bbf0..6769b94cde 100644 --- a/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py +++ b/api/events/event_handlers/update_app_dataset_join_when_app_published_workflow_updated.py @@ -1,11 +1,11 @@ from typing import cast -from graphon.nodes import BuiltinNodeTypes from sqlalchemy import delete, select from core.workflow.nodes.knowledge_retrieval.entities import KnowledgeRetrievalNodeData from events.app_event import app_published_workflow_was_updated from extensions.ext_database import db +from graphon.nodes import BuiltinNodeTypes from models.dataset import AppDatasetJoin from models.workflow import Workflow diff --git a/api/extensions/ext_sentry.py b/api/extensions/ext_sentry.py index 5cc58f27c4..69d1f1ab07 100644 --- a/api/extensions/ext_sentry.py +++ b/api/extensions/ext_sentry.py @@ -5,11 +5,12 @@ from dify_app import DifyApp def init_app(app: DifyApp): if dify_config.SENTRY_DSN: import sentry_sdk - from graphon.model_runtime.errors.invoke import InvokeRateLimitError from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.flask import FlaskIntegration from werkzeug.exceptions import HTTPException + from graphon.model_runtime.errors.invoke import InvokeRateLimitError + try: from langfuse._utils import parse_error diff --git a/api/extensions/ext_socketio.py b/api/extensions/ext_socketio.py new file mode 100644 index 0000000000..5ed82bac8d --- /dev/null +++ b/api/extensions/ext_socketio.py @@ -0,0 +1,5 @@ +import socketio # type: ignore[reportMissingTypeStubs] + +from configs import dify_config + +sio = socketio.Server(async_mode="gevent", cors_allowed_origins=dify_config.CONSOLE_CORS_ALLOW_ORIGINS) diff --git a/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py b/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py index db599c5d49..64ff0f0674 100644 --- a/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_api_workflow_node_execution_repository.py @@ -11,12 +11,12 @@ from collections.abc import Sequence from datetime import datetime from typing import Any -from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy.orm import sessionmaker from extensions.logstore.aliyun_logstore import AliyunLogStore from extensions.logstore.repositories import safe_float, safe_int from extensions.logstore.sql_escape import escape_identifier, escape_logstore_query_value +from graphon.enums import WorkflowNodeExecutionStatus from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom from repositories.api_workflow_node_execution_repository import DifyAPIWorkflowNodeExecutionRepository diff --git a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py index 2745141431..7f77a0437a 100644 --- a/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py +++ b/api/extensions/logstore/repositories/logstore_api_workflow_run_repository.py @@ -20,12 +20,12 @@ from collections.abc import Sequence from datetime import datetime from typing import Any, cast -from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import sessionmaker from extensions.logstore.aliyun_logstore import AliyunLogStore from extensions.logstore.repositories import safe_float, safe_int from extensions.logstore.sql_escape import escape_identifier, escape_logstore_query_value, escape_sql_string +from graphon.enums import WorkflowExecutionStatus from libs.infinite_scroll_pagination import InfiniteScrollPagination from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowRun, WorkflowType diff --git a/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py b/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py index d0f3e2e244..544109276d 100644 --- a/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_workflow_execution_repository.py @@ -3,14 +3,14 @@ import logging import os import time -from graphon.entities import WorkflowExecution -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.factory import WorkflowExecutionRepository from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository from extensions.logstore.aliyun_logstore import AliyunLogStore +from graphon.entities import WorkflowExecution +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from models import ( Account, diff --git a/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py b/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py index 37952d6464..dc7654a25c 100644 --- a/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py +++ b/api/extensions/logstore/repositories/logstore_workflow_node_execution_repository.py @@ -13,10 +13,6 @@ from collections.abc import Sequence from datetime import datetime from typing import Any -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker @@ -26,6 +22,10 @@ from core.repositories.factory import OrderConfig, WorkflowNodeExecutionReposito from extensions.logstore.aliyun_logstore import AliyunLogStore from extensions.logstore.repositories import safe_float, safe_int from extensions.logstore.sql_escape import escape_identifier +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from libs.helper import extract_tenant_id from models import ( Account, diff --git a/api/extensions/otel/parser/base.py b/api/extensions/otel/parser/base.py index 23d324f9ea..fbf379b3e5 100644 --- a/api/extensions/otel/parser/base.py +++ b/api/extensions/otel/parser/base.py @@ -10,17 +10,17 @@ Gate is only active in EE (``ENTERPRISE_ENABLED=True``) when import json from typing import Any, Protocol -from graphon.enums import BuiltinNodeTypes -from graphon.file import File -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node -from graphon.variables import Segment from opentelemetry.trace import Span from opentelemetry.trace.status import Status, StatusCode from pydantic import BaseModel from configs import dify_config from extensions.otel.semconv.gen_ai import ChainAttributes, GenAIAttributes +from graphon.enums import BuiltinNodeTypes +from graphon.file import File +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node +from graphon.variables import Segment def should_include_content() -> bool: diff --git a/api/extensions/otel/parser/llm.py b/api/extensions/otel/parser/llm.py index 335c5cc29e..ec3c78a12d 100644 --- a/api/extensions/otel/parser/llm.py +++ b/api/extensions/otel/parser/llm.py @@ -6,12 +6,12 @@ import logging from collections.abc import Mapping from typing import Any -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node from opentelemetry.trace import Span from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps from extensions.otel.semconv.gen_ai import LLMAttributes +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node logger = logging.getLogger(__name__) diff --git a/api/extensions/otel/parser/retrieval.py b/api/extensions/otel/parser/retrieval.py index 6df5f62c15..56672d1fd4 100644 --- a/api/extensions/otel/parser/retrieval.py +++ b/api/extensions/otel/parser/retrieval.py @@ -6,13 +6,13 @@ import logging from collections.abc import Sequence from typing import Any -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node -from graphon.variables import Segment from opentelemetry.trace import Span from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps from extensions.otel.semconv.gen_ai import RetrieverAttributes +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node +from graphon.variables import Segment logger = logging.getLogger(__name__) diff --git a/api/extensions/otel/parser/tool.py b/api/extensions/otel/parser/tool.py index b9fdd9e1ca..75ddbba448 100644 --- a/api/extensions/otel/parser/tool.py +++ b/api/extensions/otel/parser/tool.py @@ -2,14 +2,14 @@ Parser for tool nodes that captures tool-specific metadata. """ -from graphon.enums import WorkflowNodeExecutionMetadataKey -from graphon.graph_events import GraphNodeEventBase -from graphon.nodes.base.node import Node -from graphon.nodes.tool.entities import ToolNodeData from opentelemetry.trace import Span from extensions.otel.parser.base import DefaultNodeOTelParser, safe_json_dumps from extensions.otel.semconv.gen_ai import ToolAttributes +from graphon.enums import WorkflowNodeExecutionMetadataKey +from graphon.graph_events import GraphNodeEventBase +from graphon.nodes.base.node import Node +from graphon.nodes.tool.entities import ToolNodeData class ToolNodeOTelParser: diff --git a/api/factories/file_factory/builders.py b/api/factories/file_factory/builders.py index 7516d18c8e..288d37d265 100644 --- a/api/factories/file_factory/builders.py +++ b/api/factories/file_factory/builders.py @@ -7,12 +7,12 @@ import uuid from collections.abc import Mapping, Sequence from typing import Any -from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig, helpers, standardize_file_type from sqlalchemy import select from core.app.file_access import FileAccessControllerProtocol from core.workflow.file_reference import build_file_reference from extensions.ext_database import db +from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig, helpers, standardize_file_type from models import ToolFile, UploadFile from .common import resolve_mapping_file_id diff --git a/api/factories/file_factory/message_files.py b/api/factories/file_factory/message_files.py index 5582b85c95..4b3d514238 100644 --- a/api/factories/file_factory/message_files.py +++ b/api/factories/file_factory/message_files.py @@ -4,9 +4,8 @@ from __future__ import annotations from collections.abc import Sequence -from graphon.file import File, FileBelongsTo, FileTransferMethod, FileUploadConfig - from core.app.file_access import FileAccessControllerProtocol +from graphon.file import File, FileBelongsTo, FileTransferMethod, FileUploadConfig from models import MessageFile from .builders import build_from_mapping diff --git a/api/factories/file_factory/storage_keys.py b/api/factories/file_factory/storage_keys.py index db3a7f3015..dba4c84407 100644 --- a/api/factories/file_factory/storage_keys.py +++ b/api/factories/file_factory/storage_keys.py @@ -5,12 +5,12 @@ from __future__ import annotations import uuid from collections.abc import Mapping, Sequence -from graphon.file import File, FileTransferMethod from sqlalchemy import select from sqlalchemy.orm import Session from core.app.file_access import FileAccessControllerProtocol from core.workflow.file_reference import build_file_reference, parse_file_reference +from graphon.file import File, FileTransferMethod from models import ToolFile, UploadFile diff --git a/api/factories/variable_factory.py b/api/factories/variable_factory.py index 57205b5739..fd7acb14d3 100644 --- a/api/factories/variable_factory.py +++ b/api/factories/variable_factory.py @@ -8,6 +8,11 @@ shared conversion functions for legacy callers and tests. from collections.abc import Mapping, Sequence from typing import Any, cast +from configs import dify_config +from core.workflow.variable_prefixes import ( + CONVERSATION_VARIABLE_NODE_ID, + ENVIRONMENT_VARIABLE_NODE_ID, +) from graphon.variables.exc import VariableError from graphon.variables.factory import ( TypeMismatchError, @@ -31,12 +36,6 @@ from graphon.variables.variables import ( VariableBase, ) -from configs import dify_config -from core.workflow.variable_prefixes import ( - CONVERSATION_VARIABLE_NODE_ID, - ENVIRONMENT_VARIABLE_NODE_ID, -) - __all__ = [ "TypeMismatchError", "UnsupportedSegmentTypeError", diff --git a/api/fields/conversation_fields.py b/api/fields/conversation_fields.py index 1afcbdb5b9..bf5c9ffcb1 100644 --- a/api/fields/conversation_fields.py +++ b/api/fields/conversation_fields.py @@ -3,10 +3,10 @@ from __future__ import annotations from datetime import datetime from typing import Any -from graphon.file import File from pydantic import Field, field_validator, model_validator from fields.base import ResponseModel +from graphon.file import File type JSONValue = Any @@ -96,7 +96,7 @@ class ConversationAnnotation(ResponseModel): class ConversationAnnotationHitHistory(ResponseModel): - annotation_id: str + annotation_id: str = Field(validation_alias="id") annotation_create_account: SimpleAccount | None = None created_at: int | None = None @@ -143,7 +143,7 @@ class MessageDetail(ResponseModel): query: str message: JSONValue message_tokens: int - answer: str + answer: str = Field(validation_alias="re_sign_file_url_answer") answer_tokens: int provider_response_latency: float from_source: str @@ -156,7 +156,7 @@ class MessageDetail(ResponseModel): created_at: int | None = None agent_thoughts: list[AgentThought] message_files: list[MessageFile] - metadata: JSONValue + metadata: JSONValue = Field(validation_alias="message_metadata_dict") status: str error: str | None = None parent_message_id: str | None = None @@ -196,7 +196,7 @@ class ModelConfig(ResponseModel): class SimpleModelConfig(ResponseModel): - model: JSONValue | None = None + model: JSONValue | None = Field(default=None, validation_alias="model_dict") pre_prompt: str | None = None @@ -211,6 +211,11 @@ class SimpleMessageDetail(ResponseModel): def _normalize_inputs(cls, value: JSONValue) -> JSONValue: return format_files_contained(value) + @field_validator("message", mode="before") + @classmethod + def _normalize_message(cls, value: JSONValue) -> str: + return message_text(value) + class Conversation(ResponseModel): id: str @@ -227,15 +232,22 @@ class Conversation(ResponseModel): model_config_: SimpleModelConfig | None = Field(default=None, alias="model_config") user_feedback_stats: FeedbackStat | None = None admin_feedback_stats: FeedbackStat | None = None - message: SimpleMessageDetail | None = None + message: SimpleMessageDetail | None = Field(default=None, validation_alias="first_message") + + @field_validator("read_at", "created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return to_timestamp(value) + return value class ConversationPagination(ResponseModel): page: int - limit: int + limit: int = Field(validation_alias="per_page") total: int - has_more: bool - data: list[Conversation] + has_more: bool = Field(validation_alias="has_next") + data: list[Conversation] = Field(validation_alias="items") class ConversationMessageDetail(ResponseModel): @@ -246,7 +258,14 @@ class ConversationMessageDetail(ResponseModel): from_account_id: str | None = None created_at: int | None = None model_config_: ModelConfig | None = Field(default=None, alias="model_config") - message: MessageDetail | None = None + message: MessageDetail | None = Field(default=None, validation_alias="first_message") + + @field_validator("created_at", mode="before") + @classmethod + def _normalize_created_at(cls, value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return to_timestamp(value) + return value class ConversationWithSummary(ResponseModel): @@ -258,7 +277,7 @@ class ConversationWithSummary(ResponseModel): from_account_id: str | None = None from_account_name: str | None = None name: str - summary: str + summary: str = Field(validation_alias="summary_or_query") read_at: int | None = None created_at: int | None = None updated_at: int | None = None @@ -269,13 +288,20 @@ class ConversationWithSummary(ResponseModel): admin_feedback_stats: FeedbackStat | None = None status_count: StatusCount | None = None + @field_validator("read_at", "created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return to_timestamp(value) + return value + class ConversationWithSummaryPagination(ResponseModel): page: int - limit: int + limit: int = Field(validation_alias="per_page") total: int - has_more: bool - data: list[ConversationWithSummary] + has_more: bool = Field(validation_alias="has_next") + data: list[ConversationWithSummary] = Field(validation_alias="items") class ConversationDetail(ResponseModel): @@ -293,6 +319,13 @@ class ConversationDetail(ResponseModel): user_feedback_stats: FeedbackStat | None = None admin_feedback_stats: FeedbackStat | None = None + @field_validator("created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return to_timestamp(value) + return value + def to_timestamp(value: datetime | None) -> int | None: if value is None: diff --git a/api/fields/conversation_variable_fields.py b/api/fields/conversation_variable_fields.py index c55014a368..cf4a71d545 100644 --- a/api/fields/conversation_variable_fields.py +++ b/api/fields/conversation_variable_fields.py @@ -1,5 +1,13 @@ -from flask_restx import Namespace, fields +from __future__ import annotations +from datetime import datetime +from typing import Any + +from flask_restx import Namespace, fields +from pydantic import field_validator + +from fields.base import ResponseModel +from graphon.variables.types import SegmentType from libs.helper import TimestampField from ._value_type_serializer import serialize_value_type @@ -29,6 +37,74 @@ conversation_variable_infinite_scroll_pagination_fields = { } +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value + + +class ConversationVariableResponse(ResponseModel): + id: str + name: str + value_type: str + value: str | None = None + description: str | None = None + created_at: int | None = None + updated_at: int | None = None + + @field_validator("value_type", mode="before") + @classmethod + def _normalize_value_type(cls, value: Any) -> str: + exposed_type = getattr(value, "exposed_type", None) + if callable(exposed_type): + return str(exposed_type().value) + if isinstance(value, str): + try: + return str(SegmentType(value).exposed_type().value) + except ValueError: + return value + try: + return serialize_value_type(value) + except (AttributeError, TypeError, ValueError): + pass + + try: + return serialize_value_type({"value_type": value}) + except (AttributeError, TypeError, ValueError): + value_attr = getattr(value, "value", None) + if value_attr is not None: + return str(value_attr) + return str(value) + + @field_validator("value", mode="before") + @classmethod + def _normalize_value(cls, value: Any | None) -> str | None: + if value is None: + return None + if isinstance(value, str): + return value + return str(value) + + @field_validator("created_at", "updated_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class PaginatedConversationVariableResponse(ResponseModel): + page: int + limit: int + total: int + has_more: bool + data: list[ConversationVariableResponse] + + +class ConversationVariableInfiniteScrollPaginationResponse(ResponseModel): + limit: int + has_more: bool + data: list[ConversationVariableResponse] + + def build_conversation_variable_model(api_or_ns: Namespace): """Build the conversation variable model for the API or Namespace.""" return api_or_ns.model("ConversationVariable", conversation_variable_fields) diff --git a/api/fields/member_fields.py b/api/fields/member_fields.py index cfe0015918..67b320beaa 100644 --- a/api/fields/member_fields.py +++ b/api/fields/member_fields.py @@ -3,10 +3,10 @@ from __future__ import annotations from datetime import datetime from flask_restx import fields -from graphon.file import helpers as file_helpers from pydantic import computed_field, field_validator from fields.base import ResponseModel +from graphon.file import helpers as file_helpers simple_account_fields = { "id": fields.String, diff --git a/api/fields/message_fields.py b/api/fields/message_fields.py index 1a871204a0..ca18f1c203 100644 --- a/api/fields/message_fields.py +++ b/api/fields/message_fields.py @@ -3,12 +3,12 @@ from __future__ import annotations from datetime import datetime from uuid import uuid4 -from graphon.file import File from pydantic import Field, field_validator from core.entities.execution_extra_content import ExecutionExtraContentDomainModel from fields.base import ResponseModel from fields.conversation_fields import AgentThought, JSONValue, MessageFile +from graphon.file import File type JSONValueType = JSONValue diff --git a/api/fields/online_user_fields.py b/api/fields/online_user_fields.py new file mode 100644 index 0000000000..bdbe19679c --- /dev/null +++ b/api/fields/online_user_fields.py @@ -0,0 +1,16 @@ +from flask_restx import fields + +online_user_partial_fields = { + "user_id": fields.String, + "username": fields.String, + "avatar": fields.String, +} + +workflow_online_users_fields = { + "app_id": fields.String, + "users": fields.List(fields.Nested(online_user_partial_fields)), +} + +online_user_list_fields = { + "data": fields.List(fields.Nested(workflow_online_users_fields)), +} diff --git a/api/fields/raws.py b/api/fields/raws.py index 4c65cdab7a..ee6f53b360 100644 --- a/api/fields/raws.py +++ b/api/fields/raws.py @@ -1,4 +1,5 @@ from flask_restx import fields + from graphon.file import File diff --git a/api/fields/workflow_app_log_fields.py b/api/fields/workflow_app_log_fields.py index 195b720285..7d45bc977d 100644 --- a/api/fields/workflow_app_log_fields.py +++ b/api/fields/workflow_app_log_fields.py @@ -1,8 +1,17 @@ -from flask_restx import Namespace, fields +from __future__ import annotations -from fields.end_user_fields import simple_end_user_fields -from fields.member_fields import simple_account_fields +from datetime import datetime +from typing import Any + +from flask_restx import Namespace, fields +from pydantic import field_validator + +from fields.base import ResponseModel +from fields.end_user_fields import SimpleEndUser, simple_end_user_fields +from fields.member_fields import SimpleAccount, simple_account_fields from fields.workflow_run_fields import ( + WorkflowRunForArchivedLogResponse, + WorkflowRunForLogResponse, build_workflow_run_for_archived_log_model, build_workflow_run_for_log_model, workflow_run_for_archived_log_fields, @@ -86,3 +95,55 @@ def build_workflow_archived_log_pagination_model(api_or_ns: Namespace): copied_fields = workflow_archived_log_pagination_fields.copy() copied_fields["data"] = fields.List(fields.Nested(workflow_archived_log_partial_model)) return api_or_ns.model("WorkflowArchivedLogPagination", copied_fields) + + +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value + + +class WorkflowAppLogPartialResponse(ResponseModel): + id: str + workflow_run: WorkflowRunForLogResponse | None = None + details: Any = None + created_from: str | None = None + created_by_role: str | None = None + created_by_account: SimpleAccount | None = None + created_by_end_user: SimpleEndUser | None = None + created_at: int | None = None + + @field_validator("created_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class WorkflowArchivedLogPartialResponse(ResponseModel): + id: str + workflow_run: WorkflowRunForArchivedLogResponse | None = None + trigger_metadata: Any = None + created_by_account: SimpleAccount | None = None + created_by_end_user: SimpleEndUser | None = None + created_at: int | None = None + + @field_validator("created_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class WorkflowAppLogPaginationResponse(ResponseModel): + page: int + limit: int + total: int + has_more: bool + data: list[WorkflowAppLogPartialResponse] + + +class WorkflowArchivedLogPaginationResponse(ResponseModel): + page: int + limit: int + total: int + has_more: bool + data: list[WorkflowArchivedLogPartialResponse] diff --git a/api/fields/workflow_comment_fields.py b/api/fields/workflow_comment_fields.py new file mode 100644 index 0000000000..c708dd3460 --- /dev/null +++ b/api/fields/workflow_comment_fields.py @@ -0,0 +1,96 @@ +from flask_restx import fields + +from libs.helper import AvatarUrlField, TimestampField + +# basic account fields for comments +account_fields = { + "id": fields.String, + "name": fields.String, + "email": fields.String, + "avatar_url": AvatarUrlField, +} + +# Comment mention fields +workflow_comment_mention_fields = { + "mentioned_user_id": fields.String, + "mentioned_user_account": fields.Nested(account_fields, allow_null=True), + "reply_id": fields.String, +} + +# Comment reply fields +workflow_comment_reply_fields = { + "id": fields.String, + "content": fields.String, + "created_by": fields.String, + "created_by_account": fields.Nested(account_fields, allow_null=True), + "created_at": TimestampField, +} + +# Basic comment fields (for list views) +workflow_comment_basic_fields = { + "id": fields.String, + "position_x": fields.Float, + "position_y": fields.Float, + "content": fields.String, + "created_by": fields.String, + "created_by_account": fields.Nested(account_fields, allow_null=True), + "created_at": TimestampField, + "updated_at": TimestampField, + "resolved": fields.Boolean, + "resolved_at": TimestampField, + "resolved_by": fields.String, + "resolved_by_account": fields.Nested(account_fields, allow_null=True), + "reply_count": fields.Integer, + "mention_count": fields.Integer, + "participants": fields.List(fields.Nested(account_fields)), +} + +# Detailed comment fields (for single comment view) +workflow_comment_detail_fields = { + "id": fields.String, + "position_x": fields.Float, + "position_y": fields.Float, + "content": fields.String, + "created_by": fields.String, + "created_by_account": fields.Nested(account_fields, allow_null=True), + "created_at": TimestampField, + "updated_at": TimestampField, + "resolved": fields.Boolean, + "resolved_at": TimestampField, + "resolved_by": fields.String, + "resolved_by_account": fields.Nested(account_fields, allow_null=True), + "replies": fields.List(fields.Nested(workflow_comment_reply_fields)), + "mentions": fields.List(fields.Nested(workflow_comment_mention_fields)), +} + +# Comment creation response fields (simplified) +workflow_comment_create_fields = { + "id": fields.String, + "created_at": TimestampField, +} + +# Comment update response fields (simplified) +workflow_comment_update_fields = { + "id": fields.String, + "updated_at": TimestampField, +} + +# Comment resolve response fields +workflow_comment_resolve_fields = { + "id": fields.String, + "resolved": fields.Boolean, + "resolved_at": TimestampField, + "resolved_by": fields.String, +} + +# Reply creation response fields (simplified) +workflow_comment_reply_create_fields = { + "id": fields.String, + "created_at": TimestampField, +} + +# Reply update response fields +workflow_comment_reply_update_fields = { + "id": fields.String, + "updated_at": TimestampField, +} diff --git a/api/fields/workflow_fields.py b/api/fields/workflow_fields.py index b0b6cc0b48..f9b5e98936 100644 --- a/api/fields/workflow_fields.py +++ b/api/fields/workflow_fields.py @@ -1,8 +1,8 @@ from flask_restx import fields -from graphon.variables import SecretVariable, SegmentType, VariableBase from core.helper import encrypter from fields.member_fields import simple_account_fields +from graphon.variables import SecretVariable, SegmentType, VariableBase from libs.helper import TimestampField from ._value_type_serializer import serialize_value_type diff --git a/api/fields/workflow_run_fields.py b/api/fields/workflow_run_fields.py index 35bb442c59..8c659086ed 100644 --- a/api/fields/workflow_run_fields.py +++ b/api/fields/workflow_run_fields.py @@ -1,7 +1,14 @@ -from flask_restx import Namespace, fields +from __future__ import annotations -from fields.end_user_fields import simple_end_user_fields -from fields.member_fields import simple_account_fields +from datetime import datetime +from typing import Any + +from flask_restx import Namespace, fields +from pydantic import Field, field_validator + +from fields.base import ResponseModel +from fields.end_user_fields import SimpleEndUser, simple_end_user_fields +from fields.member_fields import SimpleAccount, simple_account_fields from libs.helper import TimestampField workflow_run_for_log_fields = { @@ -147,3 +154,174 @@ workflow_run_node_execution_fields = { workflow_run_node_execution_list_fields = { "data": fields.List(fields.Nested(workflow_run_node_execution_fields)), } + + +def _to_timestamp(value: datetime | int | None) -> int | None: + if isinstance(value, datetime): + return int(value.timestamp()) + return value + + +class WorkflowRunForLogResponse(ResponseModel): + id: str + version: str | None = None + status: str | None = None + triggered_from: str | None = None + error: str | None = None + elapsed_time: float | None = None + total_tokens: int | None = None + total_steps: int | None = None + created_at: int | None = None + finished_at: int | None = None + exceptions_count: int | None = None + + @field_validator("status", mode="before") + @classmethod + def _normalize_status(cls, value: Any) -> str | None: + if value is None or isinstance(value, str): + return value + return str(getattr(value, "value", value)) + + @field_validator("created_at", "finished_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class WorkflowRunForArchivedLogResponse(ResponseModel): + id: str + status: str | None = None + triggered_from: str | None = None + elapsed_time: float | None = None + total_tokens: int | None = None + + @field_validator("status", mode="before") + @classmethod + def _normalize_status(cls, value: Any) -> str | None: + if value is None or isinstance(value, str): + return value + return str(getattr(value, "value", value)) + + +class WorkflowRunForListResponse(ResponseModel): + id: str + version: str | None = None + status: str | None = None + elapsed_time: float | None = None + total_tokens: int | None = None + total_steps: int | None = None + created_by_account: SimpleAccount | None = None + created_at: int | None = None + finished_at: int | None = None + exceptions_count: int | None = None + retry_index: int | None = None + + @field_validator("status", mode="before") + @classmethod + def _normalize_status(cls, value: Any) -> str | None: + if value is None or isinstance(value, str): + return value + return str(getattr(value, "value", value)) + + @field_validator("created_at", "finished_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class AdvancedChatWorkflowRunForListResponse(WorkflowRunForListResponse): + conversation_id: str | None = None + message_id: str | None = None + + +class AdvancedChatWorkflowRunPaginationResponse(ResponseModel): + limit: int + has_more: bool + data: list[AdvancedChatWorkflowRunForListResponse] + + +class WorkflowRunPaginationResponse(ResponseModel): + limit: int + has_more: bool + data: list[WorkflowRunForListResponse] + + +class WorkflowRunCountResponse(ResponseModel): + total: int + running: int + succeeded: int + failed: int + stopped: int + partial_succeeded: int = Field(validation_alias="partial-succeeded") + + +class WorkflowRunDetailResponse(ResponseModel): + id: str + version: str | None = None + graph: Any = Field(validation_alias="graph_dict") + inputs: Any = Field(validation_alias="inputs_dict") + status: str | None = None + outputs: Any = Field(validation_alias="outputs_dict") + error: str | None = None + elapsed_time: float | None = None + total_tokens: int | None = None + total_steps: int | None = None + created_by_role: str | None = None + created_by_account: SimpleAccount | None = None + created_by_end_user: SimpleEndUser | None = None + created_at: int | None = None + finished_at: int | None = None + exceptions_count: int | None = None + + @field_validator("status", mode="before") + @classmethod + def _normalize_status(cls, value: Any) -> str | None: + if value is None or isinstance(value, str): + return value + return str(getattr(value, "value", value)) + + @field_validator("created_at", "finished_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class WorkflowRunNodeExecutionResponse(ResponseModel): + id: str + index: int | None = None + predecessor_node_id: str | None = None + node_id: str | None = None + node_type: str | None = None + title: str | None = None + inputs: Any = Field(default=None, validation_alias="inputs_dict") + process_data: Any = Field(default=None, validation_alias="process_data_dict") + outputs: Any = Field(default=None, validation_alias="outputs_dict") + status: str | None = None + error: str | None = None + elapsed_time: float | None = None + execution_metadata: Any = Field(default=None, validation_alias="execution_metadata_dict") + extras: Any = None + created_at: int | None = None + created_by_role: str | None = None + created_by_account: SimpleAccount | None = None + created_by_end_user: SimpleEndUser | None = None + finished_at: int | None = None + inputs_truncated: bool | None = None + outputs_truncated: bool | None = None + process_data_truncated: bool | None = None + + @field_validator("status", mode="before") + @classmethod + def _normalize_status(cls, value: Any) -> str | None: + if value is None or isinstance(value, str): + return value + return str(getattr(value, "value", value)) + + @field_validator("created_at", "finished_at", mode="before") + @classmethod + def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: + return _to_timestamp(value) + + +class WorkflowRunNodeExecutionListResponse(ResponseModel): + data: list[WorkflowRunNodeExecutionResponse] diff --git a/api/libs/email_i18n.py b/api/libs/email_i18n.py index 0828cf80bf..1519f07bb1 100644 --- a/api/libs/email_i18n.py +++ b/api/libs/email_i18n.py @@ -37,6 +37,7 @@ class EmailType(StrEnum): ENTERPRISE_CUSTOM = auto() QUEUE_MONITOR_ALERT = auto() DOCUMENT_CLEAN_NOTIFY = auto() + WORKFLOW_COMMENT_MENTION = auto() EMAIL_REGISTER = auto() EMAIL_REGISTER_WHEN_ACCOUNT_EXIST = auto() RESET_PASSWORD_WHEN_ACCOUNT_NOT_EXIST_NO_REGISTER = auto() @@ -453,6 +454,18 @@ def create_default_email_config() -> EmailI18nConfig: branded_template_path="clean_document_job_mail_template_zh-CN.html", ), }, + EmailType.WORKFLOW_COMMENT_MENTION: { + EmailLanguage.EN_US: EmailTemplate( + subject="You were mentioned in a workflow comment", + template_path="workflow_comment_mention_template_en-US.html", + branded_template_path="without-brand/workflow_comment_mention_template_en-US.html", + ), + EmailLanguage.ZH_HANS: EmailTemplate( + subject="你在工作流评论中被提及", + template_path="workflow_comment_mention_template_zh-CN.html", + branded_template_path="without-brand/workflow_comment_mention_template_zh-CN.html", + ), + }, EmailType.TRIGGER_EVENTS_LIMIT_SANDBOX: { EmailLanguage.EN_US: EmailTemplate( subject="You’ve reached your Sandbox Trigger Events limit", diff --git a/api/libs/helper.py b/api/libs/helper.py index 69bd483515..ac69a11084 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -16,8 +16,6 @@ from zoneinfo import available_timezones from flask import Response, stream_with_context from flask_restx import fields -from graphon.file import helpers as file_helpers -from graphon.model_runtime.utils.encoders import jsonable_encoder from pydantic import BaseModel, TypeAdapter from pydantic.functional_validators import AfterValidator from typing_extensions import TypedDict @@ -25,6 +23,8 @@ from typing_extensions import TypedDict from configs import dify_config from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from extensions.ext_redis import redis_client +from graphon.file import helpers as file_helpers +from graphon.model_runtime.utils.encoders import jsonable_encoder if TYPE_CHECKING: from models import Account diff --git a/api/migrations/versions/2026_04_15_1726-227822d22895_add_workflow_comments_table.py b/api/migrations/versions/2026_04_15_1726-227822d22895_add_workflow_comments_table.py new file mode 100644 index 0000000000..0548c932b5 --- /dev/null +++ b/api/migrations/versions/2026_04_15_1726-227822d22895_add_workflow_comments_table.py @@ -0,0 +1,90 @@ +"""Add workflow comments table + +Revision ID: 227822d22895 +Revises: 8574b23a38fd +Create Date: 2025-08-22 17:26:15.255980 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '227822d22895' +down_revision = '8574b23a38fd' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('workflow_comments', + sa.Column('id', models.types.StringUUID(), nullable=False), + sa.Column('tenant_id', models.types.StringUUID(), nullable=False), + sa.Column('app_id', models.types.StringUUID(), nullable=False), + sa.Column('position_x', sa.Float(), nullable=False), + sa.Column('position_y', sa.Float(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_by', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('resolved', sa.Boolean(), server_default=sa.text('false'), nullable=False), + sa.Column('resolved_at', sa.DateTime(), nullable=True), + sa.Column('resolved_by', models.types.StringUUID(), nullable=True), + sa.PrimaryKeyConstraint('id', name='workflow_comments_pkey') + ) + with op.batch_alter_table('workflow_comments', schema=None) as batch_op: + batch_op.create_index('workflow_comments_app_idx', ['tenant_id', 'app_id'], unique=False) + batch_op.create_index('workflow_comments_created_at_idx', ['created_at'], unique=False) + + op.create_table('workflow_comment_replies', + sa.Column('id', models.types.StringUUID(), nullable=False), + sa.Column('comment_id', models.types.StringUUID(), nullable=False), + sa.Column('content', sa.Text(), nullable=False), + sa.Column('created_by', models.types.StringUUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name=op.f('workflow_comment_replies_comment_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='workflow_comment_replies_pkey') + ) + with op.batch_alter_table('workflow_comment_replies', schema=None) as batch_op: + batch_op.create_index('comment_replies_comment_idx', ['comment_id'], unique=False) + batch_op.create_index('comment_replies_created_at_idx', ['created_at'], unique=False) + + op.create_table('workflow_comment_mentions', + sa.Column('id', models.types.StringUUID(), nullable=False), + sa.Column('comment_id', models.types.StringUUID(), nullable=False), + sa.Column('reply_id', models.types.StringUUID(), nullable=True), + sa.Column('mentioned_user_id', models.types.StringUUID(), nullable=False), + sa.ForeignKeyConstraint(['comment_id'], ['workflow_comments.id'], name=op.f('workflow_comment_mentions_comment_id_fkey'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['reply_id'], ['workflow_comment_replies.id'], name=op.f('workflow_comment_mentions_reply_id_fkey'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name='workflow_comment_mentions_pkey') + ) + with op.batch_alter_table('workflow_comment_mentions', schema=None) as batch_op: + batch_op.create_index('comment_mentions_comment_idx', ['comment_id'], unique=False) + batch_op.create_index('comment_mentions_reply_idx', ['reply_id'], unique=False) + batch_op.create_index('comment_mentions_user_idx', ['mentioned_user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('workflow_comment_mentions', schema=None) as batch_op: + batch_op.drop_index('comment_mentions_user_idx') + batch_op.drop_index('comment_mentions_reply_idx') + batch_op.drop_index('comment_mentions_comment_idx') + + op.drop_table('workflow_comment_mentions') + with op.batch_alter_table('workflow_comment_replies', schema=None) as batch_op: + batch_op.drop_index('comment_replies_created_at_idx') + batch_op.drop_index('comment_replies_comment_idx') + + op.drop_table('workflow_comment_replies') + with op.batch_alter_table('workflow_comments', schema=None) as batch_op: + batch_op.drop_index('workflow_comments_created_at_idx') + batch_op.drop_index('workflow_comments_app_idx') + + op.drop_table('workflow_comments') + # ### end Alembic commands ### diff --git a/api/models/__init__.py b/api/models/__init__.py index 8a605d5195..f6e43bb4ea 100644 --- a/api/models/__init__.py +++ b/api/models/__init__.py @@ -9,6 +9,11 @@ from .account import ( TenantStatus, ) from .api_based_extension import APIBasedExtension, APIBasedExtensionPoint +from .comment import ( + WorkflowComment, + WorkflowCommentMention, + WorkflowCommentReply, +) from .dataset import ( AppDatasetJoin, Dataset, @@ -223,6 +228,9 @@ __all__ = [ "WorkflowAppLog", "WorkflowAppLogCreatedFrom", "WorkflowArchiveLog", + "WorkflowComment", + "WorkflowCommentMention", + "WorkflowCommentReply", "WorkflowNodeExecutionModel", "WorkflowNodeExecutionOffload", "WorkflowNodeExecutionTriggeredFrom", diff --git a/api/models/comment.py b/api/models/comment.py new file mode 100644 index 0000000000..308339e6f6 --- /dev/null +++ b/api/models/comment.py @@ -0,0 +1,218 @@ +"""Workflow comment models.""" + +from datetime import datetime +from typing import Optional + +from sqlalchemy import Index, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from .account import Account +from .base import Base +from .engine import db +from .types import StringUUID + + +class WorkflowComment(Base): + """Workflow comment model for canvas commenting functionality. + + Comments are associated with apps rather than specific workflow versions, + since an app has only one draft workflow at a time and comments should persist + across workflow version changes. + + Attributes: + id: Comment ID + tenant_id: Workspace ID + app_id: App ID (primary association, comments belong to apps) + position_x: X coordinate on canvas + position_y: Y coordinate on canvas + content: Comment content + created_by: Creator account ID + created_at: Creation time + updated_at: Last update time + resolved: Whether comment is resolved + resolved_at: Resolution time + resolved_by: Resolver account ID + """ + + __tablename__ = "workflow_comments" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="workflow_comments_pkey"), + Index("workflow_comments_app_idx", "tenant_id", "app_id"), + Index("workflow_comments_created_at_idx", "created_at"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuidv7()")) + tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + position_x: Mapped[float] = mapped_column(db.Float) + position_y: Mapped[float] = mapped_column(db.Float) + content: Mapped[str] = mapped_column(db.Text, nullable=False) + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column( + db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + ) + resolved: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false")) + resolved_at: Mapped[datetime | None] = mapped_column(db.DateTime) + resolved_by: Mapped[str | None] = mapped_column(StringUUID) + + # Relationships + replies: Mapped[list["WorkflowCommentReply"]] = relationship( + "WorkflowCommentReply", back_populates="comment", cascade="all, delete-orphan" + ) + mentions: Mapped[list["WorkflowCommentMention"]] = relationship( + "WorkflowCommentMention", back_populates="comment", cascade="all, delete-orphan" + ) + + @property + def created_by_account(self): + """Get creator account.""" + if hasattr(self, "_created_by_account_cache"): + return self._created_by_account_cache + return db.session.get(Account, self.created_by) + + def cache_created_by_account(self, account: Account | None) -> None: + """Cache creator account to avoid extra queries.""" + self._created_by_account_cache = account + + @property + def resolved_by_account(self): + """Get resolver account.""" + if hasattr(self, "_resolved_by_account_cache"): + return self._resolved_by_account_cache + if self.resolved_by: + return db.session.get(Account, self.resolved_by) + return None + + def cache_resolved_by_account(self, account: Account | None) -> None: + """Cache resolver account to avoid extra queries.""" + self._resolved_by_account_cache = account + + @property + def reply_count(self): + """Get reply count.""" + return len(self.replies) + + @property + def mention_count(self): + """Get mention count.""" + return len(self.mentions) + + @property + def participants(self): + """Get all participants (creator + repliers + mentioned users).""" + participant_ids: set[str] = set() + participants: list[Account] = [] + + # Use account properties to reuse preloaded caches and avoid hidden N+1. + if self.created_by not in participant_ids: + participant_ids.add(self.created_by) + created_by_account = self.created_by_account + if created_by_account: + participants.append(created_by_account) + + for reply in self.replies: + if reply.created_by in participant_ids: + continue + participant_ids.add(reply.created_by) + reply_account = reply.created_by_account + if reply_account: + participants.append(reply_account) + + for mention in self.mentions: + if mention.mentioned_user_id in participant_ids: + continue + participant_ids.add(mention.mentioned_user_id) + mentioned_account = mention.mentioned_user_account + if mentioned_account: + participants.append(mentioned_account) + + return participants + + +class WorkflowCommentReply(Base): + """Workflow comment reply model. + + Attributes: + id: Reply ID + comment_id: Parent comment ID + content: Reply content + created_by: Creator account ID + created_at: Creation time + """ + + __tablename__ = "workflow_comment_replies" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="workflow_comment_replies_pkey"), + Index("comment_replies_comment_idx", "comment_id"), + Index("comment_replies_created_at_idx", "created_at"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuidv7()")) + comment_id: Mapped[str] = mapped_column( + StringUUID, db.ForeignKey("workflow_comments.id", ondelete="CASCADE"), nullable=False + ) + content: Mapped[str] = mapped_column(db.Text, nullable=False) + created_by: Mapped[str] = mapped_column(StringUUID, nullable=False) + created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp()) + updated_at: Mapped[datetime] = mapped_column( + db.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + ) + # Relationships + comment: Mapped["WorkflowComment"] = relationship("WorkflowComment", back_populates="replies") + + @property + def created_by_account(self): + """Get creator account.""" + if hasattr(self, "_created_by_account_cache"): + return self._created_by_account_cache + return db.session.get(Account, self.created_by) + + def cache_created_by_account(self, account: Account | None) -> None: + """Cache creator account to avoid extra queries.""" + self._created_by_account_cache = account + + +class WorkflowCommentMention(Base): + """Workflow comment mention model. + + Mentions are only for internal accounts since end users + cannot access workflow canvas and commenting features. + + Attributes: + id: Mention ID + comment_id: Parent comment ID + mentioned_user_id: Mentioned account ID + """ + + __tablename__ = "workflow_comment_mentions" + __table_args__ = ( + db.PrimaryKeyConstraint("id", name="workflow_comment_mentions_pkey"), + Index("comment_mentions_comment_idx", "comment_id"), + Index("comment_mentions_reply_idx", "reply_id"), + Index("comment_mentions_user_idx", "mentioned_user_id"), + ) + + id: Mapped[str] = mapped_column(StringUUID, server_default=db.text("uuidv7()")) + comment_id: Mapped[str] = mapped_column( + StringUUID, db.ForeignKey("workflow_comments.id", ondelete="CASCADE"), nullable=False + ) + reply_id: Mapped[str | None] = mapped_column( + StringUUID, db.ForeignKey("workflow_comment_replies.id", ondelete="CASCADE"), nullable=True + ) + mentioned_user_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + + # Relationships + comment: Mapped["WorkflowComment"] = relationship("WorkflowComment", back_populates="mentions") + reply: Mapped[Optional["WorkflowCommentReply"]] = relationship("WorkflowCommentReply") + + @property + def mentioned_user_account(self): + """Get mentioned account.""" + if hasattr(self, "_mentioned_user_account_cache"): + return self._mentioned_user_account_cache + return db.session.get(Account, self.mentioned_user_id) + + def cache_mentioned_user_account(self, account: Account | None) -> None: + """Cache mentioned account to avoid extra queries.""" + self._mentioned_user_account_cache = account diff --git a/api/models/dataset.py b/api/models/dataset.py index 50301dd2d7..eee5c39a0e 100644 --- a/api/models/dataset.py +++ b/api/models/dataset.py @@ -1715,7 +1715,7 @@ class SegmentAttachmentBinding(TypeBase): ) -class DocumentSegmentSummary(Base): +class DocumentSegmentSummary(TypeBase): __tablename__ = "document_segment_summaries" __table_args__ = ( sa.PrimaryKeyConstraint("id", name="document_segment_summaries_pkey"), @@ -1725,25 +1725,40 @@ class DocumentSegmentSummary(Base): sa.Index("document_segment_summaries_status_idx", "status"), ) - id: Mapped[str] = mapped_column(StringUUID, nullable=False, default=lambda: str(uuid4())) + id: Mapped[str] = mapped_column( + StringUUID, + nullable=False, + insert_default=lambda: str(uuid4()), + default_factory=lambda: str(uuid4()), + init=False, + ) dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False) document_id: Mapped[str] = mapped_column(StringUUID, nullable=False) # corresponds to DocumentSegment.id or parent chunk id chunk_id: Mapped[str] = mapped_column(StringUUID, nullable=False) - summary_content: Mapped[str] = mapped_column(LongText, nullable=True) - summary_index_node_id: Mapped[str] = mapped_column(String(255), nullable=True) - summary_index_node_hash: Mapped[str] = mapped_column(String(255), nullable=True) - tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True) - status: Mapped[str] = mapped_column( - EnumText(SummaryStatus, length=32), nullable=False, server_default=sa.text("'generating'") + summary_content: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None) + summary_index_node_id: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) + summary_index_node_hash: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None) + tokens: Mapped[int | None] = mapped_column(sa.Integer, nullable=True, default=None) + status: Mapped[SummaryStatus] = mapped_column( + EnumText(SummaryStatus, length=32), + nullable=False, + server_default=sa.text("'generating'"), + default=SummaryStatus.GENERATING, + ) + error: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None) + enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"), default=True) + disabled_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None) + disabled_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.current_timestamp(), init=False ) - error: Mapped[str] = mapped_column(LongText, nullable=True) - enabled: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true")) - disabled_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - disabled_by = mapped_column(StringUUID, nullable=True) - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.current_timestamp()) updated_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + DateTime, + nullable=False, + server_default=func.current_timestamp(), + onupdate=func.current_timestamp(), + init=False, ) def __repr__(self): diff --git a/api/models/human_input.py b/api/models/human_input.py index 79c5d62f6a..b4c7a634b6 100644 --- a/api/models/human_input.py +++ b/api/models/human_input.py @@ -3,11 +3,11 @@ from enum import StrEnum from typing import Annotated, Literal, Self, final import sqlalchemy as sa -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from pydantic import BaseModel, Field from sqlalchemy.orm import Mapped, mapped_column, relationship from core.workflow.human_input_compat import DeliveryMethodType +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.helper import generate_string from .base import Base, DefaultFieldsMixin diff --git a/api/models/model.py b/api/models/model.py index 8eabf45363..7fe0731098 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -14,9 +14,6 @@ from uuid import uuid4 import sqlalchemy as sa from flask import request from flask_login import UserMixin # type: ignore[import-untyped] -from graphon.enums import WorkflowExecutionStatus -from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType -from graphon.file import helpers as file_helpers from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker @@ -24,6 +21,9 @@ from configs import dify_config from constants import DEFAULT_FILE_NUMBER_LIMITS from core.tools.signature import sign_tool_file from extensions.storage.storage_type import StorageType +from graphon.enums import WorkflowExecutionStatus +from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType +from graphon.file import helpers as file_helpers from libs.helper import generate_string # type: ignore[import-not-found] from libs.uuid_utils import uuidv7 from models.utils.file_input_compat import build_file_from_input_mapping diff --git a/api/models/provider.py b/api/models/provider.py index 8270961b31..2bb67d605b 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -6,10 +6,10 @@ from functools import cached_property from uuid import uuid4 import sqlalchemy as sa -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import DateTime, String, func, select, text from sqlalchemy.orm import Mapped, mapped_column +from graphon.model_runtime.entities.model_entities import ModelType from libs.uuid_utils import uuidv7 from .base import TypeBase diff --git a/api/models/utils/file_input_compat.py b/api/models/utils/file_input_compat.py index 8b767779ce..a2dc8f6157 100644 --- a/api/models/utils/file_input_compat.py +++ b/api/models/utils/file_input_compat.py @@ -4,9 +4,8 @@ from collections.abc import Callable, Mapping from functools import lru_cache from typing import Any -from graphon.file import File, FileTransferMethod - from core.workflow.file_reference import parse_file_reference +from graphon.file import File, FileTransferMethod @lru_cache(maxsize=1) diff --git a/api/models/workflow.py b/api/models/workflow.py index aadb062426..467d60f6ac 100644 --- a/api/models/workflow.py +++ b/api/models/workflow.py @@ -8,19 +8,6 @@ from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast from uuid import uuid4 import sqlalchemy as sa -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter -from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause -from graphon.enums import ( - BuiltinNodeTypes, - NodeType, - WorkflowExecutionStatus, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) -from graphon.file import File -from graphon.file.constants import maybe_file_object -from graphon.variables import utils as variable_utils -from graphon.variables.variables import FloatVariable, IntegerVariable, RAGPipelineVariable, StringVariable from sqlalchemy import ( DateTime, Index, @@ -44,6 +31,19 @@ from core.workflow.variable_prefixes import ( ) from extensions.ext_storage import Storage from factories.variable_factory import TypeMismatchError, build_segment_with_type +from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter +from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause +from graphon.enums import ( + BuiltinNodeTypes, + NodeType, + WorkflowExecutionStatus, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from graphon.file import File +from graphon.file.constants import maybe_file_object +from graphon.variables import utils as variable_utils +from graphon.variables.variables import FloatVariable, IntegerVariable, RAGPipelineVariable, StringVariable from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 @@ -53,11 +53,10 @@ if TYPE_CHECKING: from .model import AppMode, UploadFile -from graphon.variables import SecretVariable, Segment, SegmentType, VariableBase - from constants import DEFAULT_FILE_NUMBER_LIMITS, HIDDEN_VALUE from core.helper import encrypter from factories import variable_factory +from graphon.variables import SecretVariable, Segment, SegmentType, VariableBase from libs import helper from .account import Account @@ -492,7 +491,7 @@ class Workflow(Base): # bug :return: hash """ - entity = {"graph": self.graph_dict, "features": self.features_dict} + entity = {"graph": self.graph_dict} return helper.generate_text_hash(json.dumps(entity, sort_keys=True)) diff --git a/api/pyproject.toml b/api/pyproject.toml index abef591783..af8eb864b0 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -4,92 +4,55 @@ version = "1.13.3" requires-python = "~=3.12.0" dependencies = [ - "aliyun-log-python-sdk~=0.9.44", - "arize-phoenix-otel~=0.15.0", - "azure-identity==1.25.3", - "beautifulsoup4==4.14.3", - "boto3==1.42.88", - "bs4~=0.0.1", - "cachetools~=7.0.5", - "celery~=5.6.3", - "charset-normalizer>=3.4.7", - "flask~=3.1.3", - "flask-compress>=1.24,<1.25", - "flask-cors~=6.0.2", - "flask-login~=0.6.3", - "flask-migrate~=4.1.0", - "flask-orjson~=2.0.0", - "flask-sqlalchemy~=3.1.1", - "gevent~=26.4.0", - "gmpy2~=2.3.0", - "google-api-core>=2.30.3", - "google-api-python-client==2.194.0", - "google-auth>=2.49.2", - "google-auth-httplib2==0.3.1", - "google-cloud-aiplatform>=1.147.0", - "googleapis-common-protos>=1.74.0", - "graphon>=0.1.2", - "gunicorn~=25.3.0", - "httpx[socks]~=0.28.1", - "jieba==0.42.1", - "json-repair>=0.59.2", - "langfuse>=4.2.0,<5.0.0", - "langsmith~=0.7.30", - "markdown~=3.10.2", - "mlflow-skinny>=3.11.1", - "numpy~=2.4.4", - "openpyxl~=3.1.5", - "opik~=1.11.2", - "litellm==1.83.0", # Pinned to avoid madoka dependency issue - "opentelemetry-api==1.41.0", - "opentelemetry-distro==0.62b0", - "opentelemetry-exporter-otlp==1.41.0", - "opentelemetry-exporter-otlp-proto-common==1.41.0", - "opentelemetry-exporter-otlp-proto-grpc==1.41.0", - "opentelemetry-exporter-otlp-proto-http==1.41.0", - "opentelemetry-instrumentation==0.62b0", - "opentelemetry-instrumentation-celery==0.62b0", - "opentelemetry-instrumentation-flask==0.62b0", - "opentelemetry-instrumentation-httpx==0.62b0", - "opentelemetry-instrumentation-redis==0.62b0", - "opentelemetry-instrumentation-sqlalchemy==0.62b0", - "opentelemetry-propagator-b3==1.41.0", - "opentelemetry-proto==1.41.0", - "opentelemetry-sdk==1.41.0", - "opentelemetry-semantic-conventions==0.62b0", - "opentelemetry-util-http==0.62b0", - "pandas[excel,output-formatting,performance]~=3.0.2", - "psycogreen~=1.0.2", - "psycopg2-binary~=2.9.11", - "pycryptodome==3.23.0", - "pydantic~=2.12.5", - "pydantic-settings~=2.13.1", - "pyjwt~=2.12.1", - "pypdfium2==5.6.0", - "python-docx~=1.2.0", - "python-dotenv==1.2.2", - "pyyaml~=6.0.1", - "readabilipy~=0.3.0", - "redis[hiredis]~=7.4.0", - "resend~=2.27.0", - "sentry-sdk[flask]~=2.57.0", - "sqlalchemy~=2.0.49", - "starlette==1.0.0", - "tiktoken~=0.12.0", - "transformers~=5.3.0", - "unstructured[docx,epub,md,ppt,pptx]~=0.21.5", - "pypandoc~=1.13", - "yarl~=1.23.0", - "sseclient-py~=1.9.0", - "httpx-sse~=0.4.0", - "sendgrid~=6.12.5", - "flask-restx~=1.3.2", - "packaging~=26.0", + # Legacy: mature and widely deployed + "bleach>=6.3.0", + "boto3>=1.42.88", + "celery>=5.6.3", "croniter>=6.2.2", - "apscheduler>=3.11.2", - "weave>=0.52.36", - "fastopenapi[flask]>=0.7.0", - "bleach~=6.3.0", + "flask-cors>=6.0.2", + "gevent>=26.4.0", + "gevent-websocket>=0.10.1", + "gmpy2>=2.3.0", + "google-api-python-client>=2.194.0", + "gunicorn>=25.3.0", + "psycogreen>=1.0.2", + "psycopg2-binary>=2.9.11", + "python-socketio>=5.13.0", + "redis[hiredis]>=7.4.0", + "sendgrid>=6.12.5", + "sseclient-py>=1.8.0", + + # Stable: production-proven, cap below the next major + "aliyun-log-python-sdk>=0.9.44,<1.0.0", + "azure-identity>=1.25.3,<2.0.0", + "flask-compress>=1.24,<2.0.0", + "flask-login>=0.6.3,<1.0.0", + "flask-migrate>=4.1.0,<5.0.0", + "flask-orjson>=2.0.0,<3.0.0", + "flask-restx>=1.3.2,<2.0.0", + "google-cloud-aiplatform>=1.147.0,<2.0.0", + "httpx[socks]>=0.28.1,<1.0.0", + "langfuse>=4.2.0,<5.0.0", + "langsmith>=0.7.31,<1.0.0", + "mlflow-skinny>=3.11.1,<4.0.0", + "opentelemetry-distro>=0.62b0,<1.0.0", + "opentelemetry-instrumentation-celery>=0.62b0,<1.0.0", + "opentelemetry-instrumentation-flask>=0.62b0,<1.0.0", + "opentelemetry-instrumentation-httpx>=0.62b0,<1.0.0", + "opentelemetry-instrumentation-redis>=0.62b0,<1.0.0", + "opentelemetry-instrumentation-sqlalchemy>=0.62b0,<1.0.0", + "opentelemetry-propagator-b3>=1.41.0,<2.0.0", + "readabilipy>=0.3.0,<1.0.0", + "resend>=2.27.0,<3.0.0", + "weave>=0.52.36,<1.0.0", + + # Emerging: newer and fast-moving, use compatible pins + "arize-phoenix-otel~=0.15.0", + "fastopenapi[flask]~=0.7.0", + "graphon~=0.1.2", + "httpx-sse~=0.4.0", + "json-repair~=0.59.2", + "opik~=1.11.2", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. @@ -147,46 +110,46 @@ override-dependencies = [ # Required for development and running tests ############################################################ dev = [ - "coverage~=7.13.4", - "dotenv-linter~=0.7.0", - "faker~=40.13.0", - "lxml-stubs~=0.5.1", - "basedpyright~=1.39.0", - "ruff~=0.15.10", - "pytest~=9.0.3", - "pytest-benchmark~=5.2.3", - "pytest-cov~=7.1.0", - "pytest-env~=1.6.0", - "pytest-mock~=3.15.1", - "testcontainers~=4.14.2", - "types-aiofiles~=25.1.0", - "types-beautifulsoup4~=4.12.0", - "types-cachetools~=6.2.0", - "types-colorama~=0.4.15", - "types-defusedxml~=0.7.0", - "types-deprecated~=1.3.1", - "types-docutils~=0.22.3", - "types-flask-cors~=6.0.0", - "types-flask-migrate~=4.1.0", - "types-gevent~=26.4.0", - "types-greenlet~=3.4.0", - "types-html5lib~=1.1.11", - "types-markdown~=3.10.2", - "types-oauthlib~=3.3.0", - "types-objgraph~=3.6.0", - "types-olefile~=0.47.0", - "types-openpyxl~=3.1.5", - "types-pexpect~=4.9.0", - "types-protobuf~=7.34.1", - "types-psutil~=7.2.2", - "types-psycopg2~=2.9.21", - "types-pygments~=2.20.0", - "types-pymysql~=1.1.0", - "types-python-dateutil~=2.9.0", - "types-pywin32~=311.0.0", - "types-pyyaml~=6.0.12", - "types-regex~=2026.4.4", - "types-shapely~=2.1.0", + "coverage>=7.13.4", + "dotenv-linter>=0.7.0", + "faker>=20.1.0", + "lxml-stubs>=0.5.1", + "basedpyright>=1.39.0", + "ruff>=0.15.10", + "pytest>=9.0.3", + "pytest-benchmark>=5.2.3", + "pytest-cov>=7.1.0", + "pytest-env>=1.6.0", + "pytest-mock>=3.15.1", + "testcontainers>=4.14.2", + "types-aiofiles>=25.1.0", + "types-beautifulsoup4>=4.12.0", + "types-cachetools>=6.2.0", + "types-colorama>=0.4.15", + "types-defusedxml>=0.7.0", + "types-deprecated>=1.3.1", + "types-docutils>=0.22.3", + "types-flask-cors>=6.0.0", + "types-flask-migrate>=4.1.0", + "types-gevent>=26.4.0", + "types-greenlet>=3.4.0", + "types-html5lib>=1.1.11", + "types-markdown>=3.10.2", + "types-oauthlib>=3.3.0", + "types-objgraph>=3.6.0", + "types-olefile>=0.47.0", + "types-openpyxl>=3.1.5", + "types-pexpect>=4.9.0", + "types-protobuf>=7.34.1", + "types-psutil>=7.2.2", + "types-psycopg2>=2.9.21", + "types-pygments>=2.20.0", + "types-pymysql>=1.1.0", + "types-python-dateutil>=2.9.0", + "types-pywin32>=311.0.0", + "types-pyyaml>=6.0.12", + "types-regex>=2026.4.4", + "types-shapely>=2.1.0", "types-simplejson>=3.20.0.20260408", "types-six>=1.17.0.20260408", "types-tensorflow>=2.18.0.20260408", @@ -198,19 +161,18 @@ dev = [ "types_pyOpenSSL>=24.1.0", "types_cffi>=2.0.0.20260408", "types_setuptools>=82.0.0.20260408", - "pandas-stubs~=3.0.0", + "pandas-stubs>=3.0.0", "scipy-stubs>=1.15.3.0", "types-python-http-client>=3.3.7.20260408", "import-linter>=2.3", "types-redis>=4.6.0.20241004", "celery-types>=0.23.0", - "mypy~=1.20.1", + "mypy>=1.20.1", # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. - "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", "pyrefly>=0.60.0", - "xinference-client~=2.4.0", + "xinference-client>=2.4.0", ] ############################################################ @@ -218,21 +180,21 @@ dev = [ # Required for storage clients ############################################################ storage = [ - "azure-storage-blob==12.28.0", - "bce-python-sdk~=0.9.69", - "cos-python-sdk-v5==1.9.41", - "esdk-obs-python==3.26.2", + "azure-storage-blob>=12.28.0", + "bce-python-sdk>=0.9.69", + "cos-python-sdk-v5>=1.9.41", + "esdk-obs-python>=3.22.2", "google-cloud-storage>=3.10.1", - "opendal~=0.46.0", - "oss2==2.19.1", - "supabase~=2.18.1", - "tos~=2.9.0", + "opendal>=0.46.0", + "oss2>=2.19.1", + "supabase>=2.18.1", + "tos>=2.9.0", ] ############################################################ # [ Tools ] dependency group ############################################################ -tools = ["cloudscraper~=1.2.71", "nltk~=3.9.1"] +tools = ["cloudscraper>=1.2.71", "nltk>=3.9.1"] ############################################################ # [ Evaluation ] dependency group @@ -308,7 +270,7 @@ vdb-vastbase = ["dify-vdb-vastbase"] vdb-vikingdb = ["dify-vdb-vikingdb"] vdb-weaviate = ["dify-vdb-weaviate"] # Optional client used by some tests / integrations (not a vector backend plugin) -vdb-xinference = ["xinference-client~=2.4.0"] +vdb-xinference = ["xinference-client>=2.4.0"] [tool.pyrefly] project-includes = ["."] diff --git a/api/repositories/api_workflow_run_repository.py b/api/repositories/api_workflow_run_repository.py index 100589804c..72b38e7906 100644 --- a/api/repositories/api_workflow_run_repository.py +++ b/api/repositories/api_workflow_run_repository.py @@ -38,11 +38,11 @@ from collections.abc import Callable, Sequence from datetime import datetime from typing import Protocol, TypedDict -from graphon.entities.pause_reason import PauseReason -from graphon.enums import WorkflowType from sqlalchemy.orm import Session from core.repositories.factory import WorkflowExecutionRepository +from graphon.entities.pause_reason import PauseReason +from graphon.enums import WorkflowType from libs.infinite_scroll_pagination import InfiniteScrollPagination from models.enums import WorkflowRunTriggeredFrom from models.workflow import WorkflowAppLog, WorkflowArchiveLog, WorkflowPause, WorkflowPauseReason, WorkflowRun diff --git a/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py index d5c6a203b1..44735eb769 100644 --- a/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_node_execution_repository.py @@ -10,11 +10,11 @@ from collections.abc import Sequence from datetime import datetime from typing import Protocol, cast -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from sqlalchemy import asc, delete, desc, func, select from sqlalchemy.engine import CursorResult from sqlalchemy.orm import Session, sessionmaker +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload from repositories.api_workflow_node_execution_repository import ( DifyAPIWorkflowNodeExecutionRepository, diff --git a/api/repositories/sqlalchemy_api_workflow_run_repository.py b/api/repositories/sqlalchemy_api_workflow_run_repository.py index b760696c5e..474b200fc5 100644 --- a/api/repositories/sqlalchemy_api_workflow_run_repository.py +++ b/api/repositories/sqlalchemy_api_workflow_run_repository.py @@ -28,15 +28,15 @@ from decimal import Decimal from typing import Any, cast import sqlalchemy as sa -from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause -from graphon.enums import WorkflowExecutionStatus, WorkflowType -from graphon.nodes.human_input.entities import FormDefinition from pydantic import ValidationError from sqlalchemy import and_, delete, func, null, or_, select, tuple_ from sqlalchemy.engine import CursorResult from sqlalchemy.orm import Session, selectinload, sessionmaker from extensions.ext_storage import storage +from graphon.entities.pause_reason import HumanInputRequired, PauseReason, PauseReasonType, SchedulingPause +from graphon.enums import WorkflowExecutionStatus, WorkflowType +from graphon.nodes.human_input.entities import FormDefinition from libs.datetime_utils import naive_utc_now from libs.helper import convert_datetime_to_date from libs.infinite_scroll_pagination import InfiniteScrollPagination diff --git a/api/repositories/sqlalchemy_execution_extra_content_repository.py b/api/repositories/sqlalchemy_execution_extra_content_repository.py index feba5f7eb6..67f8795d3f 100644 --- a/api/repositories/sqlalchemy_execution_extra_content_repository.py +++ b/api/repositories/sqlalchemy_execution_extra_content_repository.py @@ -7,9 +7,6 @@ from collections import defaultdict from collections.abc import Sequence from typing import Any -from graphon.nodes.human_input.entities import FormDefinition -from graphon.nodes.human_input.enums import HumanInputFormStatus -from graphon.nodes.human_input.human_input_node import HumanInputNode from sqlalchemy import select from sqlalchemy.orm import Session, selectinload, sessionmaker @@ -21,6 +18,9 @@ from core.entities.execution_extra_content import ( from core.entities.execution_extra_content import ( HumanInputContent as HumanInputContentDomainModel, ) +from graphon.nodes.human_input.entities import FormDefinition +from graphon.nodes.human_input.enums import HumanInputFormStatus +from graphon.nodes.human_input.human_input_node import HumanInputNode from models.execution_extra_content import ( ExecutionExtraContent as ExecutionExtraContentModel, ) diff --git a/api/repositories/workflow_collaboration_repository.py b/api/repositories/workflow_collaboration_repository.py new file mode 100644 index 0000000000..000f80496d --- /dev/null +++ b/api/repositories/workflow_collaboration_repository.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import json +from typing import TypedDict + +from extensions.ext_redis import redis_client + +SESSION_STATE_TTL_SECONDS = 3600 +WORKFLOW_ONLINE_USERS_PREFIX = "workflow_online_users:" +WORKFLOW_LEADER_PREFIX = "workflow_leader:" +WS_SID_MAP_PREFIX = "ws_sid_map:" + + +class WorkflowSessionInfo(TypedDict): + user_id: str + username: str + avatar: str | None + sid: str + connected_at: int + + +class SidMapping(TypedDict): + workflow_id: str + user_id: str + + +class WorkflowCollaborationRepository: + def __init__(self) -> None: + self._redis = redis_client + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(redis_client={self._redis})" + + @staticmethod + def workflow_key(workflow_id: str) -> str: + return f"{WORKFLOW_ONLINE_USERS_PREFIX}{workflow_id}" + + @staticmethod + def leader_key(workflow_id: str) -> str: + return f"{WORKFLOW_LEADER_PREFIX}{workflow_id}" + + @staticmethod + def sid_key(sid: str) -> str: + return f"{WS_SID_MAP_PREFIX}{sid}" + + @staticmethod + def _decode(value: str | bytes | None) -> str | None: + if value is None: + return None + if isinstance(value, bytes): + return value.decode("utf-8") + return value + + def refresh_session_state(self, workflow_id: str, sid: str) -> None: + workflow_key = self.workflow_key(workflow_id) + sid_key = self.sid_key(sid) + if self._redis.exists(workflow_key): + self._redis.expire(workflow_key, SESSION_STATE_TTL_SECONDS) + if self._redis.exists(sid_key): + self._redis.expire(sid_key, SESSION_STATE_TTL_SECONDS) + + def set_session_info(self, workflow_id: str, session_info: WorkflowSessionInfo) -> None: + workflow_key = self.workflow_key(workflow_id) + self._redis.hset(workflow_key, session_info["sid"], json.dumps(session_info)) + self._redis.set( + self.sid_key(session_info["sid"]), + json.dumps({"workflow_id": workflow_id, "user_id": session_info["user_id"]}), + ex=SESSION_STATE_TTL_SECONDS, + ) + self.refresh_session_state(workflow_id, session_info["sid"]) + + def get_sid_mapping(self, sid: str) -> SidMapping | None: + raw = self._redis.get(self.sid_key(sid)) + if not raw: + return None + value = self._decode(raw) + if not value: + return None + try: + return json.loads(value) + except (TypeError, json.JSONDecodeError): + return None + + def delete_session(self, workflow_id: str, sid: str) -> None: + self._redis.hdel(self.workflow_key(workflow_id), sid) + self._redis.delete(self.sid_key(sid)) + + def session_exists(self, workflow_id: str, sid: str) -> bool: + return bool(self._redis.hexists(self.workflow_key(workflow_id), sid)) + + def sid_mapping_exists(self, sid: str) -> bool: + return bool(self._redis.exists(self.sid_key(sid))) + + def get_session_sids(self, workflow_id: str) -> list[str]: + raw_sids = self._redis.hkeys(self.workflow_key(workflow_id)) + decoded_sids: list[str] = [] + for sid in raw_sids: + decoded = self._decode(sid) + if decoded: + decoded_sids.append(decoded) + return decoded_sids + + def list_sessions(self, workflow_id: str) -> list[WorkflowSessionInfo]: + sessions_json = self._redis.hgetall(self.workflow_key(workflow_id)) + users: list[WorkflowSessionInfo] = [] + + for session_info_json in sessions_json.values(): + value = self._decode(session_info_json) + if not value: + continue + try: + session_info = json.loads(value) + except (TypeError, json.JSONDecodeError): + continue + + if not isinstance(session_info, dict): + continue + if "user_id" not in session_info or "username" not in session_info or "sid" not in session_info: + continue + + users.append( + { + "user_id": str(session_info["user_id"]), + "username": str(session_info["username"]), + "avatar": session_info.get("avatar"), + "sid": str(session_info["sid"]), + "connected_at": int(session_info.get("connected_at") or 0), + } + ) + + return users + + def get_current_leader(self, workflow_id: str) -> str | None: + raw = self._redis.get(self.leader_key(workflow_id)) + return self._decode(raw) + + def set_leader_if_absent(self, workflow_id: str, sid: str) -> bool: + return bool(self._redis.set(self.leader_key(workflow_id), sid, nx=True, ex=SESSION_STATE_TTL_SECONDS)) + + def set_leader(self, workflow_id: str, sid: str) -> None: + self._redis.set(self.leader_key(workflow_id), sid, ex=SESSION_STATE_TTL_SECONDS) + + def delete_leader(self, workflow_id: str) -> None: + self._redis.delete(self.leader_key(workflow_id)) + + def expire_leader(self, workflow_id: str) -> None: + self._redis.expire(self.leader_key(workflow_id), SESSION_STATE_TTL_SECONDS) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 74b800606d..78806927bc 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -10,12 +10,6 @@ from uuid import uuid4 import yaml from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad -from graphon.enums import BuiltinNodeTypes -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes.llm.entities import LLMNodeData -from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData -from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData -from graphon.nodes.tool.entities import ToolNodeData from packaging import version from packaging.version import parse as parse_version from pydantic import BaseModel @@ -35,6 +29,12 @@ from core.workflow.nodes.trigger_schedule.trigger_schedule_node import TriggerSc from events.app_event import app_model_config_was_updated, app_was_created from extensions.ext_redis import redis_client from factories import variable_factory +from graphon.enums import BuiltinNodeTypes +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes.llm.entities import LLMNodeData +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData +from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData +from graphon.nodes.tool.entities import ToolNodeData from libs.datetime_utils import naive_utc_now from models import Account, App, AppMode from models.model import AppModelConfig, AppModelConfigDict, IconType diff --git a/api/services/app_service.py b/api/services/app_service.py index ef170c50ba..afd98e2975 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -4,8 +4,6 @@ from typing import Any, TypedDict, cast import sqlalchemy as sa from flask_sqlalchemy.pagination import Pagination -from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from sqlalchemy import select from configs import dify_config @@ -17,6 +15,8 @@ from core.tools.tool_manager import ToolManager from core.tools.utils.configuration import ToolParameterConfigurationManager from events.app_event import app_was_created, app_was_deleted, app_was_updated from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelPropertyKey, ModelType +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from libs.datetime_utils import naive_utc_now from libs.login import current_user from models import Account diff --git a/api/services/app_task_service.py b/api/services/app_task_service.py index 0842e9d3e7..6e9d6b1c73 100644 --- a/api/services/app_task_service.py +++ b/api/services/app_task_service.py @@ -5,11 +5,10 @@ like stopping tasks, handling both legacy Redis flag mechanism and new GraphEngine command channel mechanism. """ -from graphon.graph_engine.manager import GraphEngineManager - from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_redis import redis_client +from graphon.graph_engine.manager import GraphEngineManager from models.model import AppMode diff --git a/api/services/audio_service.py b/api/services/audio_service.py index 1c7027efb4..60948e652b 100644 --- a/api/services/audio_service.py +++ b/api/services/audio_service.py @@ -5,12 +5,12 @@ from collections.abc import Generator from typing import cast from flask import Response, stream_with_context -from graphon.model_runtime.entities.model_entities import ModelType from werkzeug.datastructures import FileStorage from constants import AUDIO_EXTENSIONS from core.model_manager import ModelManager from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from models.enums import MessageStatus from models.model import App, AppMode, Message from services.errors.audio import ( diff --git a/api/services/clear_free_plan_tenant_expired_logs.py b/api/services/clear_free_plan_tenant_expired_logs.py index ea12e40420..dcc93b4b0f 100644 --- a/api/services/clear_free_plan_tenant_expired_logs.py +++ b/api/services/clear_free_plan_tenant_expired_logs.py @@ -6,7 +6,6 @@ from concurrent.futures import ThreadPoolExecutor import click from flask import Flask, current_app -from graphon.model_runtime.utils.encoders import jsonable_encoder from sqlalchemy import delete, func, select from sqlalchemy.orm import Session, sessionmaker @@ -14,6 +13,7 @@ from configs import dify_config from enums.cloud_plan import CloudPlan from extensions.ext_database import db from extensions.ext_storage import storage +from graphon.model_runtime.utils.encoders import jsonable_encoder from models.account import Tenant from models.model import ( App, diff --git a/api/services/conversation_service.py b/api/services/conversation_service.py index f5085af59b..ee8a1c4edd 100644 --- a/api/services/conversation_service.py +++ b/api/services/conversation_service.py @@ -3,7 +3,6 @@ import logging from collections.abc import Callable, Sequence from typing import Any -from graphon.variables.types import SegmentType from sqlalchemy import asc, desc, func, or_, select from sqlalchemy.orm import Session @@ -13,6 +12,7 @@ from core.db.session_factory import session_factory from core.llm_generator.llm_generator import LLMGenerator from extensions.ext_database import db from factories import variable_factory +from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account, ConversationVariable diff --git a/api/services/conversation_variable_updater.py b/api/services/conversation_variable_updater.py index 95a8951951..287d513f48 100644 --- a/api/services/conversation_variable_updater.py +++ b/api/services/conversation_variable_updater.py @@ -1,7 +1,7 @@ -from graphon.variables.variables import VariableBase from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker +from graphon.variables.variables import VariableBase from models import ConversationVariable diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 6c6de192c6..e6f5f80a6d 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -10,9 +10,6 @@ from collections.abc import Sequence from typing import Any, Literal, TypedDict, cast import sqlalchemy as sa -from graphon.file import helpers as file_helpers -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType -from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from redis.exceptions import LockNotOwnedError from sqlalchemy import delete, exists, func, select, update from sqlalchemy.orm import Session, sessionmaker @@ -31,6 +28,9 @@ from events.dataset_event import dataset_was_deleted from events.document_event import document_was_deleted from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.file import helpers as file_helpers +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType +from graphon.model_runtime.model_providers.__base.text_embedding_model import TextEmbeddingModel from libs import helper from libs.datetime_utils import naive_utc_now from libs.login import current_user diff --git a/api/services/datasource_provider_service.py b/api/services/datasource_provider_service.py index 364c4a86a0..416bc8cef9 100644 --- a/api/services/datasource_provider_service.py +++ b/api/services/datasource_provider_service.py @@ -3,7 +3,6 @@ import time from collections.abc import Mapping from typing import Any -from graphon.model_runtime.entities.provider_entities import FormType from sqlalchemy import delete, func, select, update from sqlalchemy.orm import Session, sessionmaker @@ -18,6 +17,7 @@ from core.plugin.impl.oauth import OAuthHandler from core.tools.utils.encryption import ProviderConfigCache, ProviderConfigEncrypter, create_provider_encrypter from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.model_runtime.entities.provider_entities import FormType from models.oauth import DatasourceOauthParamConfig, DatasourceOauthTenantParamConfig, DatasourceProvider from models.provider_ids import DatasourceProviderID from services.plugin.plugin_service import PluginService diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index a944ef6acd..6679c08ebd 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -1,15 +1,6 @@ from collections.abc import Sequence from enum import StrEnum -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - ModelCredentialSchema, - ProviderCredentialSchema, - ProviderHelpEntity, - SimpleProviderEntity, -) from pydantic import BaseModel, ConfigDict, model_validator from configs import dify_config @@ -24,6 +15,15 @@ from core.entities.provider_entities import ( QuotaConfiguration, UnaddedModelConfiguration, ) +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + ModelCredentialSchema, + ProviderCredentialSchema, + ProviderHelpEntity, + SimpleProviderEntity, +) from models.provider import ProviderType diff --git a/api/services/external_knowledge_service.py b/api/services/external_knowledge_service.py index 6dcedfdced..60b457ecd0 100644 --- a/api/services/external_knowledge_service.py +++ b/api/services/external_knowledge_service.py @@ -4,13 +4,13 @@ from typing import Any, cast from urllib.parse import urlparse import httpx -from graphon.nodes.http_request.exc import InvalidHttpMethodError from sqlalchemy import func, select from constants import HIDDEN_VALUE from core.helper import ssrf_proxy from core.rag.entities import MetadataFilteringCondition from extensions.ext_database import db +from graphon.nodes.http_request.exc import InvalidHttpMethodError from libs.datetime_utils import naive_utc_now from models.dataset import ( Dataset, diff --git a/api/services/feature_service.py b/api/services/feature_service.py index 9216a7fb99..e4eb9e7582 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -164,6 +164,7 @@ class SystemFeatureModel(BaseModel): enable_email_code_login: bool = False enable_email_password_login: bool = True enable_social_oauth_login: bool = False + enable_collaboration_mode: bool = False is_allow_register: bool = False is_allow_create_workspace: bool = False is_email_setup: bool = False @@ -244,6 +245,7 @@ class FeatureService: system_features.enable_email_code_login = dify_config.ENABLE_EMAIL_CODE_LOGIN system_features.enable_email_password_login = dify_config.ENABLE_EMAIL_PASSWORD_LOGIN system_features.enable_social_oauth_login = dify_config.ENABLE_SOCIAL_OAUTH_LOGIN + system_features.enable_collaboration_mode = dify_config.ENABLE_COLLABORATION_MODE system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" diff --git a/api/services/file_service.py b/api/services/file_service.py index 79a935de4b..52da2a7951 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -8,7 +8,6 @@ from tempfile import NamedTemporaryFile from typing import Literal from zipfile import ZIP_DEFLATED, ZipFile -from graphon.file import helpers as file_helpers from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import NotFound @@ -24,6 +23,7 @@ from core.rag.extractor.extract_processor import ExtractProcessor from extensions.ext_database import db from extensions.ext_storage import storage from extensions.storage.storage_type import StorageType +from graphon.file import helpers as file_helpers from libs.datetime_utils import naive_utc_now from libs.helper import extract_tenant_id from models import Account diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index 43985e49cd..ca84b2a3d8 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -3,8 +3,6 @@ import logging import time from typing import Any, TypedDict -from graphon.model_runtime.entities import LLMMode - from core.app.app_config.entities import ModelConfig from core.rag.datasource.retrieval_service import RetrievalService from core.rag.index_processor.constant.query_type import QueryType @@ -12,6 +10,7 @@ from core.rag.models.document import Document from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.retrieval_methods import RetrievalMethod from extensions.ext_database import db +from graphon.model_runtime.entities import LLMMode from models import Account from models.dataset import Dataset, DatasetQuery from models.enums import CreatorUserRole, DatasetQuerySource diff --git a/api/services/human_input_delivery_test_service.py b/api/services/human_input_delivery_test_service.py index 77576fa4c0..68ef67dec1 100644 --- a/api/services/human_input_delivery_test_service.py +++ b/api/services/human_input_delivery_test_service.py @@ -4,7 +4,6 @@ from dataclasses import dataclass, field from enum import StrEnum from typing import Protocol -from graphon.runtime import VariablePool from sqlalchemy import Engine, select from sqlalchemy.orm import sessionmaker @@ -18,6 +17,7 @@ from core.workflow.human_input_compat import ( ) from extensions.ext_database import db from extensions.ext_mail import mail +from graphon.runtime import VariablePool from libs.email_template_renderer import render_email_template from models import Account, TenantAccountJoin from services.feature_service import FeatureService diff --git a/api/services/human_input_service.py b/api/services/human_input_service.py index 02a6620fc7..76598d31ac 100644 --- a/api/services/human_input_service.py +++ b/api/services/human_input_service.py @@ -3,12 +3,6 @@ from collections.abc import Mapping from datetime import datetime, timedelta from typing import Any -from graphon.nodes.human_input.entities import ( - FormDefinition, - HumanInputSubmissionValidationError, - validate_human_input_submission, -) -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker @@ -17,6 +11,12 @@ from core.repositories.human_input_repository import ( HumanInputFormRecord, HumanInputFormSubmissionRepository, ) +from graphon.nodes.human_input.entities import ( + FormDefinition, + HumanInputSubmissionValidationError, + validate_human_input_submission, +) +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import ensure_naive_utc, naive_utc_now from libs.exception import BaseHTTPException from models.human_input import RecipientType diff --git a/api/services/message_service.py b/api/services/message_service.py index 5b133b0c04..98f24dd6a6 100644 --- a/api/services/message_service.py +++ b/api/services/message_service.py @@ -1,6 +1,5 @@ from collections.abc import Sequence -from graphon.model_runtime.entities.model_entities import ModelType from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.orm import sessionmaker @@ -14,6 +13,7 @@ from core.ops.entities.trace_entity import TraceTaskName from core.ops.ops_trace_manager import TraceQueueManager, TraceTask from core.ops.utils import measure_time from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account from models.enums import FeedbackFromSource, FeedbackRating diff --git a/api/services/model_load_balancing_service.py b/api/services/model_load_balancing_service.py index b652e049ce..c269346f5f 100644 --- a/api/services/model_load_balancing_service.py +++ b/api/services/model_load_balancing_service.py @@ -2,12 +2,6 @@ import json import logging from typing import Any, TypedDict -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ModelCredentialSchema, - ProviderCredentialSchema, -) -from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from sqlalchemy import or_, select from constants import HIDDEN_VALUE @@ -18,6 +12,12 @@ from core.model_manager import LBModelManager from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly, create_plugin_provider_manager from core.provider_manager import ProviderManager from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ModelCredentialSchema, + ProviderCredentialSchema, +) +from graphon.model_runtime.model_providers.model_provider_factory import ModelProviderFactory from libs.datetime_utils import naive_utc_now from models.enums import CredentialSourceType from models.provider import LoadBalancingModelConfig, ProviderCredential, ProviderModelCredential diff --git a/api/services/model_provider_service.py b/api/services/model_provider_service.py index bf208c9bc7..51cda79661 100644 --- a/api/services/model_provider_service.py +++ b/api/services/model_provider_service.py @@ -1,11 +1,10 @@ import logging from typing import Any -from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule - from core.entities.model_entities import ModelWithProviderEntity, ProviderModelWithStatusEntity from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory, create_plugin_provider_manager from core.provider_manager import ProviderManager +from graphon.model_runtime.entities.model_entities import ModelType, ParameterRule from models.provider import ProviderType from services.entities.model_provider_entities import ( CustomConfigurationResponse, diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 605689226a..968600d1bc 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -9,15 +9,6 @@ from typing import Any, cast from uuid import uuid4 from flask_login import current_user -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeType, WorkflowNodeExecutionStatus -from graphon.errors import WorkflowNodeRunFailedError -from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent -from graphon.node_events import NodeRunResult -from graphon.nodes.base.node import Node -from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config -from graphon.runtime import VariablePool -from graphon.variables.variables import Variable, VariableBase from sqlalchemy import func, select from sqlalchemy.orm import Session, sessionmaker @@ -53,6 +44,15 @@ from core.workflow.variable_pool_initializer import add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry from enterprise.telemetry.draft_trace import enqueue_draft_node_execution_trace from extensions.ext_database import db +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes, ErrorStrategy, NodeType, WorkflowNodeExecutionStatus +from graphon.errors import WorkflowNodeRunFailedError +from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent +from graphon.node_events import NodeRunResult +from graphon.nodes.base.node import Node +from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config +from graphon.runtime import VariablePool +from graphon.variables.variables import Variable, VariableBase from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account from models.dataset import ( # type: ignore diff --git a/api/services/rag_pipeline/rag_pipeline_dsl_service.py b/api/services/rag_pipeline/rag_pipeline_dsl_service.py index 7dd86f1581..f315d053cb 100644 --- a/api/services/rag_pipeline/rag_pipeline_dsl_service.py +++ b/api/services/rag_pipeline/rag_pipeline_dsl_service.py @@ -13,12 +13,6 @@ import yaml # type: ignore from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from flask_login import current_user -from graphon.enums import BuiltinNodeTypes -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes.llm.entities import LLMNodeData -from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData -from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData -from graphon.nodes.tool.entities import ToolNodeData from packaging import version from pydantic import BaseModel from sqlalchemy import select @@ -33,6 +27,12 @@ from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE from core.workflow.nodes.knowledge_retrieval.entities import KnowledgeRetrievalNodeData from extensions.ext_redis import redis_client from factories import variable_factory +from graphon.enums import BuiltinNodeTypes +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes.llm.entities import LLMNodeData +from graphon.nodes.parameter_extractor.entities import ParameterExtractorNodeData +from graphon.nodes.question_classifier.entities import QuestionClassifierNodeData +from graphon.nodes.tool.entities import ToolNodeData from models import Account from models.dataset import Dataset, DatasetCollectionBinding, Pipeline from models.enums import CollectionBindingType, DatasetRuntimeMode diff --git a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py index ab60986bfe..21be411bea 100644 --- a/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py +++ b/api/services/retention/workflow_run/archive_paid_plan_workflow_run.py @@ -27,13 +27,13 @@ from dataclasses import dataclass, field from typing import Any, TypedDict import click -from graphon.enums import WorkflowType from sqlalchemy import inspect from sqlalchemy.orm import Session, sessionmaker from configs import dify_config from enums.cloud_plan import CloudPlan from extensions.ext_database import db +from graphon.enums import WorkflowType from libs.archive_storage import ( ArchiveStorage, ArchiveStorageNotConfiguredError, diff --git a/api/services/summary_index_service.py b/api/services/summary_index_service.py index c906e3bca3..cf39469be8 100644 --- a/api/services/summary_index_service.py +++ b/api/services/summary_index_service.py @@ -6,8 +6,6 @@ import uuid from datetime import UTC, datetime from typing import TypedDict, cast -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import select from sqlalchemy.orm import Session @@ -18,6 +16,8 @@ from core.rag.index_processor.constant.doc_type import DocType from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.index_processor.index_processor_base import SummaryIndexSettingDict from core.rag.models.document import Document +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.model_runtime.entities.model_entities import ModelType from libs import helper from models.dataset import Dataset, DocumentSegment, DocumentSegmentSummary from models.dataset import Document as DatasetDocument @@ -349,7 +349,6 @@ class SummaryIndexService: summary_record_id, ) summary_record_in_session = DocumentSegmentSummary( - id=summary_record_id, # Use the same ID if available dataset_id=dataset.id, document_id=segment.document_id, chunk_id=segment.id, @@ -360,6 +359,9 @@ class SummaryIndexService: status=SummaryStatus.COMPLETED, enabled=True, ) + if summary_record_in_session is None: + raise RuntimeError("summary_record_in_session should not be None at this point") + summary_record_in_session.id = summary_record_id session.add(summary_record_in_session) logger.info( "Created new summary record (id=%s) for segment %s after vectorization", diff --git a/api/services/tools/api_tools_manage_service.py b/api/services/tools/api_tools_manage_service.py index 3bfa221528..5ff2c21749 100644 --- a/api/services/tools/api_tools_manage_service.py +++ b/api/services/tools/api_tools_manage_service.py @@ -2,9 +2,9 @@ import json import logging from typing import Any, TypedDict, cast -from graphon.model_runtime.utils.encoders import jsonable_encoder from httpx import get from sqlalchemy import select +from sqlalchemy.orm import sessionmaker from core.entities.provider_entities import ProviderConfig from core.tools.__base.tool_runtime import ToolRuntime @@ -16,11 +16,13 @@ from core.tools.entities.tool_entities import ( ApiProviderAuthType, ApiProviderSchemaType, ) +from core.tools.errors import ApiToolProviderNotFoundError from core.tools.tool_label_manager import ToolLabelManager from core.tools.tool_manager import ToolManager from core.tools.utils.encryption import create_tool_provider_encrypter from core.tools.utils.parser import ApiBasedToolSchemaParser from extensions.ext_database import db +from graphon.model_runtime.utils.encoders import jsonable_encoder from models.tools import ApiToolProvider from services.tools.tools_transform_service import ToolTransformService @@ -116,71 +118,85 @@ class ApiToolManageService: privacy_policy: str, custom_disclaimer: str, labels: list[str], - ): + ) -> dict[str, Any]: """ - create api tool provider + Create a new API tool provider. + + :param user_id: The ID of the user creating the provider. + :param tenant_id: The ID of the workspace/tenant. + :param provider_name: The name of the API tool provider. + :param icon: The icon configuration for the provider. + :param credentials: The credentials for the provider. + :param schema_type: The type of schema (e.g., OpenAPI). + :param schema: The raw schema string. + :param privacy_policy: The privacy policy URL or text. + :param custom_disclaimer: Custom disclaimer text. + :param labels: A list of labels for the provider. + :return: A dictionary indicating the result status. """ + provider_name = provider_name.strip() # check if the provider exists - provider = db.session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, + # Create new session with automatic transaction management + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + provider: ApiToolProvider | None = _session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, + ) + .limit(1) ) - .limit(1) - ) - if provider is not None: - raise ValueError(f"provider {provider_name} already exists") + if provider is not None: + raise ValueError(f"provider {provider_name} already exists") - # parse openapi to tool bundle - extra_info: dict[str, str] = {} - # extra info like description will be set here - tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) + # parse openapi to tool bundle + extra_info: dict[str, str] = {} + # extra info like description will be set here + tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) - if len(tool_bundles) > 100: - raise ValueError("the number of apis should be less than 100") + if len(tool_bundles) > 100: + raise ValueError("the number of apis should be less than 100") - # create db provider - db_provider = ApiToolProvider( - tenant_id=tenant_id, - user_id=user_id, - name=provider_name, - icon=json.dumps(icon), - schema=schema, - description=extra_info.get("description", ""), - schema_type_str=schema_type, - tools_str=json.dumps(jsonable_encoder(tool_bundles)), - credentials_str="{}", - privacy_policy=privacy_policy, - custom_disclaimer=custom_disclaimer, - ) + # create API tool provider + api_tool_provider = ApiToolProvider( + tenant_id=tenant_id, + user_id=user_id, + name=provider_name, + icon=json.dumps(icon), + schema=schema, + description=extra_info.get("description", ""), + schema_type_str=schema_type, + tools_str=json.dumps(jsonable_encoder(tool_bundles)), + credentials_str="{}", + privacy_policy=privacy_policy, + custom_disclaimer=custom_disclaimer, + ) - if "auth_type" not in credentials: - raise ValueError("auth_type is required") + if "auth_type" not in credentials: + raise ValueError("auth_type is required") - # get auth type, none or api key - auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) + # get auth type, none or api key + auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) - # create provider entity - provider_controller = ApiToolProviderController.from_db(db_provider, auth_type) - # load tools into provider entity - provider_controller.load_bundled_tools(tool_bundles) + # create provider entity + provider_controller = ApiToolProviderController.from_db(api_tool_provider, auth_type) + # load tools into provider entity + provider_controller.load_bundled_tools(tool_bundles) - # encrypt credentials - encrypter, _ = create_tool_provider_encrypter( - tenant_id=tenant_id, - controller=provider_controller, - ) - db_provider.credentials_str = json.dumps(encrypter.encrypt(credentials)) + # encrypt credentials + encrypter, _ = create_tool_provider_encrypter( + tenant_id=tenant_id, + controller=provider_controller, + ) + api_tool_provider.credentials_str = json.dumps(encrypter.encrypt(credentials)) - db.session.add(db_provider) - db.session.commit() + _session.add(api_tool_provider) - # update labels - ToolLabelManager.update_tool_labels(provider_controller, labels) + # update labels + ToolLabelManager.update_tool_labels(provider_controller, labels, _session) return {"result": "success"} @@ -212,16 +228,25 @@ class ApiToolManageService: @staticmethod def list_api_tool_provider_tools(user_id: str, tenant_id: str, provider_name: str) -> list[ToolApiEntity]: """ - list api tool provider tools + List tools provided by a specific API tool provider. + + :param user_id: The ID of the user requesting the list. + :param tenant_id: The ID of the workspace/tenant. + :param provider_name: The name of the API tool provider. + :return: A list of ToolApiEntity objects. """ - provider: ApiToolProvider | None = db.session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, + + # create new session with automatic transaction management + provider: ApiToolProvider | None = None + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + provider = _session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, + ) + .limit(1) ) - .limit(1) - ) if provider is None: raise ValueError(f"you have not added provider {provider_name}") @@ -251,103 +276,133 @@ class ApiToolManageService: privacy_policy: str | None, custom_disclaimer: str, labels: list[str], - ): + ) -> dict[str, Any]: """ - update api tool provider + Update an existing API tool provider. + + :param user_id: The ID of the user updating the provider. + :param tenant_id: The ID of the workspace/tenant. + :param provider_name: The new name of the API tool provider. + :param original_provider: The original name of the API tool provider. + :param icon: The icon configuration for the provider. + :param credentials: The credentials for the provider. + :param _schema_type: The type of schema (e.g., OpenAPI). + :param schema: The raw schema string. + :param privacy_policy: The privacy policy URL or text. + :param custom_disclaimer: Custom disclaimer text. + :param labels: A list of labels for the provider. + :return: A dictionary indicating the result status. """ + provider_name = provider_name.strip() # check if the provider exists - provider = db.session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == original_provider, + # create new session with automatic transaction management + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + provider: ApiToolProvider | None = _session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == original_provider, + ) + .limit(1) ) - .limit(1) - ) - if provider is None: - raise ValueError(f"api provider {provider_name} does not exists") - # parse openapi to tool bundle - extra_info: dict[str, str] = {} - # extra info like description will be set here - tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) + if provider is None: + raise ApiToolProviderNotFoundError(provider_name=original_provider, tenant_id=tenant_id) - # update db provider - provider.name = provider_name - provider.icon = json.dumps(icon) - provider.schema = schema - provider.description = extra_info.get("description", "") - provider.schema_type_str = schema_type - provider.tools_str = json.dumps(jsonable_encoder(tool_bundles)) - provider.privacy_policy = privacy_policy - provider.custom_disclaimer = custom_disclaimer + # parse openapi to tool bundle + extra_info: dict[str, str] = {} + # extra info like description will be set here + tool_bundles, schema_type = ApiToolManageService.convert_schema_to_tool_bundles(schema, extra_info) - if "auth_type" not in credentials: - raise ValueError("auth_type is required") + # update db provider + provider.name = provider_name + provider.icon = json.dumps(icon) + provider.schema = schema + provider.description = extra_info.get("description", "") + provider.schema_type_str = schema_type + provider.tools_str = json.dumps(jsonable_encoder(tool_bundles)) + provider.privacy_policy = privacy_policy + provider.custom_disclaimer = custom_disclaimer - # get auth type, none or api key - auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) + if "auth_type" not in credentials: + raise ValueError("auth_type is required") - # create provider entity - provider_controller = ApiToolProviderController.from_db(provider, auth_type) - # load tools into provider entity - provider_controller.load_bundled_tools(tool_bundles) + # get auth type, none or api key + auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) - # get original credentials if exists - encrypter, cache = create_tool_provider_encrypter( - tenant_id=tenant_id, - controller=provider_controller, - ) + # create provider entity + provider_controller = ApiToolProviderController.from_db(provider, auth_type) + # load tools into provider entity + provider_controller.load_bundled_tools(tool_bundles) - original_credentials = encrypter.decrypt(provider.credentials) - masked_credentials = encrypter.mask_plugin_credentials(original_credentials) - # check if the credential has changed, save the original credential - for name, value in credentials.items(): - if name in masked_credentials and value == masked_credentials[name]: - credentials[name] = original_credentials[name] + # get original credentials if exists + encrypter, cache = create_tool_provider_encrypter( + tenant_id=tenant_id, + controller=provider_controller, + ) - credentials = dict(encrypter.encrypt(credentials)) - provider.credentials_str = json.dumps(credentials) + original_credentials = encrypter.decrypt(provider.credentials) + masked_credentials = encrypter.mask_plugin_credentials(original_credentials) - db.session.add(provider) - db.session.commit() + # check if the credential has changed, save the original credential + for name, value in credentials.items(): + if name in masked_credentials and value == masked_credentials[name]: + credentials[name] = original_credentials[name] + + credentials = dict(encrypter.encrypt(credentials)) + provider.credentials_str = json.dumps(credentials) + + _session.add(provider) + + # update labels + ToolLabelManager.update_tool_labels(provider_controller, labels, _session) # delete cache cache.delete() - # update labels - ToolLabelManager.update_tool_labels(provider_controller, labels) - return {"result": "success"} @staticmethod def delete_api_tool_provider(user_id: str, tenant_id: str, provider_name: str): """ - delete tool provider + Delete an API tool provider. + + :param user_id: The ID of the user performing the deletion operation. + :param tenant_id: The ID of the workspace/tenant where the provider belongs. + :param provider_name: The unique name of the API tool provider to be deleted. + :raises ValueError: If the specified provider does not exist in the tenant. + :return: A dictionary indicating the result status. """ - provider = db.session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, + + # create new session with automatic transaction management + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + provider: ApiToolProvider | None = _session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, + ) + .limit(1) ) - .limit(1) - ) - if provider is None: - raise ValueError(f"you have not added provider {provider_name}") + if provider is None: + raise ValueError(f"you have not added provider {provider_name}") - db.session.delete(provider) - db.session.commit() + _session.delete(provider) return {"result": "success"} @staticmethod - def get_api_tool_provider(user_id: str, tenant_id: str, provider: str): + def get_api_tool_provider(user_id: str, tenant_id: str, provider: str) -> dict[str, Any]: """ - get api tool provider + Get API tool provider details. + + :param user_id: The ID of the user requesting the provider. + :param tenant_id: The ID of the workspace/tenant. + :param provider: The name of the API tool provider. + :return: A dictionary containing the provider details. """ return ToolManager.user_get_api_provider(provider=provider, tenant_id=tenant_id) @@ -360,10 +415,20 @@ class ApiToolManageService: parameters: dict[str, Any], schema_type: ApiProviderSchemaType, schema: str, - ): + ) -> dict[str, Any]: """ - test api tool before adding api tool provider + Test an API tool before adding the API tool provider. + + :param tenant_id: The ID of the workspace/tenant. + :param provider_name: The name of the API tool provider. + :param tool_name: The name of the specific tool to test. + :param credentials: The credentials for the provider. + :param parameters: The parameters to pass to the tool. + :param schema_type: The type of schema (e.g., OpenAPI). + :param schema: The raw schema string. + :return: A dictionary containing the result or error message. """ + if schema_type not in [member.value for member in ApiProviderSchemaType]: raise ValueError(f"invalid schema type {schema_type}") @@ -377,18 +442,21 @@ class ApiToolManageService: if tool_bundle is None: raise ValueError(f"invalid tool name {tool_name}") - db_provider = db.session.scalar( - select(ApiToolProvider) - .where( - ApiToolProvider.tenant_id == tenant_id, - ApiToolProvider.name == provider_name, + # create new session with automatic transaction management to get the provider + provider: ApiToolProvider | None = None + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + provider = _session.scalar( + select(ApiToolProvider) + .where( + ApiToolProvider.tenant_id == tenant_id, + ApiToolProvider.name == provider_name, + ) + .limit(1) ) - .limit(1) - ) - if not db_provider: + if provider is None: # create a fake db provider - db_provider = ApiToolProvider( + provider = ApiToolProvider( tenant_id="", user_id="", name="", @@ -407,12 +475,12 @@ class ApiToolManageService: auth_type = ApiProviderAuthType.value_of(credentials["auth_type"]) # create provider entity - provider_controller = ApiToolProviderController.from_db(db_provider, auth_type) + provider_controller = ApiToolProviderController.from_db(provider, auth_type) # load tools into provider entity provider_controller.load_bundled_tools(tool_bundles) # decrypt credentials - if db_provider.id: + if provider.id: encrypter, _ = create_tool_provider_encrypter( tenant_id=tenant_id, controller=provider_controller, @@ -443,14 +511,21 @@ class ApiToolManageService: @staticmethod def list_api_tools(tenant_id: str) -> list[ToolProviderApiEntity]: """ - list api tools + List all API tools for a specific tenant. + + :param tenant_id: The ID of the workspace/tenant. + :return: A list of ToolProviderApiEntity objects. """ # get all api providers - db_providers = db.session.scalars(select(ApiToolProvider).where(ApiToolProvider.tenant_id == tenant_id)).all() + # create new session with automatic transaction management + providers: list[ApiToolProvider] = [] + with sessionmaker(db.engine, expire_on_commit=False).begin() as _session: + providers = list( + _session.scalars(select(ApiToolProvider).where(ApiToolProvider.tenant_id == tenant_id)).all() + ) result: list[ToolProviderApiEntity] = [] - - for provider in db_providers: + for provider in providers: # convert provider controller to user provider provider_controller = ToolTransformService.api_provider_to_controller(db_provider=provider) labels = ToolLabelManager.get_tool_labels(provider_controller) diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index be2572b592..8f6600af03 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from typing import Any -from graphon.model_runtime.utils.encoders import jsonable_encoder from sqlalchemy import delete, or_, select from sqlalchemy.orm import sessionmaker @@ -15,6 +14,7 @@ from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurati from core.tools.workflow_as_tool.provider import WorkflowToolProviderController from core.tools.workflow_as_tool.tool import WorkflowTool from extensions.ext_database import db +from graphon.model_runtime.utils.encoders import jsonable_encoder from models.model import App from models.tools import WorkflowToolProvider from models.workflow import Workflow diff --git a/api/services/trigger/schedule_service.py b/api/services/trigger/schedule_service.py index 25e80770b8..a827222c1d 100644 --- a/api/services/trigger/schedule_service.py +++ b/api/services/trigger/schedule_service.py @@ -2,7 +2,6 @@ import json import logging from datetime import datetime -from graphon.entities.graph_config import NodeConfigDict from sqlalchemy import select from sqlalchemy.orm import Session @@ -14,6 +13,7 @@ from core.workflow.nodes.trigger_schedule.entities import ( VisualConfig, ) from core.workflow.nodes.trigger_schedule.exc import ScheduleConfigError, ScheduleNotFoundError +from graphon.entities.graph_config import NodeConfigDict from libs.schedule_utils import calculate_next_run_at, convert_12h_to_24h from models.account import Account, TenantAccountJoin from models.trigger import WorkflowSchedulePlan diff --git a/api/services/trigger/trigger_service.py b/api/services/trigger/trigger_service.py index 5a5d13b96d..911331e357 100644 --- a/api/services/trigger/trigger_service.py +++ b/api/services/trigger/trigger_service.py @@ -5,7 +5,6 @@ from collections.abc import Mapping from typing import Any from flask import Request, Response -from graphon.entities.graph_config import NodeConfigDict from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import sessionmaker @@ -21,6 +20,7 @@ from core.trigger.utils.encryption import create_trigger_provider_encrypter_for_ from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData from extensions.ext_database import db from extensions.ext_redis import redis_client +from graphon.entities.graph_config import NodeConfigDict from models.model import App from models.provider_ids import TriggerProviderID from models.trigger import TriggerSubscription, WorkflowPluginTrigger diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py index c782bffad4..d562220fa7 100644 --- a/api/services/trigger/webhook_service.py +++ b/api/services/trigger/webhook_service.py @@ -7,9 +7,6 @@ from typing import Any, NotRequired, TypedDict import orjson from flask import request -from graphon.entities.graph_config import NodeConfigDict -from graphon.file import FileTransferMethod -from graphon.variables.types import ArrayValidation, SegmentType from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -31,6 +28,9 @@ from enums.quota_type import QuotaType from extensions.ext_database import db from extensions.ext_redis import redis_client from factories import file_factory +from graphon.entities.graph_config import NodeConfigDict +from graphon.file import FileTransferMethod +from graphon.variables.types import ArrayValidation, SegmentType from models.enums import AppTriggerStatus, AppTriggerType from models.model import App from models.trigger import AppTrigger, WorkflowWebhookTrigger diff --git a/api/services/variable_truncator.py b/api/services/variable_truncator.py index 4d58a9cf12..c96050ce13 100644 --- a/api/services/variable_truncator.py +++ b/api/services/variable_truncator.py @@ -5,6 +5,7 @@ from abc import ABC, abstractmethod from collections.abc import Mapping from typing import Any, overload +from configs import dify_config from graphon.file import File from graphon.nodes.variable_assigner.common.helpers import UpdatedVariable from graphon.variables.segments import ( @@ -21,8 +22,6 @@ from graphon.variables.segments import ( ) from graphon.variables.utils import dumps_with_segments -from configs import dify_config - _MAX_DEPTH = 100 diff --git a/api/services/vector_service.py b/api/services/vector_service.py index 9827c8dfbc..58193d75a9 100644 --- a/api/services/vector_service.py +++ b/api/services/vector_service.py @@ -1,6 +1,5 @@ import logging -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import delete, select from core.model_manager import ModelInstance, ModelManager @@ -13,6 +12,7 @@ from core.rag.index_processor.index_processor_base import BaseIndexProcessor from core.rag.index_processor.index_processor_factory import IndexProcessorFactory from core.rag.models.document import AttachmentDocument, Document from extensions.ext_database import db +from graphon.model_runtime.entities.model_entities import ModelType from models import UploadFile from models.dataset import ChildChunk, Dataset, DatasetProcessRule, DocumentSegment, SegmentAttachmentBinding from models.dataset import Document as DatasetDocument diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index 1582bcd46c..5dedb9e372 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -1,11 +1,6 @@ import json from typing import Any, TypedDict -from graphon.file import FileUploadConfig -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.utils.encoders import jsonable_encoder -from graphon.nodes import BuiltinNodeTypes -from graphon.variables.input_entities import VariableEntity from sqlalchemy import select from core.app.app_config.entities import ( @@ -24,6 +19,11 @@ from core.prompt.simple_prompt_transform import SimplePromptTransform from core.prompt.utils.prompt_template_parser import PromptTemplateParser from events.app_event import app_was_created from extensions.ext_database import db +from graphon.file import FileUploadConfig +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.utils.encoders import jsonable_encoder +from graphon.nodes import BuiltinNodeTypes +from graphon.variables.input_entities import VariableEntity from models import Account from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint from models.model import App, AppMode, AppModelConfig, IconType diff --git a/api/services/workflow_app_service.py b/api/services/workflow_app_service.py index 5ab3430883..d2d6c7a92c 100644 --- a/api/services/workflow_app_service.py +++ b/api/services/workflow_app_service.py @@ -3,10 +3,10 @@ import uuid from datetime import datetime from typing import Any, TypedDict -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import and_, func, or_, select from sqlalchemy.orm import Session +from graphon.enums import WorkflowExecutionStatus from models import Account, App, EndUser, TenantAccountJoin, WorkflowAppLog, WorkflowArchiveLog, WorkflowRun from models.enums import AppTriggerType, CreatorUserRole from models.trigger import WorkflowTriggerLog diff --git a/api/services/workflow_collaboration_service.py b/api/services/workflow_collaboration_service.py new file mode 100644 index 0000000000..cf2f509052 --- /dev/null +++ b/api/services/workflow_collaboration_service.py @@ -0,0 +1,295 @@ +from __future__ import annotations + +import logging +import time +from collections.abc import Mapping + +from sqlalchemy import select + +from core.db.session_factory import session_factory +from models.account import Account +from models.model import App +from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository, WorkflowSessionInfo + +logger = logging.getLogger(__name__) + + +class WorkflowCollaborationService: + def __init__(self, repository: WorkflowCollaborationRepository, socketio) -> None: + self._repository = repository + self._socketio = socketio + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(repository={self._repository})" + + def save_socket_identity(self, sid: str, user: Account) -> None: + """Persist the authenticated console user on the raw socket session.""" + self._socketio.save_session( + sid, + { + "user_id": user.id, + "username": user.name, + "avatar": user.avatar, + "tenant_id": user.current_tenant_id, + }, + ) + + def authorize_and_join_workflow_room(self, workflow_id: str, sid: str) -> tuple[str, bool] | None: + """ + Join a collaboration room only after validating the socket session and tenant-scoped app access. + + The Socket.IO payload still calls the room key `workflow_id`, but the identifier is the workflow app's + `App.id`. Returning `None` lets the controller reject the join before any Redis or room state is created. + """ + session = self._socketio.get_session(sid) + user_id = session.get("user_id") + tenant_id = session.get("tenant_id") + if not user_id or not tenant_id: + return None + + if not self._can_access_workflow(workflow_id, str(tenant_id)): + logger.warning( + "Workflow collaboration join rejected: workflow_id=%s tenant_id=%s user_id=%s sid=%s", + workflow_id, + tenant_id, + user_id, + sid, + ) + return None + + session_info: WorkflowSessionInfo = { + "user_id": str(user_id), + "username": str(session.get("username", "Unknown")), + "avatar": session.get("avatar"), + "sid": sid, + "connected_at": int(time.time()), + } + + self._repository.set_session_info(workflow_id, session_info) + + leader_sid = self.get_or_set_leader(workflow_id, sid) + is_leader = leader_sid == sid + + self._socketio.enter_room(sid, workflow_id) + self.broadcast_online_users(workflow_id) + + self._socketio.emit("status", {"isLeader": is_leader}, room=sid) + + return str(user_id), is_leader + + def _can_access_workflow(self, workflow_id: str, tenant_id: str) -> bool: + """Check room access without relying on Flask's app-context-bound scoped session.""" + with session_factory.create_session() as session: + app_id = session.scalar(select(App.id).where(App.id == workflow_id, App.tenant_id == tenant_id).limit(1)) + return app_id is not None + + def disconnect_session(self, sid: str) -> None: + mapping = self._repository.get_sid_mapping(sid) + if not mapping: + return + + workflow_id = mapping["workflow_id"] + self._repository.delete_session(workflow_id, sid) + + self.handle_leader_disconnect(workflow_id, sid) + self.broadcast_online_users(workflow_id) + + def relay_collaboration_event(self, sid: str, data: Mapping[str, object]) -> tuple[dict[str, str], int]: + mapping = self._repository.get_sid_mapping(sid) + if not mapping: + return {"msg": "unauthorized"}, 401 + + workflow_id = mapping["workflow_id"] + user_id = mapping["user_id"] + self.refresh_session_state(workflow_id, sid) + + event_type = data.get("type") + event_data = data.get("data") + timestamp = data.get("timestamp", int(time.time())) + + if not event_type: + return {"msg": "invalid event type"}, 400 + + if event_type == "sync_request": + leader_sid = self._repository.get_current_leader(workflow_id) + target_sid: str | None + if leader_sid and self.is_session_active(workflow_id, leader_sid): + target_sid = leader_sid + else: + if leader_sid: + self._repository.delete_leader(workflow_id) + target_sid = self._select_graph_leader(workflow_id, preferred_sid=sid) + if target_sid: + self._repository.set_leader(workflow_id, target_sid) + self.broadcast_leader_change(workflow_id, target_sid) + + if not target_sid: + return {"msg": "no_active_leader"}, 200 + + self._socketio.emit( + "collaboration_update", + {"type": event_type, "userId": user_id, "data": event_data, "timestamp": timestamp}, + room=target_sid, + ) + + return {"msg": "sync_request_forwarded"}, 200 + + self._socketio.emit( + "collaboration_update", + {"type": event_type, "userId": user_id, "data": event_data, "timestamp": timestamp}, + room=workflow_id, + skip_sid=sid, + ) + + return {"msg": "event_broadcasted"}, 200 + + def relay_graph_event(self, sid: str, data: object) -> tuple[dict[str, str], int]: + mapping = self._repository.get_sid_mapping(sid) + if not mapping: + return {"msg": "unauthorized"}, 401 + + workflow_id = mapping["workflow_id"] + self.refresh_session_state(workflow_id, sid) + + self._socketio.emit("graph_update", data, room=workflow_id, skip_sid=sid) + + return {"msg": "graph_update_broadcasted"}, 200 + + def get_or_set_leader(self, workflow_id: str, sid: str) -> str: + current_leader = self._repository.get_current_leader(workflow_id) + + if current_leader: + if self.is_session_active(workflow_id, current_leader): + return current_leader + self._repository.delete_session(workflow_id, current_leader) + self._repository.delete_leader(workflow_id) + + was_set = self._repository.set_leader_if_absent(workflow_id, sid) + + if was_set: + if current_leader: + self.broadcast_leader_change(workflow_id, sid) + return sid + + current_leader = self._repository.get_current_leader(workflow_id) + if current_leader: + return current_leader + + return sid + + def handle_leader_disconnect(self, workflow_id: str, disconnected_sid: str) -> None: + current_leader = self._repository.get_current_leader(workflow_id) + if not current_leader: + return + + if current_leader != disconnected_sid: + return + + new_leader_sid = self._select_graph_leader(workflow_id) + if new_leader_sid: + self._repository.set_leader(workflow_id, new_leader_sid) + self.broadcast_leader_change(workflow_id, new_leader_sid) + else: + self._repository.delete_leader(workflow_id) + + def broadcast_leader_change(self, workflow_id: str, new_leader_sid: str | None) -> None: + for sid in self._repository.get_session_sids(workflow_id): + try: + is_leader = new_leader_sid is not None and sid == new_leader_sid + self._socketio.emit("status", {"isLeader": is_leader}, room=sid) + except Exception: + logging.exception("Failed to emit leader status to session %s", sid) + + def get_current_leader(self, workflow_id: str) -> str | None: + return self._repository.get_current_leader(workflow_id) + + def _prune_inactive_sessions(self, workflow_id: str) -> list[WorkflowSessionInfo]: + """Remove inactive sessions from storage and return active sessions only.""" + sessions = self._repository.list_sessions(workflow_id) + if not sessions: + return [] + + active_sessions: list[WorkflowSessionInfo] = [] + stale_sids: list[str] = [] + for session in sessions: + sid = session["sid"] + if self.is_session_active(workflow_id, sid): + active_sessions.append(session) + else: + stale_sids.append(sid) + + for sid in stale_sids: + self._repository.delete_session(workflow_id, sid) + + return active_sessions + + def broadcast_online_users(self, workflow_id: str) -> None: + users = self._prune_inactive_sessions(workflow_id) + users.sort(key=lambda x: x.get("connected_at") or 0) + + leader_sid = self.get_current_leader(workflow_id) + previous_leader = leader_sid + active_sids = {user["sid"] for user in users} + if leader_sid and leader_sid not in active_sids: + self._repository.delete_leader(workflow_id) + leader_sid = None + + if not leader_sid and users: + leader_sid = self._select_graph_leader(workflow_id) + if leader_sid: + self._repository.set_leader(workflow_id, leader_sid) + + if leader_sid != previous_leader: + self.broadcast_leader_change(workflow_id, leader_sid) + + self._socketio.emit( + "online_users", + {"workflow_id": workflow_id, "users": users, "leader": leader_sid}, + room=workflow_id, + ) + + def refresh_session_state(self, workflow_id: str, sid: str) -> None: + self._repository.refresh_session_state(workflow_id, sid) + self._ensure_leader(workflow_id, sid) + + def _ensure_leader(self, workflow_id: str, sid: str) -> None: + current_leader = self._repository.get_current_leader(workflow_id) + if current_leader and self.is_session_active(workflow_id, current_leader): + self._repository.expire_leader(workflow_id) + return + + if current_leader: + self._repository.delete_leader(workflow_id) + + self._repository.set_leader(workflow_id, sid) + self.broadcast_leader_change(workflow_id, sid) + + def _select_graph_leader(self, workflow_id: str, preferred_sid: str | None = None) -> str | None: + session_sids = [ + session["sid"] + for session in self._repository.list_sessions(workflow_id) + if session.get("graph_active", True) and self.is_session_active(workflow_id, session["sid"]) + ] + if not session_sids: + return None + if preferred_sid and preferred_sid in session_sids: + return preferred_sid + return session_sids[0] + + def is_session_active(self, workflow_id: str, sid: str) -> bool: + if not sid: + return False + + try: + if not self._socketio.manager.is_connected(sid, "/"): + return False + except AttributeError: + return False + + if not self._repository.session_exists(workflow_id, sid): + return False + + if not self._repository.sid_mapping_exists(sid): + return False + + return True diff --git a/api/services/workflow_comment_service.py b/api/services/workflow_comment_service.py new file mode 100644 index 0000000000..ff47e4f253 --- /dev/null +++ b/api/services/workflow_comment_service.py @@ -0,0 +1,564 @@ +import logging +from collections.abc import Sequence + +from sqlalchemy import desc, select +from sqlalchemy.orm import Session, selectinload +from werkzeug.exceptions import Forbidden, NotFound + +from configs import dify_config +from extensions.ext_database import db +from libs.datetime_utils import naive_utc_now +from libs.helper import uuid_value +from models import App, TenantAccountJoin, WorkflowComment, WorkflowCommentMention, WorkflowCommentReply +from models.account import Account +from tasks.mail_workflow_comment_task import send_workflow_comment_mention_email_task + +logger = logging.getLogger(__name__) + + +class WorkflowCommentService: + """Service for managing workflow comments.""" + + @staticmethod + def _validate_content(content: str) -> None: + if len(content.strip()) == 0: + raise ValueError("Comment content cannot be empty") + + if len(content) > 1000: + raise ValueError("Comment content cannot exceed 1000 characters") + + @staticmethod + def _filter_valid_mentioned_user_ids( + mentioned_user_ids: Sequence[str], *, session: Session, tenant_id: str + ) -> list[str]: + """Return deduplicated UUID user IDs that belong to the tenant, preserving input order.""" + unique_user_ids: list[str] = [] + seen: set[str] = set() + for user_id in mentioned_user_ids: + if not isinstance(user_id, str): + continue + if not uuid_value(user_id): + continue + if user_id in seen: + continue + seen.add(user_id) + unique_user_ids.append(user_id) + if not unique_user_ids: + return [] + + tenant_member_ids = { + str(account_id) + for account_id in session.scalars( + select(TenantAccountJoin.account_id).where( + TenantAccountJoin.tenant_id == tenant_id, + TenantAccountJoin.account_id.in_(unique_user_ids), + ) + ).all() + } + + return [user_id for user_id in unique_user_ids if user_id in tenant_member_ids] + + @staticmethod + def _format_comment_excerpt(content: str, max_length: int = 200) -> str: + """Trim comment content for email display.""" + trimmed = content.strip() + if len(trimmed) <= max_length: + return trimmed + if max_length <= 3: + return trimmed[:max_length] + return f"{trimmed[: max_length - 3].rstrip()}..." + + @staticmethod + def _build_mention_email_payloads( + session: Session, + tenant_id: str, + app_id: str, + mentioner_id: str, + mentioned_user_ids: Sequence[str], + content: str, + ) -> list[dict[str, str]]: + """Prepare email payloads for mentioned users, including workflow app link.""" + if not mentioned_user_ids: + return [] + + candidate_user_ids = [user_id for user_id in mentioned_user_ids if user_id != mentioner_id] + if not candidate_user_ids: + return [] + + app_name_value = session.scalar(select(App.name).where(App.id == app_id, App.tenant_id == tenant_id)) + app_name = app_name_value if isinstance(app_name_value, str) and app_name_value else "Dify app" + commenter_name_value = session.scalar(select(Account.name).where(Account.id == mentioner_id)) + commenter_name = ( + commenter_name_value if isinstance(commenter_name_value, str) and commenter_name_value else "Dify user" + ) + comment_excerpt = WorkflowCommentService._format_comment_excerpt(content) + base_url = dify_config.CONSOLE_WEB_URL.rstrip("/") + app_url = f"{base_url}/app/{app_id}/workflow" + + accounts = session.scalars( + select(Account) + .join(TenantAccountJoin, TenantAccountJoin.account_id == Account.id) + .where(TenantAccountJoin.tenant_id == tenant_id, Account.id.in_(candidate_user_ids)) + ).all() + + payloads: list[dict[str, str]] = [] + for account in accounts: + email = account.email + if not isinstance(email, str) or not email: + continue + mentioned_name = account.name if isinstance(account.name, str) and account.name else email + language = ( + account.interface_language + if isinstance(account.interface_language, str) and account.interface_language + else "en-US" + ) + payloads.append( + { + "language": language, + "to": email, + "mentioned_name": mentioned_name, + "commenter_name": commenter_name, + "app_name": app_name, + "comment_content": comment_excerpt, + "app_url": app_url, + } + ) + return payloads + + @staticmethod + def _dispatch_mention_emails(payloads: Sequence[dict[str, str]]) -> None: + """Enqueue mention notification emails.""" + for payload in payloads: + send_workflow_comment_mention_email_task.delay(**payload) + + @staticmethod + def get_comments(tenant_id: str, app_id: str) -> Sequence[WorkflowComment]: + """Get all comments for a workflow.""" + with Session(db.engine) as session: + # Get all comments with eager loading + stmt = ( + select(WorkflowComment) + .options(selectinload(WorkflowComment.replies), selectinload(WorkflowComment.mentions)) + .where(WorkflowComment.tenant_id == tenant_id, WorkflowComment.app_id == app_id) + .order_by(desc(WorkflowComment.created_at)) + ) + + comments = session.scalars(stmt).all() + + # Batch preload all Account objects to avoid N+1 queries + WorkflowCommentService._preload_accounts(session, comments) + + return comments + + @staticmethod + def _preload_accounts(session: Session, comments: Sequence[WorkflowComment]) -> None: + """Batch preload Account objects for comments, replies, and mentions.""" + # Collect all user IDs + user_ids: set[str] = set() + for comment in comments: + user_ids.add(comment.created_by) + if comment.resolved_by: + user_ids.add(comment.resolved_by) + user_ids.update(reply.created_by for reply in comment.replies) + user_ids.update(mention.mentioned_user_id for mention in comment.mentions) + + if not user_ids: + return + + # Batch query all accounts + accounts = session.scalars(select(Account).where(Account.id.in_(user_ids))).all() + account_map = {str(account.id): account for account in accounts} + + # Cache accounts on objects + for comment in comments: + comment.cache_created_by_account(account_map.get(comment.created_by)) + comment.cache_resolved_by_account(account_map.get(comment.resolved_by) if comment.resolved_by else None) + for reply in comment.replies: + reply.cache_created_by_account(account_map.get(reply.created_by)) + for mention in comment.mentions: + mention.cache_mentioned_user_account(account_map.get(mention.mentioned_user_id)) + + @staticmethod + def get_comment(tenant_id: str, app_id: str, comment_id: str, session: Session | None = None) -> WorkflowComment: + """Get a specific comment.""" + + def _get_comment(session: Session) -> WorkflowComment: + stmt = ( + select(WorkflowComment) + .options(selectinload(WorkflowComment.replies), selectinload(WorkflowComment.mentions)) + .where( + WorkflowComment.id == comment_id, + WorkflowComment.tenant_id == tenant_id, + WorkflowComment.app_id == app_id, + ) + ) + comment = session.scalar(stmt) + + if not comment: + raise NotFound("Comment not found") + + # Preload accounts to avoid N+1 queries + WorkflowCommentService._preload_accounts(session, [comment]) + + return comment + + if session is not None: + return _get_comment(session) + else: + with Session(db.engine, expire_on_commit=False) as session: + return _get_comment(session) + + @staticmethod + def create_comment( + tenant_id: str, + app_id: str, + created_by: str, + content: str, + position_x: float, + position_y: float, + mentioned_user_ids: list[str] | None = None, + ) -> dict: + """Create a new workflow comment and send mention notification emails.""" + WorkflowCommentService._validate_content(content) + + with Session(db.engine) as session: + comment = WorkflowComment( + tenant_id=tenant_id, + app_id=app_id, + position_x=position_x, + position_y=position_y, + content=content, + created_by=created_by, + ) + + session.add(comment) + session.flush() # Get the comment ID for mentions + + # Create mentions if specified + mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids( + mentioned_user_ids or [], + session=session, + tenant_id=tenant_id, + ) + for user_id in mentioned_user_ids: + mention = WorkflowCommentMention( + comment_id=comment.id, + reply_id=None, # This is a comment mention, not reply mention + mentioned_user_id=user_id, + ) + session.add(mention) + + mention_email_payloads = WorkflowCommentService._build_mention_email_payloads( + session=session, + tenant_id=tenant_id, + app_id=app_id, + mentioner_id=created_by, + mentioned_user_ids=mentioned_user_ids, + content=content, + ) + + session.commit() + WorkflowCommentService._dispatch_mention_emails(mention_email_payloads) + + # Return only what we need - id and created_at + return {"id": comment.id, "created_at": comment.created_at} + + @staticmethod + def update_comment( + tenant_id: str, + app_id: str, + comment_id: str, + user_id: str, + content: str, + position_x: float | None = None, + position_y: float | None = None, + mentioned_user_ids: list[str] | None = None, + ) -> dict: + """Update a workflow comment and notify newly mentioned users. + + `mentioned_user_ids=None` means "leave mentions unchanged". + Passing an explicit list replaces the existing comment mentions, including clearing them with `[]`. + """ + WorkflowCommentService._validate_content(content) + + with Session(db.engine, expire_on_commit=False) as session: + # Get comment with validation + stmt = select(WorkflowComment).where( + WorkflowComment.id == comment_id, + WorkflowComment.tenant_id == tenant_id, + WorkflowComment.app_id == app_id, + ) + comment = session.scalar(stmt) + + if not comment: + raise NotFound("Comment not found") + + # Only the creator can update the comment + if comment.created_by != user_id: + raise Forbidden("Only the comment creator can update it") + + # Update comment fields + comment.content = content + if position_x is not None: + comment.position_x = position_x + if position_y is not None: + comment.position_y = position_y + + mention_email_payloads: list[dict[str, str]] = [] + if mentioned_user_ids is not None: + # Replace comment mentions only when the client explicitly sends the mention list. + existing_mentions = session.scalars( + select(WorkflowCommentMention).where( + WorkflowCommentMention.comment_id == comment.id, + WorkflowCommentMention.reply_id.is_(None), # Only comment mentions, not reply mentions + ) + ).all() + existing_mentioned_user_ids = {mention.mentioned_user_id for mention in existing_mentions} + for mention in existing_mentions: + session.delete(mention) + + filtered_mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids( + mentioned_user_ids, + session=session, + tenant_id=tenant_id, + ) + new_mentioned_user_ids = [ + mentioned_user_id + for mentioned_user_id in filtered_mentioned_user_ids + if mentioned_user_id not in existing_mentioned_user_ids + ] + for mentioned_user_id in filtered_mentioned_user_ids: + mention = WorkflowCommentMention( + comment_id=comment.id, + reply_id=None, # This is a comment mention + mentioned_user_id=mentioned_user_id, + ) + session.add(mention) + + mention_email_payloads = WorkflowCommentService._build_mention_email_payloads( + session=session, + tenant_id=tenant_id, + app_id=app_id, + mentioner_id=user_id, + mentioned_user_ids=new_mentioned_user_ids, + content=content, + ) + + session.commit() + WorkflowCommentService._dispatch_mention_emails(mention_email_payloads) + + return {"id": comment.id, "updated_at": comment.updated_at} + + @staticmethod + def delete_comment(tenant_id: str, app_id: str, comment_id: str, user_id: str) -> None: + """Delete a workflow comment.""" + with Session(db.engine, expire_on_commit=False) as session: + comment = WorkflowCommentService.get_comment(tenant_id, app_id, comment_id, session) + + # Only the creator can delete the comment + if comment.created_by != user_id: + raise Forbidden("Only the comment creator can delete it") + + # Delete associated mentions (both comment and reply mentions) + mentions = session.scalars( + select(WorkflowCommentMention).where(WorkflowCommentMention.comment_id == comment_id) + ).all() + for mention in mentions: + session.delete(mention) + + # Delete associated replies + replies = session.scalars( + select(WorkflowCommentReply).where(WorkflowCommentReply.comment_id == comment_id) + ).all() + for reply in replies: + session.delete(reply) + + session.delete(comment) + session.commit() + + @staticmethod + def resolve_comment(tenant_id: str, app_id: str, comment_id: str, user_id: str) -> WorkflowComment: + """Resolve a workflow comment.""" + with Session(db.engine, expire_on_commit=False) as session: + comment = WorkflowCommentService.get_comment(tenant_id, app_id, comment_id, session) + if comment.resolved: + return comment + + comment.resolved = True + comment.resolved_at = naive_utc_now() + comment.resolved_by = user_id + session.commit() + + return comment + + @staticmethod + def create_reply( + comment_id: str, content: str, created_by: str, mentioned_user_ids: list[str] | None = None + ) -> dict: + """Add a reply to a workflow comment and notify mentioned users.""" + WorkflowCommentService._validate_content(content) + + with Session(db.engine, expire_on_commit=False) as session: + # Check if comment exists + comment = session.get(WorkflowComment, comment_id) + if not comment: + raise NotFound("Comment not found") + + reply = WorkflowCommentReply(comment_id=comment_id, content=content, created_by=created_by) + + session.add(reply) + session.flush() # Get the reply ID for mentions + + # Create mentions if specified + mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids( + mentioned_user_ids or [], + session=session, + tenant_id=comment.tenant_id, + ) + for user_id in mentioned_user_ids: + # Create mention linking to specific reply + mention = WorkflowCommentMention(comment_id=comment_id, reply_id=reply.id, mentioned_user_id=user_id) + session.add(mention) + + mention_email_payloads = WorkflowCommentService._build_mention_email_payloads( + session=session, + tenant_id=comment.tenant_id, + app_id=comment.app_id, + mentioner_id=created_by, + mentioned_user_ids=mentioned_user_ids, + content=content, + ) + + session.commit() + WorkflowCommentService._dispatch_mention_emails(mention_email_payloads) + + return {"id": reply.id, "created_at": reply.created_at} + + @staticmethod + def _get_reply_in_comment_scope( + *, + session: Session, + tenant_id: str, + app_id: str, + comment_id: str, + reply_id: str, + ) -> WorkflowCommentReply: + """Get a reply scoped to tenant/app/comment to prevent cross-thread mutations.""" + stmt = ( + select(WorkflowCommentReply) + .join(WorkflowComment, WorkflowComment.id == WorkflowCommentReply.comment_id) + .where( + WorkflowCommentReply.id == reply_id, + WorkflowCommentReply.comment_id == comment_id, + WorkflowComment.tenant_id == tenant_id, + WorkflowComment.app_id == app_id, + ) + .limit(1) + ) + reply = session.scalar(stmt) + if not reply: + raise NotFound("Reply not found") + return reply + + @staticmethod + def update_reply( + tenant_id: str, + app_id: str, + comment_id: str, + reply_id: str, + user_id: str, + content: str, + mentioned_user_ids: list[str] | None = None, + ) -> dict: + """Update a comment reply and notify newly mentioned users.""" + WorkflowCommentService._validate_content(content) + + with Session(db.engine, expire_on_commit=False) as session: + reply = WorkflowCommentService._get_reply_in_comment_scope( + session=session, + tenant_id=tenant_id, + app_id=app_id, + comment_id=comment_id, + reply_id=reply_id, + ) + + # Only the creator can update the reply + if reply.created_by != user_id: + raise Forbidden("Only the reply creator can update it") + + reply.content = content + + # Update mentions - first remove existing mentions for this reply + existing_mentions = session.scalars( + select(WorkflowCommentMention).where(WorkflowCommentMention.reply_id == reply.id) + ).all() + existing_mentioned_user_ids = {mention.mentioned_user_id for mention in existing_mentions} + for mention in existing_mentions: + session.delete(mention) + + # Add mentions + raw_mentioned_user_ids = mentioned_user_ids or [] + comment = session.get(WorkflowComment, reply.comment_id) + mentioned_user_ids = [] + if comment: + mentioned_user_ids = WorkflowCommentService._filter_valid_mentioned_user_ids( + raw_mentioned_user_ids, + session=session, + tenant_id=comment.tenant_id, + ) + new_mentioned_user_ids = [ + user_id for user_id in mentioned_user_ids if user_id not in existing_mentioned_user_ids + ] + for user_id_str in mentioned_user_ids: + mention = WorkflowCommentMention( + comment_id=reply.comment_id, reply_id=reply.id, mentioned_user_id=user_id_str + ) + session.add(mention) + + mention_email_payloads: list[dict[str, str]] = [] + if comment: + mention_email_payloads = WorkflowCommentService._build_mention_email_payloads( + session=session, + tenant_id=comment.tenant_id, + app_id=comment.app_id, + mentioner_id=user_id, + mentioned_user_ids=new_mentioned_user_ids, + content=content, + ) + + session.commit() + session.refresh(reply) # Refresh to get updated timestamp + WorkflowCommentService._dispatch_mention_emails(mention_email_payloads) + + return {"id": reply.id, "updated_at": reply.updated_at} + + @staticmethod + def delete_reply(tenant_id: str, app_id: str, comment_id: str, reply_id: str, user_id: str) -> None: + """Delete a comment reply.""" + with Session(db.engine, expire_on_commit=False) as session: + reply = WorkflowCommentService._get_reply_in_comment_scope( + session=session, + tenant_id=tenant_id, + app_id=app_id, + comment_id=comment_id, + reply_id=reply_id, + ) + + # Only the creator can delete the reply + if reply.created_by != user_id: + raise Forbidden("Only the reply creator can delete it") + + # Delete associated mentions first + mentions = session.scalars( + select(WorkflowCommentMention).where(WorkflowCommentMention.reply_id == reply_id) + ).all() + for mention in mentions: + session.delete(mention) + + session.delete(reply) + session.commit() + + @staticmethod + def validate_comment_access(comment_id: str, tenant_id: str, app_id: str) -> WorkflowComment: + """Validate that a comment belongs to the specified tenant and app.""" + return WorkflowCommentService.get_comment(tenant_id, app_id, comment_id) diff --git a/api/services/workflow_draft_variable_service.py b/api/services/workflow_draft_variable_service.py index fae5dea3cb..8afb565955 100644 --- a/api/services/workflow_draft_variable_service.py +++ b/api/services/workflow_draft_variable_service.py @@ -7,19 +7,6 @@ from datetime import datetime from enum import StrEnum from typing import Any, ClassVar, NotRequired, TypedDict -from graphon.enums import NodeType -from graphon.file import File -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.variable_assigner.common.helpers import get_updated_variables -from graphon.variable_loader import VariableLoader -from graphon.variables import Segment, StringSegment, VariableBase -from graphon.variables.consts import SELECTORS_LENGTH -from graphon.variables.segments import ( - ArrayFileSegment, - FileSegment, -) -from graphon.variables.types import SegmentType -from graphon.variables.utils import dumps_with_segments from sqlalchemy import Engine, delete, orm, select from sqlalchemy.dialects.mysql import insert as mysql_insert from sqlalchemy.dialects.postgresql import insert as pg_insert @@ -40,6 +27,19 @@ from core.workflow.variable_prefixes import ( from extensions.ext_storage import storage from factories.file_factory import StorageKeyLoader from factories.variable_factory import build_segment, segment_to_variable +from graphon.enums import NodeType +from graphon.file import File +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.variable_assigner.common.helpers import get_updated_variables +from graphon.variable_loader import VariableLoader +from graphon.variables import Segment, StringSegment, VariableBase +from graphon.variables.consts import SELECTORS_LENGTH +from graphon.variables.segments import ( + ArrayFileSegment, + FileSegment, +) +from graphon.variables.types import SegmentType +from graphon.variables.utils import dumps_with_segments from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 from models import Account, App, Conversation diff --git a/api/services/workflow_event_snapshot_service.py b/api/services/workflow_event_snapshot_service.py index 601e9261fc..5fca444723 100644 --- a/api/services/workflow_event_snapshot_service.py +++ b/api/services/workflow_event_snapshot_service.py @@ -9,10 +9,6 @@ from collections.abc import Generator, Mapping, Sequence from dataclasses import dataclass from typing import Any -from graphon.entities import WorkflowStartReason -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import desc, select from sqlalchemy.orm import Session, sessionmaker @@ -26,6 +22,10 @@ from core.app.entities.task_entities import ( WorkflowStartStreamResponse, ) from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext +from graphon.entities import WorkflowStartReason +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from models.model import AppMode, Message from models.workflow import WorkflowNodeExecutionTriggeredFrom, WorkflowRun from repositories.api_workflow_node_execution_repository import WorkflowNodeExecutionSnapshot diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 747e5e7042..6786623d7a 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -5,32 +5,7 @@ import uuid from collections.abc import Callable, Generator, Mapping, Sequence from typing import Any, cast -from graphon.entities import GraphInitParams, WorkflowNodeExecution -from graphon.entities.graph_config import NodeConfigDict -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import ( - ErrorStrategy, - NodeType, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) -from graphon.errors import WorkflowNodeRunFailedError -from graphon.file import File -from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent -from graphon.node_events import NodeRunResult -from graphon.nodes import BuiltinNodeTypes -from graphon.nodes.base.node import Node -from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config -from graphon.nodes.human_input.entities import HumanInputNodeData, validate_human_input_submission -from graphon.nodes.human_input.enums import HumanInputFormKind -from graphon.nodes.human_input.human_input_node import HumanInputNode -from graphon.nodes.start.entities import StartNodeData -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variable_loader import load_into_variable_pool -from graphon.variables import VariableBase -from graphon.variables.input_entities import VariableEntityType -from graphon.variables.variables import Variable -from sqlalchemy import and_, exists, or_, select +from sqlalchemy import exists, select from sqlalchemy.orm import Session, sessionmaker from configs import dify_config @@ -64,6 +39,31 @@ from events.app_event import app_draft_workflow_was_synced, app_published_workfl from extensions.ext_database import db from extensions.ext_storage import storage from factories.file_factory import build_from_mapping, build_from_mappings +from graphon.entities import WorkflowNodeExecution +from graphon.entities.graph_config import NodeConfigDict +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import ( + ErrorStrategy, + NodeType, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) +from graphon.errors import WorkflowNodeRunFailedError +from graphon.file import File +from graphon.graph_events import GraphNodeEventBase, NodeRunFailedEvent, NodeRunSucceededEvent +from graphon.node_events import NodeRunResult +from graphon.nodes import BuiltinNodeTypes +from graphon.nodes.base.node import Node +from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config +from graphon.nodes.human_input.entities import HumanInputNodeData, validate_human_input_submission +from graphon.nodes.human_input.enums import HumanInputFormKind +from graphon.nodes.human_input.human_input_node import HumanInputNode +from graphon.nodes.start.entities import StartNodeData +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variable_loader import load_into_variable_pool +from graphon.variables import VariableBase +from graphon.variables.input_entities import VariableEntityType +from graphon.variables.variables import Variable from libs.datetime_utils import naive_utc_now from libs.helper import escape_like_pattern from models import Account @@ -209,6 +209,16 @@ class WorkflowService: return workflow + def get_accessible_app_ids(self, app_ids: Sequence[str], tenant_id: str) -> set[str]: + """ + Return app IDs that belong to the given tenant. + """ + if not app_ids: + return set() + + stmt = select(App.id).where(App.id.in_(app_ids), App.tenant_id == tenant_id) + return {str(app_id) for app_id in db.session.scalars(stmt).all()} + def get_all_published_workflow( self, *, @@ -359,6 +369,78 @@ class WorkflowService: # return draft workflow return workflow + def update_draft_workflow_environment_variables( + self, + *, + app_model: App, + environment_variables: Sequence[VariableBase], + account: Account, + ): + """ + Update draft workflow environment variables + """ + # fetch draft workflow by app_model + workflow = self.get_draft_workflow(app_model=app_model) + + if not workflow: + raise ValueError("No draft workflow found.") + + workflow.environment_variables = environment_variables + workflow.updated_by = account.id + workflow.updated_at = naive_utc_now() + + # commit db session changes + db.session.commit() + + def update_draft_workflow_conversation_variables( + self, + *, + app_model: App, + conversation_variables: Sequence[VariableBase], + account: Account, + ): + """ + Update draft workflow conversation variables + """ + # fetch draft workflow by app_model + workflow = self.get_draft_workflow(app_model=app_model) + + if not workflow: + raise ValueError("No draft workflow found.") + + workflow.conversation_variables = conversation_variables + workflow.updated_by = account.id + workflow.updated_at = naive_utc_now() + + # commit db session changes + db.session.commit() + + def update_draft_workflow_features( + self, + *, + app_model: App, + features: dict, + account: Account, + ): + """ + Update draft workflow features + """ + # fetch draft workflow by app_model + workflow = self.get_draft_workflow(app_model=app_model) + + if not workflow: + raise ValueError("No draft workflow found.") + + # validate features structure + self.validate_features_structure(app_model=app_model, features=features) + + workflow.features = json.dumps(features) + workflow.updated_by = account.id + workflow.updated_at = naive_utc_now() + + # commit db session changes + db.session.commit() + def restore_published_workflow_to_draft( self, *, diff --git a/api/tasks/app_generate/workflow_execute_task.py b/api/tasks/app_generate/workflow_execute_task.py index 8f2f5f261e..c22e7e9918 100644 --- a/api/tasks/app_generate/workflow_execute_task.py +++ b/api/tasks/app_generate/workflow_execute_task.py @@ -7,7 +7,6 @@ from typing import Annotated, Any from celery import shared_task from flask import current_app, json -from graphon.runtime import GraphRuntimeState from pydantic import BaseModel, Discriminator, Field, Tag from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker @@ -23,6 +22,7 @@ from core.app.entities.app_invoke_entities import ( from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig, WorkflowResumptionContext from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.runtime import GraphRuntimeState from libs.flask_utils import set_login_user from models.account import Account from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom diff --git a/api/tasks/async_workflow_tasks.py b/api/tasks/async_workflow_tasks.py index 9ff34c7c48..5809268992 100644 --- a/api/tasks/async_workflow_tasks.py +++ b/api/tasks/async_workflow_tasks.py @@ -10,7 +10,6 @@ from datetime import UTC, datetime from typing import Any, NotRequired from celery import shared_task -from graphon.runtime import GraphRuntimeState from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker from typing_extensions import TypedDict @@ -24,6 +23,7 @@ from core.app.layers.trigger_post_layer import TriggerPostLayer from core.db.session_factory import session_factory from core.repositories import DifyCoreRepositoryFactory from extensions.ext_database import db +from graphon.runtime import GraphRuntimeState from models.account import Account from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom, WorkflowTriggerStatus from models.model import App, EndUser, Tenant diff --git a/api/tasks/batch_create_segment_to_index_task.py b/api/tasks/batch_create_segment_to_index_task.py index 4db551c73c..beb23d8354 100644 --- a/api/tasks/batch_create_segment_to_index_task.py +++ b/api/tasks/batch_create_segment_to_index_task.py @@ -8,7 +8,6 @@ from typing import Any import click import pandas as pd from celery import shared_task -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import func, select from core.db.session_factory import session_factory @@ -16,6 +15,7 @@ from core.model_manager import ModelManager from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from extensions.ext_redis import redis_client from extensions.ext_storage import storage +from graphon.model_runtime.entities.model_entities import ModelType from libs import helper from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, Document, DocumentSegment diff --git a/api/tasks/human_input_timeout_tasks.py b/api/tasks/human_input_timeout_tasks.py index ca73b4d374..fd743205a1 100644 --- a/api/tasks/human_input_timeout_tasks.py +++ b/api/tasks/human_input_timeout_tasks.py @@ -2,8 +2,6 @@ import logging from datetime import timedelta from celery import shared_task -from graphon.enums import WorkflowExecutionStatus -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from sqlalchemy import or_, select from sqlalchemy.orm import sessionmaker @@ -11,6 +9,8 @@ from configs import dify_config from core.repositories.human_input_repository import HumanInputFormSubmissionRepository from extensions.ext_database import db from extensions.ext_storage import storage +from graphon.enums import WorkflowExecutionStatus +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import ensure_naive_utc, naive_utc_now from models.human_input import HumanInputForm from models.workflow import WorkflowPause, WorkflowRun diff --git a/api/tasks/mail_human_input_delivery_task.py b/api/tasks/mail_human_input_delivery_task.py index a316eec7b9..f8ae3f4b6e 100644 --- a/api/tasks/mail_human_input_delivery_task.py +++ b/api/tasks/mail_human_input_delivery_task.py @@ -6,7 +6,6 @@ from typing import Any import click from celery import shared_task -from graphon.runtime import GraphRuntimeState, VariablePool from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker @@ -15,6 +14,7 @@ from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext from core.workflow.human_input_compat import EmailDeliveryConfig, EmailDeliveryMethod from extensions.ext_database import db from extensions.ext_mail import mail +from graphon.runtime import GraphRuntimeState, VariablePool from models.human_input import ( DeliveryMethodType, HumanInputDelivery, diff --git a/api/tasks/mail_workflow_comment_task.py b/api/tasks/mail_workflow_comment_task.py new file mode 100644 index 0000000000..36d51f0514 --- /dev/null +++ b/api/tasks/mail_workflow_comment_task.py @@ -0,0 +1,65 @@ +import logging +import time + +import click +from celery import shared_task + +from extensions.ext_mail import mail +from libs.email_i18n import EmailType, get_email_i18n_service + +logger = logging.getLogger(__name__) + + +@shared_task(queue="mail") +def send_workflow_comment_mention_email_task( + language: str, + to: str, + mentioned_name: str, + commenter_name: str, + app_name: str, + comment_content: str, + app_url: str, +): + """ + Send workflow comment mention email with internationalization support. + + Args: + language: Language code for email localization + to: Recipient email address + mentioned_name: Name of the mentioned user + commenter_name: Name of the comment author + app_name: Name of the app where the comment was made + comment_content: Comment content excerpt + app_url: Link to the app workflow page + """ + if not mail.is_inited(): + return + + logger.info(click.style(f"Start workflow comment mention mail to {to}", fg="green")) + start_at = time.perf_counter() + + try: + email_service = get_email_i18n_service() + email_service.send_email( + email_type=EmailType.WORKFLOW_COMMENT_MENTION, + language_code=language, + to=to, + template_context={ + "to": to, + "mentioned_name": mentioned_name, + "commenter_name": commenter_name, + "app_name": app_name, + "comment_content": comment_content, + "app_url": app_url, + }, + ) + + end_at = time.perf_counter() + logger.info( + click.style( + f"Send workflow comment mention mail to {to} succeeded: latency: {end_at - start_at}", + fg="green", + ) + ) + except Exception: + logger.exception("workflow comment mention email to %s failed", to) diff --git a/api/tasks/trigger_processing_tasks.py b/api/tasks/trigger_processing_tasks.py index b9f382eccf..25ea53dfac 100644 --- a/api/tasks/trigger_processing_tasks.py +++ b/api/tasks/trigger_processing_tasks.py @@ -12,7 +12,6 @@ from datetime import UTC, datetime from typing import Any from celery import shared_task -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import func, select from sqlalchemy.orm import Session @@ -28,7 +27,8 @@ from core.trigger.entities.entities import TriggerProviderEntity from core.trigger.provider import PluginTriggerProviderController from core.trigger.trigger_manager import TriggerManager from core.workflow.nodes.trigger_plugin.entities import TriggerEventNodeData -from enums.quota_type import QuotaType +from enums.quota_type import QuotaType, unlimited +from graphon.enums import WorkflowExecutionStatus from models.enums import ( AppTriggerType, CreatorUserRole, @@ -42,7 +42,6 @@ from models.workflow import Workflow, WorkflowAppLog, WorkflowAppLogCreatedFrom, from services.async_workflow_service import AsyncWorkflowService from services.end_user_service import EndUserService from services.errors.app import QuotaExceededError -from services.quota_service import QuotaService, unlimited from services.trigger.app_trigger_service import AppTriggerService from services.trigger.trigger_provider_service import TriggerProviderService from services.trigger.trigger_request_service import TriggerHttpRequestCachingService @@ -299,10 +298,10 @@ def dispatch_triggered_workflow( icon_dark_filename=trigger_entity.identity.icon_dark or "", ) - # reserve quota before invoking trigger + # consume quota before invoking trigger quota_charge = unlimited() try: - quota_charge = QuotaService.reserve(QuotaType.TRIGGER, subscription.tenant_id) + quota_charge = QuotaType.TRIGGER.consume(subscription.tenant_id) except QuotaExceededError: AppTriggerService.mark_tenant_triggers_rate_limited(subscription.tenant_id) logger.info( @@ -388,7 +387,6 @@ def dispatch_triggered_workflow( raise ValueError(f"End user not found for app {plugin_trigger.app_id}") AsyncWorkflowService.trigger_workflow_async(session=session, user=end_user, trigger_data=trigger_data) - quota_charge.commit() dispatched_count += 1 logger.info( "Triggered workflow for app %s with trigger event %s", diff --git a/api/tasks/workflow_execution_tasks.py b/api/tasks/workflow_execution_tasks.py index b4f975f4da..5ca04fd7c2 100644 --- a/api/tasks/workflow_execution_tasks.py +++ b/api/tasks/workflow_execution_tasks.py @@ -10,11 +10,11 @@ import logging from typing import Any from celery import shared_task -from graphon.entities import WorkflowExecution -from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from sqlalchemy import select from core.db.session_factory import session_factory +from graphon.entities import WorkflowExecution +from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter from models import CreatorUserRole, WorkflowRun from models.enums import WorkflowRunTriggeredFrom diff --git a/api/tasks/workflow_node_execution_tasks.py b/api/tasks/workflow_node_execution_tasks.py index 128cdd72e1..0d5475a56d 100644 --- a/api/tasks/workflow_node_execution_tasks.py +++ b/api/tasks/workflow_node_execution_tasks.py @@ -10,13 +10,13 @@ import logging from typing import Any from celery import shared_task +from sqlalchemy import select + +from core.db.session_factory import session_factory from graphon.entities.workflow_node_execution import ( WorkflowNodeExecution, ) from graphon.workflow_type_encoder import WorkflowRuntimeTypeConverter -from sqlalchemy import select - -from core.db.session_factory import session_factory from models import CreatorUserRole, WorkflowNodeExecutionModel from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/templates/without-brand/workflow_comment_mention_template_en-US.html b/api/templates/without-brand/workflow_comment_mention_template_en-US.html new file mode 100644 index 0000000000..1ef8fe4e3f --- /dev/null +++ b/api/templates/without-brand/workflow_comment_mention_template_en-US.html @@ -0,0 +1,119 @@ + + + + + + + + +
+
+ Dify Logo +
+

You were mentioned in a workflow comment

+
+

Hi {{ mentioned_name }},

+

{{ commenter_name }} mentioned you in {{ app_name }}.

+
+
+

{{ comment_content }}

+
+

Open {{ application_title }} to reply to the comment.

+
+ + + diff --git a/api/templates/without-brand/workflow_comment_mention_template_zh-CN.html b/api/templates/without-brand/workflow_comment_mention_template_zh-CN.html new file mode 100644 index 0000000000..8b9b2dbe71 --- /dev/null +++ b/api/templates/without-brand/workflow_comment_mention_template_zh-CN.html @@ -0,0 +1,119 @@ + + + + + + + + +
+
+ Dify Logo +
+

你在工作流评论中被提及

+
+

你好,{{ mentioned_name }}:

+

{{ commenter_name }} 在 {{ app_name }} 中提及了你。

+
+
+

{{ comment_content }}

+
+

请在 {{ application_title }} 中查看并回复此评论。

+
+ + + diff --git a/api/templates/workflow_comment_mention_template_en-US.html b/api/templates/workflow_comment_mention_template_en-US.html new file mode 100644 index 0000000000..1ef8fe4e3f --- /dev/null +++ b/api/templates/workflow_comment_mention_template_en-US.html @@ -0,0 +1,119 @@ + + + + + + + + +
+
+ Dify Logo +
+

You were mentioned in a workflow comment

+
+

Hi {{ mentioned_name }},

+

{{ commenter_name }} mentioned you in {{ app_name }}.

+
+
+

{{ comment_content }}

+
+

Open {{ application_title }} to reply to the comment.

+
+ + + diff --git a/api/templates/workflow_comment_mention_template_zh-CN.html b/api/templates/workflow_comment_mention_template_zh-CN.html new file mode 100644 index 0000000000..8b9b2dbe71 --- /dev/null +++ b/api/templates/workflow_comment_mention_template_zh-CN.html @@ -0,0 +1,119 @@ + + + + + + + + +
+
+ Dify Logo +
+

你在工作流评论中被提及

+
+

你好,{{ mentioned_name }}:

+

{{ commenter_name }} 在 {{ app_name }} 中提及了你。

+
+
+

{{ comment_content }}

+
+

请在 {{ application_title }} 中查看并回复此评论。

+
+ + + diff --git a/api/tests/integration_tests/conftest.py b/api/tests/integration_tests/conftest.py index b2e8dda443..09078d196d 100644 --- a/api/tests/integration_tests/conftest.py +++ b/api/tests/integration_tests/conftest.py @@ -48,7 +48,7 @@ os.environ["OPENDAL_FS_ROOT"] = "/tmp/dify-storage" os.environ.setdefault("STORAGE_TYPE", "opendal") os.environ.setdefault("OPENDAL_SCHEME", "fs") -_CACHED_APP = create_app() +_SIO_APP, _CACHED_APP = create_app() @pytest.fixture(scope="session") diff --git a/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py b/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py deleted file mode 100644 index 038f37af5f..0000000000 --- a/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py +++ /dev/null @@ -1,47 +0,0 @@ -import uuid -from unittest import mock - -from controllers.console.app import workflow_draft_variable as draft_variable_api -from controllers.console.app import wraps -from factories.variable_factory import build_segment -from models import App, AppMode -from models.workflow import WorkflowDraftVariable -from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService - - -def _get_mock_srv_class() -> type[WorkflowDraftVariableService]: - return mock.create_autospec(WorkflowDraftVariableService) - - -class TestWorkflowDraftNodeVariableListApi: - def test_get(self, test_client, auth_header, monkeypatch): - srv_class = _get_mock_srv_class() - mock_app_model: App = App() - mock_app_model.id = str(uuid.uuid4()) - test_node_id = "test_node_id" - mock_app_model.mode = AppMode.ADVANCED_CHAT - mock_load_app_model = mock.Mock(return_value=mock_app_model) - - monkeypatch.setattr(draft_variable_api, "WorkflowDraftVariableService", srv_class) - monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model) - - var1 = WorkflowDraftVariable.new_node_variable( - app_id="test_app_1", - node_id="test_node_1", - name="str_var", - value=build_segment("str_value"), - node_execution_id=str(uuid.uuid4()), - ) - srv_instance = mock.create_autospec(WorkflowDraftVariableService, instance=True) - srv_class.return_value = srv_instance - srv_instance.list_node_variables.return_value = WorkflowDraftVariableList(variables=[var1]) - - response = test_client.get( - f"/console/api/apps/{mock_app_model.id}/workflows/draft/nodes/{test_node_id}/variables", - headers=auth_header, - ) - assert response.status_code == 200 - response_dict = response.json - assert isinstance(response_dict, dict) - assert "items" in response_dict - assert len(response_dict["items"]) == 1 diff --git a/api/tests/integration_tests/controllers/console/workspace/test_trigger_provider_permissions.py b/api/tests/integration_tests/controllers/console/workspace/test_trigger_provider_permissions.py deleted file mode 100644 index e55c12e678..0000000000 --- a/api/tests/integration_tests/controllers/console/workspace/test_trigger_provider_permissions.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Integration tests for Trigger Provider subscription permission verification.""" - -import uuid -from unittest import mock - -import pytest -from flask.testing import FlaskClient - -from controllers.console.workspace import trigger_providers as trigger_providers_api -from libs.datetime_utils import naive_utc_now -from models import Tenant -from models.account import Account, TenantAccountJoin, TenantAccountRole - - -class TestTriggerProviderSubscriptionPermissions: - """Test permission verification for Trigger Provider subscription endpoints.""" - - @pytest.fixture - def mock_account(self, monkeypatch: pytest.MonkeyPatch): - """Create a mock Account for testing.""" - - account = Account(name="Test User", email="test@example.com") - account.id = str(uuid.uuid4()) - account.last_active_at = naive_utc_now() - account.created_at = naive_utc_now() - account.updated_at = naive_utc_now() - - # Create mock tenant - tenant = Tenant(name="Test Tenant") - tenant.id = str(uuid.uuid4()) - - mock_session_instance = mock.Mock() - - mock_tenant_join = TenantAccountJoin(role=TenantAccountRole.OWNER) - monkeypatch.setattr(mock_session_instance, "scalar", mock.Mock(return_value=mock_tenant_join)) - - mock_scalars_result = mock.Mock() - mock_scalars_result.one.return_value = tenant - monkeypatch.setattr(mock_session_instance, "scalars", mock.Mock(return_value=mock_scalars_result)) - - mock_session_context = mock.Mock() - mock_session_context.__enter__.return_value = mock_session_instance - monkeypatch.setattr("models.account.Session", lambda _, expire_on_commit: mock_session_context) - - account.current_tenant = tenant - account.current_tenant_id = tenant.id - return account - - @pytest.mark.parametrize( - ("role", "list_status", "get_status", "update_status", "create_status", "build_status", "delete_status"), - [ - # Admin/Owner can do everything - (TenantAccountRole.OWNER, 200, 200, 200, 200, 200, 200), - (TenantAccountRole.ADMIN, 200, 200, 200, 200, 200, 200), - # Editor can list, get, update (parameters), but not create, build, or delete - (TenantAccountRole.EDITOR, 200, 200, 200, 403, 403, 403), - # Normal user cannot do anything - (TenantAccountRole.NORMAL, 403, 403, 403, 403, 403, 403), - # Dataset operator cannot do anything - (TenantAccountRole.DATASET_OPERATOR, 403, 403, 403, 403, 403, 403), - ], - ) - def test_trigger_subscription_permissions( - self, - test_client: FlaskClient, - auth_header, - monkeypatch, - mock_account, - role: TenantAccountRole, - list_status: int, - get_status: int, - update_status: int, - create_status: int, - build_status: int, - delete_status: int, - ): - """Test that different roles have appropriate permissions for trigger subscription operations.""" - # Set user role - mock_account.role = role - - # Mock current user - monkeypatch.setattr(trigger_providers_api, "current_user", mock_account) - - # Mock AccountService.load_user to prevent authentication issues - from services.account_service import AccountService - - mock_load_user = mock.Mock(return_value=mock_account) - monkeypatch.setattr(AccountService, "load_user", mock_load_user) - - # Test data - provider = "some_provider/some_trigger" - subscription_builder_id = str(uuid.uuid4()) - subscription_id = str(uuid.uuid4()) - - # Mock service methods - mock_list_subscriptions = mock.Mock(return_value=[]) - monkeypatch.setattr( - "services.trigger.trigger_provider_service.TriggerProviderService.list_trigger_provider_subscriptions", - mock_list_subscriptions, - ) - - mock_get_subscription_builder = mock.Mock(return_value={"id": subscription_builder_id}) - monkeypatch.setattr( - "services.trigger.trigger_subscription_builder_service.TriggerSubscriptionBuilderService.get_subscription_builder_by_id", - mock_get_subscription_builder, - ) - - mock_update_subscription_builder = mock.Mock(return_value={"id": subscription_builder_id}) - monkeypatch.setattr( - "services.trigger.trigger_subscription_builder_service.TriggerSubscriptionBuilderService.update_trigger_subscription_builder", - mock_update_subscription_builder, - ) - - mock_create_subscription_builder = mock.Mock(return_value={"id": subscription_builder_id}) - monkeypatch.setattr( - "services.trigger.trigger_subscription_builder_service.TriggerSubscriptionBuilderService.create_trigger_subscription_builder", - mock_create_subscription_builder, - ) - - mock_update_and_build_builder = mock.Mock() - monkeypatch.setattr( - "services.trigger.trigger_subscription_builder_service.TriggerSubscriptionBuilderService.update_and_build_builder", - mock_update_and_build_builder, - ) - - mock_delete_provider = mock.Mock() - mock_delete_plugin_trigger = mock.Mock() - mock_db_session = mock.Mock() - mock_db_session.commit = mock.Mock() - - def mock_session_func(engine=None): - return mock_session_context - - mock_session_context = mock.Mock() - mock_session_context.__enter__.return_value = mock_db_session - mock_session_context.__exit__.return_value = None - - monkeypatch.setattr("services.trigger.trigger_provider_service.Session", mock_session_func) - monkeypatch.setattr("services.trigger.trigger_subscription_operator_service.Session", mock_session_func) - - monkeypatch.setattr( - "services.trigger.trigger_provider_service.TriggerProviderService.delete_trigger_provider", - mock_delete_provider, - ) - monkeypatch.setattr( - "services.trigger.trigger_subscription_operator_service.TriggerSubscriptionOperatorService.delete_plugin_trigger_by_subscription", - mock_delete_plugin_trigger, - ) - - # Test 1: List subscriptions (should work for Editor, Admin, Owner) - response = test_client.get( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/list", - headers=auth_header, - ) - assert response.status_code == list_status - - # Test 2: Get subscription builder (should work for Editor, Admin, Owner) - response = test_client.get( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/builder/{subscription_builder_id}", - headers=auth_header, - ) - assert response.status_code == get_status - - # Test 3: Update subscription builder parameters (should work for Editor, Admin, Owner) - response = test_client.post( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/builder/update/{subscription_builder_id}", - headers=auth_header, - json={"parameters": {"webhook_url": "https://example.com/webhook"}}, - ) - assert response.status_code == update_status - - # Test 4: Create subscription builder (should only work for Admin, Owner) - response = test_client.post( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/builder/create", - headers=auth_header, - json={"credential_type": "api_key"}, - ) - assert response.status_code == create_status - - # Test 5: Build/activate subscription (should only work for Admin, Owner) - response = test_client.post( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/builder/build/{subscription_builder_id}", - headers=auth_header, - json={"name": "Test Subscription"}, - ) - assert response.status_code == build_status - - # Test 6: Delete subscription (should only work for Admin, Owner) - response = test_client.post( - f"/console/api/workspaces/current/trigger-provider/{subscription_id}/subscriptions/delete", - headers=auth_header, - ) - assert response.status_code == delete_status - - @pytest.mark.parametrize( - ("role", "status"), - [ - (TenantAccountRole.OWNER, 200), - (TenantAccountRole.ADMIN, 200), - # Editor should be able to access logs for debugging - (TenantAccountRole.EDITOR, 200), - (TenantAccountRole.NORMAL, 403), - (TenantAccountRole.DATASET_OPERATOR, 403), - ], - ) - def test_trigger_subscription_logs_permissions( - self, - test_client: FlaskClient, - auth_header, - monkeypatch, - mock_account, - role: TenantAccountRole, - status: int, - ): - """Test that different roles have appropriate permissions for accessing subscription logs.""" - # Set user role - mock_account.role = role - - # Mock current user - monkeypatch.setattr(trigger_providers_api, "current_user", mock_account) - - # Mock AccountService.load_user to prevent authentication issues - from services.account_service import AccountService - - mock_load_user = mock.Mock(return_value=mock_account) - monkeypatch.setattr(AccountService, "load_user", mock_load_user) - - # Test data - provider = "some_provider/some_trigger" - subscription_builder_id = str(uuid.uuid4()) - - # Mock service method - mock_list_logs = mock.Mock(return_value=[]) - monkeypatch.setattr( - "services.trigger.trigger_subscription_builder_service.TriggerSubscriptionBuilderService.list_logs", - mock_list_logs, - ) - - # Test access to logs - response = test_client.get( - f"/console/api/workspaces/current/trigger-provider/{provider}/subscriptions/builder/logs/{subscription_builder_id}", - headers=auth_header, - ) - assert response.status_code == status diff --git a/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py b/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py index 91245e879e..a876b0c4aa 100644 --- a/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py +++ b/api/tests/integration_tests/core/datasource/test_datasource_manager_integration.py @@ -1,9 +1,8 @@ from collections.abc import Generator -from graphon.node_events import StreamCompletedEvent - from core.datasource.datasource_manager import DatasourceManager from core.datasource.entities.datasource_entities import DatasourceMessage +from graphon.node_events import StreamCompletedEvent def _gen_var_stream() -> Generator[DatasourceMessage, None, None]: diff --git a/api/tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py b/api/tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py index 3fdea10976..b5318aaa2b 100644 --- a/api/tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py +++ b/api/tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py @@ -1,8 +1,7 @@ -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.node_events import NodeRunResult, StreamCompletedEvent - from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY from core.workflow.nodes.datasource.datasource_node import DatasourceNode +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.node_events import NodeRunResult, StreamCompletedEvent class _Seg: diff --git a/api/tests/integration_tests/factories/test_storage_key_loader.py b/api/tests/integration_tests/factories/test_storage_key_loader.py deleted file mode 100644 index c1bb8e1245..0000000000 --- a/api/tests/integration_tests/factories/test_storage_key_loader.py +++ /dev/null @@ -1,375 +0,0 @@ -import unittest -from datetime import UTC, datetime -from unittest.mock import patch -from uuid import uuid4 - -import pytest -from graphon.file import File, FileTransferMethod, FileType -from sqlalchemy.orm import Session - -from core.app.file_access import DatabaseFileAccessController -from extensions.ext_database import db -from extensions.storage.storage_type import StorageType -from factories.file_factory import StorageKeyLoader -from models import ToolFile, UploadFile -from models.enums import CreatorUserRole - - -@pytest.mark.usefixtures("flask_req_ctx") -class TestStorageKeyLoader(unittest.TestCase): - """ - Integration tests for StorageKeyLoader class. - - Tests the batched loading of storage keys from the database for files - with different transfer methods: LOCAL_FILE, REMOTE_URL, and TOOL_FILE. - """ - - def setUp(self): - """Set up test data before each test method.""" - self.session = db.session() - self.tenant_id = str(uuid4()) - self.user_id = str(uuid4()) - self.conversation_id = str(uuid4()) - - # Create test data that will be cleaned up after each test - self.test_upload_files = [] - self.test_tool_files = [] - - # Create StorageKeyLoader instance - self.loader = StorageKeyLoader( - self.session, - self.tenant_id, - access_controller=DatabaseFileAccessController(), - ) - - def tearDown(self): - """Clean up test data after each test method.""" - self.session.rollback() - - def _create_upload_file( - self, file_id: str | None = None, storage_key: str | None = None, tenant_id: str | None = None - ) -> UploadFile: - """Helper method to create an UploadFile record for testing.""" - if file_id is None: - file_id = str(uuid4()) - if storage_key is None: - storage_key = f"test_storage_key_{uuid4()}" - if tenant_id is None: - tenant_id = self.tenant_id - - upload_file = UploadFile( - tenant_id=tenant_id, - storage_type=StorageType.LOCAL, - key=storage_key, - name="test_file.txt", - size=1024, - extension=".txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, - created_by=self.user_id, - created_at=datetime.now(UTC), - used=False, - ) - upload_file.id = file_id - - self.session.add(upload_file) - self.session.flush() - self.test_upload_files.append(upload_file) - - return upload_file - - def _create_tool_file( - self, file_id: str | None = None, file_key: str | None = None, tenant_id: str | None = None - ) -> ToolFile: - """Helper method to create a ToolFile record for testing.""" - if file_id is None: - file_id = str(uuid4()) - if file_key is None: - file_key = f"test_file_key_{uuid4()}" - if tenant_id is None: - tenant_id = self.tenant_id - - tool_file = ToolFile( - user_id=self.user_id, - tenant_id=tenant_id, - conversation_id=self.conversation_id, - file_key=file_key, - mimetype="text/plain", - original_url="http://example.com/file.txt", - name="test_tool_file.txt", - size=2048, - ) - tool_file.id = file_id - self.session.add(tool_file) - self.session.flush() - self.test_tool_files.append(tool_file) - - return tool_file - - def _create_file(self, related_id: str, transfer_method: FileTransferMethod, tenant_id: str | None = None) -> File: - """Helper method to create a File object for testing.""" - if tenant_id is None: - tenant_id = self.tenant_id - - # Set related_id for LOCAL_FILE and TOOL_FILE transfer methods - file_related_id = None - remote_url = None - - if transfer_method in (FileTransferMethod.LOCAL_FILE, FileTransferMethod.TOOL_FILE): - file_related_id = related_id - elif transfer_method == FileTransferMethod.REMOTE_URL: - remote_url = "https://example.com/test_file.txt" - file_related_id = related_id - - return File( - id=str(uuid4()), # Generate new UUID for File.id - tenant_id=tenant_id, - type=FileType.DOCUMENT, - transfer_method=transfer_method, - related_id=file_related_id, - remote_url=remote_url, - filename="test_file.txt", - extension=".txt", - mime_type="text/plain", - size=1024, - storage_key="initial_key", - ) - - def test_load_storage_keys_local_file(self): - """Test loading storage keys for LOCAL_FILE transfer method.""" - # Create test data - upload_file = self._create_upload_file() - file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) - - # Load storage keys - self.loader.load_storage_keys([file]) - - # Verify storage key was loaded correctly - assert file._storage_key == upload_file.key - - def test_load_storage_keys_remote_url(self): - """Test loading storage keys for REMOTE_URL transfer method.""" - # Create test data - upload_file = self._create_upload_file() - file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.REMOTE_URL) - - # Load storage keys - self.loader.load_storage_keys([file]) - - # Verify storage key was loaded correctly - assert file._storage_key == upload_file.key - - def test_load_storage_keys_tool_file(self): - """Test loading storage keys for TOOL_FILE transfer method.""" - # Create test data - tool_file = self._create_tool_file() - file = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) - - # Load storage keys - self.loader.load_storage_keys([file]) - - # Verify storage key was loaded correctly - assert file._storage_key == tool_file.file_key - - def test_load_storage_keys_mixed_methods(self): - """Test batch loading with mixed transfer methods.""" - # Create test data for different transfer methods - upload_file1 = self._create_upload_file() - upload_file2 = self._create_upload_file() - tool_file = self._create_tool_file() - - file1 = self._create_file(related_id=upload_file1.id, transfer_method=FileTransferMethod.LOCAL_FILE) - file2 = self._create_file(related_id=upload_file2.id, transfer_method=FileTransferMethod.REMOTE_URL) - file3 = self._create_file(related_id=tool_file.id, transfer_method=FileTransferMethod.TOOL_FILE) - - files = [file1, file2, file3] - - # Load storage keys - self.loader.load_storage_keys(files) - - # Verify all storage keys were loaded correctly - assert file1._storage_key == upload_file1.key - assert file2._storage_key == upload_file2.key - assert file3._storage_key == tool_file.file_key - - def test_load_storage_keys_empty_list(self): - """Test with empty file list.""" - # Should not raise any exceptions - self.loader.load_storage_keys([]) - - def test_load_storage_keys_ignores_legacy_file_tenant_id(self): - """Legacy file tenant_id should not override the loader tenant scope.""" - upload_file = self._create_upload_file() - file = self._create_file( - related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=str(uuid4()) - ) - - self.loader.load_storage_keys([file]) - - assert file._storage_key == upload_file.key - - def test_load_storage_keys_missing_file_id(self): - """Test with None file.related_id.""" - # Create a file with valid parameters first, then manually set related_id to None - file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) - file.related_id = None - - # Should raise ValueError for None file related_id - with pytest.raises(ValueError) as context: - self.loader.load_storage_keys([file]) - - assert str(context.value) == "file id should not be None." - - def test_load_storage_keys_nonexistent_upload_file_records(self): - """Test with missing UploadFile database records.""" - # Create file with non-existent upload file id - non_existent_id = str(uuid4()) - file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.LOCAL_FILE) - - # Should raise ValueError for missing record - with pytest.raises(ValueError): - self.loader.load_storage_keys([file]) - - def test_load_storage_keys_nonexistent_tool_file_records(self): - """Test with missing ToolFile database records.""" - # Create file with non-existent tool file id - non_existent_id = str(uuid4()) - file = self._create_file(related_id=non_existent_id, transfer_method=FileTransferMethod.TOOL_FILE) - - # Should raise ValueError for missing record - with pytest.raises(ValueError): - self.loader.load_storage_keys([file]) - - def test_load_storage_keys_invalid_uuid(self): - """Test with invalid UUID format.""" - # Create a file with valid parameters first, then manually set invalid related_id - file = self._create_file(related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE) - file.related_id = "invalid-uuid-format" - - # Should raise ValueError for invalid UUID - with pytest.raises(ValueError): - self.loader.load_storage_keys([file]) - - def test_load_storage_keys_batch_efficiency(self): - """Test batched operations use efficient queries.""" - # Create multiple files of different types - upload_files = [self._create_upload_file() for _ in range(3)] - tool_files = [self._create_tool_file() for _ in range(2)] - - files = [] - files.extend( - [self._create_file(related_id=uf.id, transfer_method=FileTransferMethod.LOCAL_FILE) for uf in upload_files] - ) - files.extend( - [self._create_file(related_id=tf.id, transfer_method=FileTransferMethod.TOOL_FILE) for tf in tool_files] - ) - - # Mock the session to count queries - with patch.object(self.session, "scalars", wraps=self.session.scalars) as mock_scalars: - self.loader.load_storage_keys(files) - - # Should make exactly 2 queries (one for upload_files, one for tool_files) - assert mock_scalars.call_count == 2 - - # Verify all storage keys were loaded correctly - for i, file in enumerate(files[:3]): - assert file._storage_key == upload_files[i].key - for i, file in enumerate(files[3:]): - assert file._storage_key == tool_files[i].file_key - - def test_load_storage_keys_tenant_isolation(self): - """Test that tenant isolation works correctly.""" - # Create files for different tenants - other_tenant_id = str(uuid4()) - - # Create upload file for current tenant - upload_file_current = self._create_upload_file() - file_current = self._create_file( - related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE - ) - - # Create upload file for other tenant (but don't add to cleanup list) - upload_file_other = UploadFile( - tenant_id=other_tenant_id, - storage_type=StorageType.LOCAL, - key="other_tenant_key", - name="other_file.txt", - size=1024, - extension=".txt", - mime_type="text/plain", - created_by_role=CreatorUserRole.ACCOUNT, - created_by=self.user_id, - created_at=datetime.now(UTC), - used=False, - ) - upload_file_other.id = str(uuid4()) - self.session.add(upload_file_other) - self.session.flush() - - # Create file for other tenant but try to load with current tenant's loader - file_other = self._create_file( - related_id=upload_file_other.id, transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id - ) - - # Should raise ValueError due to tenant mismatch - with pytest.raises(ValueError) as context: - self.loader.load_storage_keys([file_other]) - - assert "Upload file not found for id:" in str(context.value) - - # Current tenant's file should still work - self.loader.load_storage_keys([file_current]) - assert file_current._storage_key == upload_file_current.key - - def test_load_storage_keys_mixed_tenant_batch(self): - """Test batch with mixed tenant files (should fail on first mismatch).""" - # Create files for current tenant - upload_file_current = self._create_upload_file() - file_current = self._create_file( - related_id=upload_file_current.id, transfer_method=FileTransferMethod.LOCAL_FILE - ) - - # Create file for different tenant - other_tenant_id = str(uuid4()) - file_other = self._create_file( - related_id=str(uuid4()), transfer_method=FileTransferMethod.LOCAL_FILE, tenant_id=other_tenant_id - ) - - # Should raise ValueError on tenant mismatch - with pytest.raises(ValueError) as context: - self.loader.load_storage_keys([file_current, file_other]) - - assert "Upload file not found for id:" in str(context.value) - - def test_load_storage_keys_duplicate_file_ids(self): - """Test handling of duplicate file IDs in the batch.""" - # Create upload file - upload_file = self._create_upload_file() - - # Create two File objects with same related_id - file1 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) - file2 = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) - - # Should handle duplicates gracefully - self.loader.load_storage_keys([file1, file2]) - - # Both files should have the same storage key - assert file1._storage_key == upload_file.key - assert file2._storage_key == upload_file.key - - def test_load_storage_keys_session_isolation(self): - """Test that the loader uses the provided session correctly.""" - # Create test data - upload_file = self._create_upload_file() - file = self._create_file(related_id=upload_file.id, transfer_method=FileTransferMethod.LOCAL_FILE) - - # Create loader with different session (same underlying connection) - - with Session(bind=db.engine) as other_session: - other_loader = StorageKeyLoader( - other_session, - self.tenant_id, - access_controller=DatabaseFileAccessController(), - ) - with pytest.raises(ValueError): - other_loader.load_storage_keys([file]) diff --git a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py index ce04a158a8..c4146d5ccd 100644 --- a/api/tests/integration_tests/model_runtime/__mock/plugin_model.py +++ b/api/tests/integration_tests/model_runtime/__mock/plugin_model.py @@ -4,6 +4,9 @@ from collections.abc import Generator, Sequence from decimal import Decimal from json import dumps +from core.plugin.entities.plugin_daemon import PluginModelProviderEntity +from core.plugin.impl.model import PluginModelClient + # import monkeypatch from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.llm_entities import ( @@ -23,9 +26,6 @@ from graphon.model_runtime.entities.model_entities import ( ) from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity -from core.plugin.entities.plugin_daemon import PluginModelProviderEntity -from core.plugin.impl.model import PluginModelClient - class MockModelClass(PluginModelClient): def fetch_model_providers(self, tenant_id: str) -> Sequence[PluginModelProviderEntity]: diff --git a/api/tests/integration_tests/services/test_workflow_draft_variable_service.py b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py index c7bb90f019..e130644338 100644 --- a/api/tests/integration_tests/services/test_workflow_draft_variable_service.py +++ b/api/tests/integration_tests/services/test_workflow_draft_variable_service.py @@ -3,10 +3,6 @@ import unittest import uuid import pytest -from graphon.nodes import BuiltinNodeTypes -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType -from graphon.variables.variables import StringVariable from sqlalchemy import delete, func, select from sqlalchemy.orm import Session @@ -15,6 +11,10 @@ from extensions.ext_database import db from extensions.ext_storage import storage from extensions.storage.storage_type import StorageType from factories.variable_factory import build_segment +from graphon.nodes import BuiltinNodeTypes +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType +from graphon.variables.variables import StringVariable from libs import datetime_utils from models.enums import CreatorUserRole from models.model import UploadFile diff --git a/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py b/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py index 3dfedd811d..4f444598b1 100644 --- a/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py +++ b/api/tests/integration_tests/tasks/test_remove_app_and_related_data_task.py @@ -2,11 +2,11 @@ import uuid from unittest.mock import patch import pytest -from graphon.variables.segments import StringSegment from sqlalchemy import delete, func, select from core.db.session_factory import session_factory from extensions.storage.storage_type import StorageType +from graphon.variables.segments import StringSegment from models import Tenant from models.enums import CreatorUserRole from models.model import App, UploadFile @@ -209,7 +209,6 @@ class TestDeleteDraftVariablesWithOffloadIntegration: def setup_offload_test_data(self, app_and_tenant): tenant, app = app_and_tenant from graphon.variables.types import SegmentType - from libs.datetime_utils import naive_utc_now with session_factory.create_session() as session: @@ -453,7 +452,6 @@ class TestDeleteDraftVariablesSessionCommit: def setup_offload_test_data(self, app_and_tenant): """Create test data with offload files for session commit tests.""" from graphon.variables.types import SegmentType - from libs.datetime_utils import naive_utc_now tenant, app = app_and_tenant diff --git a/api/tests/integration_tests/workflow/nodes/__mock/model.py b/api/tests/integration_tests/workflow/nodes/__mock/model.py index c0143faa85..a9a2617bae 100644 --- a/api/tests/integration_tests/workflow/nodes/__mock/model.py +++ b/api/tests/integration_tests/workflow/nodes/__mock/model.py @@ -1,12 +1,11 @@ from unittest.mock import MagicMock -from graphon.model_runtime.entities.model_entities import ModelType - from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle from core.entities.provider_entities import CustomConfiguration, CustomProviderConfiguration, SystemConfiguration from core.model_manager import ModelInstance from core.plugin.impl.model_runtime_factory import create_plugin_model_provider_factory +from graphon.model_runtime.entities.model_entities import ModelType from models.provider import ProviderType diff --git a/api/tests/integration_tests/workflow/nodes/code_executor/test_code_jinja2.py b/api/tests/integration_tests/workflow/nodes/code_executor/test_code_jinja2.py deleted file mode 100644 index c8eb9ec3e4..0000000000 --- a/api/tests/integration_tests/workflow/nodes/code_executor/test_code_jinja2.py +++ /dev/null @@ -1,95 +0,0 @@ -import base64 - -from core.helper.code_executor.code_executor import CodeExecutor, CodeLanguage -from core.helper.code_executor.jinja2.jinja2_transformer import Jinja2TemplateTransformer - -CODE_LANGUAGE = CodeLanguage.JINJA2 - - -def test_jinja2(): - """Test basic Jinja2 template rendering.""" - template = "Hello {{template}}" - # Template must be base64 encoded to match the new safe embedding approach - template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8") - inputs = base64.b64encode(b'{"template": "World"}').decode("utf-8") - code = ( - Jinja2TemplateTransformer.get_runner_script() - .replace(Jinja2TemplateTransformer._template_b64_placeholder, template_b64) - .replace(Jinja2TemplateTransformer._inputs_placeholder, inputs) - ) - result = CodeExecutor.execute_code( - language=CODE_LANGUAGE, preload=Jinja2TemplateTransformer.get_preload_script(), code=code - ) - assert result == "<>Hello World<>\n" - - -def test_jinja2_with_code_template(): - """Test template rendering via the high-level workflow API.""" - result = CodeExecutor.execute_workflow_code_template( - language=CODE_LANGUAGE, code="Hello {{template}}", inputs={"template": "World"} - ) - assert result == {"result": "Hello World"} - - -def test_jinja2_get_runner_script(): - """Test that runner script contains required placeholders.""" - runner_script = Jinja2TemplateTransformer.get_runner_script() - assert runner_script.count(Jinja2TemplateTransformer._template_b64_placeholder) == 1 - assert runner_script.count(Jinja2TemplateTransformer._inputs_placeholder) == 1 - assert runner_script.count(Jinja2TemplateTransformer._result_tag) == 2 - - -def test_jinja2_template_with_special_characters(): - """ - Test that templates with special characters (quotes, newlines) render correctly. - This is a regression test for issue #26818 where textarea pre-fill values - containing special characters would break template rendering. - """ - # Template with triple quotes, single quotes, double quotes, and newlines - template = """ - - - -

Status: "{{ status }}"

-
'''code block'''
- -""" - inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"} - - result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs) - - # Verify the template rendered correctly with all special characters - output = result["result"] - assert 'value="TASK-123"' in output - assert "" in output - assert 'Status: "completed"' in output - assert "'''code block'''" in output - - -def test_jinja2_template_with_html_textarea_prefill(): - """ - Specific test for HTML textarea with Jinja2 variable pre-fill. - Verifies fix for issue #26818. - """ - template = "" - notes_content = "This is a multi-line note.\nWith special chars: 'single' and \"double\" quotes." - inputs = {"notes": notes_content} - - result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs) - - expected_output = f"" - assert result["result"] == expected_output - - -def test_jinja2_assemble_runner_script_encodes_template(): - """Test that assemble_runner_script properly base64 encodes the template.""" - template = "Hello {{ name }}!" - inputs = {"name": "World"} - - script = Jinja2TemplateTransformer.assemble_runner_script(template, inputs) - - # The template should be base64 encoded in the script - template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8") - assert template_b64 in script - # The raw template should NOT appear in the script (it's encoded) - assert "Hello {{ name }}!" not in script diff --git a/api/tests/integration_tests/workflow/nodes/test_code.py b/api/tests/integration_tests/workflow/nodes/test_code.py index 4f41396c22..e3476c292b 100644 --- a/api/tests/integration_tests/workflow/nodes/test_code.py +++ b/api/tests/integration_tests/workflow/nodes/test_code.py @@ -2,17 +2,17 @@ import time import uuid import pytest + +from configs import dify_config +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph import Graph from graphon.node_events import NodeRunResult from graphon.nodes.code.code_node import CodeNode from graphon.nodes.code.limits import CodeNodeLimits from graphon.runtime import GraphRuntimeState, VariablePool - -from configs import dify_config -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.code_executor",) diff --git a/api/tests/integration_tests/workflow/nodes/test_http.py b/api/tests/integration_tests/workflow/nodes/test_http.py index b1f937e738..aa6cf1e021 100644 --- a/api/tests/integration_tests/workflow/nodes/test_http.py +++ b/api/tests/integration_tests/workflow/nodes/test_http.py @@ -3,11 +3,6 @@ import uuid from urllib.parse import urlencode import pytest -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.file.file_manager import file_manager -from graphon.graph import Graph -from graphon.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig -from graphon.runtime import GraphRuntimeState, VariablePool from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom @@ -16,6 +11,11 @@ from core.tools.tool_file_manager import ToolFileManager from core.workflow.node_factory import DifyNodeFactory from core.workflow.node_runtime import DifyFileReferenceFactory from core.workflow.system_variables import build_system_variables +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.file.file_manager import file_manager +from graphon.graph import Graph +from graphon.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig +from graphon.runtime import GraphRuntimeState, VariablePool from tests.workflow_test_utils import build_test_graph_init_params pytest_plugins = ("tests.integration_tests.workflow.nodes.__mock.http",) @@ -192,6 +192,7 @@ def test_custom_authorization_header(setup_http_mock): @pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True) def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock): """Test: In custom authentication mode, when the api_key is empty, AuthorizationConfigError should be raised.""" + from core.workflow.system_variables import build_system_variables from graphon.enums import BuiltinNodeTypes from graphon.nodes.http_request.entities import ( HttpRequestNodeAuthorization, @@ -202,8 +203,6 @@ def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock): from graphon.nodes.http_request.executor import Executor from graphon.runtime import VariablePool - from core.workflow.system_variables import build_system_variables - # Create variable pool variable_pool = VariablePool( system_variables=build_system_variables(user_id="test", files=[]), diff --git a/api/tests/integration_tests/workflow/nodes/test_llm.py b/api/tests/integration_tests/workflow/nodes/test_llm.py index f0f3fcead1..fa5d63cfbf 100644 --- a/api/tests/integration_tests/workflow/nodes/test_llm.py +++ b/api/tests/integration_tests/workflow/nodes/test_llm.py @@ -4,6 +4,11 @@ import uuid from collections.abc import Generator from unittest.mock import MagicMock, patch +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.llm_generator.output_parser.structured_output import _parse_structured_output +from core.model_manager import ModelInstance +from core.workflow.system_variables import build_system_variables +from extensions.ext_database import db from graphon.enums import WorkflowNodeExecutionStatus from graphon.node_events import StreamCompletedEvent from graphon.nodes.llm.file_saver import LLMFileSaver @@ -12,12 +17,6 @@ from graphon.nodes.llm.protocols import CredentialsProvider, ModelFactory from graphon.nodes.llm.runtime_protocols import PromptMessageSerializerProtocol from graphon.nodes.protocols import HttpClientProtocol from graphon.runtime import GraphRuntimeState, VariablePool - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.llm_generator.output_parser.structured_output import _parse_structured_output -from core.model_manager import ModelInstance -from core.workflow.system_variables import build_system_variables -from extensions.ext_database import db from tests.workflow_test_utils import build_test_graph_init_params """FOR MOCK FIXTURES, DO NOT REMOVE""" diff --git a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py b/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py index fe512c2585..52886855b8 100644 --- a/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py +++ b/api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py @@ -3,17 +3,16 @@ import time import uuid from unittest.mock import MagicMock -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.model_runtime.entities import AssistantPromptMessage, UserPromptMessage -from graphon.nodes.llm.protocols import CredentialsProvider, ModelFactory -from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.model_manager import ModelInstance from core.workflow.node_runtime import DifyPromptMessageSerializer from core.workflow.system_variables import build_system_variables from extensions.ext_database import db +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.model_runtime.entities import AssistantPromptMessage, UserPromptMessage +from graphon.nodes.llm.protocols import CredentialsProvider, ModelFactory +from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode +from graphon.runtime import GraphRuntimeState, VariablePool from tests.integration_tests.workflow.nodes.__mock.model import get_mocked_fetch_model_instance from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/integration_tests/workflow/nodes/test_template_transform.py b/api/tests/integration_tests/workflow/nodes/test_template_transform.py index 2d728569be..9e3e1a47e3 100644 --- a/api/tests/integration_tests/workflow/nodes/test_template_transform.py +++ b/api/tests/integration_tests/workflow/nodes/test_template_transform.py @@ -1,15 +1,14 @@ import time import uuid +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph import Graph from graphon.nodes.template_transform.template_transform_node import TemplateTransformNode from graphon.runtime import GraphRuntimeState, VariablePool from graphon.template_rendering import TemplateRenderError - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/integration_tests/workflow/nodes/test_tool.py b/api/tests/integration_tests/workflow/nodes/test_tool.py index 750ced7075..f9ec51ee10 100644 --- a/api/tests/integration_tests/workflow/nodes/test_tool.py +++ b/api/tests/integration_tests/workflow/nodes/test_tool.py @@ -2,18 +2,17 @@ import time import uuid from unittest.mock import MagicMock, patch -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.graph import Graph -from graphon.node_events import StreamCompletedEvent -from graphon.nodes.protocols import ToolFileManagerProtocol -from graphon.nodes.tool.tool_node import ToolNode -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.tools.utils.configuration import ToolParameterConfigurationManager from core.workflow.node_factory import DifyNodeFactory from core.workflow.node_runtime import DifyToolNodeRuntime from core.workflow.system_variables import build_system_variables +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.graph import Graph +from graphon.node_events import StreamCompletedEvent +from graphon.nodes.protocols import ToolFileManagerProtocol +from graphon.nodes.tool.tool_node import ToolNode +from graphon.runtime import GraphRuntimeState, VariablePool from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/test_containers_integration_tests/conftest.py b/api/tests/test_containers_integration_tests/conftest.py index ef74893f07..66a25e5daf 100644 --- a/api/tests/test_containers_integration_tests/conftest.py +++ b/api/tests/test_containers_integration_tests/conftest.py @@ -369,7 +369,7 @@ def _create_app_with_containers() -> Flask: # Create and configure the Flask application logger.info("Initializing Flask application...") - app = create_app() + sio_app, app = create_app() logger.info("Flask application created successfully") # Initialize database schema diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py index 5cc458fe2e..5a22f81a69 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_chat_conversation_status_count_api.py @@ -4,15 +4,15 @@ import json import uuid from flask.testing import FlaskClient -from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import Session from configs import dify_config from constants import HEADER_NAME_CSRF_TOKEN +from graphon.enums import WorkflowExecutionStatus from libs.datetime_utils import naive_utc_now from libs.token import _real_cookie_name, generate_csrf_token from models import Account, DifySetup, Tenant, TenantAccountJoin -from models.account import AccountStatus, TenantAccountRole +from models.account import AccountStatus, TenantAccountRole, TenantStatus from models.enums import ConversationFromSource, CreatorUserRole from models.model import App, AppMode, Conversation, Message from models.workflow import WorkflowRun @@ -30,7 +30,7 @@ def _create_account_and_tenant(db_session: Session) -> tuple[Account, Tenant]: db_session.add(account) db_session.commit() - tenant = Tenant(name="Test Tenant", status="normal") + tenant = Tenant(name="Test Tenant", status=TenantStatus.NORMAL) db_session.add(tenant) db_session.commit() diff --git a/api/tests/test_containers_integration_tests/controllers/console/app/test_workflow_draft_variable.py b/api/tests/test_containers_integration_tests/controllers/console/app/test_workflow_draft_variable.py index 8ddf867370..290be87697 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/app/test_workflow_draft_variable.py +++ b/api/tests/test_containers_integration_tests/controllers/console/app/test_workflow_draft_variable.py @@ -3,12 +3,12 @@ import uuid from flask.testing import FlaskClient -from graphon.variables.segments import StringSegment from sqlalchemy import select from sqlalchemy.orm import Session from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID from factories.variable_factory import segment_to_variable +from graphon.variables.segments import StringSegment from models import Workflow from models.model import AppMode from models.workflow import WorkflowDraftVariable diff --git a/api/tests/test_containers_integration_tests/controllers/console/helpers.py b/api/tests/test_containers_integration_tests/controllers/console/helpers.py index 9e2084f393..a8ecf94da1 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/helpers.py +++ b/api/tests/test_containers_integration_tests/controllers/console/helpers.py @@ -11,7 +11,7 @@ from constants import HEADER_NAME_CSRF_TOKEN from libs.datetime_utils import naive_utc_now from libs.token import _real_cookie_name, generate_csrf_token from models import Account, DifySetup, Tenant, TenantAccountJoin -from models.account import AccountStatus, TenantAccountRole +from models.account import AccountStatus, TenantAccountRole, TenantStatus from models.model import App, AppMode from services.account_service import AccountService @@ -37,7 +37,7 @@ def create_console_account_and_tenant(db_session: Session) -> tuple[Account, Ten db_session.add(account) db_session.commit() - tenant = Tenant(name="Test Tenant", status="normal") + tenant = Tenant(name="Test Tenant", status=TenantStatus.NORMAL) db_session.add(tenant) db_session.commit() diff --git a/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py b/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py index c9ee67863d..c342e8994b 100644 --- a/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py +++ b/api/tests/test_containers_integration_tests/core/app/layers/test_pause_state_persist_layer.py @@ -22,13 +22,6 @@ import uuid from time import time import pytest -from graphon.entities.pause_reason import SchedulingPause -from graphon.enums import WorkflowExecutionStatus -from graphon.graph_engine.entities.commands import GraphEngineCommand -from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError -from graphon.graph_events import GraphRunPausedEvent -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool from sqlalchemy import Engine, delete, select from sqlalchemy.orm import Session @@ -40,6 +33,13 @@ from core.app.layers.pause_state_persist_layer import ( ) from core.workflow.system_variables import build_system_variables from extensions.ext_storage import storage +from graphon.entities.pause_reason import SchedulingPause +from graphon.enums import WorkflowExecutionStatus +from graphon.graph_engine.entities.commands import GraphEngineCommand +from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError +from graphon.graph_events import GraphRunPausedEvent +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool from libs.datetime_utils import naive_utc_now from models import Account from models import WorkflowPause as WorkflowPauseModel @@ -88,11 +88,11 @@ class TestPauseStatePersistenceLayerTestContainers: def setup_test_data(self, db_session_with_containers, file_service, workflow_run_service): """Set up test data for each test method using TestContainers.""" # Create test tenant and account - from models.account import Tenant, TenantAccountJoin, TenantAccountRole + from models.account import AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus tenant = Tenant( name="Test Tenant", - status="normal", + status=TenantStatus.NORMAL, ) db_session_with_containers.add(tenant) db_session_with_containers.commit() @@ -101,7 +101,7 @@ class TestPauseStatePersistenceLayerTestContainers: email="test@example.com", name="Test User", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.commit() diff --git a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py index 13caad799e..14d5740072 100644 --- a/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/test_containers_integration_tests/core/repositories/test_human_input_form_repository_impl.py @@ -4,7 +4,6 @@ from __future__ import annotations from uuid import uuid4 -from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserAction from sqlalchemy import Engine, select from sqlalchemy.orm import Session @@ -18,7 +17,15 @@ from core.workflow.human_input_compat import ( MemberRecipient, WebAppDeliveryMethod, ) -from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole +from graphon.nodes.human_input.entities import FormDefinition, HumanInputNodeData, UserAction +from models.account import ( + Account, + AccountStatus, + Tenant, + TenantAccountJoin, + TenantAccountRole, + TenantStatus, +) from models.human_input import ( EmailExternalRecipientPayload, EmailMemberRecipientPayload, @@ -29,7 +36,7 @@ from models.human_input import ( def _create_tenant_with_members(session: Session, member_emails: list[str]) -> tuple[Tenant, list[Account]]: - tenant = Tenant(name="Test Tenant", status="normal") + tenant = Tenant(name="Test Tenant", status=TenantStatus.NORMAL) session.add(tenant) session.flush() @@ -39,7 +46,7 @@ def _create_tenant_with_members(session: Session, member_emails: list[str]) -> t email=email, name=f"Member {index}", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) session.add(account) session.flush() diff --git a/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py b/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py index 0a9b476afc..da4f8847d6 100644 --- a/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py +++ b/api/tests/test_containers_integration_tests/core/workflow/test_human_input_resume_node_execution.py @@ -4,6 +4,17 @@ from datetime import timedelta from unittest.mock import MagicMock import pytest +from sqlalchemy import delete, select +from sqlalchemy.orm import Session + +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity +from core.app.workflow.layers import PersistenceWorkflowInfo, WorkflowPersistenceLayer +from core.repositories.human_input_repository import HumanInputFormEntity, HumanInputFormRepository +from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository +from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import build_system_variables from graphon.enums import WorkflowType from graphon.graph import Graph from graphon.graph_engine import GraphEngine @@ -16,20 +27,9 @@ from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState, VariablePool -from sqlalchemy import delete, select -from sqlalchemy.orm import Session - -from core.app.app_config.entities import WorkflowUIBasedAppConfig -from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity -from core.app.workflow.layers import PersistenceWorkflowInfo, WorkflowPersistenceLayer -from core.repositories.human_input_repository import HumanInputFormEntity, HumanInputFormRepository -from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository -from core.repositories.sqlalchemy_workflow_node_execution_repository import SQLAlchemyWorkflowNodeExecutionRepository -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import build_system_variables from libs.datetime_utils import naive_utc_now from models import Account -from models.account import Tenant, TenantAccountJoin, TenantAccountRole +from models.account import AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.model import App, AppMode, IconType from models.workflow import Workflow, WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom, WorkflowRun @@ -175,7 +175,7 @@ class TestHumanInputResumeNodeExecutionIntegration: def setup_test_data(self, db_session_with_containers: Session): tenant = Tenant( name="Test Tenant", - status="normal", + status=TenantStatus.NORMAL, ) db_session_with_containers.add(tenant) db_session_with_containers.commit() @@ -184,7 +184,7 @@ class TestHumanInputResumeNodeExecutionIntegration: email="test@example.com", name="Test User", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.commit() diff --git a/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py index cc72dc1cf3..2e207ddc67 100644 --- a/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py +++ b/api/tests/test_containers_integration_tests/factories/test_storage_key_loader.py @@ -4,13 +4,13 @@ from unittest.mock import patch from uuid import uuid4 import pytest -from graphon.file import File, FileTransferMethod, FileType from sqlalchemy.orm import Session from core.app.file_access import DatabaseFileAccessController from extensions.ext_database import db from extensions.storage.storage_type import StorageType from factories.file_factory import StorageKeyLoader +from graphon.file import File, FileTransferMethod, FileType from models import ToolFile, UploadFile from models.enums import CreatorUserRole diff --git a/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py b/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py index b745aed141..2fd289dfbc 100644 --- a/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py +++ b/api/tests/test_containers_integration_tests/helpers/execution_extra_content.py @@ -6,7 +6,6 @@ from decimal import Decimal from uuid import uuid4 from graphon.nodes.human_input.entities import FormDefinition, UserAction - from libs.datetime_utils import naive_utc_now from models.account import Account, Tenant, TenantAccountJoin from models.enums import ConversationFromSource, InvokeFrom diff --git a/api/tests/test_containers_integration_tests/models/test_conversation_message_inputs.py b/api/tests/test_containers_integration_tests/models/test_conversation_message_inputs.py index e922c19a5a..f10f519e25 100644 --- a/api/tests/test_containers_integration_tests/models/test_conversation_message_inputs.py +++ b/api/tests/test_containers_integration_tests/models/test_conversation_message_inputs.py @@ -10,10 +10,10 @@ from unittest.mock import patch from uuid import uuid4 import pytest -from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from sqlalchemy.orm import Session from core.workflow.file_reference import build_file_reference +from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from models.model import App, AppMode, Conversation, Message diff --git a/api/tests/test_containers_integration_tests/models/test_conversation_status_count.py b/api/tests/test_containers_integration_tests/models/test_conversation_status_count.py index 4ca87de52d..6352f815df 100644 --- a/api/tests/test_containers_integration_tests/models/test_conversation_status_count.py +++ b/api/tests/test_containers_integration_tests/models/test_conversation_status_count.py @@ -9,9 +9,9 @@ from collections.abc import Generator from uuid import uuid4 import pytest -from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import Session +from graphon.enums import WorkflowExecutionStatus from models.enums import ConversationFromSource, InvokeFrom from models.model import App, AppMode, Conversation, Message, Site from models.workflow import Workflow, WorkflowRun, WorkflowRunTriggeredFrom, WorkflowType diff --git a/api/tests/test_containers_integration_tests/models/test_types_enum_text.py b/api/tests/test_containers_integration_tests/models/test_types_enum_text.py index 957b7145d3..b325c97f7d 100644 --- a/api/tests/test_containers_integration_tests/models/test_types_enum_text.py +++ b/api/tests/test_containers_integration_tests/models/test_types_enum_text.py @@ -4,13 +4,13 @@ from typing import Any, NamedTuple import pytest import sqlalchemy as sa -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy import exc as sa_exc from sqlalchemy import insert, select from sqlalchemy.engine import Connection, Engine from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column from sqlalchemy.sql.sqltypes import VARCHAR +from graphon.model_runtime.entities.model_entities import ModelType from models.types import EnumText _USER_TABLE = "enum_text_users" diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py index a68b3a08c7..641399c7f9 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_node_execution_repository.py @@ -5,10 +5,10 @@ from __future__ import annotations from datetime import timedelta from uuid import uuid4 -from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy import Engine, delete from sqlalchemy.orm import Session, sessionmaker +from graphon.enums import WorkflowNodeExecutionStatus from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py index 64c93ac07c..aebe87839c 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_api_workflow_run_repository.py @@ -8,15 +8,15 @@ from unittest.mock import Mock from uuid import uuid4 import pytest +from sqlalchemy import Engine, delete, select +from sqlalchemy.orm import Session, sessionmaker + +from extensions.ext_storage import storage from graphon.entities import WorkflowExecution from graphon.entities.pause_reason import HumanInputRequired, PauseReasonType from graphon.enums import WorkflowExecutionStatus from graphon.nodes.human_input.entities import FormDefinition, FormInput, UserAction from graphon.nodes.human_input.enums import FormInputType, HumanInputFormStatus -from sqlalchemy import Engine, delete, select -from sqlalchemy.orm import Session, sessionmaker - -from extensions.ext_storage import storage from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.human_input import ( diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py index 7f44eb6ca3..aaf9a85d60 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_execution_extra_content_repository.py @@ -12,11 +12,11 @@ from decimal import Decimal from uuid import uuid4 import pytest -from graphon.nodes.human_input.entities import FormDefinition, UserAction -from graphon.nodes.human_input.enums import HumanInputFormStatus from sqlalchemy import Engine, delete, select from sqlalchemy.orm import Session, sessionmaker +from graphon.nodes.human_input.entities import FormDefinition, UserAction +from graphon.nodes.human_input.enums import HumanInputFormStatus from libs.datetime_utils import naive_utc_now from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.enums import ConversationFromSource, InvokeFrom diff --git a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_workflow_node_execution_repository.py b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_workflow_node_execution_repository.py index 22e0aa34ff..fa78f1c28b 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_workflow_node_execution_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_sqlalchemy_workflow_node_execution_repository.py @@ -7,6 +7,11 @@ from datetime import datetime from decimal import Decimal from uuid import uuid4 +from sqlalchemy import Engine +from sqlalchemy.orm import Session, sessionmaker + +from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository +from core.repositories.factory import OrderConfig from graphon.entities import WorkflowNodeExecution from graphon.enums import ( BuiltinNodeTypes, @@ -14,11 +19,6 @@ from graphon.enums import ( WorkflowNodeExecutionStatus, ) from graphon.model_runtime.utils.encoders import jsonable_encoder -from sqlalchemy import Engine -from sqlalchemy.orm import Session, sessionmaker - -from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository -from core.repositories.factory import OrderConfig from models.account import Account, Tenant from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/test_containers_integration_tests/repositories/test_workflow_run_repository.py b/api/tests/test_containers_integration_tests/repositories/test_workflow_run_repository.py index c5e9201ee3..d6f0657380 100644 --- a/api/tests/test_containers_integration_tests/repositories/test_workflow_run_repository.py +++ b/api/tests/test_containers_integration_tests/repositories/test_workflow_run_repository.py @@ -7,12 +7,12 @@ from datetime import timedelta from uuid import uuid4 import pytest -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import Engine, delete from sqlalchemy import exc as sa_exc from sqlalchemy.orm import Session, sessionmaker +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowRun, WorkflowType diff --git a/api/tests/test_containers_integration_tests/services/test_account_service.py b/api/tests/test_containers_integration_tests/services/test_account_service.py index cc9596d15f..9a53ff087c 100644 --- a/api/tests/test_containers_integration_tests/services/test_account_service.py +++ b/api/tests/test_containers_integration_tests/services/test_account_service.py @@ -9,7 +9,7 @@ from werkzeug.exceptions import Unauthorized from configs import dify_config from controllers.console.error import AccountNotFound, NotAllowedCreateWorkspace -from models import AccountStatus, TenantAccountJoin +from models import AccountStatus, TenantAccountJoin, TenantStatus from services.account_service import AccountService, RegisterService, TenantService, TokenPair from services.errors.account import ( AccountAlreadyInTenantError, @@ -2851,7 +2851,7 @@ class TestRegisterService: interface_language="en-US", password=existing_pending_member_password, ) - existing_account.status = "pending" + existing_account.status = AccountStatus.PENDING db_session_with_containers.commit() @@ -2941,7 +2941,7 @@ class TestRegisterService: interface_language="en-US", password=already_in_tenant_password, ) - existing_account.status = "active" + existing_account.status = AccountStatus.ACTIVE db_session_with_containers.commit() @@ -3331,7 +3331,7 @@ class TestRegisterService: TenantService.create_tenant_member(tenant, account, role="normal") # Change tenant status to non-normal - tenant.status = "archive" + tenant.status = TenantStatus.ARCHIVE db_session_with_containers.commit() diff --git a/api/tests/test_containers_integration_tests/services/test_agent_service.py b/api/tests/test_containers_integration_tests/services/test_agent_service.py index 4f3c0e4200..00a2f9a59f 100644 --- a/api/tests/test_containers_integration_tests/services/test_agent_service.py +++ b/api/tests/test_containers_integration_tests/services/test_agent_service.py @@ -842,7 +842,6 @@ class TestAgentService: conversation, message = self._create_test_conversation_and_message(db_session_with_containers, app, account) from graphon.file import FileTransferMethod, FileType - from models.enums import CreatorUserRole # Add files to message diff --git a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py index 6c15587058..77ce28b999 100644 --- a/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py +++ b/api/tests/test_containers_integration_tests/services/test_app_dsl_service.py @@ -9,7 +9,6 @@ from uuid import uuid4 import pytest import yaml from faker import Faker -from graphon.enums import BuiltinNodeTypes from core.trigger.constants import ( TRIGGER_PLUGIN_NODE_TYPE, @@ -17,6 +16,7 @@ from core.trigger.constants import ( TRIGGER_WEBHOOK_NODE_TYPE, ) from extensions.ext_redis import redis_client +from graphon.enums import BuiltinNodeTypes from models import Account, AppMode from models.model import AppModelConfig, IconType from services import app_dsl_service diff --git a/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py b/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py new file mode 100644 index 0000000000..0b7bd9ca64 --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_conversation_service_variables.py @@ -0,0 +1,524 @@ +from __future__ import annotations + +from datetime import datetime, timedelta +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from sqlalchemy.orm import sessionmaker + +from core.app.entities.app_invoke_entities import InvokeFrom +from extensions.ext_database import db +from graphon.variables import FloatVariable, IntegerVariable, StringVariable +from models.account import Account, Tenant, TenantAccountJoin +from models.enums import ConversationFromSource +from models.model import App, Conversation, EndUser +from models.workflow import ConversationVariable +from services.conversation_service import ConversationService +from services.errors.conversation import ( + ConversationVariableNotExistsError, + ConversationVariableTypeMismatchError, + LastConversationNotExistsError, +) + + +class ConversationServiceVariableIntegrationFactory: + @staticmethod + def create_app_and_account(db_session_with_containers): + tenant = Tenant(name=f"Tenant {uuid4()}") + db_session_with_containers.add(tenant) + db_session_with_containers.flush() + + account = Account( + name=f"Account {uuid4()}", + email=f"conversation-variable-{uuid4()}@example.com", + password="hashed-password", + password_salt="salt", + interface_language="en-US", + timezone="UTC", + ) + db_session_with_containers.add(account) + db_session_with_containers.flush() + + tenant_join = TenantAccountJoin( + tenant_id=tenant.id, + account_id=account.id, + role="owner", + current=True, + ) + db_session_with_containers.add(tenant_join) + db_session_with_containers.flush() + + app = App( + tenant_id=tenant.id, + name=f"App {uuid4()}", + description="", + mode="chat", + icon_type="emoji", + icon="bot", + icon_background="#FFFFFF", + enable_site=False, + enable_api=True, + api_rpm=100, + api_rph=100, + is_demo=False, + is_public=False, + is_universal=False, + created_by=account.id, + updated_by=account.id, + ) + db_session_with_containers.add(app) + db_session_with_containers.commit() + + return app, account + + @staticmethod + def create_end_user(db_session_with_containers, app: App): + end_user = EndUser( + tenant_id=app.tenant_id, + app_id=app.id, + type=InvokeFrom.SERVICE_API.value, + external_user_id=f"external-{uuid4()}", + name=f"End User {uuid4()}", + is_anonymous=False, + session_id=f"session-{uuid4()}", + ) + db_session_with_containers.add(end_user) + db_session_with_containers.commit() + return end_user + + @staticmethod + def create_conversation( + db_session_with_containers, + app: App, + user: Account | EndUser, + *, + name: str | None = None, + invoke_from: InvokeFrom = InvokeFrom.WEB_APP, + created_at: datetime | None = None, + updated_at: datetime | None = None, + ) -> Conversation: + conversation = Conversation( + app_id=app.id, + app_model_config_id=None, + model_provider=None, + model_id="", + override_model_configs=None, + mode=app.mode, + name=name or f"Conversation {uuid4()}", + summary="", + inputs={}, + introduction="", + system_instruction="", + system_instruction_tokens=0, + status="normal", + invoke_from=invoke_from.value, + from_source=ConversationFromSource.API if isinstance(user, EndUser) else ConversationFromSource.CONSOLE, + from_end_user_id=user.id if isinstance(user, EndUser) else None, + from_account_id=user.id if isinstance(user, Account) else None, + dialogue_count=0, + is_deleted=False, + ) + conversation.inputs = {} + if created_at is not None: + conversation.created_at = created_at + if updated_at is not None: + conversation.updated_at = updated_at + + db_session_with_containers.add(conversation) + db_session_with_containers.commit() + return conversation + + @staticmethod + def create_variable( + db_session_with_containers, + *, + app: App, + conversation: Conversation, + variable: StringVariable | FloatVariable | IntegerVariable, + created_at: datetime | None = None, + ) -> ConversationVariable: + row = ConversationVariable.from_variable(app_id=app.id, conversation_id=conversation.id, variable=variable) + if created_at is not None: + row.created_at = created_at + row.updated_at = created_at + + db_session_with_containers.add(row) + db_session_with_containers.commit() + return row + + +@pytest.fixture +def real_conversation_service_session_factory(flask_app_with_containers): + del flask_app_with_containers + real_session_maker = sessionmaker(bind=db.engine, expire_on_commit=False) + + with ( + patch("services.conversation_service.session_factory.create_session", side_effect=lambda: real_session_maker()), + patch("services.conversation_service.session_factory.get_session_maker", return_value=real_session_maker), + ): + yield + + +class TestConversationServiceVariables: + def test_get_conversational_variable_success( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + older_time = datetime(2024, 1, 1, 12, 0, 0) + newer_time = older_time + timedelta(minutes=5) + + first_variable = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="topic", value="billing"), + created_at=older_time, + ) + second_variable = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="priority", value="high"), + created_at=newer_time, + ) + + result = ConversationService.get_conversational_variable( + app_model=app, + conversation_id=conversation.id, + user=account, + limit=10, + last_id=None, + ) + + assert [item["id"] for item in result.data] == [first_variable.id, second_variable.id] + assert [item["name"] for item in result.data] == ["topic", "priority"] + assert result.limit == 10 + assert result.has_more is False + + def test_get_conversational_variable_with_last_id( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + base_time = datetime(2024, 1, 1, 9, 0, 0) + + first_variable = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="topic", value="billing"), + created_at=base_time, + ) + second_variable = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="priority", value="high"), + created_at=base_time + timedelta(minutes=1), + ) + third_variable = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="owner", value="alice"), + created_at=base_time + timedelta(minutes=2), + ) + + result = ConversationService.get_conversational_variable( + app_model=app, + conversation_id=conversation.id, + user=account, + limit=10, + last_id=first_variable.id, + ) + + assert [item["id"] for item in result.data] == [second_variable.id, third_variable.id] + assert result.has_more is False + + def test_get_conversational_variable_last_id_not_found_raises_error( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + + with pytest.raises(ConversationVariableNotExistsError): + ConversationService.get_conversational_variable( + app_model=app, + conversation_id=conversation.id, + user=account, + limit=10, + last_id=str(uuid4()), + ) + + def test_get_conversational_variable_sets_has_more( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + + for index in range(3): + factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name=f"var_{index}", value=f"value_{index}"), + created_at=datetime(2024, 1, 1, 10, 0, index), + ) + + result = ConversationService.get_conversational_variable( + app_model=app, + conversation_id=conversation.id, + user=account, + limit=2, + last_id=None, + ) + + assert len(result.data) == 2 + assert result.has_more is True + + def test_update_conversation_variable_success( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + existing = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=StringVariable(id=str(uuid4()), name="topic", value="billing"), + ) + updated_at = datetime(2024, 1, 1, 15, 0, 0) + + with patch("services.conversation_service.naive_utc_now", return_value=updated_at): + result = ConversationService.update_conversation_variable( + app_model=app, + conversation_id=conversation.id, + variable_id=existing.id, + user=account, + new_value="support", + ) + + db_session_with_containers.expire_all() + persisted = db_session_with_containers.get(ConversationVariable, (existing.id, conversation.id)) + + assert persisted is not None + assert persisted.to_variable().value == "support" + assert result["id"] == existing.id + assert result["value"] == "support" + assert result["updated_at"] == updated_at + + def test_update_conversation_variable_not_found_raises_error( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + + with pytest.raises(ConversationVariableNotExistsError): + ConversationService.update_conversation_variable( + app_model=app, + conversation_id=conversation.id, + variable_id=str(uuid4()), + user=account, + new_value="support", + ) + + def test_update_conversation_variable_type_mismatch_raises_error( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + existing = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=FloatVariable(id=str(uuid4()), name="score", value=1.5), + ) + + with pytest.raises(ConversationVariableTypeMismatchError, match="expects float"): + ConversationService.update_conversation_variable( + app_model=app, + conversation_id=conversation.id, + variable_id=existing.id, + user=account, + new_value="wrong-type", + ) + + def test_update_conversation_variable_integer_number_compatibility( + self, db_session_with_containers, real_conversation_service_session_factory + ): + del real_conversation_service_session_factory + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + conversation = factory.create_conversation(db_session_with_containers, app, account) + existing = factory.create_variable( + db_session_with_containers, + app=app, + conversation=conversation, + variable=IntegerVariable(id=str(uuid4()), name="attempts", value=1), + ) + + result = ConversationService.update_conversation_variable( + app_model=app, + conversation_id=conversation.id, + variable_id=existing.id, + user=account, + new_value=42, + ) + + db_session_with_containers.expire_all() + persisted = db_session_with_containers.get(ConversationVariable, (existing.id, conversation.id)) + + assert persisted is not None + assert persisted.to_variable().value == 42 + assert result["value"] == 42 + + +class TestConversationServicePaginationWithContainers: + def test_pagination_by_last_id_raises_error_when_last_id_missing(self, db_session_with_containers): + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + + with pytest.raises(LastConversationNotExistsError): + ConversationService.pagination_by_last_id( + session=db_session_with_containers, + app_model=app, + user=account, + last_id=str(uuid4()), + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + def test_pagination_by_last_id_with_default_desc_updated_at(self, db_session_with_containers): + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + base_time = datetime(2024, 1, 1, 8, 0, 0) + newest = factory.create_conversation( + db_session_with_containers, + app, + account, + name="Newest", + updated_at=base_time + timedelta(minutes=2), + ) + middle = factory.create_conversation( + db_session_with_containers, + app, + account, + name="Middle", + updated_at=base_time + timedelta(minutes=1), + ) + oldest = factory.create_conversation( + db_session_with_containers, + app, + account, + name="Oldest", + updated_at=base_time, + ) + + result = ConversationService.pagination_by_last_id( + session=db_session_with_containers, + app_model=app, + user=account, + last_id=middle.id, + limit=10, + invoke_from=InvokeFrom.WEB_APP, + ) + + assert newest.id != middle.id + assert [conversation.id for conversation in result.data] == [oldest.id] + + def test_pagination_by_last_id_with_name_sort(self, db_session_with_containers): + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + alpha = factory.create_conversation(db_session_with_containers, app, account, name="Alpha") + beta = factory.create_conversation(db_session_with_containers, app, account, name="Beta") + gamma = factory.create_conversation(db_session_with_containers, app, account, name="Gamma") + + result = ConversationService.pagination_by_last_id( + session=db_session_with_containers, + app_model=app, + user=account, + last_id=beta.id, + limit=10, + invoke_from=InvokeFrom.WEB_APP, + sort_by="name", + ) + + assert alpha.id != beta.id + assert [conversation.id for conversation in result.data] == [gamma.id] + + def test_pagination_filters_to_end_user_api_source(self, db_session_with_containers): + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + end_user = factory.create_end_user(db_session_with_containers, app) + account_conversation = factory.create_conversation( + db_session_with_containers, + app, + account, + name="Console Conversation", + invoke_from=InvokeFrom.WEB_APP, + ) + end_user_conversation = factory.create_conversation( + db_session_with_containers, + app, + end_user, + name="API Conversation", + invoke_from=InvokeFrom.SERVICE_API, + ) + + result = ConversationService.pagination_by_last_id( + session=db_session_with_containers, + app_model=app, + user=end_user, + last_id=None, + limit=20, + invoke_from=InvokeFrom.SERVICE_API, + ) + + assert account_conversation.id != end_user_conversation.id + assert [conversation.id for conversation in result.data] == [end_user_conversation.id] + + def test_pagination_filters_to_account_console_source(self, db_session_with_containers): + factory = ConversationServiceVariableIntegrationFactory + app, account = factory.create_app_and_account(db_session_with_containers) + end_user = factory.create_end_user(db_session_with_containers, app) + account_conversation = factory.create_conversation( + db_session_with_containers, + app, + account, + name="Console Conversation", + invoke_from=InvokeFrom.WEB_APP, + ) + factory.create_conversation( + db_session_with_containers, + app, + end_user, + name="API Conversation", + invoke_from=InvokeFrom.SERVICE_API, + ) + + result = ConversationService.pagination_by_last_id( + session=db_session_with_containers, + app_model=app, + user=account, + last_id=None, + limit=20, + invoke_from=InvokeFrom.WEB_APP, + ) + + assert [conversation.id for conversation in result.data] == [account_conversation.id] diff --git a/api/tests/test_containers_integration_tests/services/test_conversation_variable_updater.py b/api/tests/test_containers_integration_tests/services/test_conversation_variable_updater.py index fb0adbbcc2..02ab3f8314 100644 --- a/api/tests/test_containers_integration_tests/services/test_conversation_variable_updater.py +++ b/api/tests/test_containers_integration_tests/services/test_conversation_variable_updater.py @@ -3,10 +3,10 @@ from uuid import uuid4 import pytest -from graphon.variables import StringVariable from sqlalchemy.orm import sessionmaker from extensions.ext_database import db +from graphon.variables import StringVariable from models.workflow import ConversationVariable from services.conversation_variable_updater import ConversationVariableNotFoundError, ConversationVariableUpdater diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service.py b/api/tests/test_containers_integration_tests/services/test_dataset_service.py index f9bfa570cb..0de3c64c4f 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service.py @@ -9,11 +9,11 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.retrieval.retrieval_methods import RetrievalMethod +from graphon.model_runtime.entities.model_entities import ModelType from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.dataset import Dataset, DatasetPermissionEnum, Document, ExternalKnowledgeBindings, Pipeline from models.enums import DatasetRuntimeMode, DataSourceType, DocumentCreatedFrom, IndexingStatus diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service_document.py b/api/tests/test_containers_integration_tests/services/test_dataset_service_document.py new file mode 100644 index 0000000000..2bec703f0c --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service_document.py @@ -0,0 +1,650 @@ +"""Testcontainers integration tests for SQL-backed DocumentService paths.""" + +import datetime +import json +from unittest.mock import create_autospec, patch +from uuid import uuid4 + +import pytest +from werkzeug.exceptions import Forbidden, NotFound + +from core.rag.index_processor.constant.index_type import IndexStructureType +from extensions.storage.storage_type import StorageType +from models import Account +from models.dataset import Dataset, Document +from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, IndexingStatus +from models.model import UploadFile +from services.dataset_service import DocumentService +from services.errors.account import NoPermissionError + +FIXED_UPLOAD_CREATED_AT = datetime.datetime(2024, 1, 1, 0, 0, 0) + + +class DocumentServiceIntegrationFactory: + @staticmethod + def create_dataset( + db_session_with_containers, + *, + tenant_id: str | None = None, + created_by: str | None = None, + name: str | None = None, + ) -> Dataset: + dataset = Dataset( + tenant_id=tenant_id or str(uuid4()), + name=name or f"dataset-{uuid4()}", + data_source_type=DataSourceType.UPLOAD_FILE, + created_by=created_by or str(uuid4()), + ) + db_session_with_containers.add(dataset) + db_session_with_containers.commit() + return dataset + + @staticmethod + def create_document( + db_session_with_containers, + *, + dataset: Dataset, + name: str = "doc.txt", + position: int = 1, + tenant_id: str | None = None, + indexing_status: str = IndexingStatus.COMPLETED, + enabled: bool = True, + archived: bool = False, + is_paused: bool = False, + need_summary: bool = False, + doc_form: str = IndexStructureType.PARAGRAPH_INDEX, + batch: str | None = None, + data_source_type: str = DataSourceType.UPLOAD_FILE, + data_source_info: dict | None = None, + created_by: str | None = None, + ) -> Document: + document = Document( + tenant_id=tenant_id or dataset.tenant_id, + dataset_id=dataset.id, + position=position, + data_source_type=data_source_type, + data_source_info=json.dumps(data_source_info or {}), + batch=batch or f"batch-{uuid4()}", + name=name, + created_from=DocumentCreatedFrom.WEB, + created_by=created_by or dataset.created_by, + doc_form=doc_form, + ) + document.indexing_status = indexing_status + document.enabled = enabled + document.archived = archived + document.is_paused = is_paused + document.need_summary = need_summary + if indexing_status == IndexingStatus.COMPLETED: + document.completed_at = FIXED_UPLOAD_CREATED_AT + db_session_with_containers.add(document) + db_session_with_containers.commit() + return document + + @staticmethod + def create_upload_file( + db_session_with_containers, + *, + tenant_id: str, + created_by: str, + file_id: str | None = None, + name: str = "source.txt", + ) -> UploadFile: + upload_file = UploadFile( + tenant_id=tenant_id, + storage_type=StorageType.LOCAL, + key=f"uploads/{uuid4()}", + name=name, + size=128, + extension="txt", + mime_type="text/plain", + created_by_role=CreatorUserRole.ACCOUNT, + created_by=created_by, + created_at=FIXED_UPLOAD_CREATED_AT, + used=False, + ) + if file_id: + upload_file.id = file_id + db_session_with_containers.add(upload_file) + db_session_with_containers.commit() + return upload_file + + +@pytest.fixture +def current_user_mock(): + with patch("services.dataset_service.current_user", create_autospec(Account, instance=True)) as current_user: + current_user.id = str(uuid4()) + current_user.current_tenant_id = str(uuid4()) + current_user.current_role = None + yield current_user + + +def test_get_document_returns_none_when_document_id_is_missing(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + assert DocumentService.get_document(dataset.id, None) is None + + +def test_get_document_queries_by_dataset_and_document_id(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document(db_session_with_containers, dataset=dataset) + + result = DocumentService.get_document(dataset.id, document.id) + + assert result is not None + assert result.id == document.id + + +def test_get_documents_by_ids_returns_empty_for_empty_input(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + result = DocumentService.get_documents_by_ids(dataset.id, []) + + assert result == [] + + +def test_get_documents_by_ids_uses_single_batch_query(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + doc_a = DocumentServiceIntegrationFactory.create_document(db_session_with_containers, dataset=dataset, name="a.txt") + doc_b = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + name="b.txt", + position=2, + ) + + result = DocumentService.get_documents_by_ids(dataset.id, [doc_a.id, doc_b.id]) + + assert {document.id for document in result} == {doc_a.id, doc_b.id} + + +def test_update_documents_need_summary_returns_zero_for_empty_input(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + assert DocumentService.update_documents_need_summary(dataset.id, []) == 0 + + +def test_update_documents_need_summary_updates_matching_non_qa_documents(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + paragraph_doc = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + need_summary=True, + ) + qa_doc = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + need_summary=True, + doc_form=IndexStructureType.QA_INDEX, + ) + + updated_count = DocumentService.update_documents_need_summary( + dataset.id, + [paragraph_doc.id, qa_doc.id], + need_summary=False, + ) + + db_session_with_containers.expire_all() + refreshed_paragraph = db_session_with_containers.get(Document, paragraph_doc.id) + refreshed_qa = db_session_with_containers.get(Document, qa_doc.id) + assert updated_count == 1 + assert refreshed_paragraph is not None + assert refreshed_qa is not None + assert refreshed_paragraph.need_summary is False + assert refreshed_qa.need_summary is True + + +def test_get_document_download_url_uses_signed_url_helper(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + ) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file.id}, + ) + + with patch("services.dataset_service.file_helpers.get_signed_file_url", return_value="signed-url") as get_url: + result = DocumentService.get_document_download_url(document) + + assert result == "signed-url" + get_url.assert_called_once_with(upload_file_id=upload_file.id, as_attachment=True) + + +def test_get_upload_file_id_for_upload_file_document_rejects_invalid_source_type(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_type=DataSourceType.WEBSITE_CRAWL, + data_source_info={"url": "https://example.com"}, + ) + + with pytest.raises(NotFound, match="invalid source"): + DocumentService._get_upload_file_id_for_upload_file_document( + document, + invalid_source_message="invalid source", + missing_file_message="missing file", + ) + + +def test_get_upload_file_id_for_upload_file_document_rejects_missing_upload_file_id(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={}, + ) + + with pytest.raises(NotFound, match="missing file"): + DocumentService._get_upload_file_id_for_upload_file_document( + document, + invalid_source_message="invalid source", + missing_file_message="missing file", + ) + + +def test_get_upload_file_id_for_upload_file_document_returns_string_id(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": 99}, + ) + + result = DocumentService._get_upload_file_id_for_upload_file_document( + document, + invalid_source_message="invalid source", + missing_file_message="missing file", + ) + + assert result == "99" + + +def test_get_upload_file_for_upload_file_document_raises_when_file_service_returns_nothing(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": "missing-file"}, + ) + + with patch("services.dataset_service.FileService.get_upload_files_by_ids", return_value={}): + with pytest.raises(NotFound, match="Uploaded file not found"): + DocumentService._get_upload_file_for_upload_file_document(document) + + +def test_get_upload_file_for_upload_file_document_returns_upload_file(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + ) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file.id}, + ) + + result = DocumentService._get_upload_file_for_upload_file_document(document) + + assert result.id == upload_file.id + + +def test_get_upload_files_by_document_id_for_zip_download_raises_for_missing_documents(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + with pytest.raises(NotFound, match="Document not found"): + DocumentService._get_upload_files_by_document_id_for_zip_download( + dataset_id=dataset.id, + document_ids=[str(uuid4())], + tenant_id=dataset.tenant_id, + ) + + +def test_get_upload_files_by_document_id_for_zip_download_rejects_cross_tenant_access(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + ) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + tenant_id=str(uuid4()), + data_source_info={"upload_file_id": upload_file.id}, + ) + + with pytest.raises(Forbidden, match="No permission"): + DocumentService._get_upload_files_by_document_id_for_zip_download( + dataset_id=dataset.id, + document_ids=[document.id], + tenant_id=dataset.tenant_id, + ) + + +def test_get_upload_files_by_document_id_for_zip_download_rejects_missing_upload_files(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": str(uuid4())}, + ) + + with pytest.raises(NotFound, match="Only uploaded-file documents can be downloaded as ZIP"): + DocumentService._get_upload_files_by_document_id_for_zip_download( + dataset_id=dataset.id, + document_ids=[document.id], + tenant_id=dataset.tenant_id, + ) + + +def test_get_upload_files_by_document_id_for_zip_download_returns_document_keyed_mapping(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file_a = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="a.txt", + ) + upload_file_b = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="b.txt", + ) + document_a = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file_a.id}, + ) + document_b = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + data_source_info={"upload_file_id": upload_file_b.id}, + ) + + mapping = DocumentService._get_upload_files_by_document_id_for_zip_download( + dataset_id=dataset.id, + document_ids=[document_a.id, document_b.id], + tenant_id=dataset.tenant_id, + ) + + assert mapping[document_a.id].id == upload_file_a.id + assert mapping[document_b.id].id == upload_file_b.id + + +def test_prepare_document_batch_download_zip_raises_not_found_for_missing_dataset( + current_user_mock, flask_app_with_containers +): + with flask_app_with_containers.app_context(): + with pytest.raises(NotFound, match="Dataset not found"): + DocumentService.prepare_document_batch_download_zip( + dataset_id=str(uuid4()), + document_ids=[str(uuid4())], + tenant_id=current_user_mock.current_tenant_id, + current_user=current_user_mock, + ) + + +def test_prepare_document_batch_download_zip_translates_permission_error_to_forbidden( + db_session_with_containers, + current_user_mock, +): + dataset = DocumentServiceIntegrationFactory.create_dataset( + db_session_with_containers, + tenant_id=current_user_mock.current_tenant_id, + created_by=current_user_mock.id, + ) + + with patch( + "services.dataset_service.DatasetService.check_dataset_permission", + side_effect=NoPermissionError("denied"), + ): + with pytest.raises(Forbidden, match="denied"): + DocumentService.prepare_document_batch_download_zip( + dataset_id=dataset.id, + document_ids=[], + tenant_id=current_user_mock.current_tenant_id, + current_user=current_user_mock, + ) + + +def test_prepare_document_batch_download_zip_returns_upload_files_in_requested_order( + db_session_with_containers, + current_user_mock, +): + dataset = DocumentServiceIntegrationFactory.create_dataset( + db_session_with_containers, + tenant_id=current_user_mock.current_tenant_id, + created_by=current_user_mock.id, + ) + upload_file_a = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="a.txt", + ) + upload_file_b = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="b.txt", + ) + document_a = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file_a.id}, + ) + document_b = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + data_source_info={"upload_file_id": upload_file_b.id}, + ) + + upload_files, download_name = DocumentService.prepare_document_batch_download_zip( + dataset_id=dataset.id, + document_ids=[document_b.id, document_a.id], + tenant_id=current_user_mock.current_tenant_id, + current_user=current_user_mock, + ) + + assert [upload_file.id for upload_file in upload_files] == [upload_file_b.id, upload_file_a.id] + assert download_name.endswith(".zip") + + +def test_get_document_by_dataset_id_returns_enabled_documents(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + enabled_document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + enabled=True, + ) + DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + enabled=False, + ) + + result = DocumentService.get_document_by_dataset_id(dataset.id) + + assert [document.id for document in result] == [enabled_document.id] + + +def test_get_working_documents_by_dataset_id_returns_completed_enabled_unarchived_documents(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + available_document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + indexing_status=IndexingStatus.COMPLETED, + enabled=True, + archived=False, + ) + DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + indexing_status=IndexingStatus.ERROR, + ) + + result = DocumentService.get_working_documents_by_dataset_id(dataset.id) + + assert [document.id for document in result] == [available_document.id] + + +def test_get_error_documents_by_dataset_id_returns_error_and_paused_documents(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + error_document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + indexing_status=IndexingStatus.ERROR, + ) + paused_document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + indexing_status=IndexingStatus.PAUSED, + ) + DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=3, + indexing_status=IndexingStatus.COMPLETED, + ) + + result = DocumentService.get_error_documents_by_dataset_id(dataset.id) + + assert {document.id for document in result} == {error_document.id, paused_document.id} + + +def test_get_batch_documents_filters_by_current_user_tenant(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + batch = f"batch-{uuid4()}" + matching_document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + batch=batch, + ) + DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + tenant_id=str(uuid4()), + batch=batch, + ) + + with patch("services.dataset_service.current_user", create_autospec(Account, instance=True)) as current_user: + current_user.current_tenant_id = dataset.tenant_id + result = DocumentService.get_batch_documents(dataset.id, batch) + + assert [document.id for document in result] == [matching_document.id] + + +def test_get_document_file_detail_returns_upload_file(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + ) + + result = DocumentService.get_document_file_detail(upload_file.id) + + assert result is not None + assert result.id == upload_file.id + + +def test_delete_document_emits_signal_and_commits(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + upload_file = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + ) + document = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file.id}, + ) + + with patch("services.dataset_service.document_was_deleted.send") as signal_send: + DocumentService.delete_document(document) + + assert db_session_with_containers.get(Document, document.id) is None + signal_send.assert_called_once_with( + document.id, + dataset_id=document.dataset_id, + doc_form=document.doc_form, + file_id=upload_file.id, + ) + + +def test_delete_documents_ignores_empty_input(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + with patch("services.dataset_service.batch_clean_document_task.delay") as delay: + DocumentService.delete_documents(dataset, []) + + delay.assert_not_called() + + +def test_delete_documents_deletes_rows_and_dispatches_cleanup_task(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + dataset.chunk_structure = IndexStructureType.PARAGRAPH_INDEX + db_session_with_containers.commit() + upload_file_a = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="a.txt", + ) + upload_file_b = DocumentServiceIntegrationFactory.create_upload_file( + db_session_with_containers, + tenant_id=dataset.tenant_id, + created_by=dataset.created_by, + name="b.txt", + ) + document_a = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + data_source_info={"upload_file_id": upload_file_a.id}, + ) + document_b = DocumentServiceIntegrationFactory.create_document( + db_session_with_containers, + dataset=dataset, + position=2, + data_source_info={"upload_file_id": upload_file_b.id}, + ) + + with patch("services.dataset_service.batch_clean_document_task.delay") as delay: + DocumentService.delete_documents(dataset, [document_a.id, document_b.id]) + + assert db_session_with_containers.get(Document, document_a.id) is None + assert db_session_with_containers.get(Document, document_b.id) is None + delay.assert_called_once() + args = delay.call_args.args + assert args[0] == [document_a.id, document_b.id] + assert args[1] == dataset.id + assert set(args[3]) == {upload_file_a.id, upload_file_b.id} + + +def test_get_documents_position_returns_next_position_when_documents_exist(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + DocumentServiceIntegrationFactory.create_document(db_session_with_containers, dataset=dataset, position=3) + + assert DocumentService.get_documents_position(dataset.id) == 4 + + +def test_get_documents_position_defaults_to_one_when_dataset_is_empty(db_session_with_containers): + dataset = DocumentServiceIntegrationFactory.create_dataset(db_session_with_containers) + + assert DocumentService.get_documents_position(dataset.id) == 1 diff --git a/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py b/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py index 2a2d86a8a6..ac0483a45d 100644 --- a/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py +++ b/api/tests/test_containers_integration_tests/services/test_dataset_service_update_dataset.py @@ -3,11 +3,18 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexTechniqueType -from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole +from graphon.model_runtime.entities.model_entities import ModelType +from models.account import ( + Account, + AccountStatus, + Tenant, + TenantAccountJoin, + TenantAccountRole, + TenantStatus, +) from models.dataset import Dataset, ExternalKnowledgeApis, ExternalKnowledgeBindings from models.enums import DataSourceType from services.dataset_service import DatasetService @@ -26,12 +33,12 @@ class DatasetUpdateTestDataFactory: email=f"{uuid4()}@example.com", name=f"user-{uuid4()}", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.commit() - tenant = Tenant(name=f"tenant-{account.id}", status="normal") + tenant = Tenant(name=f"tenant-{account.id}", status=TenantStatus.NORMAL) db_session_with_containers.add(tenant) db_session_with_containers.commit() diff --git a/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py b/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py index c8f04e9215..fe426ae516 100644 --- a/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py +++ b/api/tests/test_containers_integration_tests/services/test_delete_archived_workflow_run.py @@ -5,9 +5,9 @@ Testcontainers integration tests for archived workflow run deletion service. from datetime import UTC, datetime, timedelta from uuid import uuid4 -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import select +from graphon.enums import WorkflowExecutionStatus from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowArchiveLog, WorkflowRun from services.retention.workflow_run.delete_archived_workflow_run import ArchivedWorkflowRunDeletion diff --git a/api/tests/test_containers_integration_tests/services/test_feature_service.py b/api/tests/test_containers_integration_tests/services/test_feature_service.py index b3e7dd2a59..315936d721 100644 --- a/api/tests/test_containers_integration_tests/services/test_feature_service.py +++ b/api/tests/test_containers_integration_tests/services/test_feature_service.py @@ -274,6 +274,7 @@ class TestFeatureService: mock_config.ENABLE_EMAIL_CODE_LOGIN = True mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False + mock_config.ENABLE_COLLABORATION_MODE = True mock_config.ALLOW_REGISTER = False mock_config.ALLOW_CREATE_WORKSPACE = False mock_config.MAIL_TYPE = "smtp" @@ -298,6 +299,7 @@ class TestFeatureService: # Verify authentication settings assert result.enable_email_code_login is True assert result.enable_email_password_login is False + assert result.enable_collaboration_mode is True assert result.is_allow_register is False assert result.is_allow_create_workspace is False @@ -401,6 +403,7 @@ class TestFeatureService: mock_config.ENABLE_EMAIL_CODE_LOGIN = True mock_config.ENABLE_EMAIL_PASSWORD_LOGIN = True mock_config.ENABLE_SOCIAL_OAUTH_LOGIN = False + mock_config.ENABLE_COLLABORATION_MODE = False mock_config.ALLOW_REGISTER = True mock_config.ALLOW_CREATE_WORKSPACE = True mock_config.MAIL_TYPE = "smtp" @@ -422,6 +425,7 @@ class TestFeatureService: assert result.enable_email_code_login is True assert result.enable_email_password_login is True assert result.enable_social_oauth_login is False + assert result.enable_collaboration_mode is False assert result.is_allow_register is True assert result.is_allow_create_workspace is True assert result.is_email_setup is True diff --git a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py index c46b8fba0b..18c5320d0a 100644 --- a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py +++ b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test.py @@ -3,8 +3,6 @@ import uuid from unittest.mock import MagicMock import pytest -from graphon.enums import BuiltinNodeTypes -from graphon.nodes.human_input.entities import HumanInputNodeData from core.workflow.human_input_compat import ( EmailDeliveryConfig, @@ -12,6 +10,8 @@ from core.workflow.human_input_compat import ( EmailRecipients, ExternalRecipient, ) +from graphon.enums import BuiltinNodeTypes +from graphon.nodes.human_input.entities import HumanInputNodeData from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.model import App, AppMode from models.workflow import Workflow, WorkflowType diff --git a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test_service.py b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test_service.py index 0f252515f7..21a54e909e 100644 --- a/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test_service.py +++ b/api/tests/test_containers_integration_tests/services/test_human_input_delivery_test_service.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from graphon.runtime import VariablePool from sqlalchemy.engine import Engine from configs import dify_config @@ -16,6 +15,7 @@ from core.workflow.human_input_compat import ( ExternalRecipient, MemberRecipient, ) +from graphon.runtime import VariablePool from models.account import Account, TenantAccountJoin from services import human_input_delivery_test_service as service_module from services.human_input_delivery_test_service import ( diff --git a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py index 2340dd2a03..cd63d3ad6c 100644 --- a/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py +++ b/api/tests/test_containers_integration_tests/services/test_messages_clean_service.py @@ -8,11 +8,11 @@ from unittest.mock import MagicMock, patch import pytest from faker import Faker -from graphon.file import FileType from sqlalchemy.orm import Session from enums.cloud_plan import CloudPlan from extensions.ext_redis import redis_client +from graphon.file import FileType from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.enums import ( ConversationFromSource, diff --git a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py index ba926bf675..8955a3b5f2 100644 --- a/api/tests/test_containers_integration_tests/services/test_model_provider_service.py +++ b/api/tests/test_containers_integration_tests/services/test_model_provider_service.py @@ -2,10 +2,10 @@ from unittest.mock import MagicMock, patch import pytest from faker import Faker -from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType from sqlalchemy.orm import Session from core.entities.model_entities import ModelStatus +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType from models import Account, Tenant, TenantAccountJoin, TenantAccountRole from models.provider import Provider, ProviderModel, ProviderModelSetting, ProviderType from services.model_provider_service import ModelProviderService @@ -405,11 +405,10 @@ class TestModelProviderService: mock_provider_manager = mock_external_service_dependencies["provider_manager"].return_value # Create mock models + from core.entities.model_entities import ModelWithProviderEntity, SimpleModelProviderEntity from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.provider_entities import ProviderEntity - from core.entities.model_entities import ModelWithProviderEntity, SimpleModelProviderEntity - # Create real model objects instead of mocks provider_entity_1 = SimpleModelProviderEntity( ProviderEntity( @@ -644,9 +643,8 @@ class TestModelProviderService: mock_provider_manager = mock_external_service_dependencies["provider_manager"].return_value # Create mock default model response - from graphon.model_runtime.entities.common_entities import I18nObject - from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity + from graphon.model_runtime.entities.common_entities import I18nObject mock_default_model = DefaultModelEntity( model="gpt-3.5-turbo", diff --git a/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py new file mode 100644 index 0000000000..ec10c51e04 --- /dev/null +++ b/api/tests/test_containers_integration_tests/services/test_webhook_service_relationships.py @@ -0,0 +1,507 @@ +from __future__ import annotations + +import json +from types import SimpleNamespace +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +import pytest +from sqlalchemy import select +from sqlalchemy.orm import Session + +from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE +from models.account import Account, Tenant, TenantAccountJoin, TenantAccountRole +from models.enums import AppTriggerStatus, AppTriggerType +from models.model import App +from models.trigger import AppTrigger, WorkflowWebhookTrigger +from models.workflow import Workflow +from services.errors.app import QuotaExceededError +from services.trigger.webhook_service import WebhookService + + +class WebhookServiceRelationshipFactory: + @staticmethod + def create_account_and_tenant(db_session_with_containers: Session) -> tuple[Account, Tenant]: + account = Account( + name=f"Account {uuid4()}", + email=f"webhook-{uuid4()}@example.com", + password="hashed-password", + password_salt="salt", + interface_language="en-US", + timezone="UTC", + ) + db_session_with_containers.add(account) + db_session_with_containers.commit() + + tenant = Tenant(name=f"Tenant {uuid4()}", plan="basic", status="normal") + db_session_with_containers.add(tenant) + db_session_with_containers.commit() + + join = TenantAccountJoin( + tenant_id=tenant.id, + account_id=account.id, + role=TenantAccountRole.OWNER, + current=True, + ) + db_session_with_containers.add(join) + db_session_with_containers.commit() + + account.current_tenant = tenant + return account, tenant + + @staticmethod + def create_app(db_session_with_containers: Session, tenant: Tenant, account: Account) -> App: + app = App( + tenant_id=tenant.id, + name=f"Webhook App {uuid4()}", + description="", + mode="workflow", + icon_type="emoji", + icon="bot", + icon_background="#FFFFFF", + enable_site=False, + enable_api=True, + api_rpm=100, + api_rph=100, + is_demo=False, + is_public=False, + is_universal=False, + created_by=account.id, + updated_by=account.id, + ) + db_session_with_containers.add(app) + db_session_with_containers.commit() + return app + + @staticmethod + def create_workflow( + db_session_with_containers: Session, + *, + app: App, + account: Account, + node_ids: list[str], + version: str, + ) -> Workflow: + graph = { + "nodes": [ + { + "id": node_id, + "data": { + "type": TRIGGER_WEBHOOK_NODE_TYPE, + "title": f"Webhook {node_id}", + "method": "post", + "content_type": "application/json", + "headers": [], + "params": [], + "body": [], + "status_code": 200, + "response_body": '{"status": "ok"}', + "timeout": 30, + }, + } + for node_id in node_ids + ], + "edges": [], + } + + workflow = Workflow( + tenant_id=app.tenant_id, + app_id=app.id, + type="workflow", + graph=json.dumps(graph), + features=json.dumps({}), + created_by=account.id, + updated_by=account.id, + environment_variables=[], + conversation_variables=[], + version=version, + ) + db_session_with_containers.add(workflow) + db_session_with_containers.commit() + return workflow + + @staticmethod + def create_webhook_trigger( + db_session_with_containers: Session, + *, + app: App, + account: Account, + node_id: str, + webhook_id: str | None = None, + ) -> WorkflowWebhookTrigger: + webhook_trigger = WorkflowWebhookTrigger( + app_id=app.id, + node_id=node_id, + tenant_id=app.tenant_id, + webhook_id=webhook_id or uuid4().hex[:24], + created_by=account.id, + ) + db_session_with_containers.add(webhook_trigger) + db_session_with_containers.commit() + return webhook_trigger + + @staticmethod + def create_app_trigger( + db_session_with_containers: Session, + *, + app: App, + node_id: str, + status: AppTriggerStatus, + ) -> AppTrigger: + app_trigger = AppTrigger( + tenant_id=app.tenant_id, + app_id=app.id, + node_id=node_id, + trigger_type=AppTriggerType.TRIGGER_WEBHOOK, + provider_name="webhook", + title=f"Webhook {node_id}", + status=status, + ) + db_session_with_containers.add(app_trigger) + db_session_with_containers.commit() + return app_trigger + + +class TestWebhookServiceLookupWithContainers: + def test_get_webhook_trigger_and_workflow_raises_when_app_trigger_missing( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + + with pytest.raises(ValueError, match="App trigger not found"): + WebhookService.get_webhook_trigger_and_workflow(webhook_trigger.webhook_id) + + def test_get_webhook_trigger_and_workflow_raises_when_app_trigger_rate_limited( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + factory.create_app_trigger( + db_session_with_containers, app=app, node_id="node-1", status=AppTriggerStatus.RATE_LIMITED + ) + + with pytest.raises(ValueError, match="rate limited"): + WebhookService.get_webhook_trigger_and_workflow(webhook_trigger.webhook_id) + + def test_get_webhook_trigger_and_workflow_raises_when_app_trigger_disabled( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + factory.create_app_trigger( + db_session_with_containers, app=app, node_id="node-1", status=AppTriggerStatus.DISABLED + ) + + with pytest.raises(ValueError, match="disabled"): + WebhookService.get_webhook_trigger_and_workflow(webhook_trigger.webhook_id) + + def test_get_webhook_trigger_and_workflow_raises_when_workflow_missing( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + factory.create_app_trigger( + db_session_with_containers, app=app, node_id="node-1", status=AppTriggerStatus.ENABLED + ) + + with pytest.raises(ValueError, match="Workflow not found"): + WebhookService.get_webhook_trigger_and_workflow(webhook_trigger.webhook_id) + + def test_get_webhook_trigger_and_workflow_returns_debug_draft_workflow( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + factory.create_workflow( + db_session_with_containers, + app=app, + account=account, + node_ids=["published-node"], + version="2026-04-14.001", + ) + draft_workflow = factory.create_workflow( + db_session_with_containers, + app=app, + account=account, + node_ids=["debug-node"], + version=Workflow.VERSION_DRAFT, + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="debug-node" + ) + + got_trigger, got_workflow, got_node_config = WebhookService.get_webhook_trigger_and_workflow( + webhook_trigger.webhook_id, + is_debug=True, + ) + + assert got_trigger.id == webhook_trigger.id + assert got_workflow.id == draft_workflow.id + assert got_node_config["id"] == "debug-node" + + +class TestWebhookServiceTriggerExecutionWithContainers: + def test_trigger_workflow_execution_triggers_async_workflow_successfully( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + + end_user = SimpleNamespace(id=str(uuid4())) + webhook_data = {"body": {"value": 1}, "headers": {}, "query_params": {}, "files": {}, "method": "POST"} + + with ( + patch( + "services.trigger.webhook_service.EndUserService.get_or_create_end_user_by_type", + return_value=end_user, + ), + patch("services.trigger.webhook_service.QuotaType.TRIGGER.consume") as mock_consume, + patch("services.trigger.webhook_service.AsyncWorkflowService.trigger_workflow_async") as mock_trigger, + ): + WebhookService.trigger_workflow_execution(webhook_trigger, webhook_data, workflow) + + mock_consume.assert_called_once_with(webhook_trigger.tenant_id) + mock_trigger.assert_called_once() + trigger_args = mock_trigger.call_args.args + assert trigger_args[1] is end_user + assert trigger_args[2].workflow_id == workflow.id + assert trigger_args[2].root_node_id == webhook_trigger.node_id + + def test_trigger_workflow_execution_marks_tenant_rate_limited_when_quota_exceeded( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + + with ( + patch( + "services.trigger.webhook_service.EndUserService.get_or_create_end_user_by_type", + return_value=SimpleNamespace(id=str(uuid4())), + ), + patch( + "services.trigger.webhook_service.QuotaType.TRIGGER.consume", + side_effect=QuotaExceededError(feature="trigger", tenant_id=tenant.id, required=1), + ), + patch( + "services.trigger.webhook_service.AppTriggerService.mark_tenant_triggers_rate_limited" + ) as mock_mark_rate_limited, + ): + with pytest.raises(QuotaExceededError): + WebhookService.trigger_workflow_execution( + webhook_trigger, + {"body": {}, "headers": {}, "query_params": {}, "files": {}, "method": "POST"}, + workflow, + ) + + mock_mark_rate_limited.assert_called_once_with(tenant.id) + + def test_trigger_workflow_execution_logs_and_reraises_unexpected_errors( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version="2026-04-14.001" + ) + webhook_trigger = factory.create_webhook_trigger( + db_session_with_containers, app=app, account=account, node_id="node-1" + ) + + with ( + patch( + "services.trigger.webhook_service.EndUserService.get_or_create_end_user_by_type", + side_effect=RuntimeError("boom"), + ), + patch("services.trigger.webhook_service.logger.exception") as mock_logger_exception, + ): + with pytest.raises(RuntimeError, match="boom"): + WebhookService.trigger_workflow_execution( + webhook_trigger, + {"body": {}, "headers": {}, "query_params": {}, "files": {}, "method": "POST"}, + workflow, + ) + + mock_logger_exception.assert_called_once() + + +class TestWebhookServiceRelationshipSyncWithContainers: + def test_sync_webhook_relationships_raises_when_workflow_exceeds_node_limit( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + node_ids = [f"node-{index}" for index in range(WebhookService.MAX_WEBHOOK_NODES_PER_WORKFLOW + 1)] + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=node_ids, version=Workflow.VERSION_DRAFT + ) + + with pytest.raises(ValueError, match="maximum webhook node limit"): + WebhookService.sync_webhook_relationships(app, workflow) + + def test_sync_webhook_relationships_raises_when_lock_not_acquired( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=["node-1"], version=Workflow.VERSION_DRAFT + ) + lock = MagicMock() + lock.acquire.return_value = False + + with patch("services.trigger.webhook_service.redis_client.lock", return_value=lock): + with pytest.raises(RuntimeError, match="Failed to acquire lock"): + WebhookService.sync_webhook_relationships(app, workflow) + + def test_sync_webhook_relationships_creates_missing_records_and_deletes_stale_records( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + stale_trigger = factory.create_webhook_trigger( + db_session_with_containers, + app=app, + account=account, + node_id="node-stale", + webhook_id="stale-webhook-id-000001", + ) + stale_trigger_id = stale_trigger.id + workflow = factory.create_workflow( + db_session_with_containers, + app=app, + account=account, + node_ids=["node-new"], + version=Workflow.VERSION_DRAFT, + ) + + with patch( + "services.trigger.webhook_service.WebhookService.generate_webhook_id", return_value="new-webhook-id-000001" + ): + WebhookService.sync_webhook_relationships(app, workflow) + + db_session_with_containers.expire_all() + records = db_session_with_containers.scalars( + select(WorkflowWebhookTrigger).where(WorkflowWebhookTrigger.app_id == app.id) + ).all() + + assert [record.node_id for record in records] == ["node-new"] + assert records[0].webhook_id == "new-webhook-id-000001" + assert db_session_with_containers.get(WorkflowWebhookTrigger, stale_trigger_id) is None + + def test_sync_webhook_relationships_sets_redis_cache_for_new_record( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, + app=app, + account=account, + node_ids=["node-cache"], + version=Workflow.VERSION_DRAFT, + ) + cache_key = f"{WebhookService.__WEBHOOK_NODE_CACHE_KEY__}:{app.id}:node-cache" + + with patch( + "services.trigger.webhook_service.WebhookService.generate_webhook_id", return_value="cache-webhook-id-00001" + ): + WebhookService.sync_webhook_relationships(app, workflow) + + cached_payload = WebhookServiceRelationshipFactory._read_cache(cache_key) + assert cached_payload is not None + assert cached_payload["node_id"] == "node-cache" + assert cached_payload["webhook_id"] == "cache-webhook-id-00001" + + def test_sync_webhook_relationships_logs_when_lock_release_fails( + self, db_session_with_containers: Session, flask_app_with_containers + ): + del flask_app_with_containers + factory = WebhookServiceRelationshipFactory + account, tenant = factory.create_account_and_tenant(db_session_with_containers) + app = factory.create_app(db_session_with_containers, tenant, account) + workflow = factory.create_workflow( + db_session_with_containers, app=app, account=account, node_ids=[], version=Workflow.VERSION_DRAFT + ) + lock = MagicMock() + lock.acquire.return_value = True + lock.release.side_effect = RuntimeError("release failed") + + with ( + patch("services.trigger.webhook_service.redis_client.lock", return_value=lock), + patch("services.trigger.webhook_service.logger.exception") as mock_logger_exception, + ): + WebhookService.sync_webhook_relationships(app, workflow) + + mock_logger_exception.assert_called_once() + + +def _read_cache(cache_key: str) -> dict[str, str] | None: + from extensions.ext_redis import redis_client + + cached = redis_client.get(cache_key) + if not cached: + return None + if isinstance(cached, bytes): + cached = cached.decode("utf-8") + return json.loads(cached) + + +WebhookServiceRelationshipFactory._read_cache = staticmethod(_read_cache) diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py index 749c6fff5b..1e57b5603d 100644 --- a/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workflow_app_service.py @@ -8,9 +8,9 @@ from unittest.mock import patch import pytest from faker import Faker -from graphon.enums import WorkflowExecutionStatus from sqlalchemy.orm import Session +from graphon.enums import WorkflowExecutionStatus from models import EndUser, Workflow, WorkflowAppLog, WorkflowArchiveLog, WorkflowRun from models.enums import AppTriggerType, CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import WorkflowAppLogCreatedFrom diff --git a/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py b/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py index 0c281c8c33..86cf2327c7 100644 --- a/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py +++ b/api/tests/test_containers_integration_tests/services/test_workflow_draft_variable_service.py @@ -1,9 +1,9 @@ import pytest from faker import Faker -from graphon.variables.segments import StringSegment from sqlalchemy.orm import Session from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID +from graphon.variables.segments import StringSegment from models import App, Workflow from models.enums import DraftVariableType from models.workflow import WorkflowDraftVariable diff --git a/api/tests/test_containers_integration_tests/services/tools/test_api_tools_manage_service.py b/api/tests/test_containers_integration_tests/services/tools/test_api_tools_manage_service.py index d3e765055a..af83adaae0 100644 --- a/api/tests/test_containers_integration_tests/services/tools/test_api_tools_manage_service.py +++ b/api/tests/test_containers_integration_tests/services/tools/test_api_tools_manage_service.py @@ -1,3 +1,5 @@ +import inspect +import json from unittest.mock import patch import pytest @@ -6,6 +8,8 @@ from pydantic import TypeAdapter, ValidationError from sqlalchemy.orm import Session from core.tools.entities.tool_entities import ApiProviderSchemaType +from core.tools.errors import ApiToolProviderNotFoundError +from core.tools.tool_label_manager import ToolLabelManager from models import Account, Tenant from models.tools import ApiToolProvider from services.tools.api_tools_manage_service import ApiToolManageService @@ -590,30 +594,204 @@ class TestApiToolManageService: with pytest.raises(ValueError, match="you have not added provider"): ApiToolManageService.delete_api_tool_provider(account.id, tenant.id, "nonexistent") - def test_update_api_tool_provider_not_found( + def test_update_api_tool_provider_success( self, flask_req_ctx_with_containers, db_session_with_containers: Session, mock_external_service_dependencies ): - """Test update raises ValueError when original provider not found.""" fake = Faker() + + # Firmware fix for cache.delete() in update flow + mock_encrypter = mock_external_service_dependencies["encrypter"] + from unittest.mock import MagicMock + + mock_cache = MagicMock() + mock_cache.delete.return_value = None + mock_encrypter.return_value = (mock_encrypter, mock_cache) + + # Get fake account and tenant account, tenant = self._create_test_account_and_tenant( db_session_with_containers, mock_external_service_dependencies ) - with pytest.raises(ValueError, match="does not exists"): - ApiToolManageService.update_api_tool_provider( + # original provider name + original_name = "original-provider" + + # Create original provider + _ = ApiToolManageService.create_api_tool_provider( + user_id=account.id, + tenant_id=tenant.id, + provider_name=original_name, + icon={"type": "emoji", "value": "🔧"}, + credentials={"auth_type": "none"}, + schema_type=ApiProviderSchemaType.OPENAPI, + schema=self._create_test_openapi_schema(), + privacy_policy="", + custom_disclaimer="", + labels=["old-label"], + ) + + # new provide name and new labels for update + new_name = "updated-provider" + new_labels = ["new-label-1", "new-label-2"] + + # Reset mock history so assertions focus on update path only + mock_external_service_dependencies["encrypter"].reset_mock() + mock_external_service_dependencies["provider_controller"].from_db.reset_mock() + mock_external_service_dependencies["tool_label_manager"].update_tool_labels.reset_mock() + + # Act: Update the provider with new values + result = ApiToolManageService.update_api_tool_provider( + user_id=account.id, + tenant_id=tenant.id, + # new provider name - changed 1 + provider_name=new_name, + original_provider=original_name, + # new icon - changed 2 + icon={"type": "emoji", "value": "🚀"}, + credentials={"auth_type": "none"}, + _schema_type=ApiProviderSchemaType.OPENAPI, + schema=self._create_test_openapi_schema(), + # new privacy policy - changed 3 + privacy_policy="https://new-policy.com", + # new custom disclaimer - changed 4 + custom_disclaimer="New disclaimer", + # new labels - changed 5 (However, we will not verify this, not this layer responsibility.) + labels=new_labels, + ) + + # Assert: Verify the result + assert result == {"result": "success"} + + # Get the updated provider from the database + updated_provider: ApiToolProvider | None = ( + db_session_with_containers.query(ApiToolProvider) + .filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == new_name) + .first() + ) + + # Verify the provider was updated successfully + assert updated_provider is not None + + # Manually refresh to keep object detachment + db_session_with_containers.refresh(updated_provider) + # Verify all the updated fields + # - changed 1 + assert updated_provider.name == new_name + # - changed 2 + icon_data = json.loads(updated_provider.icon) + assert icon_data["type"] == "emoji" + assert icon_data["value"] == "🚀" + # - changed 3 + assert updated_provider.privacy_policy == "https://new-policy.com" + # - changed 4 + assert updated_provider.custom_disclaimer == "New disclaimer" + + # Verify old provider name no longer exists after rename + original_provider: ApiToolProvider | None = ( + db_session_with_containers.query(ApiToolProvider) + .filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == original_name) + .first() + ) + assert original_provider is None + + # Verify update flow calls critical collaborators + mock_external_service_dependencies["provider_controller"].from_db.assert_called_once() + mock_external_service_dependencies["encrypter"].assert_called_once() + mock_cache.delete.assert_called_once() + + # Deeply verify on session propagation of labels update logics: + # Since in refactoring, we pass session down to label manager to keep atomicity. + # The assertion here is to verify this. + sig = inspect.signature(ToolLabelManager.update_tool_labels) + args, kwargs = mock_external_service_dependencies["tool_label_manager"].update_tool_labels.call_args + bound_args = sig.bind(*args, **kwargs) + passed_session = bound_args.arguments.get("session") + # Ensure the type: Session + assert isinstance(passed_session, Session), f"Expected Session object, got {type(passed_session)}" + assert passed_session is not None, ( + "Atomicity Failure: Session cannot be passed to Label Manager in update_api_tool_provider" + ) + + def test_update_api_tool_provider_not_found( + self, flask_req_ctx_with_containers, db_session_with_containers: Session, mock_external_service_dependencies + ): + """ + Test update raises ValueError when original provider not found. + + This test verifies: + - Proper error when trying to update a non-existing original provider + - No accidental upsert/new provider creation + - No external dependency invocation on early failure path + """ + # Arrange: Create test account and tenant + account, tenant = self._create_test_account_and_tenant( + db_session_with_containers, mock_external_service_dependencies + ) + + # Keep an existing provider in DB to ensure unrelated data remains unchanged + existing_provider_name = "existing-provider" + _ = ApiToolManageService.create_api_tool_provider( + user_id=account.id, + tenant_id=tenant.id, + provider_name=existing_provider_name, + icon={"type": "emoji", "value": "🔧"}, + credentials={"auth_type": "none"}, + schema_type=ApiProviderSchemaType.OPENAPI, + schema=self._create_test_openapi_schema(), + privacy_policy="https://existing-policy.com", + custom_disclaimer="Existing disclaimer", + labels=["existing-label"], + ) + + # Reset mock history so assertions focus on update failure path only + mock_external_service_dependencies["tool_label_manager"].update_tool_labels.reset_mock() + mock_external_service_dependencies["encrypter"].reset_mock() + mock_external_service_dependencies["provider_controller"].from_db.reset_mock() + + # Act & Assert: Verify update fails with clear error message + target_new_name = "new-provider-name" + missing_original_name = "missing-original-provider" + with pytest.raises(ApiToolProviderNotFoundError) as exc_info: + _ = ApiToolManageService.update_api_tool_provider( user_id=account.id, tenant_id=tenant.id, - provider_name="new-name", - original_provider="nonexistent", - icon={}, + provider_name=target_new_name, + original_provider=missing_original_name, + icon={"type": "emoji", "value": "🚀"}, credentials={"auth_type": "none"}, _schema_type=ApiProviderSchemaType.OPENAPI, schema=self._create_test_openapi_schema(), - privacy_policy=None, - custom_disclaimer="", - labels=[], + privacy_policy="https://new-policy.com", + custom_disclaimer="New disclaimer", + labels=["new-label"], ) + error = exc_info.value + assert error.provider_name == missing_original_name + assert error.tenant_id == tenant.id + assert error.error_code == "api_tool_provider_not_found" + + # Assert: Existing provider should remain unchanged + existing_provider: ApiToolProvider | None = ( + db_session_with_containers.query(ApiToolProvider) + .filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == existing_provider_name) + .first() + ) + assert existing_provider is not None + assert existing_provider.name == existing_provider_name + + # Assert: No new provider should be created + unexpected_new_provider: ApiToolProvider | None = ( + db_session_with_containers.query(ApiToolProvider) + .filter(ApiToolProvider.tenant_id == tenant.id, ApiToolProvider.name == target_new_name) + .first() + ) + assert unexpected_new_provider is None + + # Assert: Early failure should skip all downstream external interactions + mock_external_service_dependencies["tool_label_manager"].update_tool_labels.assert_not_called() + mock_external_service_dependencies["encrypter"].assert_not_called() + mock_external_service_dependencies["provider_controller"].from_db.assert_not_called() + def test_update_api_tool_provider_missing_auth_type( self, flask_req_ctx_with_containers, db_session_with_containers: Session, mock_external_service_dependencies ): diff --git a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py index ce2fd2eeb1..ce5c2bd162 100644 --- a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py +++ b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_converter.py @@ -5,9 +5,6 @@ from unittest.mock import MagicMock, patch import pytest from faker import Faker -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.message_entities import PromptMessageRole -from graphon.variables.input_entities import VariableEntity, VariableEntityType from sqlalchemy.orm import Session from core.app.app_config.entities import ( @@ -21,6 +18,9 @@ from core.app.app_config.entities import ( PromptTemplateEntity, ) from core.prompt.utils.prompt_template_parser import PromptTemplateParser +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.message_entities import PromptMessageRole +from graphon.variables.input_entities import VariableEntity, VariableEntityType from models import Account, Tenant from models.api_based_extension import APIBasedExtension, APIBasedExtensionPoint from models.model import App, AppMode, AppModelConfig diff --git a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py index 7c43bf676b..4dab895135 100644 --- a/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py +++ b/api/tests/test_containers_integration_tests/services/workflow/test_workflow_node_execution_service_repository.py @@ -1,10 +1,10 @@ from datetime import datetime, timedelta from uuid import uuid4 -from graphon.enums import WorkflowNodeExecutionStatus from sqlalchemy import Engine, select from sqlalchemy.orm import Session, sessionmaker +from graphon.enums import WorkflowNodeExecutionStatus from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole from models.workflow import WorkflowNodeExecutionModel diff --git a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py index 2fb62e0fc0..fa3ac12cf0 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_clean_notion_document_task.py @@ -11,7 +11,8 @@ from unittest.mock import Mock, patch import pytest from faker import Faker -from sqlalchemy import func, select +from sqlalchemy import ColumnElement, func, select +from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexStructureType from models.dataset import Dataset, Document, DocumentSegment @@ -21,6 +22,14 @@ from tasks.clean_notion_document_task import clean_notion_document_task from tests.test_containers_integration_tests.helpers import generate_valid_password +def _count_documents(session: Session, condition: ColumnElement[bool]) -> int: + return session.scalar(select(func.count()).select_from(Document).where(condition)) or 0 + + +def _count_segments(session: Session, condition: ColumnElement[bool]) -> int: + return session.scalar(select(func.count()).select_from(DocumentSegment).where(condition)) or 0 + + class TestCleanNotionDocumentTask: """Integration tests for clean_notion_document_task using testcontainers.""" @@ -146,29 +155,14 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify data exists before cleanup - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.id.in_(document_ids)) - ) - == 3 - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) - ) - == 6 - ) + assert _count_documents(db_session_with_containers, Document.id.in_(document_ids)) == 3 + assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(document_ids)) == 6 # Execute cleanup task clean_notion_document_task(document_ids, dataset.id) # Verify segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(document_ids)) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(document_ids)) == 0 # Verify index processor was called mock_processor = mock_index_processor_factory.return_value.init_index_processor.return_value @@ -328,12 +322,7 @@ class TestCleanNotionDocumentTask: # The task properly handles various index types and document configurations. # Verify segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 # Reset mock for next iteration mock_index_processor_factory.reset_mock() @@ -416,12 +405,7 @@ class TestCleanNotionDocumentTask: clean_notion_document_task([document.id], dataset.id) # Verify segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 # Note: This test successfully verifies that segments without index_node_ids # are properly deleted from the database. @@ -507,18 +491,8 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) - ) - == 5 - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) - ) - == 10 - ) + assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == 5 + assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 10 # Clean up only first 3 documents documents_to_clean = [doc.id for doc in documents[:3]] @@ -528,29 +502,12 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(documents_to_clean, dataset.id) # Verify only specified documents' segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()) - .select_from(DocumentSegment) - .where(DocumentSegment.document_id.in_(documents_to_clean)) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(documents_to_clean)) == 0 # Verify remaining documents and segments are intact remaining_docs = [doc.id for doc in documents[3:]] - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs)) - ) - == 2 - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs)) - ) - == 4 - ) + assert _count_documents(db_session_with_containers, Document.id.in_(remaining_docs)) == 2 + assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(remaining_docs)) == 4 # Note: This test successfully verifies partial document cleanup operations. # The database operations work correctly, isolating only the specified documents. @@ -634,23 +591,13 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all segments exist before cleanup - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 4 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 4 # Execute cleanup task clean_notion_document_task([document.id], dataset.id) # Verify all segments are deleted regardless of status - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 # Note: This test successfully verifies database operations. # IndexProcessor verification would require more sophisticated mocking. @@ -820,16 +767,9 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup + assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == num_documents assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) - ) - == num_documents - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) - ) + _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == num_documents * num_segments_per_doc ) @@ -838,12 +778,7 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(all_document_ids, dataset.id) # Verify all segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 0 # Note: This test successfully verifies bulk document cleanup operations. # The database efficiently handles large-scale deletions. @@ -950,29 +885,12 @@ class TestCleanNotionDocumentTask: clean_notion_document_task([target_document.id], target_dataset.id) # Verify only documents' segments from target dataset are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()) - .select_from(DocumentSegment) - .where(DocumentSegment.document_id == target_document.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == target_document.id) == 0 # Verify documents from other datasets remain intact remaining_docs = [doc.id for doc in all_documents[1:]] - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.id.in_(remaining_docs)) - ) - == 2 - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id.in_(remaining_docs)) - ) - == 6 - ) + assert _count_documents(db_session_with_containers, Document.id.in_(remaining_docs)) == 2 + assert _count_segments(db_session_with_containers, DocumentSegment.document_id.in_(remaining_docs)) == 6 # Note: This test successfully verifies multi-tenant isolation. # Only documents from the target dataset are affected, maintaining tenant separation. @@ -1067,13 +985,9 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify all data exists before cleanup - assert db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.dataset_id == dataset.id) - ) == len(document_statuses) + assert _count_documents(db_session_with_containers, Document.dataset_id == dataset.id) == len(document_statuses) assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) - ) + _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == len(document_statuses) * 2 ) @@ -1082,12 +996,7 @@ class TestCleanNotionDocumentTask: clean_notion_document_task(all_document_ids, dataset.id) # Verify all segments are deleted regardless of status - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.dataset_id == dataset.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.dataset_id == dataset.id) == 0 # Note: This test successfully verifies cleanup of documents in various states. # All documents are deleted regardless of their indexing status. @@ -1185,29 +1094,14 @@ class TestCleanNotionDocumentTask: db_session_with_containers.commit() # Verify data exists before cleanup - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(Document).where(Document.id == document.id) - ) - == 1 - ) - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 3 - ) + assert _count_documents(db_session_with_containers, Document.id == document.id) == 1 + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 3 # Execute cleanup task clean_notion_document_task([document.id], dataset.id) # Verify segments are deleted - assert ( - db_session_with_containers.scalar( - select(func.count()).select_from(DocumentSegment).where(DocumentSegment.document_id == document.id) - ) - == 0 - ) + assert _count_segments(db_session_with_containers, DocumentSegment.document_id == document.id) == 0 # Note: This test successfully verifies cleanup of documents with rich metadata. # The task properly handles complex document structures and metadata fields. diff --git a/api/tests/test_containers_integration_tests/tasks/test_deal_dataset_vector_index_task.py b/api/tests/test_containers_integration_tests/tasks/test_deal_dataset_vector_index_task.py index d457b59d58..48fec441c5 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_deal_dataset_vector_index_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_deal_dataset_vector_index_task.py @@ -11,6 +11,7 @@ from unittest.mock import ANY, Mock, patch import pytest from faker import Faker +from sqlalchemy import select from core.rag.index_processor.constant.index_type import IndexStructureType from models.dataset import Dataset, Document, DocumentSegment @@ -221,7 +222,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify document status was updated to indexing then completed - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify index processor load method was called @@ -322,7 +325,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "update") # Verify document status was updated to indexing then completed - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify index processor clean and load methods were called @@ -431,7 +436,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify document status was updated to indexing then completed - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify that no index processor load was called since no segments exist @@ -564,7 +571,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify document status was updated to error - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.ERROR assert "Test exception during indexing" in updated_document.error @@ -635,7 +644,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify document status was updated to indexing then completed - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify index processor was initialized with custom index type @@ -711,7 +722,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify document status was updated to indexing then completed - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify index processor was initialized with the document's index type @@ -815,7 +828,9 @@ class TestDealDatasetVectorIndexTask: # Verify all documents were processed for document in documents: - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED # Verify index processor load was called multiple times @@ -917,7 +932,9 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify final document status - updated_document = db_session_with_containers.query(Document).filter_by(id=document.id).first() + updated_document = db_session_with_containers.scalar( + select(Document).where(Document.id == document.id).limit(1) + ) assert updated_document.indexing_status == IndexingStatus.COMPLETED def test_deal_dataset_vector_index_task_with_disabled_documents( @@ -1027,12 +1044,14 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify only enabled document was processed - updated_enabled_document = db_session_with_containers.query(Document).filter_by(id=enabled_document.id).first() + updated_enabled_document = db_session_with_containers.scalar( + select(Document).where(Document.id == enabled_document.id).limit(1) + ) assert updated_enabled_document.indexing_status == IndexingStatus.COMPLETED # Verify disabled document status remains unchanged - updated_disabled_document = ( - db_session_with_containers.query(Document).filter_by(id=disabled_document.id).first() + updated_disabled_document = db_session_with_containers.scalar( + select(Document).where(Document.id == disabled_document.id).limit(1) ) assert updated_disabled_document.indexing_status == IndexingStatus.COMPLETED # Should not change @@ -1148,12 +1167,14 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify only active document was processed - updated_active_document = db_session_with_containers.query(Document).filter_by(id=active_document.id).first() + updated_active_document = db_session_with_containers.scalar( + select(Document).where(Document.id == active_document.id).limit(1) + ) assert updated_active_document.indexing_status == IndexingStatus.COMPLETED # Verify archived document status remains unchanged - updated_archived_document = ( - db_session_with_containers.query(Document).filter_by(id=archived_document.id).first() + updated_archived_document = db_session_with_containers.scalar( + select(Document).where(Document.id == archived_document.id).limit(1) ) assert updated_archived_document.indexing_status == IndexingStatus.COMPLETED # Should not change @@ -1269,14 +1290,14 @@ class TestDealDatasetVectorIndexTask: deal_dataset_vector_index_task(dataset.id, "add") # Verify only completed document was processed - updated_completed_document = ( - db_session_with_containers.query(Document).filter_by(id=completed_document.id).first() + updated_completed_document = db_session_with_containers.scalar( + select(Document).where(Document.id == completed_document.id).limit(1) ) assert updated_completed_document.indexing_status == IndexingStatus.COMPLETED # Verify incomplete document status remains unchanged - updated_incomplete_document = ( - db_session_with_containers.query(Document).filter_by(id=incomplete_document.id).first() + updated_incomplete_document = db_session_with_containers.scalar( + select(Document).where(Document.id == incomplete_document.id).limit(1) ) assert updated_incomplete_document.indexing_status == IndexingStatus.INDEXING # Should not change diff --git a/api/tests/test_containers_integration_tests/tasks/test_document_indexing_sync_task.py b/api/tests/test_containers_integration_tests/tasks/test_document_indexing_sync_task.py index d94c1fdf24..b6e7e6e5c9 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_document_indexing_sync_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_document_indexing_sync_task.py @@ -16,7 +16,7 @@ from sqlalchemy import delete, func, select, update from core.indexing_runner import DocumentIsPausedError, IndexingRunner from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType -from models import Account, Tenant, TenantAccountJoin, TenantAccountRole +from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus from models.dataset import Dataset, Document, DocumentSegment from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus from tasks.document_indexing_sync_task import document_indexing_sync_task @@ -31,12 +31,12 @@ class DocumentIndexingSyncTaskTestDataFactory: email=f"{uuid4()}@example.com", name=f"user-{uuid4()}", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.flush() - tenant = Tenant(name=f"tenant-{account.id}", status="normal") + tenant = Tenant(name=f"tenant-{account.id}", status=TenantStatus.NORMAL) db_session_with_containers.add(tenant) db_session_with_containers.flush() diff --git a/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py b/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py index 6a8e186958..39c58987fd 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_duplicate_document_indexing_task.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock, patch import pytest from faker import Faker +from sqlalchemy import select from core.indexing_runner import DocumentIsPausedError from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType @@ -317,7 +318,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify documents were updated to parsing status # Re-query documents from database since _duplicate_document_indexing_task uses a different session for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING assert updated_document.processing_started_at is not None @@ -362,14 +363,14 @@ class TestDuplicateDocumentIndexingTasks: # Verify segments were deleted from database # Re-query segments from database using captured IDs to avoid stale ORM instances for seg_id in segment_ids: - deleted_segment = ( - db_session_with_containers.query(DocumentSegment).where(DocumentSegment.id == seg_id).first() + deleted_segment = db_session_with_containers.scalar( + select(DocumentSegment).where(DocumentSegment.id == seg_id).limit(1) ) assert deleted_segment is None # Verify documents were updated to parsing status for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING assert updated_document.processing_started_at is not None @@ -438,7 +439,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify only existing documents were updated # Re-query documents from database since _duplicate_document_indexing_task uses a different session for doc_id in existing_document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING assert updated_document.processing_started_at is not None @@ -485,7 +486,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify documents were still updated to parsing status before the exception # Re-query documents from database since _duplicate_document_indexing_task close the session for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING assert updated_document.processing_started_at is not None @@ -543,7 +544,7 @@ class TestDuplicateDocumentIndexingTasks: # Assert: Verify error handling # Re-query documents from database since _duplicate_document_indexing_task uses a different session for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.ERROR assert updated_document.error is not None assert "batch upload" in updated_document.error.lower() @@ -585,7 +586,7 @@ class TestDuplicateDocumentIndexingTasks: # Assert: Verify error handling # Re-query documents from database since _duplicate_document_indexing_task uses a different session for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.ERROR assert updated_document.error is not None assert "limit" in updated_document.error.lower() @@ -649,7 +650,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify documents were processed for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING @patch("tasks.duplicate_document_indexing_task.TenantIsolatedTaskQueue", autospec=True) @@ -692,7 +693,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify documents were processed for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING @patch("tasks.duplicate_document_indexing_task.TenantIsolatedTaskQueue", autospec=True) @@ -736,7 +737,7 @@ class TestDuplicateDocumentIndexingTasks: # Verify documents were processed for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.indexing_status == IndexingStatus.PARSING @patch("tasks.duplicate_document_indexing_task.TenantIsolatedTaskQueue", autospec=True) @@ -851,7 +852,7 @@ class TestDuplicateDocumentIndexingTasks: # Assert for doc_id in document_ids: - updated_document = db_session_with_containers.query(Document).where(Document.id == doc_id).first() + updated_document = db_session_with_containers.scalar(select(Document).where(Document.id == doc_id).limit(1)) assert updated_document.is_paused is True assert updated_document.indexing_status == IndexingStatus.PARSING assert updated_document.display_status == "paused" diff --git a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py index 1b4dcf28ea..328bdbf055 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_mail_human_input_delivery_task.py @@ -3,9 +3,6 @@ from datetime import UTC, datetime from unittest.mock import patch import pytest -from graphon.enums import WorkflowExecutionStatus -from graphon.nodes.human_input.entities import HumanInputNodeData -from graphon.runtime import GraphRuntimeState, VariablePool from sqlalchemy import delete from configs import dify_config @@ -21,6 +18,9 @@ from core.workflow.human_input_compat import ( MemberRecipient, ) from extensions.ext_storage import storage +from graphon.enums import WorkflowExecutionStatus +from graphon.nodes.human_input.entities import HumanInputNodeData +from graphon.runtime import GraphRuntimeState, VariablePool from models.account import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.human_input import HumanInputDelivery, HumanInputForm, HumanInputFormRecipient diff --git a/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py b/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py index b5bef145d5..b43b622870 100644 --- a/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py +++ b/api/tests/test_containers_integration_tests/tasks/test_remove_app_and_related_data_task.py @@ -2,12 +2,12 @@ import uuid from unittest.mock import ANY, call, patch import pytest -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType from sqlalchemy import delete, func, select from core.db.session_factory import session_factory from extensions.storage.storage_type import StorageType +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from models import Tenant from models.enums import CreatorUserRole diff --git a/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py b/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py index 4bc022c415..b00d827e37 100644 --- a/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py +++ b/api/tests/test_containers_integration_tests/test_workflow_pause_integration.py @@ -24,16 +24,16 @@ from dataclasses import dataclass from datetime import timedelta import pytest -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import delete, func, select from sqlalchemy.orm import Session, selectinload, sessionmaker from extensions.ext_storage import storage +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus from libs.datetime_utils import naive_utc_now from models import Account from models import WorkflowPause as WorkflowPauseModel -from models.account import Tenant, TenantAccountJoin, TenantAccountRole +from models.account import AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus from models.model import UploadFile from models.workflow import Workflow, WorkflowRun from repositories.sqlalchemy_api_workflow_run_repository import ( @@ -181,7 +181,7 @@ class TestWorkflowPauseIntegration: tenant = Tenant( name="Test Tenant", - status="normal", + status=TenantStatus.NORMAL, ) db_session_with_containers.add(tenant) db_session_with_containers.commit() @@ -190,7 +190,7 @@ class TestWorkflowPauseIntegration: email="test@example.com", name="Test User", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) db_session_with_containers.add(account) db_session_with_containers.commit() @@ -696,7 +696,7 @@ class TestWorkflowPauseIntegration: tenant2 = Tenant( name="Test Tenant 2", - status="normal", + status=TenantStatus.NORMAL, ) self.session.add(tenant2) self.session.commit() @@ -705,7 +705,7 @@ class TestWorkflowPauseIntegration: email="test2@example.com", name="Test User 2", interface_language="en-US", - status="active", + status=AccountStatus.ACTIVE, ) self.session.add(account2) self.session.commit() diff --git a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py index 7c4553d4a0..9c20118e27 100644 --- a/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py +++ b/api/tests/test_containers_integration_tests/trigger/test_trigger_e2e.py @@ -10,7 +10,6 @@ from typing import Any import pytest from flask import Flask, Response from flask.testing import FlaskClient -from graphon.enums import BuiltinNodeTypes from sqlalchemy import select from sqlalchemy.orm import Session @@ -25,6 +24,7 @@ from core.trigger.debug import event_selectors from core.trigger.debug.event_bus import TriggerDebugEventBus from core.trigger.debug.event_selectors import PluginTriggerDebugEventPoller, WebhookTriggerDebugEventPoller from core.trigger.debug.events import PluginTriggerDebugEvent, build_plugin_pool_key +from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models.account import Account, Tenant from models.enums import AppTriggerStatus, AppTriggerType, CreatorUserRole, WorkflowTriggerStatus diff --git a/api/tests/unit_tests/controllers/console/app/test_audio.py b/api/tests/unit_tests/controllers/console/app/test_audio.py index c52bc02420..2d218dac7e 100644 --- a/api/tests/unit_tests/controllers/console/app/test_audio.py +++ b/api/tests/unit_tests/controllers/console/app/test_audio.py @@ -4,7 +4,6 @@ import io from types import SimpleNamespace import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.datastructures import FileStorage from werkzeug.exceptions import InternalServerError @@ -21,6 +20,7 @@ from controllers.console.app.error import ( UnsupportedAudioTypeError, ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from services.audio_service import AudioService from services.errors.app_model_config import AppModelConfigBrokenError from services.errors.audio import ( diff --git a/api/tests/unit_tests/controllers/console/app/test_conversation_api.py b/api/tests/unit_tests/controllers/console/app/test_conversation_api.py index 11b3b3470d..24b7e39f73 100644 --- a/api/tests/unit_tests/controllers/console/app/test_conversation_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_conversation_api.py @@ -33,12 +33,17 @@ def test_completion_conversation_list_returns_paginated_result(app, monkeypatch: monkeypatch.setattr(conversation_module, "parse_time_range", lambda *_args, **_kwargs: (None, None)) paginate_result = MagicMock() + paginate_result.page = 1 + paginate_result.per_page = 20 + paginate_result.total = 0 + paginate_result.has_next = False + paginate_result.items = [] monkeypatch.setattr(conversation_module.db, "paginate", lambda *_args, **_kwargs: paginate_result) with app.test_request_context("/console/api/apps/app-1/completion-conversations", method="GET"): response = method(app_model=SimpleNamespace(id="app-1")) - assert response is paginate_result + assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []} def test_completion_conversation_list_invalid_time_range(app, monkeypatch: pytest.MonkeyPatch) -> None: @@ -71,12 +76,17 @@ def test_chat_conversation_list_advanced_chat_calls_paginate(app, monkeypatch: p monkeypatch.setattr(conversation_module, "parse_time_range", lambda *_args, **_kwargs: (None, None)) paginate_result = MagicMock() + paginate_result.page = 1 + paginate_result.per_page = 20 + paginate_result.total = 0 + paginate_result.has_next = False + paginate_result.items = [] monkeypatch.setattr(conversation_module.db, "paginate", lambda *_args, **_kwargs: paginate_result) with app.test_request_context("/console/api/apps/app-1/chat-conversations", method="GET"): response = method(app_model=SimpleNamespace(id="app-1", mode=AppMode.ADVANCED_CHAT)) - assert response is paginate_result + assert response == {"page": 1, "limit": 20, "total": 0, "has_more": False, "data": []} def test_get_conversation_updates_read_at(monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py index 42b3420c31..1a412aff29 100644 --- a/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_conversation_variables_api.py @@ -5,10 +5,10 @@ from datetime import UTC, datetime from types import SimpleNamespace import pytest -from graphon.variables.types import SegmentType from pydantic import ValidationError from controllers.console.app import conversation_variables as conversation_variables_module +from graphon.variables.types import SegmentType def _unwrap(func): diff --git a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py index baac4cd4e0..1af15d8dc6 100644 --- a/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py +++ b/api/tests/unit_tests/controllers/console/app/test_mcp_server_response.py @@ -1,6 +1,25 @@ import datetime +from types import SimpleNamespace +from unittest.mock import PropertyMock, patch -from controllers.console.app.mcp_server import AppMCPServerResponse +from flask import Flask + +from controllers.console import console_ns +from controllers.console.app.mcp_server import AppMCPServerController, AppMCPServerResponse + + +def unwrap(func): + while hasattr(func, "__wrapped__"): + func = func.__wrapped__ + return func + + +class _ValidatedResponse: + def __init__(self, payload): + self._payload = payload + + def model_dump(self, mode="json"): + return self._payload class TestAppMCPServerResponse: @@ -40,6 +59,18 @@ class TestAppMCPServerResponse: resp = AppMCPServerResponse.model_validate(data) assert resp.parameters == {"already": "parsed"} + def test_parameters_json_array_parsed(self): + data = { + "id": "s1", + "name": "test", + "server_code": "code", + "description": "desc", + "status": "active", + "parameters": '["a", "b"]', + } + resp = AppMCPServerResponse.model_validate(data) + assert resp.parameters == ["a", "b"] + def test_timestamps_normalized(self): dt = datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC) data = { @@ -68,3 +99,40 @@ class TestAppMCPServerResponse: resp = AppMCPServerResponse.model_validate(data) assert resp.created_at is None assert resp.updated_at is None + + +class TestAppMCPServerController: + def test_get_returns_empty_dict_when_server_missing(self): + api = AppMCPServerController() + method = unwrap(api.get) + + with patch("controllers.console.app.mcp_server.db.session.scalar", return_value=None): + response = method(api, app_model=SimpleNamespace(id="app-1")) + + assert response == {} + + def test_post_returns_201(self): + api = AppMCPServerController() + method = unwrap(api.post) + payload = {"parameters": {"timeout": 30}} + app = Flask(__name__) + app.config["TESTING"] = True + + with ( + app.test_request_context("/", json=payload), + patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), + patch("controllers.console.app.mcp_server.current_account_with_tenant", return_value=(None, "tenant-1")), + patch("controllers.console.app.mcp_server.db.session.add"), + patch("controllers.console.app.mcp_server.db.session.commit"), + patch("controllers.console.app.mcp_server.AppMCPServer.generate_server_code", return_value="server-code"), + patch( + "controllers.console.app.mcp_server.AppMCPServerResponse.model_validate", + return_value=_ValidatedResponse({"id": "server-1"}), + ), + ): + response, status_code = method( + api, app_model=SimpleNamespace(id="app-1", name="Demo App", description="App description") + ) + + assert response == {"id": "server-1"} + assert status_code == 201 diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow.py b/api/tests/unit_tests/controllers/console/app/test_workflow.py index f32d0ef0ec..6ff3b19362 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow.py @@ -1,15 +1,16 @@ from __future__ import annotations +import json from datetime import datetime from types import SimpleNamespace from unittest.mock import Mock import pytest -from graphon.file import File, FileTransferMethod, FileType from werkzeug.exceptions import HTTPException, NotFound from controllers.console.app import workflow as workflow_module from controllers.console.app.error import DraftWorkflowNotExist, DraftWorkflowNotSync +from graphon.file import File, FileTransferMethod, FileType def _unwrap(func): @@ -347,3 +348,87 @@ def test_advanced_chat_run_conversation_not_exists(app, monkeypatch: pytest.Monk ): with pytest.raises(NotFound): handler(api, app_model=SimpleNamespace(id="app")) + + +def test_workflow_online_users_filters_inaccessible_workflow(app, monkeypatch: pytest.MonkeyPatch) -> None: + app_id_1 = "11111111-1111-1111-1111-111111111111" + app_id_2 = "22222222-2222-2222-2222-222222222222" + signed_avatar_url = "https://files.example.com/signed/avatar-1" + sign_avatar = Mock(return_value=signed_avatar_url) + monkeypatch.setattr(workflow_module, "current_account_with_tenant", lambda: (SimpleNamespace(), "tenant-1")) + monkeypatch.setattr( + workflow_module, + "WorkflowService", + lambda: SimpleNamespace(get_accessible_app_ids=lambda app_ids, tenant_id: {app_id_1}), + ) + monkeypatch.setattr(workflow_module.file_helpers, "get_signed_file_url", sign_avatar) + + workflow_module.redis_client.hgetall.side_effect = lambda key: ( + { + b"sid-1": json.dumps( + { + "user_id": "u-1", + "username": "Alice", + "avatar": "avatar-file-id", + "sid": "sid-1", + } + ) + } + if key == f"{workflow_module.WORKFLOW_ONLINE_USERS_PREFIX}{app_id_1}" + else {} + ) + + api = workflow_module.WorkflowOnlineUsersApi() + handler = _unwrap(api.get) + + with app.test_request_context( + f"/apps/workflows/online-users?app_ids={app_id_1},{app_id_2}", + method="GET", + ): + response = handler(api) + + assert response == { + "data": [ + { + "app_id": app_id_1, + "users": [ + { + "user_id": "u-1", + "username": "Alice", + "avatar": signed_avatar_url, + "sid": "sid-1", + } + ], + } + ] + } + workflow_module.redis_client.hgetall.assert_called_once_with( + f"{workflow_module.WORKFLOW_ONLINE_USERS_PREFIX}{app_id_1}" + ) + sign_avatar.assert_called_once_with("avatar-file-id") + + +def test_workflow_online_users_rejects_excessive_workflow_ids(app, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(workflow_module, "current_account_with_tenant", lambda: (SimpleNamespace(), "tenant-1")) + accessible_app_ids = Mock(return_value=set()) + monkeypatch.setattr( + workflow_module, + "WorkflowService", + lambda: SimpleNamespace(get_accessible_app_ids=accessible_app_ids), + ) + + excessive_ids = ",".join(f"wf-{index}" for index in range(workflow_module.MAX_WORKFLOW_ONLINE_USERS_QUERY_IDS + 1)) + + api = workflow_module.WorkflowOnlineUsersApi() + handler = _unwrap(api.get) + + with app.test_request_context( + f"/apps/workflows/online-users?app_ids={excessive_ids}", + method="GET", + ): + with pytest.raises(HTTPException) as exc: + handler(api) + + assert exc.value.code == 400 + assert "Maximum" in exc.value.description + accessible_app_ids.assert_not_called() diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py index 2adb69c704..a9853f25b0 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_app_log_api.py @@ -2,9 +2,8 @@ from __future__ import annotations from datetime import UTC, datetime -from graphon.enums import WorkflowExecutionStatus - from controllers.console.app import workflow_app_log as workflow_app_log_module +from graphon.enums import WorkflowExecutionStatus def test_workflow_app_log_query_parses_bool_and_datetime(): diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py new file mode 100644 index 0000000000..85afcf0e60 --- /dev/null +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_comment_api.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +from contextlib import nullcontext +from dataclasses import dataclass +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import MagicMock, PropertyMock, patch + +import pytest +from flask import Flask +from werkzeug.exceptions import Forbidden + +from controllers.console import console_ns +from controllers.console import wraps as console_wraps +from controllers.console.app import workflow_comment as workflow_comment_module +from controllers.console.app import wraps as app_wraps +from libs import login as login_lib +from models.account import Account, AccountStatus, TenantAccountRole + + +def _make_account(role: TenantAccountRole) -> Account: + account = Account(name="tester", email="tester@example.com") + account.status = AccountStatus.ACTIVE + account.role = role + account.id = "account-123" # type: ignore[assignment] + account._current_tenant = SimpleNamespace(id="tenant-123") # type: ignore[attr-defined] + account._get_current_object = lambda: account # type: ignore[attr-defined] + return account + + +def _make_app() -> SimpleNamespace: + return SimpleNamespace(id="app-123", tenant_id="tenant-123", status="normal", mode="workflow") + + +def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app_model: SimpleNamespace) -> None: + monkeypatch.setattr(login_lib.dify_config, "LOGIN_DISABLED", True) + monkeypatch.setattr(login_lib, "current_user", account) + monkeypatch.setattr(login_lib, "current_account_with_tenant", lambda: (account, account.current_tenant_id)) + monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None) + monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id)) + monkeypatch.setattr(console_wraps.dify_config, "EDITION", "CLOUD") + monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id)) + monkeypatch.setattr(app_wraps, "_load_app_model", lambda _app_id: app_model) + monkeypatch.setattr(workflow_comment_module, "current_user", account) + + +def _patch_write_services(monkeypatch: pytest.MonkeyPatch) -> None: + for method_name in ( + "create_comment", + "update_comment", + "delete_comment", + "resolve_comment", + "validate_comment_access", + "create_reply", + "update_reply", + "delete_reply", + ): + monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, method_name, MagicMock()) + + +def _patch_payload(payload: dict[str, object] | None): + if payload is None: + return nullcontext() + return patch.object( + type(console_ns), + "payload", + new_callable=PropertyMock, + return_value=payload, + ) + + +@dataclass(frozen=True) +class WriteCase: + resource_cls: type + method_name: str + path: str + kwargs: dict[str, str] + payload: dict[str, object] | None = None + + +@pytest.mark.parametrize( + "case", + [ + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentListApi, + method_name="post", + path="/console/api/apps/app-123/workflow/comments", + kwargs={"app_id": "app-123"}, + payload={"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentDetailApi, + method_name="put", + path="/console/api/apps/app-123/workflow/comments/comment-1", + kwargs={"app_id": "app-123", "comment_id": "comment-1"}, + payload={"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentDetailApi, + method_name="delete", + path="/console/api/apps/app-123/workflow/comments/comment-1", + kwargs={"app_id": "app-123", "comment_id": "comment-1"}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentResolveApi, + method_name="post", + path="/console/api/apps/app-123/workflow/comments/comment-1/resolve", + kwargs={"app_id": "app-123", "comment_id": "comment-1"}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentReplyApi, + method_name="post", + path="/console/api/apps/app-123/workflow/comments/comment-1/replies", + kwargs={"app_id": "app-123", "comment_id": "comment-1"}, + payload={"content": "reply", "mentioned_user_ids": []}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentReplyDetailApi, + method_name="put", + path="/console/api/apps/app-123/workflow/comments/comment-1/replies/reply-1", + kwargs={"app_id": "app-123", "comment_id": "comment-1", "reply_id": "reply-1"}, + payload={"content": "reply", "mentioned_user_ids": []}, + ), + WriteCase( + resource_cls=workflow_comment_module.WorkflowCommentReplyDetailApi, + method_name="delete", + path="/console/api/apps/app-123/workflow/comments/comment-1/replies/reply-1", + kwargs={"app_id": "app-123", "comment_id": "comment-1", "reply_id": "reply-1"}, + ), + ], +) +def test_write_endpoints_require_edit_permission(app: Flask, monkeypatch: pytest.MonkeyPatch, case: WriteCase) -> None: + app.config.setdefault("RESTX_MASK_HEADER", "X-Fields") + account = _make_account(TenantAccountRole.NORMAL) + app_model = _make_app() + _patch_console_guards(monkeypatch, account, app_model) + _patch_write_services(monkeypatch) + + with app.test_request_context(case.path, method=case.method_name.upper(), json=case.payload): + with _patch_payload(case.payload): + handler = getattr(case.resource_cls(), case.method_name) + with pytest.raises(Forbidden): + handler(**case.kwargs) + + +def test_create_comment_allows_editor(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None: + app.config.setdefault("RESTX_MASK_HEADER", "X-Fields") + account = _make_account(TenantAccountRole.EDITOR) + app_model = _make_app() + _patch_console_guards(monkeypatch, account, app_model) + + create_comment_mock = MagicMock(return_value={"id": "comment-1"}) + monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "create_comment", create_comment_mock) + payload = {"content": "hello", "position_x": 1.0, "position_y": 2.0, "mentioned_user_ids": []} + + with app.test_request_context("/console/api/apps/app-123/workflow/comments", method="POST", json=payload): + with _patch_payload(payload): + result = workflow_comment_module.WorkflowCommentListApi().post(app_id="app-123") + + if isinstance(result, tuple): + response = result[0] + else: + response = result + assert response["id"] == "comment-1" + create_comment_mock.assert_called_once_with( + tenant_id="tenant-123", + app_id="app-123", + created_by="account-123", + content="hello", + position_x=1.0, + position_y=2.0, + mentioned_user_ids=[], + ) + + +def test_update_comment_omits_mentions_when_payload_does_not_include_them( + app: Flask, monkeypatch: pytest.MonkeyPatch +) -> None: + app.config.setdefault("RESTX_MASK_HEADER", "X-Fields") + account = _make_account(TenantAccountRole.EDITOR) + app_model = _make_app() + _patch_console_guards(monkeypatch, account, app_model) + + update_comment_mock = MagicMock(return_value={"id": "comment-1", "updated_at": datetime(2024, 1, 1, 12, 0, 0)}) + monkeypatch.setattr(workflow_comment_module.WorkflowCommentService, "update_comment", update_comment_mock) + payload = {"content": "hello", "position_x": 10.0, "position_y": 20.0} + + with app.test_request_context("/console/api/apps/app-123/workflow/comments/comment-1", method="PUT", json=payload): + with _patch_payload(payload): + workflow_comment_module.WorkflowCommentDetailApi().put(app_id="app-123", comment_id="comment-1") + + update_comment_mock.assert_called_once_with( + tenant_id="tenant-123", + app_id="app-123", + comment_id="comment-1", + user_id="account-123", + content="hello", + position_x=10.0, + position_y=20.0, + mentioned_user_ids=None, + ) diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py index e11102acb1..c4a8148446 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_pause_details_api.py @@ -6,14 +6,14 @@ from unittest.mock import Mock import pytest from flask import Flask -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus -from graphon.nodes.human_input.entities import FormInput, UserAction -from graphon.nodes.human_input.enums import FormInputType from controllers.console import wraps as console_wraps from controllers.console.app import workflow_run as workflow_run_module from controllers.web.error import NotFoundError +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus +from graphon.nodes.human_input.entities import FormInput, UserAction +from graphon.nodes.human_input.enums import FormInputType from libs import login as login_lib from models.account import Account, AccountStatus, TenantAccountRole from models.workflow import WorkflowRun diff --git a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py index 740da1f1df..b19a1740eb 100644 --- a/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py +++ b/api/tests/unit_tests/controllers/console/app/workflow_draft_variables_test.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch import pytest from flask_restx import marshal -from graphon.variables.types import SegmentType from controllers.console.app.workflow_draft_variable import ( _WORKFLOW_DRAFT_VARIABLE_FIELDS, @@ -16,6 +15,7 @@ from controllers.console.app.workflow_draft_variable import ( ) from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID from factories.variable_factory import build_segment +from graphon.variables.types import SegmentType from libs.datetime_utils import naive_utc_now from libs.uuid_utils import uuidv7 from models.workflow import WorkflowDraftVariable, WorkflowDraftVariableFile diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py index 9c9f8da87c..5136922e88 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_datasource_auth.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from werkzeug.exceptions import Forbidden, NotFound from controllers.console import console_ns @@ -18,6 +17,7 @@ from controllers.console.datasets.rag_pipeline.datasource_auth import ( DatasourceUpdateProviderNameApi, ) from core.plugin.impl.oauth import OAuthHandler +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from services.datasource_provider_service import DatasourceProviderService from services.plugin.oauth_service import OAuthProxyService diff --git a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py index 6ef8ccfdbd..63950736c5 100644 --- a/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py +++ b/api/tests/unit_tests/controllers/console/datasets/rag_pipeline/test_rag_pipeline_draft_variable.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch import pytest from flask import Response -from graphon.variables.types import SegmentType from controllers.console import console_ns from controllers.console.app.error import DraftWorkflowNotExist @@ -16,6 +15,7 @@ from controllers.console.datasets.rag_pipeline.rag_pipeline_draft_variable impor ) from controllers.web.error import InvalidArgumentError, NotFoundError from core.workflow.variable_prefixes import SYSTEM_VARIABLE_NODE_ID +from graphon.variables.types import SegmentType from models.account import Account diff --git a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py index ce2278de4f..d9b02ac453 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_datasets_document.py @@ -1,3 +1,4 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest @@ -215,17 +216,23 @@ class TestDatasetDocumentListApi: method = unwrap(api.post) payload = {"indexing_technique": "economy"} + created_dataset = SimpleNamespace(id="ds-1", name="Dataset", indexing_technique="economy") + created_document = SimpleNamespace(id="doc-1", name="Document", doc_metadata_details=None) with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", payload), + patch( + "controllers.console.datasets.datasets_document.DatasetService.get_dataset", + return_value=created_dataset, + ), patch( "controllers.console.datasets.datasets_document.DocumentService.document_create_args_validate", return_value=None, ), patch( "controllers.console.datasets.datasets_document.DocumentService.save_document_with_dataset_id", - return_value=([MagicMock()], "batch-1"), + return_value=([created_document], "batch-1"), ), ): response = method(api, "ds-1") diff --git a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py index 710c9be684..e4acd91b76 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services @@ -21,6 +20,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.errors.invoke import InvokeError from models.account import Account from services.dataset_service import DatasetService from services.hit_testing_service import HitTestingService diff --git a/api/tests/unit_tests/controllers/console/explore/test_audio.py b/api/tests/unit_tests/controllers/console/explore/test_audio.py index 66c9ba48c5..b4b57022e2 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_audio.py +++ b/api/tests/unit_tests/controllers/console/explore/test_audio.py @@ -2,7 +2,6 @@ from io import BytesIO from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError import controllers.console.explore.audio as audio_module @@ -20,6 +19,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.errors.invoke import InvokeError from services.errors.audio import ( AudioTooLargeServiceError, NoAudioUploadedServiceError, diff --git a/api/tests/unit_tests/controllers/console/explore/test_message.py b/api/tests/unit_tests/controllers/console/explore/test_message.py index 2e4ca4f2a4..145cc9cdd7 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_message.py +++ b/api/tests/unit_tests/controllers/console/explore/test_message.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import InternalServerError, NotFound import controllers.console.explore.message as module @@ -22,6 +21,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.errors.invoke import InvokeError from services.errors.conversation import ConversationNotExistsError from services.errors.message import ( FirstMessageNotExistsError, diff --git a/api/tests/unit_tests/controllers/console/explore/test_trial.py b/api/tests/unit_tests/controllers/console/explore/test_trial.py index 04beb31389..3625056af9 100644 --- a/api/tests/unit_tests/controllers/console/explore/test_trial.py +++ b/api/tests/unit_tests/controllers/console/explore/test_trial.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import controllers.console.explore.trial as module @@ -26,6 +25,7 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.errors.invoke import InvokeError from models import Account from models.account import TenantStatus from models.model import AppMode @@ -94,7 +94,7 @@ class TestTrialAppWorkflowRunApi: with app.test_request_context("/"): with pytest.raises(NotWorkflowAppError): - method(MagicMock(mode=AppMode.CHAT)) + method(api, MagicMock(mode=AppMode.CHAT)) def test_success(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -106,7 +106,7 @@ class TestTrialAppWorkflowRunApi: patch.object(module.AppGenerateService, "generate", return_value=MagicMock()), patch.object(module.RecommendedAppService, "add_trial_app_record"), ): - result = method(trial_app_workflow) + result = method(api, trial_app_workflow) assert result is not None @@ -124,7 +124,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderNotInitializeError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_quota_exceeded(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -140,7 +140,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderQuotaExceededError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_model_not_support(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -156,7 +156,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ProviderModelCurrentlyNotSupportError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_invoke_error(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -172,7 +172,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(CompletionRequestError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_rate_limit_error(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -188,7 +188,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(InvokeRateLimitHttpError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_value_error(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -204,7 +204,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(ValueError): - method(trial_app_workflow) + method(api, trial_app_workflow) def test_workflow_generic_exception(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowRunApi() @@ -220,7 +220,7 @@ class TestTrialAppWorkflowRunApi: ), ): with pytest.raises(InternalServerError): - method(trial_app_workflow) + method(api, trial_app_workflow) class TestTrialChatApi: @@ -566,7 +566,7 @@ class TestTrialMessageSuggestedQuestionApi: with app.test_request_context("/"): with pytest.raises(NotChatAppError): - method(api, MagicMock(mode="completion"), str(uuid4())) + method(MagicMock(mode="completion"), str(uuid4())) def test_success(self, app, trial_app_chat, account): api = module.TrialMessageSuggestedQuestionApi() @@ -581,7 +581,7 @@ class TestTrialMessageSuggestedQuestionApi: return_value=["q1", "q2"], ), ): - result = method(api, trial_app_chat, str(uuid4())) + result = method(trial_app_chat, str(uuid4())) assert result == {"data": ["q1", "q2"]} @@ -599,7 +599,7 @@ class TestTrialMessageSuggestedQuestionApi: ), ): with pytest.raises(NotFound): - method(api, trial_app_chat, str(uuid4())) + method(trial_app_chat, str(uuid4())) class TestTrialAppParameterApi: @@ -931,7 +931,7 @@ class TestTrialAppWorkflowTaskStopApi: with app.test_request_context("/"): with pytest.raises(NotWorkflowAppError): - method(trial_app_chat, str(uuid4())) + method(api, trial_app_chat, str(uuid4())) def test_success(self, app, trial_app_workflow, account): api = module.TrialAppWorkflowTaskStopApi() @@ -944,7 +944,7 @@ class TestTrialAppWorkflowTaskStopApi: patch.object(module.AppQueueManager, "set_stop_flag_no_user_check") as mock_set_flag, patch.object(module.GraphEngineManager, "send_stop_command") as mock_send_cmd, ): - result = method(trial_app_workflow, task_id) + result = method(api, trial_app_workflow, task_id) assert result == {"result": "success"} mock_set_flag.assert_called_once_with(task_id) diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py index e89b89c8b1..2be5a21f28 100644 --- a/api/tests/unit_tests/controllers/console/tag/test_tags.py +++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py @@ -1,9 +1,11 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, PropertyMock, patch import pytest from flask import Flask from werkzeug.exceptions import Forbidden +import controllers.console.tag.tags as module from controllers.console import console_ns from controllers.console.tag.tags import ( TagBindingCreateApi, @@ -83,13 +85,20 @@ class TestTagListApi: ), patch( "controllers.console.tag.tags.TagService.get_tags", - return_value=[{"id": "1", "name": "tag"}], + return_value=[ + SimpleNamespace( + id="1", + name="tag", + type=TagType.KNOWLEDGE, + binding_count=1, + ) + ], ), ): result, status = method(api) assert status == 200 - assert isinstance(result, list) + assert result == [{"id": "1", "name": "tag", "type": "knowledge", "binding_count": "1"}] def test_post_success(self, app, admin_user, tag, payload_patch): api = TagListApi() @@ -113,6 +122,7 @@ class TestTagListApi: assert status == 200 assert result["name"] == "test-tag" + assert result["binding_count"] == "0" def test_post_forbidden(self, app, readonly_user, payload_patch): api = TagListApi() @@ -158,7 +168,7 @@ class TestTagUpdateDeleteApi: result, status = method(api, "tag-1") assert status == 200 - assert result["binding_count"] == 3 + assert result["binding_count"] == "3" def test_patch_forbidden(self, app, readonly_user, payload_patch): api = TagUpdateDeleteApi() @@ -277,3 +287,13 @@ class TestTagBindingDeleteApi: ): with pytest.raises(Forbidden): method(api) + + +class TestTagResponseModel: + def test_tag_response_normalizes_enum_type(self): + payload = module.TagResponse.model_validate( + {"id": "tag-1", "name": "tag", "type": TagType.KNOWLEDGE, "binding_count": 1} + ).model_dump(mode="json") + + assert payload["type"] == "knowledge" + assert payload["binding_count"] == "1" diff --git a/api/tests/unit_tests/controllers/console/test_workspace_account.py b/api/tests/unit_tests/controllers/console/test_workspace_account.py index dd643faac9..c513be950b 100644 --- a/api/tests/unit_tests/controllers/console/test_workspace_account.py +++ b/api/tests/unit_tests/controllers/console/test_workspace_account.py @@ -11,7 +11,7 @@ from controllers.console.workspace.account import ( ChangeEmailSendEmailApi, CheckEmailUnique, ) -from models import Account +from models import Account, AccountStatus from services.account_service import AccountService @@ -33,7 +33,7 @@ def _build_account(email: str, account_id: str = "acc", tenant: object | None = account = Account(name=account_id, email=email) account.email = email account.id = account_id - account.status = "active" + account.status = AccountStatus.ACTIVE account._current_tenant = tenant_obj return account diff --git a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py index 9c42ee9529..b2f949c6e2 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_load_balancing_config.py @@ -11,9 +11,10 @@ from unittest.mock import MagicMock import pytest from flask import Flask from flask.views import MethodView +from werkzeug.exceptions import Forbidden + from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.errors.validate import CredentialsValidateFailedError -from werkzeug.exceptions import Forbidden if not hasattr(builtins, "MethodView"): builtins.MethodView = MethodView # type: ignore[attr-defined] diff --git a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py index fb9eec98cb..168479af1e 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_model_providers.py @@ -1,7 +1,6 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic_core import ValidationError from werkzeug.exceptions import Forbidden @@ -14,6 +13,7 @@ from controllers.console.workspace.model_providers import ( ModelProviderValidateApi, PreferredProviderTypeUpdateApi, ) +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError VALID_UUID = "123e4567-e89b-12d3-a456-426614174000" INVALID_UUID = "123" diff --git a/api/tests/unit_tests/controllers/console/workspace/test_models.py b/api/tests/unit_tests/controllers/console/workspace/test_models.py index c829327bc7..f0d32f81fb 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_models.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_models.py @@ -2,8 +2,6 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from controllers.console.workspace.models import ( DefaultModelApi, @@ -16,6 +14,8 @@ from controllers.console.workspace.models import ( ModelProviderModelParameterRuleApi, ModelProviderModelValidateApi, ) +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError def unwrap(func): diff --git a/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py b/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py index b7419009f0..71381e6a2b 100644 --- a/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py +++ b/api/tests/unit_tests/controllers/inner_api/app/test_dsl.py @@ -18,6 +18,7 @@ from controllers.inner_api.app.dsl import ( InnerAppDSLImportPayload, _get_active_account, ) +from models.account import AccountStatus from services.app_dsl_service import ImportStatus @@ -63,7 +64,7 @@ class TestGetActiveAccount: @patch("controllers.inner_api.app.dsl.db") def test_returns_active_account(self, mock_db): mock_account = MagicMock() - mock_account.status = "active" + mock_account.status = AccountStatus.ACTIVE mock_db.session.scalar.return_value = mock_account result = _get_active_account("user@example.com") @@ -74,7 +75,7 @@ class TestGetActiveAccount: @patch("controllers.inner_api.app.dsl.db") def test_returns_none_for_inactive_account(self, mock_db): mock_account = MagicMock() - mock_account.status = "banned" + mock_account.status = AccountStatus.BANNED mock_db.session.scalar.return_value = mock_account result = _get_active_account("banned@example.com") diff --git a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py index 0895fac3a4..d1b09c3a58 100644 --- a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py +++ b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py @@ -41,17 +41,22 @@ class TestTenantUserPayload: class TestGetUser: """Test get_user function""" + @patch("controllers.inner_api.plugin.wraps.select") @patch("controllers.inner_api.plugin.wraps.EndUser") @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") - def test_should_return_existing_user_by_id(self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask): + def test_should_return_existing_user_by_id( + self, mock_db, mock_sessionmaker, mock_enduser_class, mock_select, app: Flask + ): """Test returning existing user when found by ID""" # Arrange mock_user = MagicMock() mock_user.id = "user123" mock_session = MagicMock() mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session - mock_session.get.return_value = mock_user + mock_session.scalar.return_value = mock_user + mock_query = MagicMock() + mock_select.return_value.where.return_value.limit.return_value = mock_query # Act with app.app_context(): @@ -59,13 +64,45 @@ class TestGetUser: # Assert assert result == mock_user - mock_session.get.assert_called_once() + mock_session.scalar.assert_called_once() + @patch("controllers.inner_api.plugin.wraps.select") + @patch("controllers.inner_api.plugin.wraps.EndUser") + @patch("controllers.inner_api.plugin.wraps.sessionmaker") + @patch("controllers.inner_api.plugin.wraps.db") + def test_should_not_resolve_non_anonymous_users_across_tenants( + self, + mock_db, + mock_sessionmaker, + mock_enduser_class, + mock_select, + app: Flask, + ): + """Test that explicit user IDs remain scoped to the current tenant.""" + # Arrange + mock_session = MagicMock() + mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session + mock_session.scalar.return_value = None + mock_new_user = MagicMock() + mock_new_user.tenant_id = "tenant-current" + mock_enduser_class.return_value = mock_new_user + + # Act + with app.app_context(): + result = get_user("tenant-current", "foreign-user-id") + + # Assert + assert result == mock_new_user + mock_session.get.assert_not_called() + mock_session.scalar.assert_called_once() + mock_session.add.assert_called_once_with(mock_new_user) + + @patch("controllers.inner_api.plugin.wraps.select") @patch("controllers.inner_api.plugin.wraps.EndUser") @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") def test_should_return_existing_anonymous_user_by_session_id( - self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask + self, mock_db, mock_sessionmaker, mock_enduser_class, mock_select, app: Flask ): """Test returning existing anonymous user by session_id""" # Arrange @@ -73,8 +110,9 @@ class TestGetUser: mock_user.session_id = "anonymous_session" mock_session = MagicMock() mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session - # non-anonymous path uses session.get(); anonymous uses session.scalar() - mock_session.get.return_value = mock_user + mock_session.scalar.return_value = mock_user + mock_query = MagicMock() + mock_select.return_value.where.return_value.limit.return_value = mock_query # Act with app.app_context(): @@ -83,17 +121,22 @@ class TestGetUser: # Assert assert result == mock_user + @patch("controllers.inner_api.plugin.wraps.select") @patch("controllers.inner_api.plugin.wraps.EndUser") @patch("controllers.inner_api.plugin.wraps.sessionmaker") @patch("controllers.inner_api.plugin.wraps.db") - def test_should_create_new_user_when_not_found(self, mock_db, mock_sessionmaker, mock_enduser_class, app: Flask): + def test_should_create_new_user_when_not_found( + self, mock_db, mock_sessionmaker, mock_enduser_class, mock_select, app: Flask + ): """Test creating new user when not found in database""" # Arrange mock_session = MagicMock() mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session - mock_session.get.return_value = None + mock_session.scalar.return_value = None mock_new_user = MagicMock() mock_enduser_class.return_value = mock_new_user + mock_query = MagicMock() + mock_select.return_value.where.return_value.limit.return_value = mock_query # Act with app.app_context(): @@ -134,7 +177,7 @@ class TestGetUser: # Arrange mock_session = MagicMock() mock_sessionmaker.return_value.begin.return_value.__enter__.return_value = mock_session - mock_session.get.side_effect = Exception("Database error") + mock_session.scalar.side_effect = Exception("Database error") # Act & Assert with app.app_context(): diff --git a/api/tests/unit_tests/controllers/inner_api/workspace/test_workspace.py b/api/tests/unit_tests/controllers/inner_api/workspace/test_workspace.py index 56a8f94963..7d2193adc6 100644 --- a/api/tests/unit_tests/controllers/inner_api/workspace/test_workspace.py +++ b/api/tests/unit_tests/controllers/inner_api/workspace/test_workspace.py @@ -20,6 +20,7 @@ from controllers.inner_api.workspace.workspace import ( WorkspaceCreatePayload, WorkspaceOwnerlessPayload, ) +from models.account import TenantStatus class TestWorkspaceCreatePayload: @@ -98,7 +99,7 @@ class TestEnterpriseWorkspace: mock_tenant.id = "tenant-id" mock_tenant.name = "My Workspace" mock_tenant.plan = "sandbox" - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_tenant.created_at = now mock_tenant.updated_at = now mock_tenant_svc.create_tenant.return_value = mock_tenant @@ -162,7 +163,7 @@ class TestEnterpriseWorkspaceNoOwnerEmail: mock_tenant.name = "My Workspace" mock_tenant.encrypt_public_key = "pub-key" mock_tenant.plan = "sandbox" - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_tenant.custom_config = None mock_tenant.created_at = now mock_tenant.updated_at = now diff --git a/api/tests/unit_tests/controllers/service_api/app/test_app.py b/api/tests/unit_tests/controllers/service_api/app/test_app.py index 1507bf7a5f..f48ace427d 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_app.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_app.py @@ -10,6 +10,7 @@ from flask import Flask from controllers.service_api.app.app import AppInfoApi, AppMetaApi, AppParameterApi from controllers.service_api.app.error import AppUnavailableError +from models.account import TenantStatus from models.model import App, AppMode from tests.unit_tests.conftest import setup_mock_tenant_account_query @@ -62,7 +63,7 @@ class TestAppParameterApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL # Mock DB queries for app and tenant mock_db.session.get.side_effect = [ @@ -110,7 +111,7 @@ class TestAppParameterApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app_model, @@ -151,7 +152,7 @@ class TestAppParameterApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app_model, @@ -190,7 +191,7 @@ class TestAppParameterApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app_model, @@ -253,7 +254,7 @@ class TestAppMetaApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app_model, @@ -321,7 +322,7 @@ class TestAppInfoApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app_model, @@ -378,7 +379,7 @@ class TestAppInfoApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app, @@ -424,7 +425,7 @@ class TestAppInfoApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app, @@ -476,7 +477,7 @@ class TestAppInfoApi: mock_validate_token.return_value = mock_api_token mock_tenant = Mock() - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_db.session.get.side_effect = [ mock_app, diff --git a/api/tests/unit_tests/controllers/service_api/app/test_audio.py b/api/tests/unit_tests/controllers/service_api/app/test_audio.py index a26fea8fbd..c16ebad739 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_audio.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_audio.py @@ -13,7 +13,6 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeError from werkzeug.datastructures import FileStorage from werkzeug.exceptions import InternalServerError @@ -30,6 +29,7 @@ from controllers.service_api.app.error import ( UnsupportedAudioTypeError, ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from services.audio_service import AudioService from services.errors.app_model_config import AppModelConfigBrokenError from services.errors.audio import ( diff --git a/api/tests/unit_tests/controllers/service_api/app/test_completion.py b/api/tests/unit_tests/controllers/service_api/app/test_completion.py index 57681d8f5b..3364c07e62 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_completion.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_completion.py @@ -16,7 +16,6 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeError from pydantic import ValidationError from werkzeug.exceptions import BadRequest, NotFound @@ -35,6 +34,7 @@ from controllers.service_api.app.error import ( NotChatAppError, ) from core.errors.error import QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from models.model import App, AppMode, EndUser from services.app_generate_service import AppGenerateService from services.app_task_service import AppTaskService diff --git a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py index 97fdf1a011..14c35a9ed5 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_conversation.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_conversation.py @@ -20,7 +20,6 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest -from graphon.variables.types import SegmentType from werkzeug.exceptions import BadRequest, NotFound import services @@ -38,6 +37,7 @@ from controllers.service_api.app.conversation import ( ConversationVariableUpdatePayload, ) from controllers.service_api.app.error import NotChatAppError +from graphon.variables.types import SegmentType from models.model import App, AppMode, EndUser from services.conversation_service import ConversationService from services.errors.conversation import ( diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py index 74a3c75839..da09ec13ce 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow.py @@ -20,7 +20,6 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest -from graphon.enums import WorkflowExecutionStatus from werkzeug.exceptions import BadRequest, NotFound from controllers.service_api.app.error import NotWorkflowAppError @@ -37,6 +36,7 @@ from controllers.service_api.app.workflow import ( WorkflowTaskStopApi, ) from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError +from graphon.enums import WorkflowExecutionStatus from models.model import App, AppMode from services.app_generate_service import AppGenerateService from services.errors.app import IsDraftWorkflowError, WorkflowNotFoundError diff --git a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py index 4b8e3a738c..eda270258d 100644 --- a/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py +++ b/api/tests/unit_tests/controllers/service_api/app/test_workflow_fields.py @@ -1,8 +1,7 @@ from types import SimpleNamespace -from graphon.enums import WorkflowExecutionStatus - from controllers.service_api.app.workflow import WorkflowRunOutputsField, WorkflowRunStatusField +from graphon.enums import WorkflowExecutionStatus def test_workflow_run_status_field_with_enum() -> None: diff --git a/api/tests/unit_tests/controllers/web/test_audio.py b/api/tests/unit_tests/controllers/web/test_audio.py index cbfc8fa613..a6ca441801 100644 --- a/api/tests/unit_tests/controllers/web/test_audio.py +++ b/api/tests/unit_tests/controllers/web/test_audio.py @@ -8,7 +8,6 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask -from graphon.model_runtime.errors.invoke import InvokeError from controllers.web.audio import AudioApi, TextApi from controllers.web.error import ( @@ -22,6 +21,7 @@ from controllers.web.error import ( UnsupportedAudioTypeError, ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError from services.errors.audio import ( AudioTooLargeServiceError, NoAudioUploadedServiceError, diff --git a/api/tests/unit_tests/controllers/web/test_completion.py b/api/tests/unit_tests/controllers/web/test_completion.py index 49039d03fe..4f8d848637 100644 --- a/api/tests/unit_tests/controllers/web/test_completion.py +++ b/api/tests/unit_tests/controllers/web/test_completion.py @@ -7,7 +7,6 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask -from graphon.model_runtime.errors.invoke import InvokeError from controllers.web.completion import ChatApi, ChatStopApi, CompletionApi, CompletionStopApi from controllers.web.error import ( @@ -19,6 +18,7 @@ from controllers.web.error import ( ProviderQuotaExceededError, ) from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeError def _completion_app() -> SimpleNamespace: diff --git a/api/tests/unit_tests/core/agent/test_cot_agent_runner.py b/api/tests/unit_tests/core/agent/test_cot_agent_runner.py index bc7aea0ef9..cde8820e00 100644 --- a/api/tests/unit_tests/core/agent/test_cot_agent_runner.py +++ b/api/tests/unit_tests/core/agent/test_cot_agent_runner.py @@ -2,11 +2,11 @@ import json from unittest.mock import MagicMock import pytest -from graphon.model_runtime.entities.llm_entities import LLMUsage from core.agent.cot_agent_runner import CotAgentRunner from core.agent.entities import AgentScratchpadUnit from core.agent.errors import AgentMaxIterationError +from graphon.model_runtime.entities.llm_entities import LLMUsage class DummyRunner(CotAgentRunner): diff --git a/api/tests/unit_tests/core/agent/test_cot_chat_agent_runner.py b/api/tests/unit_tests/core/agent/test_cot_chat_agent_runner.py index 97206019b9..ea8cc8aa86 100644 --- a/api/tests/unit_tests/core/agent/test_cot_chat_agent_runner.py +++ b/api/tests/unit_tests/core/agent/test_cot_chat_agent_runner.py @@ -1,9 +1,9 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from core.agent.cot_chat_agent_runner import CotChatAgentRunner +from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from tests.unit_tests.core.agent.conftest import ( DummyAgentConfig, DummyAppConfig, diff --git a/api/tests/unit_tests/core/agent/test_cot_completion_agent_runner.py b/api/tests/unit_tests/core/agent/test_cot_completion_agent_runner.py index defc8b4b64..2f5873d865 100644 --- a/api/tests/unit_tests/core/agent/test_cot_completion_agent_runner.py +++ b/api/tests/unit_tests/core/agent/test_cot_completion_agent_runner.py @@ -1,6 +1,8 @@ import json import pytest + +from core.agent.cot_completion_agent_runner import CotCompletionAgentRunner from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, ImagePromptMessageContent, @@ -8,8 +10,6 @@ from graphon.model_runtime.entities.message_entities import ( UserPromptMessage, ) -from core.agent.cot_completion_agent_runner import CotCompletionAgentRunner - # ----------------------------- # Fixtures # ----------------------------- diff --git a/api/tests/unit_tests/core/agent/test_fc_agent_runner.py b/api/tests/unit_tests/core/agent/test_fc_agent_runner.py index a44a0650eb..17ab5babcb 100644 --- a/api/tests/unit_tests/core/agent/test_fc_agent_runner.py +++ b/api/tests/unit_tests/core/agent/test_fc_agent_runner.py @@ -3,6 +3,11 @@ from typing import Any from unittest.mock import MagicMock import pytest + +from core.agent.errors import AgentMaxIterationError +from core.agent.fc_agent_runner import FunctionCallAgentRunner +from core.app.apps.base_app_queue_manager import PublishFrom +from core.app.entities.queue_entities import QueueMessageFileEvent from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.model_runtime.entities.message_entities import ( DocumentPromptMessageContent, @@ -11,11 +16,6 @@ from graphon.model_runtime.entities.message_entities import ( UserPromptMessage, ) -from core.agent.errors import AgentMaxIterationError -from core.agent.fc_agent_runner import FunctionCallAgentRunner -from core.app.apps.base_app_queue_manager import PublishFrom -from core.app.entities.queue_entities import QueueMessageFileEvent - # ============================== # Dummy Helper Classes # ============================== diff --git a/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_model_config_converter.py b/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_model_config_converter.py index 5ee66da94a..186b4a501d 100644 --- a/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_model_config_converter.py +++ b/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_model_config_converter.py @@ -2,8 +2,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.model_entities import ModelPropertyKey from core.app.app_config.easy_ui_based_app.model_config.converter import ModelConfigConverter from core.entities.model_entities import ModelStatus @@ -12,6 +10,8 @@ from core.errors.error import ( ProviderTokenNotInitError, QuotaExceededError, ) +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.model_entities import ModelPropertyKey class TestModelConfigConverter: diff --git a/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_variables_manager.py b/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_variables_manager.py index e2f3c16335..d9fe7004ff 100644 --- a/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_variables_manager.py +++ b/api/tests/unit_tests/core/app/app_config/easy_ui_based_app/test_variables_manager.py @@ -1,9 +1,9 @@ import pytest -from graphon.variables.input_entities import VariableEntityType from core.app.app_config.easy_ui_based_app.variables.manager import ( BasicVariablesConfigManager, ) +from graphon.variables.input_entities import VariableEntityType class TestBasicVariablesConfigManagerConvert: diff --git a/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py b/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py index 8bde9c1f97..11b53dd0f9 100644 --- a/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py +++ b/api/tests/unit_tests/core/app/app_config/features/file_upload/test_manager.py @@ -1,8 +1,7 @@ +from core.app.app_config.features.file_upload.manager import FileUploadConfigManager from graphon.file import FileTransferMethod, FileUploadConfig, ImageConfig from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent -from core.app.app_config.features.file_upload.manager import FileUploadConfigManager - def test_convert_with_vision(): config = { diff --git a/api/tests/unit_tests/core/app/app_config/test_entities.py b/api/tests/unit_tests/core/app/app_config/test_entities.py index 000f83cd5a..f2bc3076da 100644 --- a/api/tests/unit_tests/core/app/app_config/test_entities.py +++ b/api/tests/unit_tests/core/app/app_config/test_entities.py @@ -1,10 +1,10 @@ import pytest -from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.app.app_config.entities import ( DatasetRetrieveConfigEntity, PromptTemplateEntity, ) +from graphon.variables.input_entities import VariableEntity, VariableEntityType class TestAppConfigEntities: diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py index 1fb0dc6cf1..45d4b0e321 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_app_runner_conversation_variables.py @@ -3,12 +3,12 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 -from graphon.variables import SegmentType from sqlalchemy.orm import Session from core.app.apps.advanced_chat.app_runner import AdvancedChatAppRunner from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom from factories import variable_factory +from graphon.variables import SegmentType from models import ConversationVariable, Workflow MINIMAL_GRAPH = { diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_response_converter.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_response_converter.py index e9fdeefee4..f2df35d7d0 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_response_converter.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_response_converter.py @@ -1,7 +1,5 @@ from collections.abc import Generator -from graphon.enums import WorkflowNodeExecutionStatus - from core.app.apps.advanced_chat.generate_response_converter import AdvancedChatAppGenerateResponseConverter from core.app.entities.task_entities import ( ChatbotAppBlockingResponse, @@ -12,6 +10,7 @@ from core.app.entities.task_entities import ( NodeStartStreamResponse, PingStreamResponse, ) +from graphon.enums import WorkflowNodeExecutionStatus class TestAdvancedChatGenerateResponseConverter: diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline.py index a6d8598955..99a386cd45 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline.py @@ -6,8 +6,6 @@ from types import SimpleNamespace from unittest import mock import pytest -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus from core.app.apps.advanced_chat import generate_task_pipeline as pipeline_module from core.app.entities.app_invoke_entities import InvokeFrom @@ -19,6 +17,8 @@ from core.app.entities.queue_entities import ( QueueWorkflowSucceededEvent, ) from core.app.entities.task_entities import StreamEvent +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus from models.enums import MessageStatus from models.execution_extra_content import HumanInputContent from models.model import AppMode, EndUser diff --git a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py index 82b2e51019..29fd63c063 100644 --- a/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/advanced_chat/test_generate_task_pipeline_core.py @@ -4,8 +4,6 @@ from contextlib import contextmanager from types import SimpleNamespace import pytest -from graphon.enums import BuiltinNodeTypes -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.app_config.entities import AppAdditionalFeatures, WorkflowUIBasedAppConfig from core.app.apps.advanced_chat.generate_task_pipeline import ( @@ -49,6 +47,8 @@ from core.app.entities.task_entities import ( ) from core.base.tts.app_generator_tts_publisher import AudioTrunk from core.workflow.system_variables import build_system_variables +from graphon.enums import BuiltinNodeTypes +from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import naive_utc_now from models.enums import MessageStatus from models.model import AppMode, EndUser diff --git a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py index 7dc4358150..80f7f94b1a 100644 --- a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_generator.py @@ -1,12 +1,12 @@ import contextlib import pytest -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError from core.app.apps.agent_chat.app_generator import AgentChatAppGenerator from core.app.apps.exc import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError class DummyAccount: diff --git a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_runner.py b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_runner.py index 08250bc3b6..4567b35480 100644 --- a/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/agent_chat/test_agent_chat_app_runner.py @@ -1,10 +1,10 @@ import pytest -from graphon.model_runtime.entities.llm_entities import LLMMode -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey from core.agent.entities import AgentEntity from core.app.apps.agent_chat.app_runner import AgentChatAppRunner from core.moderation.base import ModerationError +from graphon.model_runtime.entities.llm_entities import LLMMode +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelPropertyKey @pytest.fixture diff --git a/api/tests/unit_tests/core/app/apps/chat/test_app_generator_and_runner.py b/api/tests/unit_tests/core/app/apps/chat/test_app_generator_and_runner.py index 68bcffb0e8..8f3c41701b 100644 --- a/api/tests/unit_tests/core/app/apps/chat/test_app_generator_and_runner.py +++ b/api/tests/unit_tests/core/app/apps/chat/test_app_generator_and_runner.py @@ -2,7 +2,6 @@ from types import SimpleNamespace from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from core.app.apps.chat.app_generator import ChatAppGenerator from core.app.apps.chat.app_runner import ChatAppRunner @@ -10,6 +9,7 @@ from core.app.apps.exc import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueAnnotationReplyEvent from core.moderation.base import ModerationError +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py index f255d2c7df..b3ea1a464f 100644 --- a/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py +++ b/api/tests/unit_tests/core/app/apps/chat/test_base_app_runner_multimodal.py @@ -4,13 +4,13 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest -from graphon.file import FileTransferMethod, FileType -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from core.app.apps.base_app_queue_manager import PublishFrom from core.app.apps.base_app_runner import AppRunner from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueMessageFileEvent +from graphon.file import FileTransferMethod, FileType +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent from models.enums import CreatorUserRole diff --git a/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py b/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py index 4a94a2b4f1..201923e0e4 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py +++ b/api/tests/unit_tests/core/app/apps/common/test_graph_runtime_state_support.py @@ -1,11 +1,11 @@ from types import SimpleNamespace import pytest -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.apps.common.graph_runtime_state_support import GraphRuntimeStateSupport from core.workflow.system_variables import build_system_variables from core.workflow.variable_pool_initializer import add_variables_to_pool +from graphon.runtime import GraphRuntimeState, VariablePool def _make_state(workflow_run_id: str | None) -> GraphRuntimeState: diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py index 328cd12f12..3ab63aed25 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter.py @@ -1,10 +1,9 @@ from collections.abc import Mapping, Sequence +from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from graphon.file import FILE_MODEL_IDENTITY, File, FileTransferMethod, FileType from graphon.variables.segments import ArrayFileSegment, FileSegment -from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter - class TestWorkflowResponseConverterFetchFilesFromVariableValue: """Test class for WorkflowResponseConverter._fetch_files_from_variable_value method""" diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py index bc11bf4174..1bef6f69cd 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_human_input.py @@ -1,13 +1,12 @@ from datetime import UTC, datetime from types import SimpleNamespace -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueHumanInputFormFilledEvent, QueueHumanInputFormTimeoutEvent from core.workflow.system_variables import build_system_variables +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState, VariablePool def _build_converter(): diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py index c9e146ff12..936ac37e55 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_resumption.py @@ -1,11 +1,10 @@ from types import SimpleNamespace -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter from core.app.entities.app_invoke_entities import InvokeFrom from core.workflow.system_variables import build_system_variables +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState, VariablePool def _build_converter() -> WorkflowResponseConverter: diff --git a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py index 0fde7565d2..b3c0eb74fa 100644 --- a/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py +++ b/api/tests/unit_tests/core/app/apps/common/test_workflow_response_converter_truncation.py @@ -10,8 +10,6 @@ from typing import Any from unittest.mock import Mock import pytest -from graphon.entities import WorkflowStartReason -from graphon.enums import BuiltinNodeTypes from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter @@ -27,6 +25,8 @@ from core.app.entities.queue_entities import ( QueueNodeSucceededEvent, ) from core.workflow.system_variables import build_system_variables +from graphon.entities import WorkflowStartReason +from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models import Account from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/apps/completion/test_app_runner.py b/api/tests/unit_tests/core/app/apps/completion/test_app_runner.py index 619d66085a..aa2085177e 100644 --- a/api/tests/unit_tests/core/app/apps/completion/test_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/completion/test_app_runner.py @@ -2,11 +2,11 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent import core.app.apps.completion.app_runner as module from core.app.apps.completion.app_runner import CompletionAppRunner from core.moderation.base import ModerationError +from graphon.model_runtime.entities.message_entities import ImagePromptMessageContent @pytest.fixture diff --git a/api/tests/unit_tests/core/app/apps/completion/test_completion_completion_app_generator.py b/api/tests/unit_tests/core/app/apps/completion/test_completion_completion_app_generator.py index 96af9fbdee..f2e35f9900 100644 --- a/api/tests/unit_tests/core/app/apps/completion/test_completion_completion_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/completion/test_completion_completion_app_generator.py @@ -3,13 +3,13 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from pydantic import ValidationError import core.app.apps.completion.app_generator as module from core.app.apps.completion.app_generator import CompletionAppGenerator from core.app.apps.exc import GenerateTaskStoppedError from core.app.entities.app_invoke_entities import InvokeFrom +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError from services.errors.app import MoreLikeThisDisabledError from services.errors.message import MessageNotExistsError diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generate_response_converter.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generate_response_converter.py index 6cdcab29ab..cfe797aa76 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generate_response_converter.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_generate_response_converter.py @@ -1,7 +1,5 @@ from collections.abc import Generator -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus - from core.app.apps.pipeline.generate_response_converter import WorkflowAppGenerateResponseConverter from core.app.entities.task_entities import ( AppStreamResponse, @@ -12,6 +10,7 @@ from core.app.entities.task_entities import ( WorkflowAppBlockingResponse, WorkflowAppStreamResponse, ) +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus def test_convert_blocking_full_and_simple_response(): diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_queue_manager.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_queue_manager.py index 4fe82efcb3..9db83f5531 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_queue_manager.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_queue_manager.py @@ -1,5 +1,4 @@ import pytest -from graphon.model_runtime.entities.llm_entities import LLMResult import core.app.apps.pipeline.pipeline_queue_manager as module from core.app.apps.base_app_queue_manager import PublishFrom @@ -14,6 +13,7 @@ from core.app.entities.queue_entities import ( QueueWorkflowPartialSuccessEvent, QueueWorkflowSucceededEvent, ) +from graphon.model_runtime.entities.llm_entities import LLMResult def test_publish_sets_stop_listen_and_raises_on_stopped(mocker): diff --git a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py index c8ae288e6f..618c8fd76f 100644 --- a/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py +++ b/api/tests/unit_tests/core/app/apps/pipeline/test_pipeline_runner.py @@ -22,11 +22,11 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.graph_events import GraphRunFailedEvent import core.app.apps.pipeline.pipeline_runner as module from core.app.apps.pipeline.pipeline_runner import PipelineRunner from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from graphon.graph_events import GraphRunFailedEvent def _build_app_generate_entity() -> SimpleNamespace: diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py index 6167be3bbd..b0f8b423e1 100644 --- a/api/tests/unit_tests/core/app/apps/test_base_app_generator.py +++ b/api/tests/unit_tests/core/app/apps/test_base_app_generator.py @@ -1,7 +1,7 @@ import pytest -from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.app.apps.base_app_generator import BaseAppGenerator +from graphon.variables.input_entities import VariableEntity, VariableEntityType def test_validate_inputs_with_zero(): @@ -476,9 +476,8 @@ class TestBaseAppGeneratorExtras: assert converted[1] == "event: ping\n\n" def test_get_draft_var_saver_factory_debugger(self): - from graphon.enums import BuiltinNodeTypes - from core.app.entities.app_invoke_entities import InvokeFrom + from graphon.enums import BuiltinNodeTypes from models import Account base_app_generator = BaseAppGenerator() diff --git a/api/tests/unit_tests/core/app/apps/test_base_app_runner.py b/api/tests/unit_tests/core/app/apps/test_base_app_runner.py index 1dee7fdab6..17de39ca99 100644 --- a/api/tests/unit_tests/core/app/apps/test_base_app_runner.py +++ b/api/tests/unit_tests/core/app/apps/test_base_app_runner.py @@ -4,15 +4,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - ImagePromptMessageContent, - PromptMessageRole, - TextPromptMessageContent, -) -from graphon.model_runtime.entities.model_entities import ModelPropertyKey -from graphon.model_runtime.errors.invoke import InvokeBadRequestError from core.app.app_config.entities import ( AdvancedChatMessageEntity, @@ -23,6 +14,15 @@ from core.app.app_config.entities import ( from core.app.apps.base_app_runner import AppRunner from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueAgentMessageEvent, QueueLLMChunkEvent, QueueMessageEndEvent +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + ImagePromptMessageContent, + PromptMessageRole, + TextPromptMessageContent, +) +from graphon.model_runtime.entities.model_entities import ModelPropertyKey +from graphon.model_runtime.errors.invoke import InvokeBadRequestError from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/apps/test_pause_resume.py b/api/tests/unit_tests/core/app/apps/test_pause_resume.py index a126bc85f7..a04a7b7576 100644 --- a/api/tests/unit_tests/core/app/apps/test_pause_resume.py +++ b/api/tests/unit_tests/core/app/apps/test_pause_resume.py @@ -4,6 +4,11 @@ from types import ModuleType, SimpleNamespace from typing import Any import graphon.nodes.human_input.entities # noqa: F401 +from core.app.apps.advanced_chat import app_generator as adv_app_gen_module +from core.app.apps.workflow import app_generator as wf_app_gen_module +from core.app.entities.app_invoke_entities import InvokeFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables from graphon.entities import WorkflowStartReason from graphon.entities.base_node_data import BaseNodeData, RetryConfig from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter @@ -25,12 +30,6 @@ from graphon.nodes.base.node import Node from graphon.nodes.end.entities import EndNodeData from graphon.nodes.start.entities import StartNodeData from graphon.runtime import GraphRuntimeState, VariablePool - -from core.app.apps.advanced_chat import app_generator as adv_app_gen_module -from core.app.apps.workflow import app_generator as wf_app_gen_module -from core.app.entities.app_invoke_entities import InvokeFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params if "core.ops.ops_trace_manager" not in sys.modules: diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py index de5bca161c..58c7bfa4bc 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_core.py @@ -4,6 +4,23 @@ from datetime import UTC, datetime from types import SimpleNamespace import pytest + +from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.app.entities.queue_entities import ( + QueueAgentLogEvent, + QueueIterationCompletedEvent, + QueueLoopCompletedEvent, + QueueNodeExceptionEvent, + QueueNodeFailedEvent, + QueueNodeRetryEvent, + QueueNodeSucceededEvent, + QueueTextChunkEvent, + QueueWorkflowPausedEvent, + QueueWorkflowStartedEvent, + QueueWorkflowSucceededEvent, +) +from core.workflow.system_variables import default_system_variables from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import BuiltinNodeTypes from graphon.graph_events import ( @@ -24,23 +41,6 @@ from graphon.node_events import NodeRunResult from graphon.runtime import GraphRuntimeState, VariablePool from graphon.variables.variables import StringVariable -from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.app.entities.queue_entities import ( - QueueAgentLogEvent, - QueueIterationCompletedEvent, - QueueLoopCompletedEvent, - QueueNodeExceptionEvent, - QueueNodeFailedEvent, - QueueNodeRetryEvent, - QueueNodeSucceededEvent, - QueueTextChunkEvent, - QueueWorkflowPausedEvent, - QueueWorkflowStartedEvent, - QueueWorkflowSucceededEvent, -) -from core.workflow.system_variables import default_system_variables - class TestWorkflowBasedAppRunner: def test_resolve_user_from(self): diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py index aa789d9ff3..10fb2271f4 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_notifications.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock import pytest -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph_events import GraphRunPausedEvent from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner from core.app.entities.queue_entities import QueueWorkflowPausedEvent +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph_events import GraphRunPausedEvent class _DummyQueueManager: diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py index 9e30faecf2..620a153204 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_app_runner_single_node.py @@ -4,14 +4,14 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.app_runner import WorkflowAppRunner from core.app.apps.workflow_app_runner import WorkflowBasedAppRunner from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.workflow.system_variables import default_system_variables +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.runtime import GraphRuntimeState, VariablePool from models.workflow import Workflow diff --git a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py index 8a717e1dcc..a3ab379b66 100644 --- a/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py +++ b/api/tests/unit_tests/core/app/apps/test_workflow_pause_events.py @@ -3,11 +3,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.entities import WorkflowStartReason -from graphon.entities.pause_reason import HumanInputRequired -from graphon.graph_events import GraphRunPausedEvent -from graphon.nodes.human_input.entities import FormInput, UserAction -from graphon.nodes.human_input.enums import FormInputType from core.app.apps.common import workflow_response_converter from core.app.apps.common.workflow_response_converter import WorkflowResponseConverter @@ -16,6 +11,11 @@ from core.app.entities.app_invoke_entities import InvokeFrom from core.app.entities.queue_entities import QueueWorkflowPausedEvent from core.app.entities.task_entities import HumanInputRequiredResponse, WorkflowPauseStreamResponse from core.workflow.system_variables import build_system_variables +from graphon.entities import WorkflowStartReason +from graphon.entities.pause_reason import HumanInputRequired +from graphon.graph_events import GraphRunPausedEvent +from graphon.nodes.human_input.entities import FormInput, UserAction +from graphon.nodes.human_input.enums import FormInputType from models.account import Account from models.human_input import RecipientType diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_response_converter.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_response_converter.py index b768e813bd..7dd7ffd727 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_response_converter.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_response_converter.py @@ -1,7 +1,5 @@ from collections.abc import Generator -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus - from core.app.apps.workflow.generate_response_converter import WorkflowAppGenerateResponseConverter from core.app.entities.task_entities import ( ErrorStreamResponse, @@ -11,6 +9,7 @@ from core.app.entities.task_entities import ( WorkflowAppBlockingResponse, WorkflowAppStreamResponse, ) +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus class TestWorkflowGenerateResponseConverter: diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py index 29df903aa8..1f6e7e12ef 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline.py @@ -2,15 +2,14 @@ import time from contextlib import contextmanager from unittest.mock import MagicMock -from graphon.entities import WorkflowStartReason -from graphon.runtime import GraphRuntimeState - from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.queue_entities import QueueWorkflowStartedEvent from core.workflow.system_variables import build_system_variables +from graphon.entities import WorkflowStartReason +from graphon.runtime import GraphRuntimeState from models.account import Account from models.model import AppMode from tests.workflow_test_utils import build_test_variable_pool diff --git a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py index d91bb85aee..99433478d3 100644 --- a/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/apps/workflow/test_generate_task_pipeline_core.py @@ -5,8 +5,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.app_config.entities import AppAdditionalFeatures, WorkflowUIBasedAppConfig from core.app.apps.workflow.generate_task_pipeline import WorkflowAppGenerateTaskPipeline @@ -47,6 +45,8 @@ from core.app.entities.task_entities import ( ) from core.base.tts.app_generator_tts_publisher import AudioTrunk from core.workflow.system_variables import build_system_variables, system_variables_to_mapping +from graphon.enums import BuiltinNodeTypes, WorkflowExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import naive_utc_now from models.enums import CreatorUserRole from models.model import AppMode, EndUser diff --git a/api/tests/unit_tests/core/app/entities/test_task_entities.py b/api/tests/unit_tests/core/app/entities/test_task_entities.py index 014a0cba72..7c79780641 100644 --- a/api/tests/unit_tests/core/app/entities/test_task_entities.py +++ b/api/tests/unit_tests/core/app/entities/test_task_entities.py @@ -1,11 +1,10 @@ -from graphon.enums import WorkflowNodeExecutionStatus - from core.app.entities.task_entities import ( NodeFinishStreamResponse, NodeRetryStreamResponse, NodeStartStreamResponse, StreamEvent, ) +from graphon.enums import WorkflowNodeExecutionStatus class TestTaskEntities: diff --git a/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py b/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py index a78c1b428f..ba55e8f695 100644 --- a/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_conversation_variable_persist_layer.py @@ -1,6 +1,9 @@ from collections.abc import Sequence from unittest.mock import Mock +from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer +from core.workflow.system_variables import SystemVariableKey +from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.graph_engine.command_channels import CommandChannel from graphon.graph_events import NodeRunSucceededEvent, NodeRunVariableUpdatedEvent @@ -8,10 +11,6 @@ from graphon.node_events import NodeRunResult from graphon.runtime import ReadOnlyGraphRuntimeState from graphon.variables import StringVariable from graphon.variables.segments import Segment, StringSegment - -from core.app.layers.conversation_variable_persist_layer import ConversationVariablePersistenceLayer -from core.workflow.system_variables import SystemVariableKey -from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py b/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py index 035e64325b..539944d683 100644 --- a/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_pause_state_persist_layer.py @@ -4,6 +4,16 @@ from time import time from unittest.mock import Mock import pytest + +from core.app.app_config.entities import WorkflowUIBasedAppConfig +from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity +from core.app.layers.pause_state_persist_layer import ( + PauseStatePersistenceLayer, + WorkflowResumptionContext, + _AdvancedChatAppGenerateEntityWrapper, + _WorkflowGenerateEntityWrapper, +) +from core.workflow.system_variables import SystemVariableKey from graphon.entities.pause_reason import SchedulingPause from graphon.graph_engine.entities.commands import GraphEngineCommand from graphon.graph_engine.layers.base import GraphEngineLayerNotInitializedError @@ -15,16 +25,6 @@ from graphon.graph_events import ( ) from graphon.runtime import ReadOnlyVariablePool from graphon.variables.segments import Segment - -from core.app.app_config.entities import WorkflowUIBasedAppConfig -from core.app.entities.app_invoke_entities import AdvancedChatAppGenerateEntity, InvokeFrom, WorkflowAppGenerateEntity -from core.app.layers.pause_state_persist_layer import ( - PauseStatePersistenceLayer, - WorkflowResumptionContext, - _AdvancedChatAppGenerateEntityWrapper, - _WorkflowGenerateEntityWrapper, -) -from core.workflow.system_variables import SystemVariableKey from models.model import AppMode from repositories.factory import DifyAPIRepositoryFactory diff --git a/api/tests/unit_tests/core/app/layers/test_suspend_layer.py b/api/tests/unit_tests/core/app/layers/test_suspend_layer.py index 95931f4f8b..12d49be0f1 100644 --- a/api/tests/unit_tests/core/app/layers/test_suspend_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_suspend_layer.py @@ -1,6 +1,5 @@ -from graphon.graph_events import GraphRunPausedEvent - from core.app.layers.suspend_layer import SuspendLayer +from graphon.graph_events import GraphRunPausedEvent class TestSuspendLayer: diff --git a/api/tests/unit_tests/core/app/layers/test_timeslice_layer.py b/api/tests/unit_tests/core/app/layers/test_timeslice_layer.py index 7cf6eb4f31..1ac9a4d8c0 100644 --- a/api/tests/unit_tests/core/app/layers/test_timeslice_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_timeslice_layer.py @@ -1,8 +1,7 @@ from unittest.mock import Mock, patch -from graphon.graph_engine.entities.commands import CommandType, GraphEngineCommand - from core.app.layers.timeslice_layer import TimeSliceLayer +from graphon.graph_engine.entities.commands import CommandType, GraphEngineCommand from services.workflow.entities import WorkflowScheduleCFSPlanEntity from services.workflow.scheduler import SchedulerCommand diff --git a/api/tests/unit_tests/core/app/layers/test_trigger_post_layer.py b/api/tests/unit_tests/core/app/layers/test_trigger_post_layer.py index aa9285789b..d3bd15b6f3 100644 --- a/api/tests/unit_tests/core/app/layers/test_trigger_post_layer.py +++ b/api/tests/unit_tests/core/app/layers/test_trigger_post_layer.py @@ -2,11 +2,10 @@ from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import Mock, patch -from graphon.graph_events import GraphRunFailedEvent, GraphRunSucceededEvent -from graphon.runtime import VariablePool - from core.app.layers.trigger_post_layer import TriggerPostLayer from core.workflow.system_variables import build_system_variables +from graphon.graph_events import GraphRunFailedEvent, GraphRunSucceededEvent +from graphon.runtime import VariablePool from models.enums import WorkflowTriggerStatus diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py b/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py index 58aa7d7478..c246f7b783 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_based_generate_task_pipeline.py @@ -2,11 +2,11 @@ from types import SimpleNamespace from unittest.mock import Mock import pytest -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from core.app.entities.queue_entities import QueueErrorEvent from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline from core.errors.error import QuotaExceededError +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from models.enums import MessageStatus diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py index 4aaa10a81a..1c1bf391d3 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline.py @@ -2,8 +2,6 @@ from types import SimpleNamespace from unittest.mock import ANY, Mock, patch import pytest -from graphon.model_runtime.entities.llm_entities import LLMResult as RuntimeLLMResult -from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from core.app.apps.base_app_queue_manager import AppQueueManager from core.app.entities.app_invoke_entities import ChatAppGenerateEntity @@ -28,6 +26,8 @@ from core.app.entities.task_entities import ( from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline from core.base.tts import AppGeneratorTTSPublisher from core.ops.ops_trace_manager import TraceQueueManager +from graphon.model_runtime.entities.llm_entities import LLMResult as RuntimeLLMResult +from graphon.model_runtime.entities.message_entities import TextPromptMessageContent from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py index f22602a400..a20d89d807 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_based_generate_task_pipeline_core.py @@ -5,9 +5,6 @@ from types import SimpleNamespace from unittest.mock import Mock import pytest -from graphon.file import FileTransferMethod -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage -from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, TextPromptMessageContent from core.app.app_config.entities import ( AppAdditionalFeatures, @@ -41,6 +38,9 @@ from core.app.entities.task_entities import ( ) from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline from core.base.tts import AudioTrunk +from graphon.file import FileTransferMethod +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage +from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, TextPromptMessageContent from models.model import AppMode diff --git a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_message_end_files.py b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_message_end_files.py index 31b7313066..595d716666 100644 --- a/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_message_end_files.py +++ b/api/tests/unit_tests/core/app/task_pipeline/test_easy_ui_message_end_files.py @@ -17,11 +17,11 @@ import uuid from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.file import FileTransferMethod, FileType from sqlalchemy.orm import Session from core.app.entities.task_entities import MessageEndStreamResponse from core.app.task_pipeline.easy_ui_based_generate_task_pipeline import EasyUIBasedGenerateTaskPipeline +from graphon.file import FileTransferMethod, FileType from models.model import MessageFile, UploadFile diff --git a/api/tests/unit_tests/core/app/test_easy_ui_model_config_manager.py b/api/tests/unit_tests/core/app/test_easy_ui_model_config_manager.py index 29df7eea86..21c761c579 100644 --- a/api/tests/unit_tests/core/app/test_easy_ui_model_config_manager.py +++ b/api/tests/unit_tests/core/app/test_easy_ui_model_config_manager.py @@ -1,10 +1,9 @@ from types import SimpleNamespace from unittest.mock import patch -from graphon.model_runtime.entities.model_entities import ModelPropertyKey - from core.app.app_config.easy_ui_based_app.model_config.manager import ModelConfigManager from core.app.app_config.entities import ModelConfigEntity +from graphon.model_runtime.entities.model_entities import ModelPropertyKey from models.provider_ids import ModelProviderID diff --git a/api/tests/unit_tests/core/app/workflow/layers/test_persistence.py b/api/tests/unit_tests/core/app/workflow/layers/test_persistence.py index dc2d82ccd6..5c50cb78da 100644 --- a/api/tests/unit_tests/core/app/workflow/layers/test_persistence.py +++ b/api/tests/unit_tests/core/app/workflow/layers/test_persistence.py @@ -2,14 +2,14 @@ from datetime import UTC, datetime from unittest.mock import Mock import pytest -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus, WorkflowType -from graphon.node_events import NodeRunResult from core.app.workflow.layers.persistence import ( PersistenceWorkflowInfo, WorkflowPersistenceLayer, _NodeRuntimeSnapshot, ) +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus, WorkflowType +from graphon.node_events import NodeRunResult def _build_layer() -> WorkflowPersistenceLayer: diff --git a/api/tests/unit_tests/core/app/workflow/test_file_runtime.py b/api/tests/unit_tests/core/app/workflow/test_file_runtime.py index 7be9d6ac1e..cddd03f4b0 100644 --- a/api/tests/unit_tests/core/app/workflow/test_file_runtime.py +++ b/api/tests/unit_tests/core/app/workflow/test_file_runtime.py @@ -8,13 +8,13 @@ from unittest.mock import MagicMock, patch from urllib.parse import parse_qs, urlparse import pytest -from graphon.file import File, FileTransferMethod, FileType from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import DatabaseFileAccessController, FileAccessScope from core.app.workflow import file_runtime from core.app.workflow.file_runtime import DifyWorkflowFileRuntime, bind_dify_workflow_file_runtime from core.workflow.file_reference import build_file_reference +from graphon.file import File, FileTransferMethod, FileType from models import ToolFile, UploadFile diff --git a/api/tests/unit_tests/core/app/workflow/test_node_factory.py b/api/tests/unit_tests/core/app/workflow/test_node_factory.py index 8497261d45..c4bfb23272 100644 --- a/api/tests/unit_tests/core/app/workflow/test_node_factory.py +++ b/api/tests/unit_tests/core/app/workflow/test_node_factory.py @@ -1,10 +1,10 @@ from types import SimpleNamespace import pytest -from graphon.enums import BuiltinNodeTypes from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, build_dify_run_context from core.workflow.node_factory import DifyNodeFactory +from graphon.enums import BuiltinNodeTypes class DummyNode: diff --git a/api/tests/unit_tests/core/app/workflow/test_observability_layer_extra.py b/api/tests/unit_tests/core/app/workflow/test_observability_layer_extra.py index a47d3db6f5..82552470a9 100644 --- a/api/tests/unit_tests/core/app/workflow/test_observability_layer_extra.py +++ b/api/tests/unit_tests/core/app/workflow/test_observability_layer_extra.py @@ -2,9 +2,8 @@ from __future__ import annotations from types import SimpleNamespace -from graphon.enums import BuiltinNodeTypes - from core.app.workflow.layers.observability import ObservabilityLayer +from graphon.enums import BuiltinNodeTypes class TestObservabilityLayerExtras: diff --git a/api/tests/unit_tests/core/app/workflow/test_persistence_layer.py b/api/tests/unit_tests/core/app/workflow/test_persistence_layer.py index d8a68f6d00..cacb4dd4fa 100644 --- a/api/tests/unit_tests/core/app/workflow/test_persistence_layer.py +++ b/api/tests/unit_tests/core/app/workflow/test_persistence_layer.py @@ -4,6 +4,10 @@ from datetime import UTC, datetime from types import SimpleNamespace import pytest + +from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity +from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer +from core.workflow.system_variables import SystemVariableKey, build_system_variables from graphon.entities import WorkflowNodeExecution from graphon.entities.pause_reason import SchedulingPause from graphon.enums import ( @@ -29,10 +33,6 @@ from graphon.graph_events import ( from graphon.node_events import NodeRunResult from graphon.runtime import GraphRuntimeState, ReadOnlyGraphRuntimeStateWrapper, VariablePool -from core.app.entities.app_invoke_entities import WorkflowAppGenerateEntity -from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer -from core.workflow.system_variables import SystemVariableKey, build_system_variables - class _RepoRecorder: def __init__(self) -> None: diff --git a/api/tests/unit_tests/core/base/test_app_generator_tts_publisher.py b/api/tests/unit_tests/core/base/test_app_generator_tts_publisher.py index 5ff9774b52..7b433ab57b 100644 --- a/api/tests/unit_tests/core/base/test_app_generator_tts_publisher.py +++ b/api/tests/unit_tests/core/base/test_app_generator_tts_publisher.py @@ -301,6 +301,7 @@ class TestAppGeneratorTTSPublisher: publisher = AppGeneratorTTSPublisher("tenant", "voice1") publisher.executor = MagicMock() + from core.app.entities.queue_entities import QueueAgentMessageEvent from graphon.model_runtime.entities.llm_entities import LLMResultChunk, LLMResultChunkDelta from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, @@ -308,8 +309,6 @@ class TestAppGeneratorTTSPublisher: TextPromptMessageContent, ) - from core.app.entities.queue_entities import QueueAgentMessageEvent - chunk = LLMResultChunk( model="model", delta=LLMResultChunkDelta( @@ -337,11 +336,10 @@ class TestAppGeneratorTTSPublisher: publisher = AppGeneratorTTSPublisher("tenant", "voice1") publisher.executor = MagicMock() + from core.app.entities.queue_entities import QueueAgentMessageEvent from graphon.model_runtime.entities.llm_entities import LLMResultChunk, LLMResultChunkDelta from graphon.model_runtime.entities.message_entities import AssistantPromptMessage - from core.app.entities.queue_entities import QueueAgentMessageEvent - chunk = LLMResultChunk( model="model", delta=LLMResultChunkDelta( diff --git a/api/tests/unit_tests/core/datasource/test_datasource_manager.py b/api/tests/unit_tests/core/datasource/test_datasource_manager.py index d338cadb77..81315d2508 100644 --- a/api/tests/unit_tests/core/datasource/test_datasource_manager.py +++ b/api/tests/unit_tests/core/datasource/test_datasource_manager.py @@ -2,15 +2,15 @@ import types from collections.abc import Generator import pytest -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.file import File, FileTransferMethod, FileType -from graphon.node_events import StreamChunkEvent, StreamCompletedEvent from contexts.wrapper import RecyclableContextVar from core.datasource.datasource_manager import DatasourceManager from core.datasource.entities.datasource_entities import DatasourceMessage, DatasourceProviderType from core.datasource.errors import DatasourceProviderNotFoundError from core.workflow.file_reference import parse_file_reference +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.file import File, FileTransferMethod, FileType +from graphon.node_events import StreamChunkEvent, StreamCompletedEvent def _gen_messages_text_only(text: str) -> Generator[DatasourceMessage, None, None]: diff --git a/api/tests/unit_tests/core/datasource/utils/test_message_transformer.py b/api/tests/unit_tests/core/datasource/utils/test_message_transformer.py index fbaf6d497d..0fca43cd0b 100644 --- a/api/tests/unit_tests/core/datasource/utils/test_message_transformer.py +++ b/api/tests/unit_tests/core/datasource/utils/test_message_transformer.py @@ -1,10 +1,10 @@ from unittest.mock import MagicMock, patch import pytest -from graphon.file import File, FileTransferMethod, FileType from core.datasource.entities.datasource_entities import DatasourceMessage from core.datasource.utils.message_transformer import DatasourceFileMessageTransformer +from graphon.file import File, FileTransferMethod, FileType from models.tools import ToolFile diff --git a/api/tests/unit_tests/core/entities/test_entities_execution_extra_content.py b/api/tests/unit_tests/core/entities/test_entities_execution_extra_content.py index ff9fd0d8f3..ef8f360dbf 100644 --- a/api/tests/unit_tests/core/entities/test_entities_execution_extra_content.py +++ b/api/tests/unit_tests/core/entities/test_entities_execution_extra_content.py @@ -1,12 +1,11 @@ -from graphon.nodes.human_input.entities import FormInput, UserAction -from graphon.nodes.human_input.enums import FormInputType - from core.entities.execution_extra_content import ( ExecutionExtraContentDomainModel, HumanInputContent, HumanInputFormDefinition, HumanInputFormSubmissionData, ) +from graphon.nodes.human_input.entities import FormInput, UserAction +from graphon.nodes.human_input.enums import FormInputType from models.execution_extra_content import ExecutionContentType diff --git a/api/tests/unit_tests/core/entities/test_entities_model_entities.py b/api/tests/unit_tests/core/entities/test_entities_model_entities.py index 2acd278a31..a0b2820157 100644 --- a/api/tests/unit_tests/core/entities/test_entities_model_entities.py +++ b/api/tests/unit_tests/core/entities/test_entities_model_entities.py @@ -8,9 +8,6 @@ drive provider mapping behavior. """ import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType -from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity from core.entities.model_entities import ( DefaultModelEntity, @@ -19,6 +16,9 @@ from core.entities.model_entities import ( ProviderModelWithStatusEntity, SimpleModelProviderEntity, ) +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType +from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity def _build_model_with_status(status: ModelStatus) -> ProviderModelWithStatusEntity: diff --git a/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py b/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py index 8cf0409c4c..fe2c226843 100644 --- a/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py +++ b/api/tests/unit_tests/core/entities/test_entities_provider_configuration.py @@ -6,17 +6,6 @@ from typing import Any from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FieldModelSchema, - FormType, - ModelCredentialSchema, - ProviderCredentialSchema, - ProviderEntity, -) from constants import HIDDEN_VALUE from core.entities.model_entities import ModelStatus @@ -35,6 +24,17 @@ from core.entities.provider_entities import ( SystemConfiguration, SystemConfigurationStatus, ) +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FieldModelSchema, + FormType, + ModelCredentialSchema, + ProviderCredentialSchema, + ProviderEntity, +) from models.enums import CredentialSourceType from models.provider import ProviderType from models.provider_ids import ModelProviderID diff --git a/api/tests/unit_tests/core/entities/test_entities_provider_entities.py b/api/tests/unit_tests/core/entities/test_entities_provider_entities.py index 8685d16283..a159d3ad4d 100644 --- a/api/tests/unit_tests/core/entities/test_entities_provider_entities.py +++ b/api/tests/unit_tests/core/entities/test_entities_provider_entities.py @@ -1,5 +1,4 @@ import pytest -from graphon.model_runtime.entities.model_entities import ModelType from core.entities.parameter_entities import AppSelectorScope from core.entities.provider_entities import ( @@ -9,6 +8,7 @@ from core.entities.provider_entities import ( ProviderQuotaType, ) from core.tools.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType def test_provider_quota_type_value_of_returns_enum_member() -> None: diff --git a/api/tests/unit_tests/core/helper/test_moderation.py b/api/tests/unit_tests/core/helper/test_moderation.py index 4a84099b74..a0dfa86d20 100644 --- a/api/tests/unit_tests/core/helper/test_moderation.py +++ b/api/tests/unit_tests/core/helper/test_moderation.py @@ -2,11 +2,11 @@ from types import SimpleNamespace from typing import cast import pytest -from graphon.model_runtime.errors.invoke import InvokeBadRequestError from pytest_mock import MockerFixture from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.helper.moderation import check_moderation +from graphon.model_runtime.errors.invoke import InvokeBadRequestError from models.provider import ProviderType diff --git a/api/tests/unit_tests/core/llm_generator/output_parser/test_structured_output.py b/api/tests/unit_tests/core/llm_generator/output_parser/test_structured_output.py index b45f6fd9a7..6ed9ddb476 100644 --- a/api/tests/unit_tests/core/llm_generator/output_parser/test_structured_output.py +++ b/api/tests/unit_tests/core/llm_generator/output_parser/test_structured_output.py @@ -2,20 +2,6 @@ import json from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.llm_entities import ( - LLMResult, - LLMResultChunk, - LLMResultChunkDelta, - LLMResultWithStructuredOutput, - LLMUsage, -) -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - SystemPromptMessage, - TextPromptMessageContent, - UserPromptMessage, -) -from graphon.model_runtime.entities.model_entities import AIModelEntity, ParameterRule, ParameterType from core.llm_generator.output_parser.errors import OutputParserError from core.llm_generator.output_parser.structured_output import ( @@ -30,6 +16,20 @@ from core.llm_generator.output_parser.structured_output import ( remove_additional_properties, ) from core.model_manager import ModelInstance +from graphon.model_runtime.entities.llm_entities import ( + LLMResult, + LLMResultChunk, + LLMResultChunkDelta, + LLMResultWithStructuredOutput, + LLMUsage, +) +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + SystemPromptMessage, + TextPromptMessageContent, + UserPromptMessage, +) +from graphon.model_runtime.entities.model_entities import AIModelEntity, ParameterRule, ParameterType class TestStructuredOutput: diff --git a/api/tests/unit_tests/core/llm_generator/test_llm_generator.py b/api/tests/unit_tests/core/llm_generator/test_llm_generator.py index 7cdfb31189..2716f4712c 100644 --- a/api/tests/unit_tests/core/llm_generator/test_llm_generator.py +++ b/api/tests/unit_tests/core/llm_generator/test_llm_generator.py @@ -2,12 +2,12 @@ import json from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.llm_entities import LLMMode, LLMResult -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError from core.app.app_config.entities import ModelConfig from core.llm_generator.entities import RuleCodeGeneratePayload, RuleGeneratePayload, RuleStructuredOutputPayload from core.llm_generator.llm_generator import LLMGenerator +from graphon.model_runtime.entities.llm_entities import LLMMode, LLMResult +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError, InvokeError class TestLLMGenerator: diff --git a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py index 9a815fb94d..57456085c3 100644 --- a/api/tests/unit_tests/core/mcp/server/test_streamable_http.py +++ b/api/tests/unit_tests/core/mcp/server/test_streamable_http.py @@ -3,7 +3,6 @@ from unittest.mock import Mock, patch import jsonschema import pytest -from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.app.features.rate_limiting.rate_limit import RateLimitGenerator from core.mcp import types @@ -19,6 +18,7 @@ from core.mcp.server.streamable_http import ( prepare_tool_arguments, process_mapping_response, ) +from graphon.variables.input_entities import VariableEntity, VariableEntityType from models.model import App, AppMCPServer, AppMode, EndUser diff --git a/api/tests/unit_tests/core/memory/test_token_buffer_memory.py b/api/tests/unit_tests/core/memory/test_token_buffer_memory.py index 9a5fb319d7..f459250b8e 100644 --- a/api/tests/unit_tests/core/memory/test_token_buffer_memory.py +++ b/api/tests/unit_tests/core/memory/test_token_buffer_memory.py @@ -4,6 +4,8 @@ from unittest.mock import MagicMock, patch from uuid import uuid4 import pytest + +from core.memory.token_buffer_memory import TokenBufferMemory from graphon.model_runtime.entities import ( AssistantPromptMessage, ImagePromptMessageContent, @@ -11,8 +13,6 @@ from graphon.model_runtime.entities import ( TextPromptMessageContent, UserPromptMessage, ) - -from core.memory.token_buffer_memory import TokenBufferMemory from models.model import AppMode # --------------------------------------------------------------------------- diff --git a/api/tests/unit_tests/core/model_runtime/test_model_provider_factory.py b/api/tests/unit_tests/core/model_runtime/test_model_provider_factory.py index 6a672fdfd5..249ecb5006 100644 --- a/api/tests/unit_tests/core/model_runtime/test_model_provider_factory.py +++ b/api/tests/unit_tests/core/model_runtime/test_model_provider_factory.py @@ -1,6 +1,7 @@ from unittest.mock import Mock import pytest + from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType from graphon.model_runtime.entities.provider_entities import ( diff --git a/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace.py b/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace.py index 62d631a754..c2324fdec4 100644 --- a/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace.py +++ b/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace.py @@ -5,8 +5,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from opentelemetry.trace import Link, SpanContext, SpanKind, Status, StatusCode, TraceFlags import core.ops.aliyun_trace.aliyun_trace as aliyun_trace_module @@ -36,6 +34,8 @@ from core.ops.entities.trace_entity import ( ToolTraceInfo, WorkflowTraceInfo, ) +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey class RecordingTraceClient: diff --git a/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace_utils.py b/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace_utils.py index 2d2be12f05..e4d8f2d5ea 100644 --- a/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace_utils.py +++ b/api/tests/unit_tests/core/ops/aliyun_trace/test_aliyun_trace_utils.py @@ -1,8 +1,6 @@ import json from unittest.mock import MagicMock -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionStatus from opentelemetry.trace import Link, StatusCode from core.ops.aliyun_trace.entities.semconv import ( @@ -26,6 +24,8 @@ from core.ops.aliyun_trace.utils import ( serialize_json_data, ) from core.rag.models.document import Document +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionStatus from models import EndUser diff --git a/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py b/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py index 374371fb42..a0bcc92795 100644 --- a/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py +++ b/api/tests/unit_tests/core/ops/langfuse_trace/test_langfuse_trace.py @@ -5,7 +5,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.enums import BuiltinNodeTypes from core.ops.entities.config_entity import LangfuseConfig from core.ops.entities.trace_entity import ( @@ -26,6 +25,7 @@ from core.ops.langfuse_trace.entities.langfuse_trace_entity import ( UnitEnum, ) from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace +from graphon.enums import BuiltinNodeTypes from models import EndUser from models.enums import MessageStatus diff --git a/api/tests/unit_tests/core/ops/langsmith_trace/test_langsmith_trace.py b/api/tests/unit_tests/core/ops/langsmith_trace/test_langsmith_trace.py index bfe916f018..34c64c54a1 100644 --- a/api/tests/unit_tests/core/ops/langsmith_trace/test_langsmith_trace.py +++ b/api/tests/unit_tests/core/ops/langsmith_trace/test_langsmith_trace.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock import pytest -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from core.ops.entities.config_entity import LangSmithConfig from core.ops.entities.trace_entity import ( @@ -22,6 +21,7 @@ from core.ops.langsmith_trace.entities.langsmith_trace_entity import ( LangSmithRunUpdateModel, ) from core.ops.langsmith_trace.langsmith_trace import LangSmithDataTrace +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser diff --git a/api/tests/unit_tests/core/ops/mlflow_trace/test_mlflow_trace.py b/api/tests/unit_tests/core/ops/mlflow_trace/test_mlflow_trace.py index f4c485a9fc..afc5726ede 100644 --- a/api/tests/unit_tests/core/ops/mlflow_trace/test_mlflow_trace.py +++ b/api/tests/unit_tests/core/ops/mlflow_trace/test_mlflow_trace.py @@ -9,7 +9,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from graphon.enums import BuiltinNodeTypes from core.ops.entities.config_entity import DatabricksConfig, MLflowConfig from core.ops.entities.trace_entity import ( @@ -22,6 +21,7 @@ from core.ops.entities.trace_entity import ( WorkflowTraceInfo, ) from core.ops.mlflow_trace.mlflow_trace import MLflowDataTrace, datetime_to_nanoseconds +from graphon.enums import BuiltinNodeTypes # ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/api/tests/unit_tests/core/ops/opik_trace/test_opik_trace.py b/api/tests/unit_tests/core/ops/opik_trace/test_opik_trace.py index 1cb32f2ee0..c02ac413f2 100644 --- a/api/tests/unit_tests/core/ops/opik_trace/test_opik_trace.py +++ b/api/tests/unit_tests/core/ops/opik_trace/test_opik_trace.py @@ -5,7 +5,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from core.ops.entities.config_entity import OpikConfig from core.ops.entities.trace_entity import ( @@ -19,6 +18,7 @@ from core.ops.entities.trace_entity import ( WorkflowTraceInfo, ) from core.ops.opik_trace.opik_trace import OpikDataTrace, prepare_opik_uuid, wrap_dict, wrap_metadata +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from models import EndUser from models.enums import MessageStatus diff --git a/api/tests/unit_tests/core/ops/tencent_trace/test_span_builder.py b/api/tests/unit_tests/core/ops/tencent_trace/test_span_builder.py index 696f859b6f..6113e5c6c8 100644 --- a/api/tests/unit_tests/core/ops/tencent_trace/test_span_builder.py +++ b/api/tests/unit_tests/core/ops/tencent_trace/test_span_builder.py @@ -1,8 +1,6 @@ from datetime import datetime from unittest.mock import MagicMock, patch -from graphon.entities import WorkflowNodeExecution -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from opentelemetry.trace import StatusCode from core.ops.entities.trace_entity import ( @@ -27,6 +25,8 @@ from core.ops.tencent_trace.entities.semconv import ( ) from core.ops.tencent_trace.span_builder import TencentSpanBuilder from core.rag.models.document import Document +from graphon.entities import WorkflowNodeExecution +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus class TestTencentSpanBuilder: diff --git a/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py b/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py index f67abba807..7afd0b824a 100644 --- a/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py +++ b/api/tests/unit_tests/core/ops/tencent_trace/test_tencent_trace.py @@ -2,8 +2,6 @@ import logging from unittest.mock import MagicMock, patch import pytest -from graphon.entities import WorkflowNodeExecution -from graphon.enums import BuiltinNodeTypes from core.ops.entities.config_entity import TencentConfig from core.ops.entities.trace_entity import ( @@ -16,6 +14,8 @@ from core.ops.entities.trace_entity import ( WorkflowTraceInfo, ) from core.ops.tencent_trace.tencent_trace import TencentDataTrace +from graphon.entities import WorkflowNodeExecution +from graphon.enums import BuiltinNodeTypes from models import Account, App, TenantAccountJoin logger = logging.getLogger(__name__) diff --git a/api/tests/unit_tests/core/ops/test_arize_phoenix_trace.py b/api/tests/unit_tests/core/ops/test_arize_phoenix_trace.py index 6b5cb5b09a..4b925390d9 100644 --- a/api/tests/unit_tests/core/ops/test_arize_phoenix_trace.py +++ b/api/tests/unit_tests/core/ops/test_arize_phoenix_trace.py @@ -1,7 +1,7 @@ -from graphon.enums import BUILT_IN_NODE_TYPES, BuiltinNodeTypes from openinference.semconv.trace import OpenInferenceSpanKindValues from core.ops.arize_phoenix_trace.arize_phoenix_trace import _NODE_TYPE_TO_SPAN_KIND, _get_node_span_kind +from graphon.enums import BUILT_IN_NODE_TYPES, BuiltinNodeTypes class TestGetNodeSpanKind: diff --git a/api/tests/unit_tests/core/ops/test_langfuse_trace.py b/api/tests/unit_tests/core/ops/test_langfuse_trace.py index f8951d2b4a..017ac8c891 100644 --- a/api/tests/unit_tests/core/ops/test_langfuse_trace.py +++ b/api/tests/unit_tests/core/ops/test_langfuse_trace.py @@ -4,11 +4,10 @@ from datetime import datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, patch -from graphon.enums import BuiltinNodeTypes - from core.ops.entities.config_entity import LangfuseConfig from core.ops.entities.trace_entity import MessageTraceInfo, WorkflowTraceInfo from core.ops.langfuse_trace.langfuse_trace import LangFuseDataTrace +from graphon.enums import BuiltinNodeTypes def _create_trace_instance() -> LangFuseDataTrace: diff --git a/api/tests/unit_tests/core/ops/weave_trace/test_weave_trace.py b/api/tests/unit_tests/core/ops/weave_trace/test_weave_trace.py index 5014f40afc..531c7de05f 100644 --- a/api/tests/unit_tests/core/ops/weave_trace/test_weave_trace.py +++ b/api/tests/unit_tests/core/ops/weave_trace/test_weave_trace.py @@ -7,7 +7,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey from weave.trace_server.trace_server_interface import TraceStatus from core.ops.entities.config_entity import WeaveConfig @@ -23,6 +22,7 @@ from core.ops.entities.trace_entity import ( ) from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel from core.ops.weave_trace.weave_trace import WeaveDataTrace +from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionMetadataKey # ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/api/tests/unit_tests/core/plugin/test_backwards_invocation_model.py b/api/tests/unit_tests/core/plugin/test_backwards_invocation_model.py index 543b278715..c24d3ac012 100644 --- a/api/tests/unit_tests/core/plugin/test_backwards_invocation_model.py +++ b/api/tests/unit_tests/core/plugin/test_backwards_invocation_model.py @@ -1,10 +1,9 @@ from types import SimpleNamespace from unittest.mock import patch -from graphon.model_runtime.entities.message_entities import UserPromptMessage - from core.plugin.backwards_invocation.model import PluginModelBackwardsInvocation from core.plugin.entities.request import RequestInvokeSummary +from graphon.model_runtime.entities.message_entities import UserPromptMessage def test_system_model_helpers_forward_user_id(): diff --git a/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py b/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py index f8d0e127b1..68aa130518 100644 --- a/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py +++ b/api/tests/unit_tests/core/plugin/test_model_runtime_adapter.py @@ -6,15 +6,15 @@ from types import SimpleNamespace from unittest.mock import Mock, sentinel import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType -from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity from core.plugin.entities.plugin_daemon import PluginModelProviderEntity from core.plugin.impl import model_runtime as model_runtime_module from core.plugin.impl.model import PluginModelClient from core.plugin.impl.model_runtime import TENANT_SCOPE_SCHEMA_CACHE_USER_ID, PluginModelRuntime from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity def _build_model_schema() -> AIModelEntity: diff --git a/api/tests/unit_tests/core/plugin/test_plugin_entities.py b/api/tests/unit_tests/core/plugin/test_plugin_entities.py index a812b01c5b..f1c4c7e700 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_entities.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_entities.py @@ -4,12 +4,6 @@ from enum import StrEnum import pytest from flask import Response -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - SystemPromptMessage, - ToolPromptMessage, - UserPromptMessage, -) from pydantic import ValidationError from core.plugin.entities.endpoint import EndpointEntityWithInstance @@ -31,6 +25,12 @@ from core.plugin.entities.request import ( ) from core.plugin.utils.http_parser import serialize_response from core.tools.entities.common_entities import I18nObject +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + SystemPromptMessage, + ToolPromptMessage, + UserPromptMessage, +) class TestEndpointEntity: diff --git a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py index a3b1e5f6b0..704b82adc0 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_runtime.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_runtime.py @@ -17,14 +17,6 @@ from unittest.mock import MagicMock, patch import httpx import pytest -from graphon.model_runtime.errors.invoke import ( - InvokeAuthorizationError, - InvokeBadRequestError, - InvokeConnectionError, - InvokeRateLimitError, - InvokeServerUnavailableError, -) -from graphon.model_runtime.errors.validate import CredentialsValidateFailedError from pydantic import BaseModel from core.plugin.entities.plugin_daemon import ( @@ -45,6 +37,14 @@ from core.plugin.impl.exc import ( ) from core.plugin.impl.plugin import PluginInstaller from core.plugin.impl.tool import PluginToolManager +from graphon.model_runtime.errors.invoke import ( + InvokeAuthorizationError, + InvokeBadRequestError, + InvokeConnectionError, + InvokeRateLimitError, + InvokeServerUnavailableError, +) +from graphon.model_runtime.errors.validate import CredentialsValidateFailedError @pytest.fixture(autouse=True) diff --git a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py index 90730dff5a..d49b6e4b71 100644 --- a/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py +++ b/api/tests/unit_tests/core/plugin/utils/test_chunk_merger.py @@ -1,12 +1,12 @@ from collections.abc import Generator import pytest -from graphon.file import File, FileTransferMethod, FileType from core.agent.entities import AgentInvokeMessage from core.plugin.utils.chunk_merger import FileChunk, merge_blob_chunks from core.plugin.utils.converter import convert_parameters_to_plugin_format from core.tools.entities.tool_entities import ToolInvokeMessage, ToolParameter, ToolSelector +from graphon.file import File, FileTransferMethod, FileType class TestChunkMerger: diff --git a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py index 2b280dd674..395d392127 100644 --- a/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_advanced_prompt_transform.py @@ -2,6 +2,13 @@ from typing import cast from unittest.mock import MagicMock, patch import pytest + +from configs import dify_config +from core.app.app_config.entities import ModelConfigEntity +from core.memory.token_buffer_memory import TokenBufferMemory +from core.prompt.advanced_prompt_transform import AdvancedPromptTransform +from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig +from core.prompt.utils.prompt_template_parser import PromptTemplateParser from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, @@ -11,13 +18,6 @@ from graphon.model_runtime.entities.message_entities import ( TextPromptMessageContent, UserPromptMessage, ) - -from configs import dify_config -from core.app.app_config.entities import ModelConfigEntity -from core.memory.token_buffer_memory import TokenBufferMemory -from core.prompt.advanced_prompt_transform import AdvancedPromptTransform -from core.prompt.entities.advanced_prompt_entities import ChatModelMessage, CompletionModelPromptTemplate, MemoryConfig -from core.prompt.utils.prompt_template_parser import PromptTemplateParser from models.model import Conversation diff --git a/api/tests/unit_tests/core/prompt/test_agent_history_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_agent_history_prompt_transform.py index 4a54649b28..803afa54d7 100644 --- a/api/tests/unit_tests/core/prompt/test_agent_history_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_agent_history_prompt_transform.py @@ -1,19 +1,18 @@ from unittest.mock import MagicMock -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - SystemPromptMessage, - ToolPromptMessage, - UserPromptMessage, -) -from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel - from core.app.entities.app_invoke_entities import ( ModelConfigWithCredentialsEntity, ) from core.entities.provider_configuration import ProviderModelBundle from core.memory.token_buffer_memory import TokenBufferMemory from core.prompt.agent_history_prompt_transform import AgentHistoryPromptTransform +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + SystemPromptMessage, + ToolPromptMessage, + UserPromptMessage, +) +from graphon.model_runtime.model_providers.__base.large_language_model import LargeLanguageModel from models.model import Conversation diff --git a/api/tests/unit_tests/core/prompt/test_prompt_message.py b/api/tests/unit_tests/core/prompt/test_prompt_message.py index a4b3960b0a..5d865d934c 100644 --- a/api/tests/unit_tests/core/prompt/test_prompt_message.py +++ b/api/tests/unit_tests/core/prompt/test_prompt_message.py @@ -1,3 +1,5 @@ +from core.prompt.simple_prompt_transform import ModelMode +from core.prompt.utils.prompt_message_util import PromptMessageUtil from graphon.model_runtime.entities.message_entities import ( AssistantPromptMessage, AudioPromptMessageContent, @@ -7,9 +9,6 @@ from graphon.model_runtime.entities.message_entities import ( UserPromptMessage, ) -from core.prompt.simple_prompt_transform import ModelMode -from core.prompt.utils.prompt_message_util import PromptMessageUtil - def test_build_prompt_message_with_prompt_message_contents(): prompt = UserPromptMessage(content=[TextPromptMessageContent(data="Hello, World!")]) diff --git a/api/tests/unit_tests/core/prompt/test_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_prompt_transform.py index e35ce2c48a..9f9ea33695 100644 --- a/api/tests/unit_tests/core/prompt/test_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_prompt_transform.py @@ -2,9 +2,9 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.model_entities import ModelPropertyKey from core.prompt.prompt_transform import PromptTransform +from graphon.model_runtime.entities.model_entities import ModelPropertyKey # from core.app.app_config.entities import ModelConfigEntity # from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle diff --git a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py index 3f188cfbb4..0dc74b33df 100644 --- a/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py +++ b/api/tests/unit_tests/core/prompt/test_simple_prompt_transform.py @@ -2,12 +2,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.message_entities import ( - AssistantPromptMessage, - ImagePromptMessageContent, - TextPromptMessageContent, - UserPromptMessage, -) from core.app.entities.app_invoke_entities import ModelConfigWithCredentialsEntity from core.memory.token_buffer_memory import TokenBufferMemory @@ -24,6 +18,12 @@ from core.prompt.prompt_templates.advanced_prompt_templates import ( CONTEXT, ) from core.prompt.simple_prompt_transform import SimplePromptTransform +from graphon.model_runtime.entities.message_entities import ( + AssistantPromptMessage, + ImagePromptMessageContent, + TextPromptMessageContent, + UserPromptMessage, +) from models.model import AppMode, Conversation diff --git a/api/tests/unit_tests/core/rag/data_post_processor/test_data_post_processor.py b/api/tests/unit_tests/core/rag/data_post_processor/test_data_post_processor.py index 006b4e7345..1f3247590c 100644 --- a/api/tests/unit_tests/core/rag/data_post_processor/test_data_post_processor.py +++ b/api/tests/unit_tests/core/rag/data_post_processor/test_data_post_processor.py @@ -1,13 +1,12 @@ from unittest.mock import MagicMock, patch -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.errors.invoke import InvokeAuthorizationError - from core.rag.data_post_processor.data_post_processor import DataPostProcessor from core.rag.data_post_processor.reorder import ReorderRunner from core.rag.index_processor.constant.query_type import QueryType from core.rag.models.document import Document from core.rag.rerank.rerank_type import RerankMode +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.errors.invoke import InvokeAuthorizationError def _doc(content: str) -> Document: diff --git a/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py b/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py index 3563186186..051a1455ae 100644 --- a/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py +++ b/api/tests/unit_tests/core/rag/embedding/test_cached_embedding.py @@ -12,11 +12,11 @@ from unittest.mock import Mock, patch import numpy as np import pytest -from graphon.model_runtime.entities.model_entities import ModelPropertyKey -from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult, EmbeddingUsage from sqlalchemy.exc import IntegrityError from core.rag.embedding.cached_embedding import CacheEmbedding +from graphon.model_runtime.entities.model_entities import ModelPropertyKey +from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult, EmbeddingUsage from models.dataset import Embedding diff --git a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py index 408cf14a51..4b8175b0b4 100644 --- a/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py +++ b/api/tests/unit_tests/core/rag/embedding/test_embedding_service.py @@ -49,6 +49,10 @@ from unittest.mock import Mock, patch import numpy as np import pytest +from sqlalchemy.exc import IntegrityError + +from core.entities.embedding_type import EmbeddingInputType +from core.rag.embedding.cached_embedding import CacheEmbedding from graphon.model_runtime.entities.model_entities import ModelPropertyKey from graphon.model_runtime.entities.text_embedding_entities import EmbeddingResult, EmbeddingUsage from graphon.model_runtime.errors.invoke import ( @@ -56,10 +60,6 @@ from graphon.model_runtime.errors.invoke import ( InvokeConnectionError, InvokeRateLimitError, ) -from sqlalchemy.exc import IntegrityError - -from core.entities.embedding_type import EmbeddingInputType -from core.rag.embedding.cached_embedding import CacheEmbedding from models.dataset import Embedding diff --git a/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py b/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py index 7ae0da03ff..4ba4d54fa0 100644 --- a/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py +++ b/api/tests/unit_tests/core/rag/indexing/processor/test_paragraph_index_processor.py @@ -3,14 +3,14 @@ from typing import Any from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage -from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, ImagePromptMessageContent -from graphon.model_runtime.entities.model_entities import ModelFeature from core.entities.knowledge_entities import PreviewDetail from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.index_processor.processor.paragraph_index_processor import ParagraphIndexProcessor from core.rag.models.document import AttachmentDocument, Document +from graphon.model_runtime.entities.llm_entities import LLMResult, LLMUsage +from graphon.model_runtime.entities.message_entities import AssistantPromptMessage, ImagePromptMessageContent +from graphon.model_runtime.entities.model_entities import ModelFeature class TestParagraphIndexProcessor: diff --git a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py index 641c5d9ba0..7c4defc180 100644 --- a/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py +++ b/api/tests/unit_tests/core/rag/indexing/test_indexing_runner.py @@ -53,7 +53,6 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.model_runtime.entities.model_entities import ModelType from sqlalchemy.orm.exc import ObjectDeletedError from core.errors.error import ProviderTokenNotInitError @@ -64,6 +63,7 @@ from core.indexing_runner import ( ) from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.rag.models.document import ChildDocument, Document +from graphon.model_runtime.entities.model_entities import ModelType from libs.datetime_utils import naive_utc_now from models.dataset import Dataset, DatasetProcessRule from models.dataset import Document as DatasetDocument diff --git a/api/tests/unit_tests/core/rag/rerank/test_reranker.py b/api/tests/unit_tests/core/rag/rerank/test_reranker.py index c279b00d3b..8bc7dbf70d 100644 --- a/api/tests/unit_tests/core/rag/rerank/test_reranker.py +++ b/api/tests/unit_tests/core/rag/rerank/test_reranker.py @@ -17,7 +17,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.model_runtime.entities.rerank_entities import RerankDocument, RerankResult from core.model_manager import ModelInstance from core.rag.index_processor.constant.doc_type import DocType @@ -29,6 +28,7 @@ from core.rag.rerank.rerank_factory import RerankRunnerFactory from core.rag.rerank.rerank_model import RerankModelRunner from core.rag.rerank.rerank_type import RerankMode from core.rag.rerank.weight_rerank import WeightRerankRunner +from graphon.model_runtime.entities.rerank_entities import RerankDocument, RerankResult def create_mock_model_instance() -> ModelInstance: diff --git a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py index 508fe80e2b..89830f7517 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_dataset_retrieval.py @@ -7,8 +7,6 @@ from uuid import uuid4 import pytest from flask import Flask, current_app -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.model_runtime.entities.model_entities import ModelFeature from core.app.app_config.entities import ( DatasetEntity, @@ -35,6 +33,8 @@ from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.retrieval_methods import RetrievalMethod from core.workflow.nodes.knowledge_retrieval import exc from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.model_runtime.entities.model_entities import ModelFeature from models.dataset import Dataset from models.enums import CreatorUserRole diff --git a/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_function_call_router.py b/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_function_call_router.py index 5a2ecb8220..43c521dcfd 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_function_call_router.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_function_call_router.py @@ -1,8 +1,7 @@ from unittest.mock import Mock -from graphon.model_runtime.entities.llm_entities import LLMUsage - from core.rag.retrieval.router.multi_dataset_function_call_router import FunctionCallMultiDatasetRouter +from graphon.model_runtime.entities.llm_entities import LLMUsage class TestFunctionCallMultiDatasetRouter: diff --git a/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_react_route.py b/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_react_route.py index 539ac0f849..c56528cf55 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_react_route.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_multi_dataset_react_route.py @@ -1,13 +1,12 @@ from types import SimpleNamespace from unittest.mock import Mock, patch +from core.rag.retrieval.output_parser.react_output import ReactAction, ReactFinish +from core.rag.retrieval.router.multi_dataset_react_route import ReactMultiDatasetRouter from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.model_runtime.entities.message_entities import PromptMessageRole from graphon.model_runtime.entities.model_entities import ModelType -from core.rag.retrieval.output_parser.react_output import ReactAction, ReactFinish -from core.rag.retrieval.router.multi_dataset_react_route import ReactMultiDatasetRouter - class TestReactMultiDatasetRouter: def test_invoke_returns_none_when_no_tools(self) -> None: diff --git a/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py b/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py index e229d5fc1a..3d3322094e 100644 --- a/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_celery_workflow_execution_repository.py @@ -9,10 +9,10 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowType from core.repositories.celery_workflow_execution_repository import CeleryWorkflowExecutionRepository +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowType from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.enums import WorkflowRunTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py b/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py index 7dbf78d0f0..05b4f3a053 100644 --- a/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_celery_workflow_node_execution_repository.py @@ -9,14 +9,14 @@ from unittest.mock import Mock, patch from uuid import uuid4 import pytest + +from core.repositories.celery_workflow_node_execution_repository import CeleryWorkflowNodeExecutionRepository +from core.repositories.factory import OrderConfig from graphon.entities.workflow_node_execution import ( WorkflowNodeExecution, WorkflowNodeExecutionStatus, ) from graphon.enums import BuiltinNodeTypes - -from core.repositories.celery_workflow_node_execution_repository import CeleryWorkflowNodeExecutionRepository -from core.repositories.factory import OrderConfig from libs.datetime_utils import naive_utc_now from models import Account, EndUser from models.workflow import WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py index 0fc82dda53..8be1ac318c 100644 --- a/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py +++ b/api/tests/unit_tests/core/repositories/test_human_input_form_repository_impl.py @@ -7,11 +7,6 @@ from datetime import datetime from types import SimpleNamespace import pytest -from graphon.nodes.human_input.entities import ( - FormDefinition, - UserAction, -) -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from core.repositories.human_input_repository import ( HumanInputFormRecord, @@ -26,6 +21,11 @@ from core.workflow.human_input_compat import ( ExternalRecipient, MemberRecipient, ) +from graphon.nodes.human_input.entities import ( + FormDefinition, + UserAction, +) +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import naive_utc_now from models.human_input import ( EmailExternalRecipientPayload, diff --git a/api/tests/unit_tests/core/repositories/test_human_input_repository.py b/api/tests/unit_tests/core/repositories/test_human_input_repository.py index 8ff0e40587..1297a95df1 100644 --- a/api/tests/unit_tests/core/repositories/test_human_input_repository.py +++ b/api/tests/unit_tests/core/repositories/test_human_input_repository.py @@ -9,8 +9,6 @@ from typing import Any from unittest.mock import MagicMock import pytest -from graphon.nodes.human_input.entities import HumanInputNodeData, UserAction -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from core.repositories.human_input_repository import ( FormCreateParams, @@ -31,6 +29,8 @@ from core.workflow.human_input_compat import ( MemberRecipient, WebAppDeliveryMethod, ) +from graphon.nodes.human_input.entities import HumanInputNodeData, UserAction +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import naive_utc_now from models.human_input import HumanInputFormRecipient, RecipientType diff --git a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py index e5c3e85487..a08c5729cb 100644 --- a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_execution_repository.py @@ -3,12 +3,12 @@ from unittest.mock import MagicMock from uuid import uuid4 import pytest -from graphon.entities import WorkflowExecution -from graphon.enums import WorkflowExecutionStatus, WorkflowType from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository +from graphon.entities import WorkflowExecution +from graphon.enums import WorkflowExecutionStatus, WorkflowType from models import Account, CreatorUserRole, EndUser, WorkflowRun from models.enums import WorkflowRunTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py index 5b4d26b780..6af7b02d4c 100644 --- a/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py +++ b/api/tests/unit_tests/core/repositories/test_sqlalchemy_workflow_node_execution_repository.py @@ -10,12 +10,6 @@ from unittest.mock import MagicMock, Mock import psycopg2.errors import pytest -from graphon.entities import WorkflowNodeExecution -from graphon.enums import ( - BuiltinNodeTypes, - WorkflowNodeExecutionMetadataKey, - WorkflowNodeExecutionStatus, -) from sqlalchemy import Engine, create_engine from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker @@ -29,6 +23,12 @@ from core.repositories.sqlalchemy_workflow_node_execution_repository import ( _find_first, _replace_or_append_offload, ) +from graphon.entities import WorkflowNodeExecution +from graphon.enums import ( + BuiltinNodeTypes, + WorkflowNodeExecutionMetadataKey, + WorkflowNodeExecutionStatus, +) from models import Account, EndUser from models.enums import ExecutionOffLoadType from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload, WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py index 84fe522388..abdbc72085 100644 --- a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py +++ b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_conflict_handling.py @@ -4,17 +4,17 @@ from unittest.mock import MagicMock, Mock import psycopg2.errors import pytest -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, - WorkflowNodeExecutionStatus, -) -from graphon.enums import BuiltinNodeTypes from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker from core.repositories.sqlalchemy_workflow_node_execution_repository import ( SQLAlchemyWorkflowNodeExecutionRepository, ) +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, + WorkflowNodeExecutionStatus, +) +from graphon.enums import BuiltinNodeTypes from libs.datetime_utils import naive_utc_now from models import Account, WorkflowNodeExecutionTriggeredFrom diff --git a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py index 27729e7f06..5af1376a0a 100644 --- a/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py +++ b/api/tests/unit_tests/core/repositories/test_workflow_node_execution_truncation.py @@ -11,17 +11,17 @@ from datetime import UTC, datetime from typing import Any from unittest.mock import MagicMock -from graphon.entities.workflow_node_execution import ( - WorkflowNodeExecution, - WorkflowNodeExecutionStatus, -) -from graphon.enums import BuiltinNodeTypes from sqlalchemy import Engine from configs import dify_config from core.repositories.sqlalchemy_workflow_node_execution_repository import ( SQLAlchemyWorkflowNodeExecutionRepository, ) +from graphon.entities.workflow_node_execution import ( + WorkflowNodeExecution, + WorkflowNodeExecutionStatus, +) +from graphon.enums import BuiltinNodeTypes from models import Account, WorkflowNodeExecutionTriggeredFrom from models.enums import ExecutionOffLoadType from models.workflow import WorkflowNodeExecutionModel, WorkflowNodeExecutionOffload diff --git a/api/tests/unit_tests/core/test_file.py b/api/tests/unit_tests/core/test_file.py index ac65d0c02b..f17927f16b 100644 --- a/api/tests/unit_tests/core/test_file.py +++ b/api/tests/unit_tests/core/test_file.py @@ -1,7 +1,6 @@ import json from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig - from models.workflow import Workflow diff --git a/api/tests/unit_tests/core/test_model_manager.py b/api/tests/unit_tests/core/test_model_manager.py index f5efb78b61..afea9144c0 100644 --- a/api/tests/unit_tests/core/test_model_manager.py +++ b/api/tests/unit_tests/core/test_model_manager.py @@ -2,12 +2,12 @@ from unittest.mock import MagicMock, patch import pytest import redis -from graphon.model_runtime.entities.model_entities import ModelType from pytest_mock import MockerFixture from core.entities.provider_entities import ModelLoadBalancingConfiguration from core.model_manager import LBModelManager from extensions.ext_redis import redis_client +from graphon.model_runtime.entities.model_entities import ModelType @pytest.fixture diff --git a/api/tests/unit_tests/core/test_provider_configuration.py b/api/tests/unit_tests/core/test_provider_configuration.py index 331166fe63..b19a21d7f4 100644 --- a/api/tests/unit_tests/core/test_provider_configuration.py +++ b/api/tests/unit_tests/core/test_provider_configuration.py @@ -1,15 +1,6 @@ from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType -from graphon.model_runtime.entities.provider_entities import ( - ConfigurateMethod, - CredentialFormSchema, - FormOption, - FormType, - ProviderEntity, -) from core.entities.provider_configuration import ProviderConfiguration, SystemConfigurationStatus from core.entities.provider_entities import ( @@ -21,6 +12,15 @@ from core.entities.provider_entities import ( RestrictModel, SystemConfiguration, ) +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType +from graphon.model_runtime.entities.provider_entities import ( + ConfigurateMethod, + CredentialFormSchema, + FormOption, + FormType, + ProviderEntity, +) from models.provider import Provider, ProviderType diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index ee26172459..f45b43082c 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -2,12 +2,12 @@ from types import SimpleNamespace from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import ModelType from pytest_mock import MockerFixture from core.entities.provider_entities import ModelSettings from core.provider_manager import ProviderManager +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import ModelType from models.provider import LoadBalancingModelConfig, ProviderModelSetting, TenantDefaultModel from models.provider_ids import ModelProviderID diff --git a/api/tests/unit_tests/core/tools/test_builtin_tool_base.py b/api/tests/unit_tests/core/tools/test_builtin_tool_base.py index 5d744f88c9..1ff81f6120 100644 --- a/api/tests/unit_tests/core/tools/test_builtin_tool_base.py +++ b/api/tests/unit_tests/core/tools/test_builtin_tool_base.py @@ -6,13 +6,13 @@ from typing import Any from unittest.mock import patch import pytest -from graphon.model_runtime.entities.message_entities import UserPromptMessage from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.__base.tool_runtime import ToolRuntime from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage, ToolProviderType +from graphon.model_runtime.entities.message_entities import UserPromptMessage class _BuiltinDummyTool(BuiltinTool): diff --git a/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py b/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py index ee0ce51eec..c7829fc0d7 100644 --- a/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py +++ b/api/tests/unit_tests/core/tools/test_builtin_tools_extra.py @@ -6,8 +6,6 @@ from datetime import date from types import SimpleNamespace import pytest -from graphon.file import FileType -from graphon.model_runtime.entities.model_entities import ModelPropertyKey from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.__base.tool_runtime import ToolRuntime @@ -29,6 +27,8 @@ from core.tools.builtin_tool.tool import BuiltinTool from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage from core.tools.errors import ToolInvokeError +from graphon.file import FileType +from graphon.model_runtime.entities.model_entities import ModelPropertyKey def _build_builtin_tool(tool_cls: type[BuiltinTool]) -> BuiltinTool: diff --git a/api/tests/unit_tests/core/tools/test_tool_file_manager.py b/api/tests/unit_tests/core/tools/test_tool_file_manager.py index 2889cb9db1..ccffdf16d1 100644 --- a/api/tests/unit_tests/core/tools/test_tool_file_manager.py +++ b/api/tests/unit_tests/core/tools/test_tool_file_manager.py @@ -12,9 +12,9 @@ from unittest.mock import MagicMock, Mock, patch import httpx import pytest -from graphon.file import FileTransferMethod from core.tools.tool_file_manager import ToolFileManager +from graphon.file import FileTransferMethod def _setup_tool_file_signing(monkeypatch: pytest.MonkeyPatch) -> dict[str, str]: diff --git a/api/tests/unit_tests/core/tools/utils/test_model_invocation_utils.py b/api/tests/unit_tests/core/tools/utils/test_model_invocation_utils.py index 84b3f71d5e..44785f939c 100644 --- a/api/tests/unit_tests/core/tools/utils/test_model_invocation_utils.py +++ b/api/tests/unit_tests/core/tools/utils/test_model_invocation_utils.py @@ -14,6 +14,8 @@ from typing import Any from unittest.mock import Mock, patch import pytest + +from core.tools.utils.model_invocation_utils import InvokeModelError, ModelInvocationUtils from graphon.model_runtime.entities.model_entities import ModelPropertyKey from graphon.model_runtime.errors.invoke import ( InvokeAuthorizationError, @@ -23,8 +25,6 @@ from graphon.model_runtime.errors.invoke import ( InvokeServerUnavailableError, ) -from core.tools.utils.model_invocation_utils import InvokeModelError, ModelInvocationUtils - def _mock_model_instance(*, schema: dict[str, Any] | None = None) -> SimpleNamespace: model_type_instance = Mock() diff --git a/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py b/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py index 0e3a7e623a..43f3fbd5c9 100644 --- a/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py +++ b/api/tests/unit_tests/core/tools/utils/test_workflow_configuration_sync.py @@ -1,9 +1,9 @@ import pytest -from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.tools.entities.tool_entities import ToolParameter, WorkflowToolParameterConfiguration from core.tools.errors import WorkflowToolHumanInputNotSupportedError from core.tools.utils.workflow_configuration_sync import WorkflowToolConfigurationUtils +from graphon.variables.input_entities import VariableEntity, VariableEntityType def test_ensure_no_human_input_nodes_passes_for_non_human_input(): diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py index 4767480a5a..5a585c609a 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_provider.py @@ -4,7 +4,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.variables.input_entities import VariableEntity, VariableEntityType from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ( @@ -14,6 +13,7 @@ from core.tools.entities.tool_entities import ( ToolProviderType, ) from core.tools.workflow_as_tool.provider import WorkflowToolProviderController +from graphon.variables.input_entities import VariableEntity, VariableEntityType def _controller() -> WorkflowToolProviderController: diff --git a/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py b/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py index c20edd7400..72a73dd936 100644 --- a/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py +++ b/api/tests/unit_tests/core/tools/workflow_as_tool/test_tool.py @@ -11,7 +11,6 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod, FileType from core.app.entities.app_invoke_entities import InvokeFrom from core.tools.__base.tool_runtime import ToolRuntime @@ -25,6 +24,7 @@ from core.tools.entities.tool_entities import ( ) from core.tools.errors import ToolInvokeError from core.tools.workflow_as_tool.tool import WorkflowTool +from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod, FileType class StubScalars: diff --git a/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py b/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py index 0c67effe90..fb7dc52838 100644 --- a/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py +++ b/api/tests/unit_tests/core/trigger/debug/test_debug_event_selectors.py @@ -12,7 +12,6 @@ from typing import Any from unittest.mock import MagicMock, patch import pytest -from graphon.enums import BuiltinNodeTypes, NodeType from core.plugin.entities.request import TriggerInvokeEventResponse from core.trigger.constants import ( @@ -28,6 +27,7 @@ from core.trigger.debug.event_selectors import ( select_trigger_debug_events, ) from core.trigger.debug.events import PluginTriggerDebugEvent, WebhookDebugEvent +from graphon.enums import BuiltinNodeTypes, NodeType from tests.unit_tests.core.trigger.conftest import VALID_PROVIDER_ID diff --git a/api/tests/unit_tests/core/variables/test_segment.py b/api/tests/unit_tests/core/variables/test_segment.py index 7406b88270..72052c8c05 100644 --- a/api/tests/unit_tests/core/variables/test_segment.py +++ b/api/tests/unit_tests/core/variables/test_segment.py @@ -2,6 +2,11 @@ import dataclasses import orjson import pytest +from pydantic import BaseModel + +from core.helper import encrypter +from core.workflow.system_variables import build_bootstrap_variables, build_system_variables +from core.workflow.variable_pool_initializer import add_variables_to_pool from graphon.file import File, FileTransferMethod, FileType from graphon.runtime import VariablePool from graphon.variables.segment_group import SegmentGroup @@ -42,11 +47,6 @@ from graphon.variables.variables import ( StringVariable, Variable, ) -from pydantic import BaseModel - -from core.helper import encrypter -from core.workflow.system_variables import build_bootstrap_variables, build_system_variables -from core.workflow.variable_pool_initializer import add_variables_to_pool def _build_variable_pool( diff --git a/api/tests/unit_tests/core/variables/test_segment_type.py b/api/tests/unit_tests/core/variables/test_segment_type.py index 37ecd2890b..d4e862220a 100644 --- a/api/tests/unit_tests/core/variables/test_segment_type.py +++ b/api/tests/unit_tests/core/variables/test_segment_type.py @@ -1,4 +1,5 @@ import pytest + from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import StringSegment from graphon.variables.types import ArrayValidation, SegmentType diff --git a/api/tests/unit_tests/core/variables/test_segment_type_validation.py b/api/tests/unit_tests/core/variables/test_segment_type_validation.py index 09254e17a3..94e788edb2 100644 --- a/api/tests/unit_tests/core/variables/test_segment_type_validation.py +++ b/api/tests/unit_tests/core/variables/test_segment_type_validation.py @@ -9,6 +9,7 @@ from dataclasses import dataclass from typing import Any import pytest + from graphon.file import File, FileTransferMethod, FileType from graphon.variables.segment_group import SegmentGroup from graphon.variables.segments import ( diff --git a/api/tests/unit_tests/core/variables/test_variables.py b/api/tests/unit_tests/core/variables/test_variables.py index 75b01bf42e..dae5e1ce98 100644 --- a/api/tests/unit_tests/core/variables/test_variables.py +++ b/api/tests/unit_tests/core/variables/test_variables.py @@ -1,4 +1,6 @@ import pytest +from pydantic import ValidationError + from graphon.variables import ( ArrayFileVariable, ArrayVariable, @@ -10,7 +12,6 @@ from graphon.variables import ( StringVariable, ) from graphon.variables.variables import VariableBase -from pydantic import ValidationError def test_frozen_variables(): diff --git a/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py b/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py index 41627f5e0b..025d79b25d 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/layers/conftest.py @@ -5,12 +5,13 @@ Shared fixtures for ObservabilityLayer tests. from unittest.mock import MagicMock, patch import pytest -from graphon.enums import BuiltinNodeTypes from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace import set_tracer_provider +from graphon.enums import BuiltinNodeTypes + @pytest.fixture def memory_span_exporter(): @@ -61,9 +62,8 @@ def mock_llm_node(): @pytest.fixture def mock_tool_node(): """Create a mock Tool Node with tool-specific attributes.""" - from graphon.nodes.tool.entities import ToolNodeData - from core.tools.entities.tool_entities import ToolProviderType + from graphon.nodes.tool.entities import ToolNodeData node = MagicMock() node.id = "test-tool-node-id" diff --git a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_llm_quota.py b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_llm_quota.py index 99d131737e..5d6667257f 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_llm_quota.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_llm_quota.py @@ -3,17 +3,16 @@ from datetime import datetime from types import SimpleNamespace from unittest.mock import MagicMock, patch +from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, UserFrom +from core.app.workflow.layers.llm_quota import LLMQuotaLayer +from core.errors.error import QuotaExceededError +from core.model_manager import ModelInstance from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.graph_engine.entities.commands import CommandType from graphon.graph_events import NodeRunSucceededEvent from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.node_events import NodeRunResult -from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, UserFrom -from core.app.workflow.layers.llm_quota import LLMQuotaLayer -from core.errors.error import QuotaExceededError -from core.model_manager import ModelInstance - def _build_dify_context() -> DifyRunContext: return DifyRunContext( diff --git a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py index 9cf72763ee..919f15efd0 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/layers/test_observability.py @@ -13,10 +13,10 @@ Test coverage: from unittest.mock import patch import pytest -from graphon.enums import BuiltinNodeTypes from opentelemetry.trace import StatusCode from core.app.workflow.layers.observability import ObservabilityLayer +from graphon.enums import BuiltinNodeTypes class TestObservabilityLayerInitialization: diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py index 88989db856..76b2984a4b 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py @@ -7,12 +7,11 @@ requiring external services (LLM, Agent, Tool, Knowledge Retrieval, HTTP Request from typing import TYPE_CHECKING, Any +from core.workflow.node_factory import DifyNodeFactory from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.base.node import Node -from core.workflow.node_factory import DifyNodeFactory - from .test_mock_nodes import ( MockAgentNode, MockCodeNode, diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py index 8b7fbd1b30..971b9b2bbf 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_nodes.py @@ -10,6 +10,10 @@ from collections.abc import Generator, Mapping from typing import TYPE_CHECKING, Any, Optional from unittest.mock import MagicMock +from core.model_manager import ModelInstance +from core.workflow.node_runtime import DifyToolNodeRuntime +from core.workflow.nodes.agent import AgentNode +from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent @@ -27,11 +31,6 @@ from graphon.nodes.template_transform import TemplateTransformNode from graphon.nodes.tool import ToolNode from graphon.template_rendering import Jinja2TemplateRenderer, TemplateRenderError -from core.model_manager import ModelInstance -from core.workflow.node_runtime import DifyToolNodeRuntime -from core.workflow.nodes.agent import AgentNode -from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode - if TYPE_CHECKING: from graphon.entities import GraphInitParams from graphon.runtime import GraphRuntimeState diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py b/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py index 8311a1e847..55a329eba9 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_parallel_human_input_join_resume.py @@ -4,6 +4,13 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, Protocol +from core.repositories.human_input_repository import ( + FormCreateParams, + HumanInputFormEntity, + HumanInputFormRepository, +) +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import build_system_variables from graphon.entities import WorkflowStartReason from graphon.graph import Graph from graphon.graph_engine import GraphEngine, GraphEngineConfig @@ -23,14 +30,6 @@ from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState, VariablePool - -from core.repositories.human_input_repository import ( - FormCreateParams, - HumanInputFormEntity, - HumanInputFormRepository, -) -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import build_system_variables from libs.datetime_utils import naive_utc_now from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py b/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py index b11f957677..7d23b63049 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_table_runner.py @@ -19,6 +19,11 @@ from functools import lru_cache from pathlib import Path from typing import Any +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.tools.utils.yaml_utils import _load_yaml_file +from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id +from core.workflow.system_variables import build_bootstrap_variables, build_system_variables +from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from graphon.entities import GraphInitParams from graphon.graph import Graph from graphon.graph_engine import GraphEngine, GraphEngineConfig @@ -39,12 +44,6 @@ from graphon.variables import ( StringVariable, ) -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.tools.utils.yaml_utils import _load_yaml_file -from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id -from core.workflow.system_variables import build_bootstrap_variables, build_system_variables -from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool - from .test_mock_config import MockConfig from .test_mock_factory import MockNodeFactory diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_message_transformer.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_message_transformer.py index cbc920705c..1f4509af9a 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent/test_message_transformer.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_message_transformer.py @@ -1,9 +1,8 @@ from unittest.mock import patch -from graphon.enums import BuiltinNodeTypes - from core.tools.utils.message_transformer import ToolFileMessageTransformer from core.workflow.nodes.agent.message_transformer import AgentMessageTransformer +from graphon.enums import BuiltinNodeTypes def test_transform_passes_conversation_id_to_tool_file_message_transformer() -> None: diff --git a/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py b/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py index 59dd763b59..c86de7f6e6 100644 --- a/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py +++ b/api/tests/unit_tests/core/workflow/nodes/agent/test_runtime_support.py @@ -1,9 +1,8 @@ from types import SimpleNamespace from unittest.mock import Mock, patch -from graphon.model_runtime.entities.model_entities import ModelType - from core.workflow.nodes.agent.runtime_support import AgentRuntimeSupport +from graphon.model_runtime.entities.model_entities import ModelType def test_fetch_model_reuses_single_model_assembly(): diff --git a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py index 7195471eb6..9c0ad25b58 100644 --- a/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py +++ b/api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py @@ -2,15 +2,14 @@ import time import uuid from unittest.mock import MagicMock -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.graph import Graph -from graphon.nodes.answer.answer_node import AnswerNode -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.workflow.node_factory import DifyNodeFactory from core.workflow.system_variables import build_system_variables from extensions.ext_database import db +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.graph import Graph +from graphon.nodes.answer.answer_node import AnswerNode +from graphon.runtime import GraphRuntimeState, VariablePool from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py index 343bcd3919..ec4cef1955 100644 --- a/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/base/test_base_node.py @@ -1,10 +1,10 @@ import pytest + +from core.workflow.node_factory import get_node_type_classes_mapping from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.base.node import Node -from core.workflow.node_factory import get_node_type_classes_mapping - # Ensures that all production node classes are imported and registered. _ = get_node_type_classes_mapping() diff --git a/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py b/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py index b9371a34f4..ef0df55995 100644 --- a/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py +++ b/api/tests/unit_tests/core/workflow/nodes/base/test_get_node_type_classes_mapping.py @@ -1,6 +1,7 @@ import types from collections.abc import Mapping +from core.workflow.node_factory import get_node_type_classes_mapping from graphon.entities.base_node_data import BaseNodeData from graphon.enums import BuiltinNodeTypes, NodeType from graphon.nodes.base.node import Node @@ -13,8 +14,6 @@ from graphon.nodes.variable_assigner.v2.node import ( VariableAssignerNode as VariableAssignerV2, ) -from core.workflow.node_factory import get_node_type_classes_mapping - def test_variable_assigner_latest_prefers_highest_numeric_version(): # Act diff --git a/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py index d155124c50..ce0c9b79c6 100644 --- a/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/code/code_node_spec.py @@ -1,3 +1,4 @@ +from configs import dify_config from graphon.nodes.code.code_node import CodeNode from graphon.nodes.code.entities import CodeLanguage, CodeNodeData from graphon.nodes.code.exc import ( @@ -8,8 +9,6 @@ from graphon.nodes.code.exc import ( from graphon.nodes.code.limits import CodeNodeLimits from graphon.variables.types import SegmentType -from configs import dify_config - CodeNode._limits = CodeNodeLimits( max_string_length=dify_config.CODE_MAX_STRING_LENGTH, max_number=dify_config.CODE_MAX_NUMBER, diff --git a/api/tests/unit_tests/core/workflow/nodes/datasource/test_datasource_node.py b/api/tests/unit_tests/core/workflow/nodes/datasource/test_datasource_node.py index fb03ae9998..9cceadde49 100644 --- a/api/tests/unit_tests/core/workflow/nodes/datasource/test_datasource_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/datasource/test_datasource_node.py @@ -1,8 +1,7 @@ -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent - from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY from core.workflow.nodes.datasource.datasource_node import DatasourceNode +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.node_events import NodeRunResult, StreamChunkEvent, StreamCompletedEvent class _VarSeg: diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index a5026b40cf..be7cc073db 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -1,4 +1,8 @@ import pytest + +from configs import dify_config +from core.helper.ssrf_proxy import ssrf_proxy +from core.workflow.system_variables import default_system_variables from graphon.file.file_manager import file_manager from graphon.nodes.http_request import ( BodyData, @@ -12,10 +16,6 @@ from graphon.nodes.http_request.exc import AuthorizationConfigError from graphon.nodes.http_request.executor import Executor from graphon.runtime import VariablePool -from configs import dify_config -from core.helper.ssrf_proxy import ssrf_proxy -from core.workflow.system_variables import default_system_variables - HTTP_REQUEST_CONFIG = HttpRequestNodeConfig( max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT, max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT, diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py index 4705b3f76e..a3cadc0681 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py @@ -3,17 +3,17 @@ from typing import Any import httpx import pytest -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.file.file_manager import file_manager -from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNode, HttpRequestNodeConfig -from graphon.nodes.http_request.entities import HttpRequestNodeTimeout, Response -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.helper.ssrf_proxy import ssrf_proxy from core.tools.tool_file_manager import ToolFileManager from core.workflow.node_runtime import DifyFileReferenceFactory from core.workflow.system_variables import build_system_variables +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.file.file_manager import file_manager +from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNode, HttpRequestNodeConfig +from graphon.nodes.http_request.entities import HttpRequestNodeTimeout, Response +from graphon.runtime import GraphRuntimeState, VariablePool from tests.workflow_test_utils import build_test_graph_init_params HTTP_REQUEST_CONFIG = HttpRequestNodeConfig( diff --git a/api/tests/unit_tests/core/workflow/nodes/human_input/test_email_delivery_config.py b/api/tests/unit_tests/core/workflow/nodes/human_input/test_email_delivery_config.py index d16e1233ac..1d6a4da7c4 100644 --- a/api/tests/unit_tests/core/workflow/nodes/human_input/test_email_delivery_config.py +++ b/api/tests/unit_tests/core/workflow/nodes/human_input/test_email_delivery_config.py @@ -1,6 +1,5 @@ -from graphon.runtime import VariablePool - from core.workflow.human_input_compat import EmailDeliveryConfig, EmailRecipients +from graphon.runtime import VariablePool def test_render_body_template_replaces_variable_values(): diff --git a/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py b/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py index a2cdbbf132..c0e21d0bf7 100644 --- a/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py +++ b/api/tests/unit_tests/core/workflow/nodes/human_input/test_entities.py @@ -10,24 +10,6 @@ from typing import Any from unittest.mock import MagicMock import pytest -from graphon.entities import GraphInitParams -from graphon.node_events import PauseRequestedEvent -from graphon.node_events.node import StreamCompletedEvent -from graphon.nodes.human_input.entities import ( - FormInput, - FormInputDefault, - HumanInputNodeData, - UserAction, -) -from graphon.nodes.human_input.enums import ( - ButtonStyle, - FormInputType, - HumanInputFormStatus, - PlaceholderType, - TimeoutUnit, -) -from graphon.nodes.human_input.human_input_node import HumanInputNode -from graphon.runtime import GraphRuntimeState, VariablePool from pydantic import ValidationError from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY @@ -50,6 +32,24 @@ from core.workflow.human_input_compat import ( ) from core.workflow.node_runtime import DifyHumanInputNodeRuntime from core.workflow.system_variables import build_system_variables +from graphon.entities import GraphInitParams +from graphon.node_events import PauseRequestedEvent +from graphon.node_events.node import StreamCompletedEvent +from graphon.nodes.human_input.entities import ( + FormInput, + FormInputDefault, + HumanInputNodeData, + UserAction, +) +from graphon.nodes.human_input.enums import ( + ButtonStyle, + FormInputType, + HumanInputFormStatus, + PlaceholderType, + TimeoutUnit, +) +from graphon.nodes.human_input.human_input_node import HumanInputNode +from graphon.runtime import GraphRuntimeState, VariablePool from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py b/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py index 52802c7ce1..bc98028d5b 100644 --- a/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py +++ b/api/tests/unit_tests/core/workflow/nodes/human_input/test_human_input_form_filled_event.py @@ -1,6 +1,9 @@ import datetime from types import SimpleNamespace +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.workflow.node_runtime import DifyHumanInputNodeRuntime +from core.workflow.system_variables import default_system_variables from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes from graphon.graph_events import ( @@ -11,10 +14,6 @@ from graphon.graph_events import ( from graphon.nodes.human_input.enums import HumanInputFormStatus from graphon.nodes.human_input.human_input_node import HumanInputNode from graphon.runtime import GraphRuntimeState, VariablePool - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.workflow.node_runtime import DifyHumanInputNodeRuntime -from core.workflow.system_variables import default_system_variables from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration_child_engine_errors.py b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration_child_engine_errors.py index bbfe350f7e..82cc734274 100644 --- a/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration_child_engine_errors.py +++ b/api/tests/unit_tests/core/workflow/nodes/iteration/test_iteration_child_engine_errors.py @@ -2,6 +2,8 @@ from collections.abc import Mapping from typing import Any import pytest + +from core.workflow.system_variables import default_system_variables from graphon.entities import GraphInitParams from graphon.nodes.iteration.exc import IterationGraphNotFoundError from graphon.nodes.iteration.iteration_node import IterationNode @@ -11,8 +13,6 @@ from graphon.runtime import ( GraphRuntimeState, VariablePool, ) - -from core.workflow.system_variables import default_system_variables from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/knowledge_index/test_knowledge_index_node.py b/api/tests/unit_tests/core/workflow/nodes/knowledge_index/test_knowledge_index_node.py index f8802138b5..a6fca1bfb4 100644 --- a/api/tests/unit_tests/core/workflow/nodes/knowledge_index/test_knowledge_index_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/knowledge_index/test_knowledge_index_node.py @@ -3,9 +3,6 @@ import uuid from unittest.mock import Mock import pytest -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variables.segments import StringSegment from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.rag.index_processor.constant.index_type import IndexTechniqueType @@ -19,6 +16,9 @@ from core.workflow.nodes.knowledge_index.protocols import ( SummaryIndexServiceProtocol, ) from core.workflow.system_variables import SystemVariableKey, build_system_variables +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variables.segments import StringSegment from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py index ab64be59ad..45e8ae7d20 100644 --- a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval/test_knowledge_retrieval_node.py @@ -3,10 +3,6 @@ import uuid from unittest.mock import Mock import pytest -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variables import StringSegment from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.workflow.nodes.knowledge_retrieval.entities import ( @@ -21,6 +17,10 @@ from core.workflow.nodes.knowledge_retrieval.exc import RateLimitExceededError from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode from core.workflow.nodes.knowledge_retrieval.retrieval import RAGRetrievalProtocol, Source from core.workflow.system_variables import build_system_variables +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variables import StringSegment from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py b/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py index fdf1706765..eca34f05be 100644 --- a/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/list_operator/node_spec.py @@ -1,14 +1,14 @@ from unittest.mock import MagicMock import pytest + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.nodes.list_operator.node import ListOperatorNode from graphon.runtime import GraphRuntimeState from graphon.variables import ArrayNumberSegment, ArrayStringSegment -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY - class TestListOperatorNode: """Comprehensive tests for ListOperatorNode.""" diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_llm_utils.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_llm_utils.py index c784f805c0..4186bbdc93 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_llm_utils.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_llm_utils.py @@ -1,6 +1,8 @@ from unittest import mock import pytest + +from core.model_manager import ModelInstance from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities import ( ImagePromptMessageContent, @@ -33,8 +35,6 @@ from graphon.nodes.llm.exc import ( from graphon.runtime import VariablePool from graphon.variables import ArrayAnySegment, ArrayFileSegment, NoneSegment -from core.model_manager import ModelInstance - def _build_model_schema( *, diff --git a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py index 7841bf05ad..b1f81b6c48 100644 --- a/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/llm/test_node.py @@ -5,6 +5,19 @@ from collections.abc import Sequence from unittest import mock import pytest + +from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, ModelConfigWithCredentialsEntity, UserFrom +from core.app.llm.model_access import ( + DifyCredentialsProvider, + DifyModelFactory, + build_dify_model_access, + fetch_model_config, +) +from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle +from core.entities.provider_entities import CustomConfiguration, SystemConfiguration +from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime +from core.prompt.entities.advanced_prompt_entities import MemoryConfig +from core.workflow.system_variables import default_system_variables from graphon.entities import GraphInitParams from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.common_entities import I18nObject @@ -67,19 +80,6 @@ from graphon.nodes.llm.runtime_protocols import PromptMessageSerializerProtocol from graphon.runtime import GraphRuntimeState, VariablePool from graphon.template_rendering import TemplateRenderError from graphon.variables import ArrayAnySegment, ArrayFileSegment, NoneSegment - -from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, ModelConfigWithCredentialsEntity, UserFrom -from core.app.llm.model_access import ( - DifyCredentialsProvider, - DifyModelFactory, - build_dify_model_access, - fetch_model_config, -) -from core.entities.provider_configuration import ProviderConfiguration, ProviderModelBundle -from core.entities.provider_entities import CustomConfiguration, SystemConfiguration -from core.plugin.impl.model_runtime_factory import create_plugin_model_runtime -from core.prompt.entities.advanced_prompt_entities import MemoryConfig -from core.workflow.system_variables import default_system_variables from models.provider import ProviderType from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py index 1c362a0a03..8f8ec49f14 100644 --- a/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/parameter_extractor/test_parameter_extractor_node.py @@ -6,6 +6,8 @@ from dataclasses import dataclass from typing import Any import pytest + +from factories.variable_factory import build_segment_with_type from graphon.model_runtime.entities import LLMMode from graphon.nodes.llm import ModelConfig, VisionConfig from graphon.nodes.parameter_extractor.entities import ParameterConfig, ParameterExtractorNodeData @@ -18,8 +20,6 @@ from graphon.nodes.parameter_extractor.exc import ( from graphon.nodes.parameter_extractor.parameter_extractor_node import ParameterExtractorNode from graphon.variables.types import SegmentType -from factories.variable_factory import build_segment_with_type - @dataclass class ValidTestCase: diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py index d86e0efe02..bc44ececd8 100644 --- a/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py +++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/template_transform_node_spec.py @@ -1,6 +1,8 @@ from unittest.mock import MagicMock import pytest + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from graphon.enums import BuiltinNodeTypes, ErrorStrategy, WorkflowNodeExecutionStatus from graphon.graph import Graph from graphon.nodes.base.entities import VariableSelector @@ -8,8 +10,6 @@ from graphon.nodes.template_transform.entities import TemplateTransformNodeData from graphon.nodes.template_transform.template_transform_node import TemplateTransformNode from graphon.runtime import GraphRuntimeState from graphon.template_rendering import TemplateRenderError - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/template_transform/test_template_transform_node.py b/api/tests/unit_tests/core/workflow/nodes/template_transform/test_template_transform_node.py index bd22a8e318..636237e56e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/template_transform/test_template_transform_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/template_transform/test_template_transform_node.py @@ -1,14 +1,14 @@ from unittest.mock import MagicMock import pytest + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from graphon.nodes.base.entities import VariableSelector from graphon.nodes.template_transform.template_transform_node import ( DEFAULT_TEMPLATE_TRANSFORM_MAX_OUTPUT_LENGTH, TemplateTransformNode, ) from graphon.runtime import GraphRuntimeState - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from tests.workflow_test_utils import build_test_graph_init_params from .template_transform_node_spec import TestTemplateTransformNode # noqa: F401 diff --git a/api/tests/unit_tests/core/workflow/nodes/test_base_node.py b/api/tests/unit_tests/core/workflow/nodes/test_base_node.py index e11ebf6eb8..0522dd9d14 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_base_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_base_node.py @@ -1,16 +1,16 @@ from collections.abc import Mapping import pytest + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.workflow.node_runtime import resolve_dify_run_context +from core.workflow.system_variables import build_system_variables from graphon.entities import GraphInitParams from graphon.entities.base_node_data import BaseNodeData from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter from graphon.enums import BuiltinNodeTypes from graphon.nodes.base.node import Node from graphon.runtime import GraphRuntimeState, VariablePool - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.workflow.node_runtime import resolve_dify_run_context -from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py index 555ff0c945..87ec2d5bce 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_document_extractor_node.py @@ -4,6 +4,8 @@ from unittest.mock import Mock, patch import pandas as pd import pytest from docx.oxml.text.paragraph import CT_P + +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from graphon.entities import GraphInitParams from graphon.enums import BuiltinNodeTypes, WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod @@ -19,8 +21,6 @@ from graphon.nodes.document_extractor.node import ( from graphon.variables import ArrayFileSegment from graphon.variables.segments import ArrayStringSegment from graphon.variables.variables import StringVariable - -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py index 1b14f0ab13..782750e02e 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_if_else.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_if_else.py @@ -3,6 +3,11 @@ import uuid from unittest.mock import MagicMock, Mock import pytest + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom +from core.workflow.node_factory import DifyNodeFactory +from core.workflow.system_variables import build_system_variables +from extensions.ext_database import db from graphon.enums import WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod, FileType from graphon.graph import Graph @@ -11,11 +16,6 @@ from graphon.nodes.if_else.if_else_node import IfElseNode from graphon.runtime import GraphRuntimeState, VariablePool from graphon.utils.condition.entities import Condition, SubCondition, SubVariableCondition from graphon.variables import ArrayFileSegment - -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom -from core.workflow.node_factory import DifyNodeFactory -from core.workflow.system_variables import build_system_variables -from extensions.ext_database import db from tests.workflow_test_utils import build_test_graph_init_params diff --git a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py index d28c3e01e5..b217e4e8e7 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_list_operator.py @@ -1,6 +1,8 @@ from unittest.mock import MagicMock import pytest + +from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom from graphon.enums import WorkflowNodeExecutionStatus from graphon.file import File, FileTransferMethod, FileType from graphon.nodes.list_operator.entities import ( @@ -16,8 +18,6 @@ from graphon.nodes.list_operator.exc import InvalidKeyError from graphon.nodes.list_operator.node import ListOperatorNode, _get_file_extract_string_func from graphon.variables import ArrayFileSegment -from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom - @pytest.fixture def list_operator_node(): diff --git a/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py b/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py index 833c303052..543f9878de 100644 --- a/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py +++ b/api/tests/unit_tests/core/workflow/nodes/test_start_node_json_object.py @@ -2,16 +2,16 @@ import json import time import pytest +from pydantic import ValidationError as PydanticValidationError + +from core.workflow.system_variables import build_system_variables +from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID from graphon.nodes.start.entities import StartNodeData from graphon.nodes.start.start_node import StartNode from graphon.runtime import GraphRuntimeState from graphon.variables import build_segment, segment_to_variable from graphon.variables.input_entities import VariableEntity, VariableEntityType from graphon.variables.variables import Variable -from pydantic import ValidationError as PydanticValidationError - -from core.workflow.system_variables import build_system_variables -from core.workflow.variable_prefixes import CONVERSATION_VARIABLE_NODE_ID, ENVIRONMENT_VARIABLE_NODE_ID from tests.workflow_test_utils import build_test_graph_init_params, build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py index 1587014802..c806181340 100644 --- a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node.py @@ -8,14 +8,14 @@ from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock import pytest + +from core.workflow.system_variables import build_system_variables from graphon.file import File, FileTransferMethod, FileType from graphon.model_runtime.entities.llm_entities import LLMUsage from graphon.node_events import StreamChunkEvent, StreamCompletedEvent from graphon.nodes.tool_runtime_entities import ToolRuntimeHandle, ToolRuntimeMessage from graphon.runtime import GraphRuntimeState, VariablePool from graphon.variables.segments import ArrayFileSegment - -from core.workflow.system_variables import build_system_variables from tests.workflow_test_utils import build_test_graph_init_params if TYPE_CHECKING: # pragma: no cover - imported for type checking only diff --git a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py index c4dfc5a179..438af211f3 100644 --- a/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py +++ b/api/tests/unit_tests/core/workflow/nodes/tool/test_tool_node_runtime.py @@ -6,11 +6,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest -from graphon.model_runtime.entities.llm_entities import LLMUsage -from graphon.nodes.tool.entities import ToolNodeData, ToolProviderType -from graphon.nodes.tool.exc import ToolRuntimeInvocationError -from graphon.nodes.tool_runtime_entities import ToolRuntimeHandle, ToolRuntimeMessage -from graphon.runtime import VariablePool from core.callback_handler.workflow_tool_callback_handler import DifyWorkflowCallbackHandler from core.plugin.impl.exc import PluginDaemonClientSideError, PluginInvokeError @@ -22,6 +17,11 @@ from core.tools.tool_manager import ToolManager from core.tools.utils.message_transformer import ToolFileMessageTransformer from core.workflow.node_runtime import DifyToolNodeRuntime from core.workflow.system_variables import build_system_variables +from graphon.model_runtime.entities.llm_entities import LLMUsage +from graphon.nodes.tool.entities import ToolNodeData, ToolProviderType +from graphon.nodes.tool.exc import ToolRuntimeInvocationError +from graphon.nodes.tool_runtime_entities import ToolRuntimeHandle, ToolRuntimeMessage +from graphon.runtime import VariablePool from tests.workflow_test_utils import build_test_graph_init_params, build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/nodes/trigger_plugin/test_trigger_event_node.py b/api/tests/unit_tests/core/workflow/nodes/trigger_plugin/test_trigger_event_node.py index 952e798430..c8ddc53284 100644 --- a/api/tests/unit_tests/core/workflow/nodes/trigger_plugin/test_trigger_event_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/trigger_plugin/test_trigger_event_node.py @@ -1,13 +1,12 @@ from collections.abc import Mapping -from graphon.entities import GraphInitParams -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter -from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState - from core.trigger.constants import TRIGGER_PLUGIN_NODE_TYPE from core.workflow.nodes.trigger_plugin.trigger_event_node import TriggerEventNode from core.workflow.system_variables import build_system_variables +from graphon.entities import GraphInitParams +from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter +from graphon.enums import WorkflowNodeExecutionMetadataKey, WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState from tests.workflow_test_utils import build_test_graph_init_params, build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py b/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py index f1132af02b..617554ee17 100644 --- a/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py +++ b/api/tests/unit_tests/core/workflow/nodes/webhook/test_exceptions.py @@ -1,5 +1,4 @@ import pytest -from graphon.entities.exc import BaseNodeError from core.workflow.nodes.trigger_webhook.exc import ( WebhookConfigError, @@ -7,6 +6,7 @@ from core.workflow.nodes.trigger_webhook.exc import ( WebhookNotFoundError, WebhookTimeoutError, ) +from graphon.entities.exc import BaseNodeError def test_webhook_node_error_inheritance(): diff --git a/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_file_conversion.py b/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_file_conversion.py index 8056217479..1bbc12b23f 100644 --- a/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_file_conversion.py +++ b/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_file_conversion.py @@ -9,10 +9,6 @@ when passing files to downstream LLM nodes. from typing import Any from unittest.mock import Mock, patch -from graphon.entities import GraphInitParams -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom from core.workflow.nodes.trigger_webhook.entities import ( ContentType, @@ -22,6 +18,9 @@ from core.workflow.nodes.trigger_webhook.entities import ( ) from core.workflow.nodes.trigger_webhook.node import TriggerWebhookNode from core.workflow.system_variables import default_system_variables +from graphon.entities import GraphInitParams +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool from tests.workflow_test_utils import build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_node.py b/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_node.py index c19e28bbd5..427afa96ec 100644 --- a/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/webhook/test_webhook_node.py @@ -2,11 +2,6 @@ from typing import Any from unittest.mock import patch import pytest -from graphon.entities import GraphInitParams -from graphon.enums import WorkflowNodeExecutionStatus -from graphon.file import File, FileTransferMethod, FileType -from graphon.runtime import GraphRuntimeState, VariablePool -from graphon.variables import FileVariable, StringVariable from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, InvokeFrom, UserFrom from core.trigger.constants import TRIGGER_WEBHOOK_NODE_TYPE @@ -19,6 +14,11 @@ from core.workflow.nodes.trigger_webhook.entities import ( ) from core.workflow.nodes.trigger_webhook.node import TriggerWebhookNode from core.workflow.system_variables import default_system_variables +from graphon.entities import GraphInitParams +from graphon.enums import WorkflowNodeExecutionStatus +from graphon.file import File, FileTransferMethod, FileType +from graphon.runtime import GraphRuntimeState, VariablePool +from graphon.variables import FileVariable, StringVariable from tests.workflow_test_utils import build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/test_human_input_compat.py b/api/tests/unit_tests/core/workflow/test_human_input_compat.py index cd41c43e4a..0623800b30 100644 --- a/api/tests/unit_tests/core/workflow/test_human_input_compat.py +++ b/api/tests/unit_tests/core/workflow/test_human_input_compat.py @@ -1,6 +1,5 @@ from types import SimpleNamespace -from graphon.enums import BuiltinNodeTypes from pydantic import BaseModel from core.workflow.human_input_compat import ( @@ -16,6 +15,7 @@ from core.workflow.human_input_compat import ( normalize_node_data_for_graph, parse_human_input_delivery_methods, ) +from graphon.enums import BuiltinNodeTypes def test_email_delivery_config_helpers_render_and_sanitize_text() -> None: diff --git a/api/tests/unit_tests/core/workflow/test_node_factory.py b/api/tests/unit_tests/core/workflow/test_node_factory.py index dfe1a47e37..424c50eb26 100644 --- a/api/tests/unit_tests/core/workflow/test_node_factory.py +++ b/api/tests/unit_tests/core/workflow/test_node_factory.py @@ -2,15 +2,15 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch, sentinel import pytest -from graphon.entities.base_node_data import BaseNodeData -from graphon.enums import BuiltinNodeTypes, NodeType -from graphon.nodes.code.entities import CodeLanguage -from graphon.variables.segments import StringSegment from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext, InvokeFrom, UserFrom from core.workflow import node_factory from core.workflow import template_rendering as workflow_template_rendering from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE +from graphon.entities.base_node_data import BaseNodeData +from graphon.enums import BuiltinNodeTypes, NodeType +from graphon.nodes.code.entities import CodeLanguage +from graphon.variables.segments import StringSegment def _assert_typed_node_config(config, *, node_id: str, node_type: NodeType, version: str = "1") -> None: diff --git a/api/tests/unit_tests/core/workflow/test_node_runtime.py b/api/tests/unit_tests/core/workflow/test_node_runtime.py index 4f9c1dad59..71a2afb28a 100644 --- a/api/tests/unit_tests/core/workflow/test_node_runtime.py +++ b/api/tests/unit_tests/core/workflow/test_node_runtime.py @@ -2,10 +2,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, Mock, sentinel import pytest -from graphon.file import FileTransferMethod, FileType -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType -from graphon.nodes.human_input.entities import HumanInputNodeData from core.app.entities.app_invoke_entities import DIFY_RUN_CONTEXT_KEY, DifyRunContext, InvokeFrom, UserFrom from core.llm_generator.output_parser.errors import OutputParserError @@ -30,6 +26,10 @@ from core.workflow.node_runtime import ( build_dify_llm_file_saver, resolve_dify_run_context, ) +from graphon.file import FileTransferMethod, FileType +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import AIModelEntity, FetchFrom, ModelType +from graphon.nodes.human_input.entities import HumanInputNodeData from tests.workflow_test_utils import build_test_run_context diff --git a/api/tests/unit_tests/core/workflow/test_system_variable.py b/api/tests/unit_tests/core/workflow/test_system_variable.py index 05ea3dc311..bdeab1eda8 100644 --- a/api/tests/unit_tests/core/workflow/test_system_variable.py +++ b/api/tests/unit_tests/core/workflow/test_system_variable.py @@ -1,14 +1,13 @@ from types import SimpleNamespace -from graphon.file import File, FileTransferMethod, FileType -from graphon.nodes import BuiltinNodeTypes - from core.workflow.system_variables import ( build_system_variables, default_system_variables, get_node_creation_preload_selectors, system_variables_to_mapping, ) +from graphon.file import File, FileTransferMethod, FileType +from graphon.nodes import BuiltinNodeTypes def test_build_system_variables_normalizes_workflow_execution_id(): diff --git a/api/tests/unit_tests/core/workflow/test_variable_pool.py b/api/tests/unit_tests/core/workflow/test_variable_pool.py index e7b2b2914a..dddd6eb00c 100644 --- a/api/tests/unit_tests/core/workflow/test_variable_pool.py +++ b/api/tests/unit_tests/core/workflow/test_variable_pool.py @@ -2,6 +2,15 @@ import uuid from collections import defaultdict import pytest + +from core.workflow.system_variables import build_system_variables, system_variables_to_mapping +from core.workflow.variable_pool_initializer import add_variables_to_pool +from core.workflow.variable_prefixes import ( + CONVERSATION_VARIABLE_NODE_ID, + ENVIRONMENT_VARIABLE_NODE_ID, + SYSTEM_VARIABLE_NODE_ID, +) +from factories.variable_factory import build_segment, segment_to_variable from graphon.file import File, FileTransferMethod, FileType from graphon.runtime import VariablePool from graphon.variables import FileSegment, StringSegment @@ -27,15 +36,6 @@ from graphon.variables.variables import ( Variable, ) -from core.workflow.system_variables import build_system_variables, system_variables_to_mapping -from core.workflow.variable_pool_initializer import add_variables_to_pool -from core.workflow.variable_prefixes import ( - CONVERSATION_VARIABLE_NODE_ID, - ENVIRONMENT_VARIABLE_NODE_ID, - SYSTEM_VARIABLE_NODE_ID, -) -from factories.variable_factory import build_segment, segment_to_variable - @pytest.fixture def pool(): diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry.py b/api/tests/unit_tests/core/workflow/test_workflow_entry.py index d8361d06c4..041c5cc612 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry.py @@ -1,12 +1,6 @@ from types import SimpleNamespace import pytest -from graphon.entities.graph_config import NodeConfigDictAdapter -from graphon.file import File, FileTransferMethod, FileType -from graphon.nodes.code.code_node import CodeNode -from graphon.nodes.code.limits import CodeNodeLimits -from graphon.runtime import VariablePool -from graphon.variables.variables import StringVariable from configs import dify_config from core.helper.code_executor.code_executor import CodeLanguage @@ -16,6 +10,12 @@ from core.workflow.variable_prefixes import ( ENVIRONMENT_VARIABLE_NODE_ID, ) from core.workflow.workflow_entry import WorkflowEntry +from graphon.entities.graph_config import NodeConfigDictAdapter +from graphon.file import File, FileTransferMethod, FileType +from graphon.nodes.code.code_node import CodeNode +from graphon.nodes.code.limits import CodeNodeLimits +from graphon.runtime import VariablePool +from graphon.variables.variables import StringVariable @pytest.fixture(autouse=True) diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py index 6dcaed1143..55800ffc03 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py @@ -4,6 +4,12 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch, sentinel import pytest + +from core.app.apps.exc import GenerateTaskStoppedError +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom +from core.model_manager import ModelInstance +from core.workflow import workflow_entry +from core.workflow.system_variables import default_system_variables from graphon.entities.base_node_data import BaseNodeData from graphon.entities.graph_config import NodeConfigDictAdapter from graphon.enums import NodeType, WorkflowNodeExecutionStatus @@ -17,12 +23,6 @@ from graphon.nodes import BuiltinNodeTypes from graphon.nodes.base.node import Node from graphon.runtime import ChildGraphNotFoundError, VariablePool from graphon.variables.variables import StringVariable - -from core.app.apps.exc import GenerateTaskStoppedError -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom -from core.model_manager import ModelInstance -from core.workflow import workflow_entry -from core.workflow.system_variables import default_system_variables from tests.workflow_test_utils import build_test_graph_init_params, build_test_variable_pool diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py b/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py index 4b2f98aeff..80dc8927fa 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry_redis_channel.py @@ -2,11 +2,10 @@ from unittest.mock import MagicMock, patch -from graphon.graph_engine.command_channels import RedisChannel -from graphon.runtime import GraphRuntimeState, VariablePool - from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.workflow.workflow_entry import WorkflowEntry +from graphon.graph_engine.command_channels import RedisChannel +from graphon.runtime import GraphRuntimeState, VariablePool class TestWorkflowEntryRedisChannel: diff --git a/api/tests/unit_tests/factories/test_build_from_mapping.py b/api/tests/unit_tests/factories/test_build_from_mapping.py index 4fe3f2cb28..511192001e 100644 --- a/api/tests/unit_tests/factories/test_build_from_mapping.py +++ b/api/tests/unit_tests/factories/test_build_from_mapping.py @@ -2,13 +2,13 @@ import uuid from unittest.mock import MagicMock, patch import pytest -from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig from httpx import Response from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import DatabaseFileAccessController, FileAccessScope, bind_file_access_scope from core.workflow.file_reference import build_file_reference, parse_file_reference, resolve_file_record_id from factories.file_factory.builders import build_from_mapping as _build_from_mapping +from graphon.file import File, FileTransferMethod, FileType, FileUploadConfig from models import ToolFile, UploadFile # Test Data diff --git a/api/tests/unit_tests/factories/test_variable_factory.py b/api/tests/unit_tests/factories/test_variable_factory.py index a06c42507d..c35e80a826 100644 --- a/api/tests/unit_tests/factories/test_variable_factory.py +++ b/api/tests/unit_tests/factories/test_variable_factory.py @@ -4,6 +4,11 @@ from typing import Any from uuid import uuid4 import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from factories import variable_factory +from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type from graphon.file import File, FileTransferMethod, FileType from graphon.variables import ( ArrayNumberVariable, @@ -31,11 +36,6 @@ from graphon.variables.segments import ( StringSegment, ) from graphon.variables.types import SegmentType -from hypothesis import HealthCheck, given, settings -from hypothesis import strategies as st - -from factories import variable_factory -from factories.variable_factory import TypeMismatchError, build_segment, build_segment_with_type def test_string_variable(): diff --git a/api/tests/unit_tests/fields/test_file_fields.py b/api/tests/unit_tests/fields/test_file_fields.py index 0e848d6ef5..9d9f626b9e 100644 --- a/api/tests/unit_tests/fields/test_file_fields.py +++ b/api/tests/unit_tests/fields/test_file_fields.py @@ -4,11 +4,11 @@ from datetime import datetime from types import SimpleNamespace import pytest -from graphon.file import File, FileTransferMethod, FileType from core.workflow.file_reference import build_file_reference from fields import conversation_fields, message_fields from fields.file_fields import FileResponse, FileWithSignedUrl, RemoteFileInfo, UploadConfig +from graphon.file import File, FileTransferMethod, FileType def test_file_response_serializes_datetime() -> None: diff --git a/api/tests/unit_tests/libs/_human_input/support.py b/api/tests/unit_tests/libs/_human_input/support.py index 13577b7ca5..e6cc23161e 100644 --- a/api/tests/unit_tests/libs/_human_input/support.py +++ b/api/tests/unit_tests/libs/_human_input/support.py @@ -6,7 +6,6 @@ from typing import Any from graphon.nodes.human_input.entities import FormInput from graphon.nodes.human_input.enums import TimeoutUnit - from libs.datetime_utils import naive_utc_now diff --git a/api/tests/unit_tests/libs/_human_input/test_form_service.py b/api/tests/unit_tests/libs/_human_input/test_form_service.py index f1ce1a2c1c..fa2c02020b 100644 --- a/api/tests/unit_tests/libs/_human_input/test_form_service.py +++ b/api/tests/unit_tests/libs/_human_input/test_form_service.py @@ -5,6 +5,7 @@ Unit tests for FormService. from datetime import timedelta import pytest + from graphon.nodes.human_input.entities import ( FormInput, UserAction, @@ -13,7 +14,6 @@ from graphon.nodes.human_input.enums import ( FormInputType, TimeoutUnit, ) - from libs.datetime_utils import naive_utc_now from .support import ( diff --git a/api/tests/unit_tests/libs/_human_input/test_models.py b/api/tests/unit_tests/libs/_human_input/test_models.py index 0babfbb315..866ee61b3e 100644 --- a/api/tests/unit_tests/libs/_human_input/test_models.py +++ b/api/tests/unit_tests/libs/_human_input/test_models.py @@ -5,6 +5,7 @@ Unit tests for human input form models. from datetime import datetime, timedelta import pytest + from graphon.nodes.human_input.entities import ( FormInput, UserAction, @@ -13,7 +14,6 @@ from graphon.nodes.human_input.enums import ( FormInputType, TimeoutUnit, ) - from libs.datetime_utils import naive_utc_now from .support import FormSubmissionData, FormSubmissionRequest, HumanInputForm diff --git a/api/tests/unit_tests/libs/test_email_i18n.py b/api/tests/unit_tests/libs/test_email_i18n.py index 962a36fe03..b4c0eaf7ee 100644 --- a/api/tests/unit_tests/libs/test_email_i18n.py +++ b/api/tests/unit_tests/libs/test_email_i18n.py @@ -503,6 +503,7 @@ class TestEmailI18nIntegration: EmailType.ACCOUNT_DELETION_VERIFICATION, EmailType.QUEUE_MONITOR_ALERT, EmailType.DOCUMENT_CLEAN_NOTIFY, + EmailType.WORKFLOW_COMMENT_MENTION, ] for email_type in expected_types: diff --git a/api/tests/unit_tests/models/test_comment_models.py b/api/tests/unit_tests/models/test_comment_models.py new file mode 100644 index 0000000000..277335cbef --- /dev/null +++ b/api/tests/unit_tests/models/test_comment_models.py @@ -0,0 +1,100 @@ +from unittest.mock import Mock, patch + +from models.comment import WorkflowComment, WorkflowCommentMention, WorkflowCommentReply + + +def test_workflow_comment_account_properties_and_cache() -> None: + comment = WorkflowComment(created_by="user-1", resolved_by="user-2", content="hello", position_x=1, position_y=2) + created_account = Mock(id="user-1") + resolved_account = Mock(id="user-2") + + with patch("models.comment.db.session.get", side_effect=[created_account, resolved_account]) as get_mock: + assert comment.created_by_account is created_account + assert comment.resolved_by_account is resolved_account + assert get_mock.call_count == 2 + + comment.cache_created_by_account(created_account) + comment.cache_resolved_by_account(resolved_account) + with patch("models.comment.db.session.get") as get_mock: + assert comment.created_by_account is created_account + assert comment.resolved_by_account is resolved_account + get_mock.assert_not_called() + + comment_without_resolver = WorkflowComment( + created_by="user-1", + resolved_by=None, + content="hello", + position_x=1, + position_y=2, + ) + with patch("models.comment.db.session.get") as get_mock: + assert comment_without_resolver.resolved_by_account is None + get_mock.assert_not_called() + + +def test_workflow_comment_counts_and_participants() -> None: + reply_1 = WorkflowCommentReply(comment_id="comment-1", content="reply-1", created_by="user-2") + reply_2 = WorkflowCommentReply(comment_id="comment-1", content="reply-2", created_by="user-2") + mention_1 = WorkflowCommentMention(comment_id="comment-1", mentioned_user_id="user-3") + mention_2 = WorkflowCommentMention(comment_id="comment-1", mentioned_user_id="user-4") + comment = WorkflowComment(created_by="user-1", resolved_by=None, content="hello", position_x=1, position_y=2) + comment.replies = [reply_1, reply_2] + comment.mentions = [mention_1, mention_2] + + account_1 = Mock(id="user-1") + account_2 = Mock(id="user-2") + account_3 = Mock(id="user-3") + account_map = { + "user-1": account_1, + "user-2": account_2, + "user-3": account_3, + "user-4": None, + } + + with patch("models.comment.db.session.get", side_effect=lambda _model, user_id: account_map[user_id]) as get_mock: + participants = comment.participants + + assert comment.reply_count == 2 + assert comment.mention_count == 2 + assert set(participants) == {account_1, account_2, account_3} + assert get_mock.call_count == 4 + + +def test_workflow_comment_participants_use_cached_accounts() -> None: + reply = WorkflowCommentReply(comment_id="comment-1", content="reply-1", created_by="user-2") + mention = WorkflowCommentMention(comment_id="comment-1", mentioned_user_id="user-3") + comment = WorkflowComment(created_by="user-1", resolved_by=None, content="hello", position_x=1, position_y=2) + comment.replies = [reply] + comment.mentions = [mention] + + account_1 = Mock(id="user-1") + account_2 = Mock(id="user-2") + account_3 = Mock(id="user-3") + comment.cache_created_by_account(account_1) + reply.cache_created_by_account(account_2) + mention.cache_mentioned_user_account(account_3) + + with patch("models.comment.db.session.get") as get_mock: + participants = comment.participants + + assert set(participants) == {account_1, account_2, account_3} + get_mock.assert_not_called() + + +def test_reply_and_mention_account_properties_and_cache() -> None: + reply = WorkflowCommentReply(comment_id="comment-1", content="reply", created_by="user-1") + mention = WorkflowCommentMention(comment_id="comment-1", mentioned_user_id="user-2") + reply_account = Mock(id="user-1") + mention_account = Mock(id="user-2") + + with patch("models.comment.db.session.get", side_effect=[reply_account, mention_account]) as get_mock: + assert reply.created_by_account is reply_account + assert mention.mentioned_user_account is mention_account + assert get_mock.call_count == 2 + + reply.cache_created_by_account(reply_account) + mention.cache_mentioned_user_account(mention_account) + with patch("models.comment.db.session.get") as get_mock: + assert reply.created_by_account is reply_account + assert mention.mentioned_user_account is mention_account + get_mock.assert_not_called() diff --git a/api/tests/unit_tests/models/test_conversation_variable.py b/api/tests/unit_tests/models/test_conversation_variable.py index 86163f1554..bb3a6db1a1 100644 --- a/api/tests/unit_tests/models/test_conversation_variable.py +++ b/api/tests/unit_tests/models/test_conversation_variable.py @@ -1,8 +1,7 @@ from uuid import uuid4 -from graphon.variables import SegmentType - from factories import variable_factory +from graphon.variables import SegmentType from models import ConversationVariable diff --git a/api/tests/unit_tests/models/test_model.py b/api/tests/unit_tests/models/test_model.py index 3f6d6bfbe3..a87dd7f15a 100644 --- a/api/tests/unit_tests/models/test_model.py +++ b/api/tests/unit_tests/models/test_model.py @@ -2,9 +2,9 @@ import importlib import types import pytest -from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from core.workflow.file_reference import build_file_reference +from graphon.file import FILE_MODEL_IDENTITY, FileTransferMethod from models.model import Conversation, Message diff --git a/api/tests/unit_tests/models/test_workflow.py b/api/tests/unit_tests/models/test_workflow.py index e7c0479757..f7bdc97eb5 100644 --- a/api/tests/unit_tests/models/test_workflow.py +++ b/api/tests/unit_tests/models/test_workflow.py @@ -3,14 +3,13 @@ import json from unittest import mock from uuid import uuid4 -from graphon.file import File, FileTransferMethod, FileType -from graphon.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable -from graphon.variables.segments import IntegerSegment, Segment - from constants import HIDDEN_VALUE from core.helper import encrypter from core.workflow.file_reference import build_file_reference from factories.variable_factory import build_segment +from graphon.file import File, FileTransferMethod, FileType +from graphon.variables import FloatVariable, IntegerVariable, SecretVariable, StringVariable +from graphon.variables.segments import IntegerSegment, Segment from models.workflow import ( Workflow, WorkflowDraftVariable, diff --git a/api/tests/unit_tests/models/test_workflow_models.py b/api/tests/unit_tests/models/test_workflow_models.py index 507e1c8c3a..eb9fef7587 100644 --- a/api/tests/unit_tests/models/test_workflow_models.py +++ b/api/tests/unit_tests/models/test_workflow_models.py @@ -13,12 +13,12 @@ from datetime import UTC, datetime from uuid import uuid4 import pytest + from graphon.enums import ( BuiltinNodeTypes, WorkflowExecutionStatus, WorkflowNodeExecutionStatus, ) - from models.enums import CreatorUserRole, WorkflowRunTriggeredFrom from models.workflow import ( Workflow, diff --git a/api/tests/unit_tests/repositories/test_workflow_collaboration_repository.py b/api/tests/unit_tests/repositories/test_workflow_collaboration_repository.py new file mode 100644 index 0000000000..1f47e8b692 --- /dev/null +++ b/api/tests/unit_tests/repositories/test_workflow_collaboration_repository.py @@ -0,0 +1,121 @@ +import json +from unittest.mock import Mock + +import pytest + +from repositories import workflow_collaboration_repository as repo_module +from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository + + +class TestWorkflowCollaborationRepository: + @pytest.fixture + def mock_redis(self, monkeypatch: pytest.MonkeyPatch) -> Mock: + mock_redis = Mock() + monkeypatch.setattr(repo_module, "redis_client", mock_redis) + return mock_redis + + def test_get_sid_mapping_returns_mapping(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.get.return_value = b'{"workflow_id":"wf-1","user_id":"u-1"}' + repository = WorkflowCollaborationRepository() + + # Act + result = repository.get_sid_mapping("sid-1") + + # Assert + assert result == {"workflow_id": "wf-1", "user_id": "u-1"} + + def test_list_sessions_filters_invalid_entries(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.hgetall.return_value = { + b"sid-1": b'{"user_id":"u-1","username":"Jane","sid":"sid-1","connected_at":2}', + b"sid-2": b'{"username":"Missing","sid":"sid-2"}', + b"sid-3": b"not-json", + } + repository = WorkflowCollaborationRepository() + + # Act + result = repository.list_sessions("wf-1") + + # Assert + assert result == [ + { + "user_id": "u-1", + "username": "Jane", + "avatar": None, + "sid": "sid-1", + "connected_at": 2, + } + ] + + def test_set_session_info_persists_payload(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.exists.return_value = True + repository = WorkflowCollaborationRepository() + payload = { + "user_id": "u-1", + "username": "Jane", + "avatar": None, + "sid": "sid-1", + "connected_at": 1, + } + + # Act + repository.set_session_info("wf-1", payload) + + # Assert + assert mock_redis.hset.called + workflow_key, sid, session_json = mock_redis.hset.call_args.args + assert workflow_key == "workflow_online_users:wf-1" + assert sid == "sid-1" + assert json.loads(session_json)["user_id"] == "u-1" + assert mock_redis.set.called + + def test_refresh_session_state_expires_keys(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.exists.return_value = True + repository = WorkflowCollaborationRepository() + + # Act + repository.refresh_session_state("wf-1", "sid-1") + + # Assert + assert mock_redis.expire.call_count == 2 + + def test_get_current_leader_decodes_bytes(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.get.return_value = b"sid-1" + repository = WorkflowCollaborationRepository() + + # Act + result = repository.get_current_leader("wf-1") + + # Assert + assert result == "sid-1" + + def test_set_leader_if_absent_uses_nx(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.set.return_value = True + repository = WorkflowCollaborationRepository() + + # Act + result = repository.set_leader_if_absent("wf-1", "sid-1") + + # Assert + assert result is True + _key, _value = mock_redis.set.call_args.args + assert _key == "workflow_leader:wf-1" + assert _value == "sid-1" + assert mock_redis.set.call_args.kwargs["nx"] is True + assert "ex" in mock_redis.set.call_args.kwargs + + def test_get_session_sids_decodes(self, mock_redis: Mock) -> None: + # Arrange + mock_redis.hkeys.return_value = [b"sid-1", "sid-2"] + repository = WorkflowCollaborationRepository() + + # Act + result = repository.get_session_sids("wf-1") + + # Assert + assert result == ["sid-1", "sid-2"] diff --git a/api/tests/unit_tests/services/dataset_service_test_helpers.py b/api/tests/unit_tests/services/dataset_service_test_helpers.py index c6972b5ef4..3349c1fd8c 100644 --- a/api/tests/unit_tests/services/dataset_service_test_helpers.py +++ b/api/tests/unit_tests/services/dataset_service_test_helpers.py @@ -11,7 +11,6 @@ from typing import Any from unittest.mock import MagicMock, Mock, create_autospec, patch import pytest -from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from werkzeug.exceptions import Forbidden, NotFound from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError @@ -20,6 +19,7 @@ from core.rag.index_processor.constant.built_in_field import BuiltInField from core.rag.index_processor.constant.index_type import IndexStructureType from core.rag.retrieval.retrieval_methods import RetrievalMethod from enums.cloud_plan import CloudPlan +from graphon.model_runtime.entities.model_entities import ModelFeature, ModelType from models import Account, TenantAccountRole from models.dataset import ( ChildChunk, diff --git a/api/tests/unit_tests/services/document_service_validation.py b/api/tests/unit_tests/services/document_service_validation.py index 6903c47a24..71df8c4e20 100644 --- a/api/tests/unit_tests/services/document_service_validation.py +++ b/api/tests/unit_tests/services/document_service_validation.py @@ -109,11 +109,11 @@ This test suite follows a comprehensive testing strategy that covers: from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.entities.model_entities import ModelType from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError from core.rag.entities import PreProcessingRule, Rule, Segmentation from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType +from graphon.model_runtime.entities.model_entities import ModelType from models.dataset import Dataset, DatasetProcessRule, Document from services.dataset_service import DatasetService, DocumentService from services.entities.knowledge_entities.knowledge_entities import ( diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py index 434a99b5ae..337659b15f 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_dsl_service.py @@ -4,10 +4,10 @@ from unittest.mock import MagicMock, Mock import pytest import yaml -from graphon.enums import BuiltinNodeTypes from sqlalchemy.orm import Session from core.workflow.nodes.knowledge_index import KNOWLEDGE_INDEX_NODE_TYPE +from graphon.enums import BuiltinNodeTypes from services.entities.knowledge_entities.rag_pipeline_entities import IconInfo, RagPipelineDatasetCreateEntity from services.rag_pipeline.rag_pipeline_dsl_service import ( ImportStatus, diff --git a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py index 941a665308..327281d07f 100644 --- a/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py +++ b/api/tests/unit_tests/services/rag_pipeline/test_rag_pipeline_service.py @@ -787,7 +787,6 @@ def test_retry_error_document_success(mocker, rag_pipeline_service) -> None: def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None: from graphon.entities.workflow_node_execution import WorkflowNodeExecution - from models.dataset import Pipeline # 1. Setup mocks @@ -1483,12 +1482,11 @@ def test_handle_node_run_result_raises_when_no_terminal_event(mocker, rag_pipeli def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker, rag_pipeline_service) -> None: + from core.app.entities.app_invoke_entities import InvokeFrom from graphon.enums import WorkflowNodeExecutionStatus from graphon.graph_events import NodeRunFailedEvent from graphon.node_events.base import NodeRunResult - from core.app.entities.app_invoke_entities import InvokeFrom - class FakeVariablePool: def __init__(self): self._values = { diff --git a/api/tests/unit_tests/services/test_account_service.py b/api/tests/unit_tests/services/test_account_service.py index eeb5d178ec..c4f5f57153 100644 --- a/api/tests/unit_tests/services/test_account_service.py +++ b/api/tests/unit_tests/services/test_account_service.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch import pytest from configs import dify_config -from models.account import Account, AccountStatus +from models.account import Account, AccountStatus, TenantStatus from services.account_service import AccountService, RegisterService, TenantService from services.errors.account import ( AccountAlreadyInTenantError, @@ -1697,7 +1697,7 @@ class TestRegisterService: # Setup test data mock_tenant = MagicMock() mock_tenant.id = "tenant-456" - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_account = TestAccountAssociatedDataFactory.create_account_mock( account_id="user-123", email="test@example.com" ) @@ -1759,7 +1759,7 @@ class TestRegisterService: # Setup test data mock_tenant = MagicMock() mock_tenant.id = "tenant-456" - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL # Mock Redis data invitation_data = { @@ -1784,7 +1784,7 @@ class TestRegisterService: # Setup test data mock_tenant = MagicMock() mock_tenant.id = "tenant-456" - mock_tenant.status = "normal" + mock_tenant.status = TenantStatus.NORMAL mock_account = TestAccountAssociatedDataFactory.create_account_mock( account_id="different-user-456", email="test@example.com" ) diff --git a/api/tests/unit_tests/services/test_conversation_service.py b/api/tests/unit_tests/services/test_conversation_service.py index 68f4c51afe..2c7f13b79f 100644 --- a/api/tests/unit_tests/services/test_conversation_service.py +++ b/api/tests/unit_tests/services/test_conversation_service.py @@ -6,26 +6,15 @@ Tests are organized by functionality and include edge cases, error handling, and both positive and negative test scenarios. """ -from datetime import timedelta from unittest.mock import MagicMock, Mock, create_autospec, patch -import pytest from sqlalchemy import asc, desc from core.app.entities.app_invoke_entities import InvokeFrom from libs.datetime_utils import naive_utc_now -from libs.infinite_scroll_pagination import InfiniteScrollPagination from models import Account, ConversationVariable -from models.enums import ConversationFromSource from models.model import App, Conversation, EndUser, Message from services.conversation_service import ConversationService -from services.errors.conversation import ( - ConversationNotExistsError, - ConversationVariableNotExistsError, - ConversationVariableTypeMismatchError, - LastConversationNotExistsError, -) -from services.errors.message import MessageNotExistsError class ConversationServiceTestDataFactory: @@ -338,330 +327,9 @@ class TestConversationServiceHelpers: assert condition is not None -class TestConversationServiceGetConversation: - """Test conversation retrieval operations.""" - - @patch("services.conversation_service.db.session") - def test_get_conversation_success_with_account(self, mock_db_session): - """ - Test successful conversation retrieval with account user. - - Should return conversation when found with proper filters. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock( - from_account_id=user.id, from_source=ConversationFromSource.CONSOLE - ) - - mock_db_session.scalar.return_value = conversation - - # Act - result = ConversationService.get_conversation(app_model, "conv-123", user) - - # Assert - assert result == conversation - - @patch("services.conversation_service.db.session") - def test_get_conversation_success_with_end_user(self, mock_db_session): - """ - Test successful conversation retrieval with end user. - - Should return conversation when found with proper filters for API user. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_end_user_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock( - from_end_user_id=user.id, from_source=ConversationFromSource.API - ) - - mock_db_session.scalar.return_value = conversation - - # Act - result = ConversationService.get_conversation(app_model, "conv-123", user) - - # Assert - assert result == conversation - - @patch("services.conversation_service.db.session") - def test_get_conversation_not_found_raises_error(self, mock_db_session): - """ - Test that get_conversation raises error when conversation not found. - - Should raise ConversationNotExistsError when no matching conversation found. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - mock_db_session.scalar.return_value = None - - # Act & Assert - with pytest.raises(ConversationNotExistsError): - ConversationService.get_conversation(app_model, "conv-123", user) - - -class TestConversationServiceRename: - """Test conversation rename operations.""" - - @patch("services.conversation_service.db.session") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_rename_with_manual_name(self, mock_get_conversation, mock_db_session): - """ - Test renaming conversation with manual name. - - Should update conversation name and timestamp when auto_generate is False. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Act - result = ConversationService.rename( - app_model=app_model, - conversation_id="conv-123", - user=user, - name="New Name", - auto_generate=False, - ) - - # Assert - assert result == conversation - assert conversation.name == "New Name" - mock_db_session.commit.assert_called_once() - - -class TestConversationServiceAutoGenerateName: - """Test conversation auto-name generation operations.""" - - @patch("services.conversation_service.db.session") - @patch("services.conversation_service.LLMGenerator") - def test_auto_generate_name_success(self, mock_llm_generator, mock_db_session): - """ - Test successful auto-generation of conversation name. - - Should generate name using LLMGenerator and update conversation. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - message = ConversationServiceTestDataFactory.create_message_mock( - conversation_id=conversation.id, app_id=app_model.id - ) - - # Mock database query to return message - mock_db_session.scalar.return_value = message - - # Mock LLM generator - mock_llm_generator.generate_conversation_name.return_value = "Generated Name" - - # Act - result = ConversationService.auto_generate_name(app_model, conversation) - - # Assert - assert result == conversation - assert conversation.name == "Generated Name" - mock_llm_generator.generate_conversation_name.assert_called_once_with( - app_model.tenant_id, message.query, conversation.id, app_model.id - ) - mock_db_session.commit.assert_called_once() - - @patch("services.conversation_service.db.session") - def test_auto_generate_name_no_message_raises_error(self, mock_db_session): - """ - Test auto-generation fails when no message found. - - Should raise MessageNotExistsError when conversation has no messages. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - # Mock database query to return None - mock_db_session.scalar.return_value = None - - # Act & Assert - with pytest.raises(MessageNotExistsError): - ConversationService.auto_generate_name(app_model, conversation) - - @patch("services.conversation_service.db.session") - @patch("services.conversation_service.LLMGenerator") - def test_auto_generate_name_handles_llm_exception(self, mock_llm_generator, mock_db_session): - """ - Test auto-generation handles LLM generator exceptions gracefully. - - Should continue without name when LLMGenerator fails. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - message = ConversationServiceTestDataFactory.create_message_mock( - conversation_id=conversation.id, app_id=app_model.id - ) - - # Mock database query to return message - mock_db_session.scalar.return_value = message - - # Mock LLM generator to raise exception - mock_llm_generator.generate_conversation_name.side_effect = Exception("LLM Error") - - # Act - result = ConversationService.auto_generate_name(app_model, conversation) - - # Assert - assert result == conversation - # Name should remain unchanged due to exception - mock_db_session.commit.assert_called_once() - - -class TestConversationServiceDelete: - """Test conversation deletion operations.""" - - @patch("services.conversation_service.delete_conversation_related_data") - @patch("services.conversation_service.db.session") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_delete_success(self, mock_get_conversation, mock_db_session, mock_delete_task): - """ - Test successful conversation deletion. - - Should delete conversation and schedule cleanup task. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock(name="Test App") - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Act - ConversationService.delete(app_model, "conv-123", user) - - # Assert - mock_db_session.delete.assert_called_once_with(conversation) - mock_db_session.commit.assert_called_once() - mock_delete_task.delay.assert_called_once_with(conversation.id) - - class TestConversationServiceConversationalVariable: """Test conversational variable operations.""" - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_get_conversational_variable_success(self, mock_get_conversation, mock_session_factory): - """ - Test successful retrieval of conversational variables. - - Should return paginated list of variables for conversation. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session and variables - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - variable1 = ConversationServiceTestDataFactory.create_conversation_variable_mock() - variable2 = ConversationServiceTestDataFactory.create_conversation_variable_mock(variable_id="var-456") - - mock_session.scalars.return_value.all.return_value = [variable1, variable2] - - # Act - result = ConversationService.get_conversational_variable( - app_model=app_model, - conversation_id="conv-123", - user=user, - limit=10, - last_id=None, - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - assert len(result.data) == 2 - assert result.limit == 10 - assert result.has_more is False - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_get_conversational_variable_with_last_id(self, mock_get_conversation, mock_session_factory): - """ - Test retrieval of variables with last_id pagination. - - Should filter variables created after last_id. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session and variables - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - last_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock( - created_at=naive_utc_now() - timedelta(hours=1) - ) - variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(created_at=naive_utc_now()) - - mock_session.scalar.return_value = last_variable - mock_session.scalars.return_value.all.return_value = [variable] - - # Act - result = ConversationService.get_conversational_variable( - app_model=app_model, - conversation_id="conv-123", - user=user, - limit=10, - last_id="var-123", - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - assert len(result.data) == 1 - assert result.limit == 10 - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_get_conversational_variable_last_id_not_found_raises_error( - self, mock_get_conversation, mock_session_factory - ): - """ - Test that invalid last_id raises ConversationVariableNotExistsError. - - Should raise error when last_id doesn't exist. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - mock_session.scalar.return_value = None - - # Act & Assert - with pytest.raises(ConversationVariableNotExistsError): - ConversationService.get_conversational_variable( - app_model=app_model, - conversation_id="conv-123", - user=user, - limit=10, - last_id="invalid-id", - ) - @patch("services.conversation_service.session_factory") @patch("services.conversation_service.ConversationService.get_conversation") @patch("services.conversation_service.dify_config") @@ -698,466 +366,3 @@ class TestConversationServiceConversationalVariable: # Assert - JSON filter should be applied assert mock_session.scalars.called - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - @patch("services.conversation_service.dify_config") - def test_get_conversational_variable_with_name_filter_postgresql( - self, mock_config, mock_get_conversation, mock_session_factory - ): - """ - Test variable filtering by name for PostgreSQL databases. - - Should apply JSON extraction filter for variable names. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - mock_config.DB_TYPE = "postgresql" - - # Mock session - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - mock_session.scalars.return_value.all.return_value = [] - - # Act - ConversationService.get_conversational_variable( - app_model=app_model, - conversation_id="conv-123", - user=user, - limit=10, - last_id=None, - variable_name="test_var", - ) - - # Assert - JSON filter should be applied - assert mock_session.scalars.called - - -class TestConversationServiceUpdateVariable: - """Test conversation variable update operations.""" - - @patch("services.conversation_service.variable_factory") - @patch("services.conversation_service.ConversationVariableUpdater") - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_update_conversation_variable_success( - self, mock_get_conversation, mock_session_factory, mock_updater_class, mock_variable_factory - ): - """ - Test successful update of conversation variable. - - Should update variable value and return updated data. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session and existing variable - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="string") - mock_session.scalar.return_value = existing_variable - - # Mock variable factory and updater - updated_variable = Mock() - updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": "new_value"} - mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable - - mock_updater = MagicMock() - mock_updater_class.return_value = mock_updater - - # Act - result = ConversationService.update_conversation_variable( - app_model=app_model, - conversation_id="conv-123", - variable_id="var-123", - user=user, - new_value="new_value", - ) - - # Assert - assert result["id"] == "var-123" - assert result["value"] == "new_value" - mock_updater.update.assert_called_once_with("conv-123", updated_variable) - mock_updater.flush.assert_called_once() - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_update_conversation_variable_not_found_raises_error(self, mock_get_conversation, mock_session_factory): - """ - Test update fails when variable doesn't exist. - - Should raise ConversationVariableNotExistsError. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - mock_session.scalar.return_value = None - - # Act & Assert - with pytest.raises(ConversationVariableNotExistsError): - ConversationService.update_conversation_variable( - app_model=app_model, - conversation_id="conv-123", - variable_id="invalid-id", - user=user, - new_value="new_value", - ) - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_update_conversation_variable_type_mismatch_raises_error(self, mock_get_conversation, mock_session_factory): - """ - Test update fails when value type doesn't match expected type. - - Should raise ConversationVariableTypeMismatchError. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session and existing variable - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="number") - mock_session.scalar.return_value = existing_variable - - # Act & Assert - Try to set string value for number variable - with pytest.raises(ConversationVariableTypeMismatchError): - ConversationService.update_conversation_variable( - app_model=app_model, - conversation_id="conv-123", - variable_id="var-123", - user=user, - new_value="string_value", # Wrong type - ) - - @patch("services.conversation_service.session_factory") - @patch("services.conversation_service.ConversationService.get_conversation") - def test_update_conversation_variable_integer_number_compatibility( - self, mock_get_conversation, mock_session_factory - ): - """ - Test that integer type accepts number values. - - Should allow number values for integer type variables. - """ - # Arrange - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - - mock_get_conversation.return_value = conversation - - # Mock session and existing variable - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - existing_variable = ConversationServiceTestDataFactory.create_conversation_variable_mock(value_type="integer") - mock_session.scalar.return_value = existing_variable - - # Mock variable factory and updater - updated_variable = Mock() - updated_variable.model_dump.return_value = {"id": "var-123", "name": "test_var", "value": 42} - - with ( - patch("services.conversation_service.variable_factory") as mock_variable_factory, - patch("services.conversation_service.ConversationVariableUpdater") as mock_updater_class, - ): - mock_variable_factory.build_conversation_variable_from_mapping.return_value = updated_variable - mock_updater = MagicMock() - mock_updater_class.return_value = mock_updater - - # Act - result = ConversationService.update_conversation_variable( - app_model=app_model, - conversation_id="conv-123", - variable_id="var-123", - user=user, - new_value=42, # Number value for integer type - ) - - # Assert - assert result["value"] == 42 - mock_updater.update.assert_called_once() - - -class TestConversationServicePaginationAdvanced: - """Advanced pagination tests for ConversationService.""" - - @patch("services.conversation_service.session_factory") - def test_pagination_by_last_id_with_last_id_not_found(self, mock_session_factory): - """ - Test pagination with invalid last_id raises error. - - Should raise LastConversationNotExistsError when last_id doesn't exist. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - mock_session.scalar.return_value = None - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act & Assert - with pytest.raises(LastConversationNotExistsError): - ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id="invalid-id", - limit=20, - invoke_from=InvokeFrom.WEB_APP, - ) - - @patch("services.conversation_service.session_factory") - def test_pagination_by_last_id_with_exclude_ids(self, mock_session_factory): - """ - Test pagination with exclude_ids filter. - - Should exclude specified conversation IDs from results. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - mock_session.scalars.return_value.all.return_value = [conversation] - mock_session.scalar.return_value = conversation - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - exclude_ids=["excluded-123"], - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - assert len(result.data) == 1 - - @patch("services.conversation_service.session_factory") - def test_pagination_by_last_id_has_more_detection(self, mock_session_factory): - """ - Test pagination has_more detection logic. - - Should set has_more=True when there are more results beyond limit. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - # Return exactly limit items to trigger has_more check - conversations = [ - ConversationServiceTestDataFactory.create_conversation_mock(conversation_id=f"conv-{i}") for i in range(20) - ] - mock_session.scalars.return_value.all.return_value = conversations - mock_session.scalar.return_value = conversations[-1] - - # Mock count query to return > 0 - mock_session.scalar.return_value = 5 # Additional items exist - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - assert result.has_more is True - - @patch("services.conversation_service.session_factory") - def test_pagination_by_last_id_with_different_sort_by(self, mock_session_factory): - """ - Test pagination with different sort fields. - - Should handle various sort_by parameters correctly. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - conversation = ConversationServiceTestDataFactory.create_conversation_mock() - mock_session.scalars.return_value.all.return_value = [conversation] - mock_session.scalar.return_value = conversation - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Test different sort fields - sort_fields = ["created_at", "-updated_at", "name", "-status"] - - for sort_by in sort_fields: - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - sort_by=sort_by, - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - - -class TestConversationServiceEdgeCases: - """Test edge cases and error scenarios.""" - - @patch("services.conversation_service.session_factory") - def test_pagination_with_end_user_api_source(self, mock_session_factory): - """ - Test pagination correctly handles EndUser with API source. - - Should use 'api' as from_source for EndUser instances. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - conversation = ConversationServiceTestDataFactory.create_conversation_mock( - from_source=ConversationFromSource.API, from_end_user_id="user-123" - ) - mock_session.scalars.return_value.all.return_value = [conversation] - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_end_user_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - - @patch("services.conversation_service.session_factory") - def test_pagination_with_account_console_source(self, mock_session_factory): - """ - Test pagination correctly handles Account with console source. - - Should use 'console' as from_source for Account instances. - """ - # Arrange - mock_session = MagicMock() - mock_session_factory.create_session.return_value.__enter__.return_value = mock_session - - conversation = ConversationServiceTestDataFactory.create_conversation_mock( - from_source=ConversationFromSource.CONSOLE, from_account_id="account-123" - ) - mock_session.scalars.return_value.all.return_value = [conversation] - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - - def test_pagination_with_include_ids_filter(self): - """ - Test pagination with include_ids filter. - - Should only return conversations with IDs in include_ids list. - """ - # Arrange - mock_session = MagicMock() - mock_session.scalars.return_value.all.return_value = [] - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - include_ids=["conv-123", "conv-456"], - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - # Verify that include_ids filter was applied - assert mock_session.scalars.called - - def test_pagination_with_empty_exclude_ids(self): - """ - Test pagination with empty exclude_ids list. - - Should handle empty exclude_ids gracefully. - """ - # Arrange - mock_session = MagicMock() - mock_session.scalars.return_value.all.return_value = [] - - app_model = ConversationServiceTestDataFactory.create_app_mock() - user = ConversationServiceTestDataFactory.create_account_mock() - - # Act - result = ConversationService.pagination_by_last_id( - session=mock_session, - app_model=app_model, - user=user, - last_id=None, - limit=20, - invoke_from=InvokeFrom.WEB_APP, - exclude_ids=[], - ) - - # Assert - assert isinstance(result, InfiniteScrollPagination) - assert result.has_more is False diff --git a/api/tests/unit_tests/services/test_dataset_service_document.py b/api/tests/unit_tests/services/test_dataset_service_document.py index 3f9386e704..1633194aa8 100644 --- a/api/tests/unit_tests/services/test_dataset_service_document.py +++ b/api/tests/unit_tests/services/test_dataset_service_document.py @@ -12,12 +12,10 @@ from .dataset_service_test_helpers import ( DocumentService, FileInfo, FileNotExistsError, - Forbidden, IndexStructureType, InfoList, KnowledgeConfig, MagicMock, - NoPermissionError, NotFound, NotionIcon, NotionInfo, @@ -35,7 +33,6 @@ from .dataset_service_test_helpers import ( _make_document, _make_features, _make_lock_context, - _make_session_context, _make_upload_knowledge_config, create_autospec, json, @@ -82,366 +79,6 @@ class TestDocumentServiceDisplayStatus: query.where.assert_called_once() -class TestDocumentServiceQueryAndDownloadHelpers: - """Unit tests for DocumentService query helpers and download flows.""" - - def test_get_document_returns_none_when_document_id_is_missing(self): - with patch("services.dataset_service.db") as mock_db: - result = DocumentService.get_document("dataset-1", None) - - assert result is None - mock_db.session.scalar.assert_not_called() - - def test_get_document_queries_by_dataset_and_document_id(self): - document = DatasetServiceUnitDataFactory.create_document_mock() - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalar.return_value = document - - result = DocumentService.get_document("dataset-1", "doc-1") - - assert result is document - - def test_get_documents_by_ids_returns_empty_for_empty_input(self): - with patch("services.dataset_service.db") as mock_db: - result = DocumentService.get_documents_by_ids("dataset-1", []) - - assert result == [] - mock_db.session.scalars.assert_not_called() - - def test_get_documents_by_ids_uses_single_batch_query(self): - document = DatasetServiceUnitDataFactory.create_document_mock() - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalars.return_value.all.return_value = [document] - - result = DocumentService.get_documents_by_ids("dataset-1", ["doc-1"]) - - assert result == [document] - mock_db.session.scalars.assert_called_once() - - def test_update_documents_need_summary_returns_zero_for_empty_input(self): - with patch("services.dataset_service.session_factory") as session_factory_mock: - result = DocumentService.update_documents_need_summary("dataset-1", []) - - assert result == 0 - session_factory_mock.create_session.assert_not_called() - - def test_update_documents_need_summary_updates_matching_documents_and_commits(self): - session = MagicMock() - session.execute.return_value.rowcount = 2 - - with patch("services.dataset_service.session_factory") as session_factory_mock: - session_factory_mock.create_session.return_value = _make_session_context(session) - - result = DocumentService.update_documents_need_summary( - "dataset-1", - ["doc-1", "doc-2"], - need_summary=False, - ) - - assert result == 2 - session.commit.assert_called_once() - - def test_get_document_download_url_uses_upload_file_lookup_and_signed_url_helper(self): - upload_file = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-1") - document = DatasetServiceUnitDataFactory.create_document_mock() - - with ( - patch.object(DocumentService, "_get_upload_file_for_upload_file_document", return_value=upload_file), - patch("services.dataset_service.file_helpers.get_signed_file_url", return_value="signed-url") as get_url, - ): - result = DocumentService.get_document_download_url(document) - - assert result == "signed-url" - get_url.assert_called_once_with(upload_file_id="file-1", as_attachment=True) - - def test_get_upload_file_id_for_upload_file_document_rejects_invalid_source_type(self): - document = DatasetServiceUnitDataFactory.create_document_mock(data_source_type="not-upload-file") - - with pytest.raises(NotFound, match="invalid source"): - DocumentService._get_upload_file_id_for_upload_file_document( - document, - invalid_source_message="invalid source", - missing_file_message="missing file", - ) - - def test_get_upload_file_id_for_upload_file_document_rejects_missing_upload_file_id(self): - document = DatasetServiceUnitDataFactory.create_document_mock(data_source_info_dict={}) - - with pytest.raises(NotFound, match="missing file"): - DocumentService._get_upload_file_id_for_upload_file_document( - document, - invalid_source_message="invalid source", - missing_file_message="missing file", - ) - - def test_get_upload_file_id_for_upload_file_document_returns_string_id(self): - document = DatasetServiceUnitDataFactory.create_document_mock(data_source_info_dict={"upload_file_id": 99}) - - result = DocumentService._get_upload_file_id_for_upload_file_document( - document, - invalid_source_message="invalid source", - missing_file_message="missing file", - ) - - assert result == "99" - - def test_get_upload_file_for_upload_file_document_raises_when_file_service_returns_nothing(self): - document = DatasetServiceUnitDataFactory.create_document_mock( - tenant_id="tenant-1", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - - with patch("services.dataset_service.FileService.get_upload_files_by_ids", return_value={}): - with pytest.raises(NotFound, match="Uploaded file not found"): - DocumentService._get_upload_file_for_upload_file_document(document) - - def test_get_upload_file_for_upload_file_document_returns_upload_file(self): - document = DatasetServiceUnitDataFactory.create_document_mock( - tenant_id="tenant-1", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - upload_file = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-1") - - with patch( - "services.dataset_service.FileService.get_upload_files_by_ids", return_value={"file-1": upload_file} - ): - result = DocumentService._get_upload_file_for_upload_file_document(document) - - assert result is upload_file - - def test_enrich_documents_with_summary_index_status_skips_lookup_when_summary_is_disabled(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock(summary_index_setting={"enable": False}) - documents = [ - DatasetServiceUnitDataFactory.create_document_mock(document_id="doc-1", need_summary=True), - DatasetServiceUnitDataFactory.create_document_mock(document_id="doc-2", need_summary=False), - ] - - DocumentService.enrich_documents_with_summary_index_status(documents, dataset, tenant_id="tenant-1") - - assert documents[0].summary_index_status is None - assert documents[1].summary_index_status is None - - def test_enrich_documents_with_summary_index_status_applies_summary_status_map(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock( - dataset_id="dataset-1", - summary_index_setting={"enable": True}, - ) - documents = [ - DatasetServiceUnitDataFactory.create_document_mock(document_id="doc-1", need_summary=True), - DatasetServiceUnitDataFactory.create_document_mock(document_id="doc-2", need_summary=True), - DatasetServiceUnitDataFactory.create_document_mock(document_id="doc-3", need_summary=False), - ] - - with patch( - "services.summary_index_service.SummaryIndexService.get_documents_summary_index_status", - return_value={"doc-1": "completed", "doc-2": None}, - ) as get_status_map: - DocumentService.enrich_documents_with_summary_index_status(documents, dataset, tenant_id="tenant-1") - - get_status_map.assert_called_once_with( - document_ids=["doc-1", "doc-2"], - dataset_id="dataset-1", - tenant_id="tenant-1", - ) - assert documents[0].summary_index_status == "completed" - assert documents[1].summary_index_status is None - assert documents[2].summary_index_status is None - - def test_generate_document_batch_download_zip_filename_uses_zip_extension(self): - fake_uuid = SimpleNamespace(hex="archive-id") - - with patch("services.dataset_service.uuid.uuid4", return_value=fake_uuid): - result = DocumentService._generate_document_batch_download_zip_filename() - - assert result == "archive-id.zip" - - def test_get_upload_files_by_document_id_for_zip_download_raises_for_missing_documents(self): - with patch.object(DocumentService, "get_documents_by_ids", return_value=[]): - with pytest.raises(NotFound, match="Document not found"): - DocumentService._get_upload_files_by_document_id_for_zip_download( - dataset_id="dataset-1", - document_ids=["doc-1"], - tenant_id="tenant-1", - ) - - def test_get_upload_files_by_document_id_for_zip_download_rejects_cross_tenant_access(self): - document = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-1", - tenant_id="tenant-other", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - - with patch.object(DocumentService, "get_documents_by_ids", return_value=[document]): - with pytest.raises(Forbidden, match="No permission"): - DocumentService._get_upload_files_by_document_id_for_zip_download( - dataset_id="dataset-1", - document_ids=["doc-1"], - tenant_id="tenant-1", - ) - - def test_get_upload_files_by_document_id_for_zip_download_rejects_missing_upload_files(self): - document = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-1", - tenant_id="tenant-1", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - - with ( - patch.object(DocumentService, "get_documents_by_ids", return_value=[document]), - patch("services.dataset_service.FileService.get_upload_files_by_ids", return_value={}), - ): - with pytest.raises(NotFound, match="Only uploaded-file documents can be downloaded as ZIP"): - DocumentService._get_upload_files_by_document_id_for_zip_download( - dataset_id="dataset-1", - document_ids=["doc-1"], - tenant_id="tenant-1", - ) - - def test_get_upload_files_by_document_id_for_zip_download_returns_document_keyed_mapping(self): - document_a = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-1", - tenant_id="tenant-1", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - document_b = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-2", - tenant_id="tenant-1", - data_source_info_dict={"upload_file_id": "file-2"}, - ) - upload_file_a = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-1") - upload_file_b = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-2") - - with ( - patch.object(DocumentService, "get_documents_by_ids", return_value=[document_a, document_b]), - patch( - "services.dataset_service.FileService.get_upload_files_by_ids", - return_value={"file-1": upload_file_a, "file-2": upload_file_b}, - ), - ): - result = DocumentService._get_upload_files_by_document_id_for_zip_download( - dataset_id="dataset-1", - document_ids=["doc-1", "doc-2"], - tenant_id="tenant-1", - ) - - assert result == {"doc-1": upload_file_a, "doc-2": upload_file_b} - - def test_prepare_document_batch_download_zip_raises_not_found_for_missing_dataset(self): - user = DatasetServiceUnitDataFactory.create_user_mock() - - with patch.object(DatasetService, "get_dataset", return_value=None): - with pytest.raises(NotFound, match="Dataset not found"): - DocumentService.prepare_document_batch_download_zip( - dataset_id="dataset-1", - document_ids=["doc-1"], - tenant_id="tenant-1", - current_user=user, - ) - - def test_prepare_document_batch_download_zip_translates_permission_error_to_forbidden(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock() - user = DatasetServiceUnitDataFactory.create_user_mock() - - with ( - patch.object(DatasetService, "get_dataset", return_value=dataset), - patch.object(DatasetService, "check_dataset_permission", side_effect=NoPermissionError("blocked")), - ): - with pytest.raises(Forbidden, match="blocked"): - DocumentService.prepare_document_batch_download_zip( - dataset_id=dataset.id, - document_ids=["doc-1"], - tenant_id="tenant-1", - current_user=user, - ) - - def test_prepare_document_batch_download_zip_returns_upload_files_in_requested_order(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock() - user = DatasetServiceUnitDataFactory.create_user_mock() - upload_file_a = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-a") - upload_file_b = DatasetServiceUnitDataFactory.create_upload_file_mock(file_id="file-b") - - with ( - patch.object(DatasetService, "get_dataset", return_value=dataset), - patch.object(DatasetService, "check_dataset_permission"), - patch.object( - DocumentService, - "_get_upload_files_by_document_id_for_zip_download", - return_value={"doc-1": upload_file_a, "doc-2": upload_file_b}, - ), - patch.object(DocumentService, "_generate_document_batch_download_zip_filename", return_value="archive.zip"), - ): - upload_files, download_name = DocumentService.prepare_document_batch_download_zip( - dataset_id=dataset.id, - document_ids=["doc-2", "doc-1"], - tenant_id="tenant-1", - current_user=user, - ) - - assert upload_files == [upload_file_b, upload_file_a] - assert download_name == "archive.zip" - - def test_get_document_by_dataset_id_returns_enabled_documents(self): - document = DatasetServiceUnitDataFactory.create_document_mock(enabled=True) - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalars.return_value.all.return_value = [document] - - result = DocumentService.get_document_by_dataset_id("dataset-1") - - assert result == [document] - - def test_get_working_documents_by_dataset_id_returns_scalars_result(self): - document = DatasetServiceUnitDataFactory.create_document_mock(indexing_status="completed", archived=False) - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalars.return_value.all.return_value = [document] - - result = DocumentService.get_working_documents_by_dataset_id("dataset-1") - - assert result == [document] - - def test_get_error_documents_by_dataset_id_returns_scalars_result(self): - document = DatasetServiceUnitDataFactory.create_document_mock(indexing_status="error") - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalars.return_value.all.return_value = [document] - - result = DocumentService.get_error_documents_by_dataset_id("dataset-1") - - assert result == [document] - - def test_get_batch_documents_filters_by_current_user_tenant(self): - class FakeAccount: - pass - - current_user = FakeAccount() - current_user.current_tenant_id = "tenant-1" - document = DatasetServiceUnitDataFactory.create_document_mock() - - with ( - patch("services.dataset_service.Account", FakeAccount), - patch("services.dataset_service.current_user", current_user), - patch("services.dataset_service.db") as mock_db, - ): - mock_db.session.scalars.return_value.all.return_value = [document] - - result = DocumentService.get_batch_documents("dataset-1", "batch-1") - - assert result == [document] - - def test_get_document_file_detail_returns_one_or_none(self): - upload_file = DatasetServiceUnitDataFactory.create_upload_file_mock() - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.get.return_value = upload_file - - result = DocumentService.get_document_file_detail(upload_file.id) - - assert result is upload_file - - class TestDocumentServiceMutations: """Unit tests for DocumentService mutation and orchestration helpers.""" @@ -466,61 +103,6 @@ class TestDocumentServiceMutations: assert DocumentService.check_archived(document) is expected - def test_delete_document_emits_signal_and_commits(self): - document = DatasetServiceUnitDataFactory.create_document_mock( - data_source_type="upload_file", - data_source_info='{"upload_file_id": "file-1"}', - data_source_info_dict={"upload_file_id": "file-1"}, - ) - - with ( - patch("services.dataset_service.document_was_deleted.send") as send_deleted_signal, - patch("services.dataset_service.db") as mock_db, - ): - DocumentService.delete_document(document) - - send_deleted_signal.assert_called_once_with( - document.id, - dataset_id=document.dataset_id, - doc_form=document.doc_form, - file_id="file-1", - ) - mock_db.session.delete.assert_called_once_with(document) - mock_db.session.commit.assert_called_once() - - def test_delete_documents_ignores_empty_input(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock() - - with patch("services.dataset_service.db") as mock_db: - DocumentService.delete_documents(dataset, []) - - mock_db.session.scalars.assert_not_called() - - def test_delete_documents_deletes_rows_and_dispatches_cleanup_task(self): - dataset = DatasetServiceUnitDataFactory.create_dataset_mock(doc_form="text_model") - document_a = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-1", - data_source_type="upload_file", - data_source_info_dict={"upload_file_id": "file-1"}, - ) - document_b = DatasetServiceUnitDataFactory.create_document_mock( - document_id="doc-2", - data_source_type="upload_file", - data_source_info_dict={"upload_file_id": "file-2"}, - ) - - with ( - patch("services.dataset_service.db") as mock_db, - patch("services.dataset_service.batch_clean_document_task") as clean_task, - ): - mock_db.session.scalars.return_value.all.return_value = [document_a, document_b] - - DocumentService.delete_documents(dataset, ["doc-1", "doc-2"]) - - assert mock_db.session.delete.call_count == 2 - mock_db.session.commit.assert_called_once() - clean_task.delay.assert_called_once_with(["doc-1", "doc-2"], dataset.id, dataset.doc_form, ["file-1", "file-2"]) - def test_rename_document_raises_when_dataset_is_missing(self, rename_account_context): with patch.object(DatasetService, "get_dataset", return_value=None): with pytest.raises(ValueError, match="Dataset not found"): @@ -620,24 +202,6 @@ class TestDocumentServiceMutations: mock_redis.setex.assert_called_once_with("document_doc-1_is_sync", 600, 1) sync_task.delay.assert_called_once_with("dataset-1", "doc-1") - def test_get_documents_position_returns_next_position_when_documents_exist(self): - document = DatasetServiceUnitDataFactory.create_document_mock(position=7) - - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalar.return_value = document - - result = DocumentService.get_documents_position("dataset-1") - - assert result == 8 - - def test_get_documents_position_defaults_to_one_when_dataset_is_empty(self): - with patch("services.dataset_service.db") as mock_db: - mock_db.session.scalar.return_value = None - - result = DocumentService.get_documents_position("dataset-1") - - assert result == 1 - class TestDocumentServiceSaveDocumentWithoutDatasetId: """Unit tests for dataset creation around save_document_without_dataset_id.""" diff --git a/api/tests/unit_tests/services/test_datasource_provider_service.py b/api/tests/unit_tests/services/test_datasource_provider_service.py index c00a4938bb..d304e0ec44 100644 --- a/api/tests/unit_tests/services/test_datasource_provider_service.py +++ b/api/tests/unit_tests/services/test_datasource_provider_service.py @@ -2,10 +2,10 @@ from unittest.mock import MagicMock, patch import httpx import pytest -from graphon.model_runtime.entities.provider_entities import FormType from sqlalchemy.orm import Session from core.plugin.entities.plugin_daemon import CredentialType +from graphon.model_runtime.entities.provider_entities import FormType from models.account import Account from models.model import EndUser from models.oauth import DatasourceProvider diff --git a/api/tests/unit_tests/services/test_human_input_service.py b/api/tests/unit_tests/services/test_human_input_service.py index 9be475d043..55af564821 100644 --- a/api/tests/unit_tests/services/test_human_input_service.py +++ b/api/tests/unit_tests/services/test_human_input_service.py @@ -3,18 +3,18 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock import pytest -from graphon.nodes.human_input.entities import ( - FormDefinition, - FormInput, - UserAction, -) -from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus import services.human_input_service as human_input_service_module from core.repositories.human_input_repository import ( HumanInputFormRecord, HumanInputFormSubmissionRepository, ) +from graphon.nodes.human_input.entities import ( + FormDefinition, + FormInput, + UserAction, +) +from graphon.nodes.human_input.enums import FormInputType, HumanInputFormKind, HumanInputFormStatus from libs.datetime_utils import naive_utc_now from models.human_input import RecipientType from services.human_input_service import ( diff --git a/api/tests/unit_tests/services/test_model_load_balancing_service.py b/api/tests/unit_tests/services/test_model_load_balancing_service.py index bea288fb9b..3119af40a2 100644 --- a/api/tests/unit_tests/services/test_model_load_balancing_service.py +++ b/api/tests/unit_tests/services/test_model_load_balancing_service.py @@ -6,6 +6,9 @@ from typing import Any, cast from unittest.mock import MagicMock import pytest +from pytest_mock import MockerFixture + +from constants import HIDDEN_VALUE from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ( @@ -15,9 +18,6 @@ from graphon.model_runtime.entities.provider_entities import ( ModelCredentialSchema, ProviderCredentialSchema, ) -from pytest_mock import MockerFixture - -from constants import HIDDEN_VALUE from models.provider import LoadBalancingModelConfig from services.model_load_balancing_service import ModelLoadBalancingService diff --git a/api/tests/unit_tests/services/test_model_provider_service.py b/api/tests/unit_tests/services/test_model_provider_service.py index 756d3e9b59..28d459eac9 100644 --- a/api/tests/unit_tests/services/test_model_provider_service.py +++ b/api/tests/unit_tests/services/test_model_provider_service.py @@ -3,10 +3,10 @@ from typing import Any from unittest.mock import MagicMock import pytest -from graphon.model_runtime.entities.common_entities import I18nObject -from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType, ParameterRule, ParameterType from core.entities.model_entities import ModelStatus +from graphon.model_runtime.entities.common_entities import I18nObject +from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType, ParameterRule, ParameterType from models.provider import ProviderType from services import model_provider_service as service_module from services.errors.app_model_config import ProviderNotFoundError diff --git a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py index 1bd979b9ec..97f3bd6f01 100644 --- a/api/tests/unit_tests/services/test_model_provider_service_sanitization.py +++ b/api/tests/unit_tests/services/test_model_provider_service_sanitization.py @@ -1,11 +1,11 @@ import types import pytest + +from core.entities.provider_entities import CredentialConfiguration, CustomModelConfiguration from graphon.model_runtime.entities.common_entities import I18nObject from graphon.model_runtime.entities.model_entities import ModelType from graphon.model_runtime.entities.provider_entities import ConfigurateMethod - -from core.entities.provider_entities import CredentialConfiguration, CustomModelConfiguration from models.provider import ProviderType from services.model_provider_service import ModelProviderService diff --git a/api/tests/unit_tests/services/test_variable_truncator.py b/api/tests/unit_tests/services/test_variable_truncator.py index 98ec6fb77c..4b864dd221 100644 --- a/api/tests/unit_tests/services/test_variable_truncator.py +++ b/api/tests/unit_tests/services/test_variable_truncator.py @@ -16,6 +16,7 @@ from typing import Any from uuid import uuid4 import pytest + from graphon.file import File, FileTransferMethod, FileType from graphon.variables.segments import ( ArrayFileSegment, @@ -28,7 +29,6 @@ from graphon.variables.segments import ( ObjectSegment, StringSegment, ) - from services.variable_truncator import ( DummyVariableTruncator, MaxDepthExceededError, diff --git a/api/tests/unit_tests/services/test_variable_truncator_additional.py b/api/tests/unit_tests/services/test_variable_truncator_additional.py index 4f705cf5f4..e9427c4ab3 100644 --- a/api/tests/unit_tests/services/test_variable_truncator_additional.py +++ b/api/tests/unit_tests/services/test_variable_truncator_additional.py @@ -2,10 +2,10 @@ from collections.abc import Mapping from typing import Any import pytest + from graphon.nodes.variable_assigner.common.helpers import UpdatedVariable from graphon.variables.segments import IntegerSegment, ObjectSegment, StringSegment from graphon.variables.types import SegmentType - from services import variable_truncator as truncator_module from services.variable_truncator import BaseTruncator, TruncationResult, VariableTruncator diff --git a/api/tests/unit_tests/services/test_webhook_service_additional.py b/api/tests/unit_tests/services/test_webhook_service_additional.py index 92f8a3fcc0..776cb5dc3f 100644 --- a/api/tests/unit_tests/services/test_webhook_service_additional.py +++ b/api/tests/unit_tests/services/test_webhook_service_additional.py @@ -1,10 +1,9 @@ from types import SimpleNamespace -from typing import Any, cast +from typing import Any from unittest.mock import MagicMock import pytest from flask import Flask -from graphon.variables.types import SegmentType from werkzeug.exceptions import RequestEntityTooLarge from core.workflow.nodes.trigger_webhook.entities import ( @@ -13,11 +12,7 @@ from core.workflow.nodes.trigger_webhook.entities import ( WebhookData, WebhookParameter, ) -from models.enums import AppTriggerStatus -from models.model import App -from models.trigger import WorkflowWebhookTrigger -from models.workflow import Workflow -from services.errors.app import QuotaExceededError +from graphon.variables.types import SegmentType from services.trigger import webhook_service as service_module from services.trigger.webhook_service import WebhookService @@ -39,156 +34,13 @@ class _FakeQuery: return self._result -class _SessionContext: - def __init__(self, session: Any) -> None: - self._session = session - - def __enter__(self) -> Any: - return self._session - - def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: - return False - - -class _SessionmakerContext: - def __init__(self, session: Any) -> None: - self._session = session - - def begin(self) -> "_SessionmakerContext": - return self - - def __enter__(self) -> Any: - return self._session - - def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool: - return False - - @pytest.fixture def flask_app() -> Flask: return Flask(__name__) -def _patch_session(monkeypatch: pytest.MonkeyPatch, session: Any) -> None: - monkeypatch.setattr(service_module, "db", SimpleNamespace(engine=MagicMock(), session=MagicMock())) - monkeypatch.setattr(service_module, "Session", lambda *args, **kwargs: _SessionContext(session)) - monkeypatch.setattr(service_module, "sessionmaker", lambda *args, **kwargs: _SessionmakerContext(session)) - - -def _workflow_trigger(**kwargs: Any) -> WorkflowWebhookTrigger: - return cast(WorkflowWebhookTrigger, SimpleNamespace(**kwargs)) - - -def _workflow(**kwargs: Any) -> Workflow: - return cast(Workflow, SimpleNamespace(**kwargs)) - - -def _app(**kwargs: Any) -> App: - return cast(App, SimpleNamespace(**kwargs)) - - -class TestWebhookServiceLookup: - def test_get_webhook_trigger_and_workflow_should_raise_when_webhook_not_found( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - fake_session = MagicMock() - fake_session.scalar.return_value = None - _patch_session(monkeypatch, fake_session) - - with pytest.raises(ValueError, match="Webhook not found"): - WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_not_found( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, None] - _patch_session(monkeypatch, fake_session) - - with pytest.raises(ValueError, match="App trigger not found"): - WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_rate_limited( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - app_trigger = SimpleNamespace(status=AppTriggerStatus.RATE_LIMITED) - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, app_trigger] - _patch_session(monkeypatch, fake_session) - - with pytest.raises(ValueError, match="rate limited"): - WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - def test_get_webhook_trigger_and_workflow_should_raise_when_app_trigger_disabled( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - app_trigger = SimpleNamespace(status=AppTriggerStatus.DISABLED) - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, app_trigger] - _patch_session(monkeypatch, fake_session) - - with pytest.raises(ValueError, match="disabled"): - WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - def test_get_webhook_trigger_and_workflow_should_raise_when_workflow_not_found( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - app_trigger = SimpleNamespace(status=AppTriggerStatus.ENABLED) - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, app_trigger, None] - _patch_session(monkeypatch, fake_session) - - with pytest.raises(ValueError, match="Workflow not found"): - WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - def test_get_webhook_trigger_and_workflow_should_return_values_for_non_debug_mode( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - app_trigger = SimpleNamespace(status=AppTriggerStatus.ENABLED) - workflow = MagicMock() - workflow.get_node_config_by_id.return_value = {"data": {"key": "value"}} - - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, app_trigger, workflow] - _patch_session(monkeypatch, fake_session) - - got_trigger, got_workflow, got_node_config = WebhookService.get_webhook_trigger_and_workflow("webhook-1") - - assert got_trigger is webhook_trigger - assert got_workflow is workflow - assert got_node_config == {"data": {"key": "value"}} - - def test_get_webhook_trigger_and_workflow_should_return_values_for_debug_mode( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = SimpleNamespace(app_id="app-1", node_id="node-1") - workflow = MagicMock() - workflow.get_node_config_by_id.return_value = {"data": {"mode": "debug"}} - - fake_session = MagicMock() - fake_session.scalar.side_effect = [webhook_trigger, workflow] - _patch_session(monkeypatch, fake_session) - - got_trigger, got_workflow, got_node_config = WebhookService.get_webhook_trigger_and_workflow( - "webhook-1", - is_debug=True, - ) - - assert got_trigger is webhook_trigger - assert got_workflow is workflow - assert got_node_config == {"data": {"mode": "debug"}} +def _workflow_trigger(**kwargs: Any) -> Any: + return SimpleNamespace(**kwargs) class TestWebhookServiceExtractionFallbacks: @@ -420,237 +272,6 @@ class TestWebhookServiceValidationAndConversion: assert result["webhook_body"] == {"b": 2} -class TestWebhookServiceExecutionAndSync: - def test_trigger_workflow_execution_should_trigger_async_workflow_successfully( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = _workflow_trigger( - app_id="app-1", - node_id="node-1", - tenant_id="tenant-1", - webhook_id="webhook-1", - ) - workflow = _workflow(id="wf-1") - webhook_data = {"body": {"x": 1}} - - session = MagicMock() - _patch_session(monkeypatch, session) - - end_user = SimpleNamespace(id="end-user-1") - monkeypatch.setattr( - service_module.EndUserService, - "get_or_create_end_user_by_type", - MagicMock(return_value=end_user), - ) - quota_type = SimpleNamespace(TRIGGER=SimpleNamespace(consume=MagicMock())) - monkeypatch.setattr(service_module, "QuotaType", quota_type) - trigger_async_mock = MagicMock() - monkeypatch.setattr(service_module.AsyncWorkflowService, "trigger_workflow_async", trigger_async_mock) - - WebhookService.trigger_workflow_execution(webhook_trigger, webhook_data, workflow) - - trigger_async_mock.assert_called_once() - - def test_trigger_workflow_execution_should_mark_tenant_rate_limited_when_quota_exceeded( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = _workflow_trigger( - app_id="app-1", - node_id="node-1", - tenant_id="tenant-1", - webhook_id="webhook-1", - ) - workflow = _workflow(id="wf-1") - - session = MagicMock() - _patch_session(monkeypatch, session) - - monkeypatch.setattr( - service_module.EndUserService, - "get_or_create_end_user_by_type", - MagicMock(return_value=SimpleNamespace(id="end-user-1")), - ) - quota_type = SimpleNamespace( - TRIGGER=SimpleNamespace( - consume=MagicMock(side_effect=QuotaExceededError(feature="trigger", tenant_id="tenant-1", required=1)) - ) - ) - monkeypatch.setattr(service_module, "QuotaType", quota_type) - mark_rate_limited_mock = MagicMock() - monkeypatch.setattr( - service_module.AppTriggerService, "mark_tenant_triggers_rate_limited", mark_rate_limited_mock - ) - - with pytest.raises(QuotaExceededError): - WebhookService.trigger_workflow_execution(webhook_trigger, {"body": {}}, workflow) - - mark_rate_limited_mock.assert_called_once_with("tenant-1") - - def test_trigger_workflow_execution_should_log_and_reraise_unexpected_errors( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - webhook_trigger = _workflow_trigger( - app_id="app-1", - node_id="node-1", - tenant_id="tenant-1", - webhook_id="webhook-1", - ) - workflow = _workflow(id="wf-1") - - session = MagicMock() - _patch_session(monkeypatch, session) - - monkeypatch.setattr( - service_module.EndUserService, - "get_or_create_end_user_by_type", - MagicMock(side_effect=RuntimeError("boom")), - ) - logger_exception_mock = MagicMock() - monkeypatch.setattr(service_module.logger, "exception", logger_exception_mock) - - with pytest.raises(RuntimeError, match="boom"): - WebhookService.trigger_workflow_execution(webhook_trigger, {"body": {}}, workflow) - - logger_exception_mock.assert_called_once() - - def test_sync_webhook_relationships_should_raise_when_workflow_exceeds_node_limit(self) -> None: - app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") - workflow = _workflow( - walk_nodes=lambda _node_type: [ - (f"node-{i}", {}) for i in range(WebhookService.MAX_WEBHOOK_NODES_PER_WORKFLOW + 1) - ] - ) - - with pytest.raises(ValueError, match="maximum webhook node limit"): - WebhookService.sync_webhook_relationships(app, workflow) - - def test_sync_webhook_relationships_should_raise_when_lock_not_acquired( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") - workflow = _workflow(walk_nodes=lambda _node_type: [("node-1", {})]) - - lock = MagicMock() - lock.acquire.return_value = False - monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) - monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) - - with pytest.raises(RuntimeError, match="Failed to acquire lock"): - WebhookService.sync_webhook_relationships(app, workflow) - - def test_sync_webhook_relationships_should_create_missing_records_and_delete_stale_records( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") - workflow = _workflow(walk_nodes=lambda _node_type: [("node-new", {})]) - - class _WorkflowWebhookTrigger: - app_id = "app_id" - tenant_id = "tenant_id" - webhook_id = "webhook_id" - node_id = "node_id" - - def __init__(self, app_id: str, tenant_id: str, node_id: str, webhook_id: str, created_by: str) -> None: - self.id = None - self.app_id = app_id - self.tenant_id = tenant_id - self.node_id = node_id - self.webhook_id = webhook_id - self.created_by = created_by - - class _Select: - def where(self, *args: Any, **kwargs: Any) -> "_Select": - return self - - class _Session: - def __init__(self) -> None: - self.added: list[Any] = [] - self.deleted: list[Any] = [] - self.commit_count = 0 - self.existing_records = [SimpleNamespace(node_id="node-stale")] - - def scalars(self, _stmt: Any) -> Any: - return SimpleNamespace(all=lambda: self.existing_records) - - def add(self, obj: Any) -> None: - self.added.append(obj) - - def flush(self) -> None: - for idx, obj in enumerate(self.added, start=1): - if obj.id is None: - obj.id = f"rec-{idx}" - - def commit(self) -> None: - self.commit_count += 1 - - def delete(self, obj: Any) -> None: - self.deleted.append(obj) - - lock = MagicMock() - lock.acquire.return_value = True - lock.release.return_value = None - - fake_session = _Session() - - monkeypatch.setattr(service_module, "WorkflowWebhookTrigger", _WorkflowWebhookTrigger) - monkeypatch.setattr(service_module, "select", MagicMock(return_value=_Select())) - monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) - monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) - redis_set_mock = MagicMock() - redis_delete_mock = MagicMock() - monkeypatch.setattr(service_module.redis_client, "set", redis_set_mock) - monkeypatch.setattr(service_module.redis_client, "delete", redis_delete_mock) - monkeypatch.setattr(WebhookService, "generate_webhook_id", MagicMock(return_value="generated-webhook-id")) - _patch_session(monkeypatch, fake_session) - - WebhookService.sync_webhook_relationships(app, workflow) - - assert len(fake_session.added) == 1 - assert len(fake_session.deleted) == 1 - redis_set_mock.assert_called_once() - redis_delete_mock.assert_called_once() - lock.release.assert_called_once() - - def test_sync_webhook_relationships_should_log_when_lock_release_fails( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - app = _app(id="app-1", tenant_id="tenant-1", created_by="user-1") - workflow = _workflow(walk_nodes=lambda _node_type: []) - - class _Select: - def where(self, *args: Any, **kwargs: Any) -> "_Select": - return self - - class _Session: - def scalars(self, _stmt: Any) -> Any: - return SimpleNamespace(all=lambda: []) - - def commit(self) -> None: - return None - - lock = MagicMock() - lock.acquire.return_value = True - lock.release.side_effect = RuntimeError("release failed") - - logger_exception_mock = MagicMock() - - monkeypatch.setattr(service_module, "select", MagicMock(return_value=_Select())) - monkeypatch.setattr(service_module.redis_client, "get", MagicMock(return_value=None)) - monkeypatch.setattr(service_module.redis_client, "lock", MagicMock(return_value=lock)) - monkeypatch.setattr(service_module.logger, "exception", logger_exception_mock) - _patch_session(monkeypatch, _Session()) - - WebhookService.sync_webhook_relationships(app, workflow) - - assert logger_exception_mock.call_count == 1 - - class TestWebhookServiceUtilities: def test_generate_webhook_response_should_fallback_when_response_body_is_not_json(self) -> None: node_config = {"data": {"status_code": 200, "response_body": "{bad-json"}} diff --git a/api/tests/unit_tests/services/test_workflow_collaboration_service.py b/api/tests/unit_tests/services/test_workflow_collaboration_service.py new file mode 100644 index 0000000000..8a6addfece --- /dev/null +++ b/api/tests/unit_tests/services/test_workflow_collaboration_service.py @@ -0,0 +1,608 @@ +from unittest.mock import Mock, patch + +import pytest + +from repositories.workflow_collaboration_repository import WorkflowCollaborationRepository +from services.workflow_collaboration_service import WorkflowCollaborationService + + +class TestWorkflowCollaborationService: + @pytest.fixture + def service(self) -> tuple[WorkflowCollaborationService, Mock, Mock]: + repository = Mock(spec=WorkflowCollaborationRepository) + socketio = Mock() + return WorkflowCollaborationService(repository, socketio), repository, socketio + + def test_authorize_and_join_workflow_room_returns_leader_status( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, socketio = service + socketio.get_session.return_value = { + "user_id": "u-1", + "username": "Jane", + "avatar": None, + "tenant_id": "t-1", + } + + with ( + patch.object(collaboration_service, "_can_access_workflow", return_value=True), + patch.object(collaboration_service, "get_or_set_leader", return_value="sid-1"), + patch.object(collaboration_service, "broadcast_online_users"), + ): + # Act + result = collaboration_service.authorize_and_join_workflow_room("wf-1", "sid-1") + + # Assert + assert result == ("u-1", True) + repository.set_session_info.assert_called_once() + socketio.enter_room.assert_called_once_with("sid-1", "wf-1") + socketio.emit.assert_called_once_with("status", {"isLeader": True}, room="sid-1") + + def test_authorize_and_join_workflow_room_returns_none_when_missing_user( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, _repository, socketio = service + socketio.get_session.return_value = {} + + # Act + result = collaboration_service.authorize_and_join_workflow_room("wf-1", "sid-1") + + # Assert + assert result is None + + def test_authorize_and_join_workflow_room_returns_none_when_missing_tenant( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + socketio.get_session.return_value = {"user_id": "u-1", "username": "Jane", "avatar": None} + + result = collaboration_service.authorize_and_join_workflow_room("wf-1", "sid-1") + + assert result is None + repository.set_session_info.assert_not_called() + socketio.enter_room.assert_not_called() + socketio.emit.assert_not_called() + + def test_authorize_and_join_workflow_room_returns_none_when_workflow_is_not_accessible( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + socketio.get_session.return_value = { + "user_id": "u-1", + "username": "Jane", + "avatar": None, + "tenant_id": "t-1", + } + + with patch.object(collaboration_service, "_can_access_workflow", return_value=False): + result = collaboration_service.authorize_and_join_workflow_room("wf-1", "sid-1") + + assert result is None + repository.set_session_info.assert_not_called() + socketio.enter_room.assert_not_called() + socketio.emit.assert_not_called() + + def test_repr_and_save_socket_identity(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + collaboration_service, _repository, socketio = service + user = Mock() + user.id = "u-1" + user.name = "Jane" + user.avatar = "avatar.png" + user.current_tenant_id = "t-1" + + assert "WorkflowCollaborationService" in repr(collaboration_service) + + collaboration_service.save_socket_identity("sid-1", user) + + socketio.save_session.assert_called_once_with( + "sid-1", + {"user_id": "u-1", "username": "Jane", "avatar": "avatar.png", "tenant_id": "t-1"}, + ) + + def test_can_access_workflow_uses_session_factory( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, _repository, _socketio = service + session = Mock() + session.scalar.return_value = "wf-1" + session_context = Mock() + session_context.__enter__ = Mock(return_value=session) + session_context.__exit__ = Mock(return_value=False) + + with patch( + "services.workflow_collaboration_service.session_factory.create_session", + return_value=session_context, + ): + result = collaboration_service._can_access_workflow("wf-1", "tenant-1") + + assert result is True + session.scalar.assert_called_once() + + def test_relay_collaboration_event_unauthorized( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_sid_mapping.return_value = None + + # Act + result = collaboration_service.relay_collaboration_event("sid-1", {}) + + # Assert + assert result == ({"msg": "unauthorized"}, 401) + + def test_relay_collaboration_event_emits_update( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + payload = {"type": "mouse_move", "data": {"x": 1}, "timestamp": 123} + + # Act + result = collaboration_service.relay_collaboration_event("sid-1", payload) + + # Assert + assert result == ({"msg": "event_broadcasted"}, 200) + socketio.emit.assert_called_once_with( + "collaboration_update", + {"type": "mouse_move", "userId": "u-1", "data": {"x": 1}, "timestamp": 123}, + room="wf-1", + skip_sid="sid-1", + ) + + def test_relay_collaboration_event_requires_event_type( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, _socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + + result = collaboration_service.relay_collaboration_event("sid-1", {"data": {"x": 1}}) + + assert result == ({"msg": "invalid event type"}, 400) + + def test_relay_collaboration_event_sync_request_forwards_to_active_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + repository.get_current_leader.return_value = "sid-leader" + payload = {"type": "sync_request", "data": {"reason": "join"}, "timestamp": 123} + + with ( + patch.object(collaboration_service, "refresh_session_state"), + patch.object(collaboration_service, "is_session_active", return_value=True), + ): + result = collaboration_service.relay_collaboration_event("sid-1", payload) + + assert result == ({"msg": "sync_request_forwarded"}, 200) + socketio.emit.assert_called_once_with( + "collaboration_update", + {"type": "sync_request", "userId": "u-1", "data": {"reason": "join"}, "timestamp": 123}, + room="sid-leader", + ) + repository.set_leader.assert_not_called() + + def test_relay_collaboration_event_sync_request_reelects_active_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + repository.get_current_leader.return_value = "sid-old" + repository.list_sessions.return_value = [ + { + "user_id": "u-2", + "username": "B", + "avatar": None, + "sid": "sid-2", + "connected_at": 1, + "graph_active": True, + }, + { + "user_id": "u-3", + "username": "C", + "avatar": None, + "sid": "sid-3", + "connected_at": 2, + "graph_active": True, + }, + ] + payload = {"type": "sync_request", "data": {"reason": "join"}, "timestamp": 123} + + def _is_session_active(_workflow_id: str, session_sid: str) -> bool: + return session_sid != "sid-old" + + with ( + patch.object(collaboration_service, "refresh_session_state"), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + patch.object(collaboration_service, "is_session_active", side_effect=_is_session_active), + ): + result = collaboration_service.relay_collaboration_event("sid-2", payload) + + assert result == ({"msg": "sync_request_forwarded"}, 200) + repository.delete_leader.assert_called_once_with("wf-1") + repository.set_leader.assert_called_once_with("wf-1", "sid-2") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-2") + socketio.emit.assert_called_once_with( + "collaboration_update", + {"type": "sync_request", "userId": "u-1", "data": {"reason": "join"}, "timestamp": 123}, + room="sid-2", + ) + + def test_relay_collaboration_event_sync_request_returns_when_no_active_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + repository.get_current_leader.return_value = "sid-old" + repository.list_sessions.return_value = [] + payload = {"type": "sync_request", "data": {"reason": "join"}, "timestamp": 123} + + with ( + patch.object(collaboration_service, "refresh_session_state"), + patch.object(collaboration_service, "is_session_active", return_value=False), + ): + result = collaboration_service.relay_collaboration_event("sid-2", payload) + + assert result == ({"msg": "no_active_leader"}, 200) + repository.delete_leader.assert_called_once_with("wf-1") + socketio.emit.assert_not_called() + + def test_relay_graph_event_unauthorized(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_sid_mapping.return_value = None + + # Act + result = collaboration_service.relay_graph_event("sid-1", {"nodes": []}) + + # Assert + assert result == ({"msg": "unauthorized"}, 401) + + def test_disconnect_session_no_mapping(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_sid_mapping.return_value = None + + # Act + collaboration_service.disconnect_session("sid-1") + + # Assert + repository.delete_session.assert_not_called() + + def test_disconnect_session_cleans_up(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + + with ( + patch.object(collaboration_service, "handle_leader_disconnect") as handle_leader_disconnect, + patch.object(collaboration_service, "broadcast_online_users") as broadcast_online_users, + ): + # Act + collaboration_service.disconnect_session("sid-1") + + # Assert + repository.delete_session.assert_called_once_with("wf-1", "sid-1") + handle_leader_disconnect.assert_called_once_with("wf-1", "sid-1") + broadcast_online_users.assert_called_once_with("wf-1") + + def test_get_or_set_leader_returns_active_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-1" + + with patch.object(collaboration_service, "is_session_active", return_value=True): + # Act + result = collaboration_service.get_or_set_leader("wf-1", "sid-2") + + # Assert + assert result == "sid-1" + repository.set_leader_if_absent.assert_not_called() + + def test_get_or_set_leader_replaces_dead_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-1" + repository.set_leader_if_absent.return_value = True + repository.list_sessions.return_value = [ + { + "user_id": "u-2", + "username": "B", + "avatar": None, + "sid": "sid-2", + "connected_at": 1, + "graph_active": True, + } + ] + + with ( + patch.object(collaboration_service, "is_session_active", side_effect=lambda _wf, sid: sid != "sid-1"), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + ): + # Act + result = collaboration_service.get_or_set_leader("wf-1", "sid-2") + + # Assert + assert result == "sid-2" + repository.delete_session.assert_called_once_with("wf-1", "sid-1") + repository.delete_leader.assert_called_once_with("wf-1") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-2") + + def test_get_or_set_leader_falls_back_to_existing( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.side_effect = [None, "sid-3"] + repository.set_leader_if_absent.return_value = False + repository.list_sessions.return_value = [ + { + "user_id": "u-2", + "username": "B", + "avatar": None, + "sid": "sid-2", + "connected_at": 1, + "graph_active": True, + } + ] + + # Act + result = collaboration_service.get_or_set_leader("wf-1", "sid-2") + + # Assert + assert result == "sid-3" + + def test_get_or_set_leader_returns_sid_when_leader_still_missing( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, _socketio = service + repository.get_current_leader.side_effect = [None, None] + repository.set_leader_if_absent.return_value = False + + result = collaboration_service.get_or_set_leader("wf-1", "sid-2") + + assert result == "sid-2" + + def test_handle_leader_disconnect_elects_new( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-1" + repository.list_sessions.return_value = [ + { + "user_id": "u-2", + "username": "B", + "avatar": None, + "sid": "sid-2", + "connected_at": 1, + "graph_active": True, + } + ] + + with ( + patch.object(collaboration_service, "is_session_active", return_value=True), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + ): + # Act + collaboration_service.handle_leader_disconnect("wf-1", "sid-1") + + # Assert + repository.set_leader.assert_called_once_with("wf-1", "sid-2") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-2") + + def test_handle_leader_disconnect_clears_when_empty( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-1" + repository.list_sessions.return_value = [] + + # Act + collaboration_service.handle_leader_disconnect("wf-1", "sid-1") + + # Assert + repository.delete_leader.assert_called_once_with("wf-1") + + def test_handle_leader_disconnect_ignores_non_leader_or_missing_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, _socketio = service + + repository.get_current_leader.return_value = None + collaboration_service.handle_leader_disconnect("wf-1", "sid-1") + + repository.get_current_leader.return_value = "sid-leader" + collaboration_service.handle_leader_disconnect("wf-1", "sid-other") + + repository.set_leader.assert_not_called() + repository.delete_leader.assert_not_called() + + def test_broadcast_leader_change_logs_emit_errors( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + repository.get_session_sids.return_value = ["sid-1", "sid-2"] + socketio.emit.side_effect = [RuntimeError("boom"), None] + + with patch("services.workflow_collaboration_service.logging.exception") as exception_mock: + collaboration_service.broadcast_leader_change("wf-1", "sid-2") + + assert exception_mock.call_count == 1 + + def test_broadcast_online_users_sorts_and_emits( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, socketio = service + repository.list_sessions.return_value = [ + {"user_id": "u-1", "username": "A", "avatar": None, "sid": "sid-1", "connected_at": 3}, + {"user_id": "u-2", "username": "B", "avatar": None, "sid": "sid-2", "connected_at": 1}, + ] + repository.get_current_leader.return_value = "sid-1" + + with patch.object(collaboration_service, "is_session_active", return_value=True): + # Act + collaboration_service.broadcast_online_users("wf-1") + + # Assert + socketio.emit.assert_called_once_with( + "online_users", + { + "workflow_id": "wf-1", + "users": [ + {"user_id": "u-2", "username": "B", "avatar": None, "sid": "sid-2", "connected_at": 1}, + {"user_id": "u-1", "username": "A", "avatar": None, "sid": "sid-1", "connected_at": 3}, + ], + "leader": "sid-1", + }, + room="wf-1", + ) + + def test_broadcast_online_users_reassigns_missing_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, socketio = service + users = [{"user_id": "u-2", "username": "B", "avatar": None, "sid": "sid-2", "connected_at": 1}] + repository.get_current_leader.return_value = "sid-old" + + with ( + patch.object(collaboration_service, "_prune_inactive_sessions", return_value=users), + patch.object(collaboration_service, "_select_graph_leader", return_value="sid-2"), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + ): + collaboration_service.broadcast_online_users("wf-1") + + repository.delete_leader.assert_called_once_with("wf-1") + repository.set_leader.assert_called_once_with("wf-1", "sid-2") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-2") + socketio.emit.assert_called_once_with( + "online_users", + {"workflow_id": "wf-1", "users": users, "leader": "sid-2"}, + room="wf-1", + ) + + def test_refresh_session_state_expires_active_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-1" + + with patch.object(collaboration_service, "is_session_active", return_value=True): + # Act + collaboration_service.refresh_session_state("wf-1", "sid-1") + + # Assert + repository.refresh_session_state.assert_called_once_with("wf-1", "sid-1") + repository.expire_leader.assert_called_once_with("wf-1") + repository.set_leader.assert_not_called() + + def test_refresh_session_state_sets_leader_when_missing( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + # Arrange + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = None + repository.list_sessions.return_value = [ + { + "user_id": "u-2", + "username": "B", + "avatar": None, + "sid": "sid-2", + "connected_at": 1, + "graph_active": True, + } + ] + + with ( + patch.object(collaboration_service, "is_session_active", return_value=True), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + ): + # Act + collaboration_service.refresh_session_state("wf-1", "sid-2") + + # Assert + repository.set_leader.assert_called_once_with("wf-1", "sid-2") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-2") + + def test_refresh_session_state_replaces_inactive_existing_leader( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, _socketio = service + repository.get_current_leader.return_value = "sid-old" + + with ( + patch.object(collaboration_service, "is_session_active", return_value=False), + patch.object(collaboration_service, "broadcast_leader_change") as broadcast_leader_change, + ): + collaboration_service.refresh_session_state("wf-1", "sid-new") + + repository.delete_leader.assert_called_once_with("wf-1") + repository.set_leader.assert_called_once_with("wf-1", "sid-new") + broadcast_leader_change.assert_called_once_with("wf-1", "sid-new") + + def test_relay_graph_event_emits_update(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + # Arrange + collaboration_service, repository, socketio = service + repository.get_sid_mapping.return_value = {"workflow_id": "wf-1", "user_id": "u-1"} + + # Act + result = collaboration_service.relay_graph_event("sid-1", {"nodes": []}) + + # Assert + assert result == ({"msg": "graph_update_broadcasted"}, 200) + repository.refresh_session_state.assert_called_once_with("wf-1", "sid-1") + socketio.emit.assert_called_once_with("graph_update", {"nodes": []}, room="wf-1", skip_sid="sid-1") + + def test_prune_inactive_sessions_handles_empty_and_removes_stale( + self, service: tuple[WorkflowCollaborationService, Mock, Mock] + ) -> None: + collaboration_service, repository, _socketio = service + repository.list_sessions.return_value = [] + assert collaboration_service._prune_inactive_sessions("wf-1") == [] + + active = {"sid": "sid-1", "user_id": "u-1", "connected_at": 1} + stale = {"sid": "sid-2", "user_id": "u-2", "connected_at": 2} + repository.list_sessions.return_value = [active, stale] + + with patch.object( + collaboration_service, + "is_session_active", + side_effect=lambda _workflow_id, sid: sid == "sid-1", + ): + users = collaboration_service._prune_inactive_sessions("wf-1") + + assert users == [active] + repository.delete_session.assert_called_with("wf-1", "sid-2") + + def test_is_session_active_guard_branches(self, service: tuple[WorkflowCollaborationService, Mock, Mock]) -> None: + collaboration_service, repository, socketio = service + socketio.manager.is_connected.return_value = True + repository.session_exists.return_value = True + repository.sid_mapping_exists.return_value = True + + assert collaboration_service.is_session_active("wf-1", "") is False + + socketio.manager.is_connected.return_value = False + assert collaboration_service.is_session_active("wf-1", "sid-1") is False + + socketio.manager.is_connected.side_effect = AttributeError("missing manager") + assert collaboration_service.is_session_active("wf-1", "sid-1") is False + socketio.manager.is_connected.side_effect = None + + socketio.manager.is_connected.return_value = True + repository.session_exists.return_value = False + assert collaboration_service.is_session_active("wf-1", "sid-1") is False + + repository.session_exists.return_value = True + repository.sid_mapping_exists.return_value = False + assert collaboration_service.is_session_active("wf-1", "sid-1") is False diff --git a/api/tests/unit_tests/services/test_workflow_comment_service.py b/api/tests/unit_tests/services/test_workflow_comment_service.py new file mode 100644 index 0000000000..e6db068e07 --- /dev/null +++ b/api/tests/unit_tests/services/test_workflow_comment_service.py @@ -0,0 +1,578 @@ +from unittest.mock import MagicMock, Mock, patch + +import pytest +from werkzeug.exceptions import Forbidden, NotFound + +from services import workflow_comment_service as service_module +from services.workflow_comment_service import WorkflowCommentService + + +@pytest.fixture +def mock_session(monkeypatch: pytest.MonkeyPatch) -> Mock: + session = Mock() + context_manager = MagicMock() + context_manager.__enter__.return_value = session + context_manager.__exit__.return_value = False + mock_db = MagicMock() + mock_db.engine = Mock() + empty_scalars = Mock() + empty_scalars.all.return_value = [] + session.scalars.return_value = empty_scalars + monkeypatch.setattr(service_module, "Session", Mock(return_value=context_manager)) + monkeypatch.setattr(service_module, "db", mock_db) + monkeypatch.setattr(service_module.send_workflow_comment_mention_email_task, "delay", Mock()) + return session + + +def _mock_scalars(result_list: list[object]) -> Mock: + scalars = Mock() + scalars.all.return_value = result_list + return scalars + + +class TestWorkflowCommentService: + def test_validate_content_rejects_empty(self) -> None: + with pytest.raises(ValueError): + WorkflowCommentService._validate_content(" ") + + def test_validate_content_rejects_too_long(self) -> None: + with pytest.raises(ValueError): + WorkflowCommentService._validate_content("a" * 1001) + + def test_filter_valid_mentioned_user_ids_filters_by_tenant_and_preserves_order(self, mock_session: Mock) -> None: + tenant_member_1 = "123e4567-e89b-12d3-a456-426614174000" + tenant_member_2 = "123e4567-e89b-12d3-a456-426614174002" + non_tenant_member = "123e4567-e89b-12d3-a456-426614174001" + mock_session.scalars.return_value = _mock_scalars([tenant_member_1, tenant_member_2]) + + result = WorkflowCommentService._filter_valid_mentioned_user_ids( + [ + tenant_member_1, + "", + 123, # type: ignore[list-item] + tenant_member_1, + non_tenant_member, + tenant_member_2, + ], + session=mock_session, + tenant_id="tenant-1", + ) + + assert result == [ + tenant_member_1, + tenant_member_2, + ] + + def test_format_comment_excerpt_handles_short_and_long_limits(self) -> None: + assert WorkflowCommentService._format_comment_excerpt(" hello ", max_length=10) == "hello" + assert WorkflowCommentService._format_comment_excerpt("abcdefghijk", max_length=3) == "abc" + assert WorkflowCommentService._format_comment_excerpt(" abcdefghijk ", max_length=8) == "abcde..." + + def test_build_mention_email_payloads_returns_empty_for_no_candidates(self, mock_session: Mock) -> None: + assert ( + WorkflowCommentService._build_mention_email_payloads( + session=mock_session, + tenant_id="tenant-1", + app_id="app-1", + mentioner_id="user-1", + mentioned_user_ids=[], + content="hello", + ) + == [] + ) + assert ( + WorkflowCommentService._build_mention_email_payloads( + session=mock_session, + tenant_id="tenant-1", + app_id="app-1", + mentioner_id="user-1", + mentioned_user_ids=["user-1"], + content="hello", + ) + == [] + ) + + def test_dispatch_mention_emails_enqueues_each_payload(self) -> None: + delay_mock = Mock() + with patch.object(service_module.send_workflow_comment_mention_email_task, "delay", delay_mock): + WorkflowCommentService._dispatch_mention_emails( + [ + {"to": "a@example.com"}, + {"to": "b@example.com"}, + ] + ) + + assert delay_mock.call_count == 2 + + def test_build_mention_email_payloads_skips_accounts_without_email(self, mock_session: Mock) -> None: + account_without_email = Mock() + account_without_email.email = None + account_without_email.name = "No Email" + account_without_email.interface_language = "en-US" + + account_with_email = Mock() + account_with_email.email = "user@example.com" + account_with_email.name = "" + account_with_email.interface_language = None + + mock_session.scalar.side_effect = ["My App", "Commenter"] + mock_session.scalars.return_value = _mock_scalars([account_without_email, account_with_email]) + + payloads = WorkflowCommentService._build_mention_email_payloads( + session=mock_session, + tenant_id="tenant-1", + app_id="app-1", + mentioner_id="user-1", + mentioned_user_ids=["user-2"], + content="hello", + ) + expected_app_url = f"{service_module.dify_config.CONSOLE_WEB_URL.rstrip('/')}/app/app-1/workflow" + + assert payloads == [ + { + "language": "en-US", + "to": "user@example.com", + "mentioned_name": "user@example.com", + "commenter_name": "Commenter", + "app_name": "My App", + "comment_content": "hello", + "app_url": expected_app_url, + } + ] + + def test_create_comment_creates_mentions(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_at = "ts" + + with ( + patch.object(service_module, "WorkflowComment", return_value=comment), + patch.object(service_module, "WorkflowCommentMention", return_value=Mock()), + patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]), + ): + result = WorkflowCommentService.create_comment( + tenant_id="tenant-1", + app_id="app-1", + created_by="user-1", + content="hello", + position_x=1.0, + position_y=2.0, + mentioned_user_ids=["user-2", "bad-id"], + ) + + assert result == {"id": "comment-1", "created_at": "ts"} + assert mock_session.add.call_args_list[0].args[0] is comment + assert mock_session.add.call_count == 2 + mock_session.commit.assert_called_once() + + def test_update_comment_raises_not_found(self, mock_session: Mock) -> None: + mock_session.scalar.return_value = None + + with pytest.raises(NotFound): + WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="user-1", + content="hello", + ) + + def test_update_comment_raises_forbidden(self, mock_session: Mock) -> None: + comment = Mock() + comment.created_by = "owner" + mock_session.scalar.return_value = comment + + with pytest.raises(Forbidden): + WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="intruder", + content="hello", + ) + + def test_update_comment_replaces_mentions(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_by = "owner" + mock_session.scalar.return_value = comment + + existing_mentions = [Mock(), Mock()] + mock_session.scalars.return_value = _mock_scalars(existing_mentions) + + with patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]): + result = WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="owner", + content="updated", + mentioned_user_ids=["user-2", "bad-id"], + ) + + assert result == {"id": "comment-1", "updated_at": comment.updated_at} + assert mock_session.delete.call_count == 2 + assert mock_session.add.call_count == 1 + mock_session.commit.assert_called_once() + + def test_update_comment_preserves_mentions_when_mentioned_user_ids_omitted(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_by = "owner" + mock_session.scalar.return_value = comment + + with ( + patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids") as filter_mentions_mock, + patch.object(WorkflowCommentService, "_build_mention_email_payloads") as build_payloads_mock, + patch.object(WorkflowCommentService, "_dispatch_mention_emails") as dispatch_mock, + ): + result = WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="owner", + content="updated", + ) + + assert result == {"id": "comment-1", "updated_at": comment.updated_at} + mock_session.delete.assert_not_called() + mock_session.add.assert_not_called() + filter_mentions_mock.assert_not_called() + build_payloads_mock.assert_not_called() + dispatch_mock.assert_called_once_with([]) + mock_session.commit.assert_called_once() + + def test_update_comment_clears_mentions_when_empty_list_provided(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_by = "owner" + mock_session.scalar.return_value = comment + + existing_mentions = [Mock(), Mock()] + mock_session.scalars.return_value = _mock_scalars(existing_mentions) + + with patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=[]): + result = WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="owner", + content="updated", + mentioned_user_ids=[], + ) + + assert result == {"id": "comment-1", "updated_at": comment.updated_at} + assert mock_session.delete.call_count == 2 + mock_session.add.assert_not_called() + mock_session.commit.assert_called_once() + + def test_update_comment_notifies_only_new_mentions(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_by = "owner" + mock_session.scalar.return_value = comment + + existing_mention = Mock() + existing_mention.mentioned_user_id = "user-2" + mock_session.scalars.return_value = _mock_scalars([existing_mention]) + + with ( + patch.object( + WorkflowCommentService, + "_filter_valid_mentioned_user_ids", + return_value=["user-2", "user-3"], + ), + patch.object( + WorkflowCommentService, + "_build_mention_email_payloads", + return_value=[], + ) as build_payloads_mock, + patch.object(WorkflowCommentService, "_dispatch_mention_emails") as dispatch_mock, + ): + WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="owner", + content="updated", + mentioned_user_ids=["user-2", "user-3"], + ) + + assert build_payloads_mock.call_args.kwargs["mentioned_user_ids"] == ["user-3"] + dispatch_mock.assert_called_once_with([]) + + def test_get_comments_preloads_related_accounts(self, mock_session: Mock) -> None: + comment = Mock() + comment.created_by = "user-1" + comment.resolved_by = "user-2" + reply = Mock() + reply.created_by = "user-3" + mention = Mock() + mention.mentioned_user_id = "user-4" + comment.replies = [reply] + comment.mentions = [mention] + comment.cache_created_by_account = Mock() + comment.cache_resolved_by_account = Mock() + reply.cache_created_by_account = Mock() + mention.cache_mentioned_user_account = Mock() + + account_1 = Mock() + account_1.id = "user-1" + account_2 = Mock() + account_2.id = "user-2" + account_3 = Mock() + account_3.id = "user-3" + account_4 = Mock() + account_4.id = "user-4" + + mock_session.scalars.side_effect = [ + _mock_scalars([comment]), + _mock_scalars([account_1, account_2, account_3, account_4]), + ] + + result = WorkflowCommentService.get_comments("tenant-1", "app-1") + + assert result == [comment] + comment.cache_created_by_account.assert_called_once_with(account_1) + comment.cache_resolved_by_account.assert_called_once_with(account_2) + reply.cache_created_by_account.assert_called_once_with(account_3) + mention.cache_mentioned_user_account.assert_called_once_with(account_4) + + def test_preload_accounts_returns_early_for_empty_comments(self, mock_session: Mock) -> None: + WorkflowCommentService._preload_accounts(mock_session, []) + + mock_session.scalars.assert_not_called() + + def test_get_comment_raises_not_found_with_provided_session(self) -> None: + session = Mock() + session.scalar.return_value = None + + with pytest.raises(NotFound): + WorkflowCommentService.get_comment("tenant-1", "app-1", "comment-1", session=session) + + def test_get_comment_uses_context_manager_when_session_not_provided(self, mock_session: Mock) -> None: + comment = Mock() + comment.created_by = "user-1" + comment.resolved_by = None + comment.replies = [] + comment.mentions = [] + comment.cache_created_by_account = Mock() + comment.cache_resolved_by_account = Mock() + mock_session.scalar.return_value = comment + mock_session.scalars.return_value = _mock_scalars([]) + + result = WorkflowCommentService.get_comment("tenant-1", "app-1", "comment-1") + + assert result is comment + comment.cache_created_by_account.assert_called_once() + comment.cache_resolved_by_account.assert_called_once_with(None) + + def test_delete_comment_raises_forbidden(self, mock_session: Mock) -> None: + comment = Mock() + comment.created_by = "owner" + + with patch.object(WorkflowCommentService, "get_comment", return_value=comment): + with pytest.raises(Forbidden): + WorkflowCommentService.delete_comment("tenant-1", "app-1", "comment-1", "intruder") + + def test_delete_comment_removes_related_entities(self, mock_session: Mock) -> None: + comment = Mock() + comment.created_by = "owner" + + mentions = [Mock(), Mock()] + replies = [Mock()] + mock_session.scalars.side_effect = [_mock_scalars(mentions), _mock_scalars(replies)] + + with patch.object(WorkflowCommentService, "get_comment", return_value=comment): + WorkflowCommentService.delete_comment("tenant-1", "app-1", "comment-1", "owner") + + assert mock_session.delete.call_count == 4 + mock_session.commit.assert_called_once() + + def test_resolve_comment_sets_fields(self, mock_session: Mock) -> None: + comment = Mock() + comment.resolved = False + comment.resolved_at = None + comment.resolved_by = None + + with ( + patch.object(WorkflowCommentService, "get_comment", return_value=comment), + patch.object(service_module, "naive_utc_now", return_value="now"), + ): + result = WorkflowCommentService.resolve_comment("tenant-1", "app-1", "comment-1", "user-1") + + assert result is comment + assert comment.resolved is True + assert comment.resolved_at == "now" + assert comment.resolved_by == "user-1" + mock_session.commit.assert_called_once() + + def test_resolve_comment_noop_when_already_resolved(self, mock_session: Mock) -> None: + comment = Mock() + comment.resolved = True + + with patch.object(WorkflowCommentService, "get_comment", return_value=comment): + result = WorkflowCommentService.resolve_comment("tenant-1", "app-1", "comment-1", "user-1") + + assert result is comment + mock_session.commit.assert_not_called() + + def test_create_reply_requires_comment(self, mock_session: Mock) -> None: + mock_session.get.return_value = None + + with pytest.raises(NotFound): + WorkflowCommentService.create_reply("comment-1", "hello", "user-1") + + def test_create_reply_creates_mentions(self, mock_session: Mock) -> None: + mock_session.get.return_value = Mock() + reply = Mock() + reply.id = "reply-1" + reply.created_at = "ts" + + with ( + patch.object(service_module, "WorkflowCommentReply", return_value=reply), + patch.object(service_module, "WorkflowCommentMention", return_value=Mock()), + patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]), + ): + result = WorkflowCommentService.create_reply( + comment_id="comment-1", + content="hello", + created_by="user-1", + mentioned_user_ids=["user-2", "bad-id"], + ) + + assert result == {"id": "reply-1", "created_at": "ts"} + assert mock_session.add.call_count == 2 + mock_session.commit.assert_called_once() + + def test_update_reply_raises_not_found(self, mock_session: Mock) -> None: + mock_session.scalar.return_value = None + + with pytest.raises(NotFound): + WorkflowCommentService.update_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="user-1", + content="hello", + ) + + def test_update_reply_raises_forbidden(self, mock_session: Mock) -> None: + reply = Mock() + reply.created_by = "owner" + mock_session.scalar.return_value = reply + + with pytest.raises(Forbidden): + WorkflowCommentService.update_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="intruder", + content="hello", + ) + + def test_update_reply_replaces_mentions(self, mock_session: Mock) -> None: + reply = Mock() + reply.id = "reply-1" + reply.comment_id = "comment-1" + reply.created_by = "owner" + reply.updated_at = "updated" + mock_session.scalar.return_value = reply + mock_session.scalars.return_value = _mock_scalars([Mock()]) + comment = Mock() + comment.tenant_id = "tenant-1" + comment.app_id = "app-1" + mock_session.get.return_value = comment + + with patch.object(WorkflowCommentService, "_filter_valid_mentioned_user_ids", return_value=["user-2"]): + result = WorkflowCommentService.update_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="owner", + content="new", + mentioned_user_ids=["user-2", "bad-id"], + ) + + assert result == {"id": "reply-1", "updated_at": "updated"} + assert mock_session.delete.call_count == 1 + assert mock_session.add.call_count == 1 + mock_session.commit.assert_called_once() + mock_session.refresh.assert_called_once_with(reply) + + def test_update_comment_updates_position_coordinates_when_provided(self, mock_session: Mock) -> None: + comment = Mock() + comment.id = "comment-1" + comment.created_by = "owner" + comment.position_x = 1.0 + comment.position_y = 2.0 + mock_session.scalar.return_value = comment + mock_session.scalars.return_value = _mock_scalars([]) + + WorkflowCommentService.update_comment( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + user_id="owner", + content="updated", + position_x=10.5, + position_y=20.5, + mentioned_user_ids=[], + ) + + assert comment.position_x == 10.5 + assert comment.position_y == 20.5 + + def test_delete_reply_raises_forbidden(self, mock_session: Mock) -> None: + reply = Mock() + reply.created_by = "owner" + mock_session.scalar.return_value = reply + + with pytest.raises(Forbidden): + WorkflowCommentService.delete_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="intruder", + ) + + def test_delete_reply_raises_not_found(self, mock_session: Mock) -> None: + mock_session.scalar.return_value = None + + with pytest.raises(NotFound): + WorkflowCommentService.delete_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="owner", + ) + + def test_delete_reply_removes_mentions(self, mock_session: Mock) -> None: + reply = Mock() + reply.created_by = "owner" + mock_session.scalar.return_value = reply + mock_session.scalars.return_value = _mock_scalars([Mock(), Mock()]) + + WorkflowCommentService.delete_reply( + tenant_id="tenant-1", + app_id="app-1", + comment_id="comment-1", + reply_id="reply-1", + user_id="owner", + ) + + assert mock_session.delete.call_count == 3 + mock_session.commit.assert_called_once() + + def test_validate_comment_access_delegates_to_get_comment(self) -> None: + comment = Mock() + with patch.object(WorkflowCommentService, "get_comment", return_value=comment) as get_comment_mock: + result = WorkflowCommentService.validate_comment_access("comment-1", "tenant-1", "app-1") + + assert result is comment + get_comment_mock.assert_called_once_with("tenant-1", "app-1", "comment-1") diff --git a/api/tests/unit_tests/services/test_workflow_run_service_pause.py b/api/tests/unit_tests/services/test_workflow_run_service_pause.py index a62c9f4555..239cc83518 100644 --- a/api/tests/unit_tests/services/test_workflow_run_service_pause.py +++ b/api/tests/unit_tests/services/test_workflow_run_service_pause.py @@ -13,10 +13,10 @@ from datetime import datetime from unittest.mock import MagicMock, create_autospec, patch import pytest -from graphon.enums import WorkflowExecutionStatus from sqlalchemy import Engine from sqlalchemy.orm import Session, sessionmaker +from graphon.enums import WorkflowExecutionStatus from models.workflow import WorkflowPause from repositories.api_workflow_run_repository import APIWorkflowRunRepository from repositories.sqlalchemy_api_workflow_run_repository import _PrivateWorkflowPauseEntity diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index 7906daacfc..287f5f2e5e 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -12,9 +12,10 @@ This test suite covers: import json import uuid from typing import Any, cast -from unittest.mock import ANY, MagicMock, patch +from unittest.mock import ANY, MagicMock, Mock, patch import pytest + from graphon.entities import WorkflowNodeExecution from graphon.enums import ( BuiltinNodeTypes, @@ -28,7 +29,6 @@ from graphon.model_runtime.entities.model_entities import ModelType from graphon.node_events import NodeRunResult from graphon.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNode, HttpRequestNodeConfig from graphon.variables.input_entities import VariableEntityType - from libs.datetime_utils import naive_utc_now from models.human_input import RecipientType from models.model import App, AppMode @@ -713,6 +713,79 @@ class TestWorkflowService: with pytest.raises(ValueError, match="Invalid app mode"): workflow_service.validate_features_structure(app, features) + # ==================== Draft Workflow Variable Update Tests ==================== + # These tests verify updating draft workflow environment/conversation variables + + def test_update_draft_workflow_environment_variables_updates_workflow(self, workflow_service, mock_db_session): + """Test update_draft_workflow_environment_variables updates draft fields.""" + app = TestWorkflowAssociatedDataFactory.create_app_mock() + account = TestWorkflowAssociatedDataFactory.create_account_mock() + workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock() + variables = [Mock()] + + with ( + patch.object(workflow_service, "get_draft_workflow", return_value=workflow), + patch("services.workflow_service.naive_utc_now", return_value="now"), + ): + workflow_service.update_draft_workflow_environment_variables( + app_model=app, + environment_variables=variables, + account=account, + ) + + assert workflow.environment_variables == variables + assert workflow.updated_by == account.id + assert workflow.updated_at == "now" + mock_db_session.session.commit.assert_called_once() + + def test_update_draft_workflow_environment_variables_raises_when_missing(self, workflow_service): + """Test update_draft_workflow_environment_variables raises when draft missing.""" + app = TestWorkflowAssociatedDataFactory.create_app_mock() + account = TestWorkflowAssociatedDataFactory.create_account_mock() + + with patch.object(workflow_service, "get_draft_workflow", return_value=None): + with pytest.raises(ValueError, match="No draft workflow found."): + workflow_service.update_draft_workflow_environment_variables( + app_model=app, + environment_variables=[], + account=account, + ) + + def test_update_draft_workflow_conversation_variables_updates_workflow(self, workflow_service, mock_db_session): + """Test update_draft_workflow_conversation_variables updates draft fields.""" + app = TestWorkflowAssociatedDataFactory.create_app_mock() + account = TestWorkflowAssociatedDataFactory.create_account_mock() + workflow = TestWorkflowAssociatedDataFactory.create_workflow_mock() + variables = [Mock()] + + with ( + patch.object(workflow_service, "get_draft_workflow", return_value=workflow), + patch("services.workflow_service.naive_utc_now", return_value="now"), + ): + workflow_service.update_draft_workflow_conversation_variables( + app_model=app, + conversation_variables=variables, + account=account, + ) + + assert workflow.conversation_variables == variables + assert workflow.updated_by == account.id + assert workflow.updated_at == "now" + mock_db_session.session.commit.assert_called_once() + + def test_update_draft_workflow_conversation_variables_raises_when_missing(self, workflow_service): + """Test update_draft_workflow_conversation_variables raises when draft missing.""" + app = TestWorkflowAssociatedDataFactory.create_app_mock() + account = TestWorkflowAssociatedDataFactory.create_account_mock() + + with patch.object(workflow_service, "get_draft_workflow", return_value=None): + with pytest.raises(ValueError, match="No draft workflow found."): + workflow_service.update_draft_workflow_conversation_variables( + app_model=app, + conversation_variables=[], + account=account, + ) + # ==================== Publish Workflow Tests ==================== # These tests verify creating published versions from draft workflows diff --git a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py index 8525672da8..60beec7964 100644 --- a/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py +++ b/api/tests/unit_tests/services/workflow/test_draft_var_loader_simple.py @@ -4,12 +4,12 @@ import json from unittest.mock import Mock, patch import pytest -from graphon.file import File, FileTransferMethod, FileType -from graphon.variables.segments import ObjectSegment, StringSegment -from graphon.variables.types import SegmentType from sqlalchemy import Engine from core.workflow.file_reference import build_file_reference +from graphon.file import File, FileTransferMethod, FileType +from graphon.variables.segments import ObjectSegment, StringSegment +from graphon.variables.types import SegmentType from models.model import UploadFile from models.workflow import WorkflowDraftVariable, WorkflowDraftVariableFile from services.workflow_draft_variable_service import DraftVarLoader diff --git a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py index e7e72793a3..f6bdb6a60e 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_draft_variable_service.py @@ -4,10 +4,6 @@ import uuid from unittest.mock import MagicMock, Mock, patch import pytest -from graphon.enums import BuiltinNodeTypes -from graphon.file import File, FileTransferMethod, FileType -from graphon.variables.segments import StringSegment -from graphon.variables.types import SegmentType from sqlalchemy import Engine from sqlalchemy.orm import Session @@ -17,6 +13,10 @@ from core.workflow.variable_prefixes import ( ENVIRONMENT_VARIABLE_NODE_ID, SYSTEM_VARIABLE_NODE_ID, ) +from graphon.enums import BuiltinNodeTypes +from graphon.file import File, FileTransferMethod, FileType +from graphon.variables.segments import StringSegment +from graphon.variables.types import SegmentType from libs.uuid_utils import uuidv7 from models.account import Account from models.enums import DraftVariableType diff --git a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py index 4146fd312b..d570dce107 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service.py @@ -6,13 +6,13 @@ from datetime import UTC, datetime from threading import Event import pytest -from graphon.entities.pause_reason import HumanInputRequired -from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper +from graphon.entities.pause_reason import HumanInputRequired +from graphon.enums import WorkflowExecutionStatus, WorkflowNodeExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool from models.enums import CreatorUserRole from models.model import AppMode from models.workflow import WorkflowRun diff --git a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service_additional.py b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service_additional.py index 5e96eb4518..d2634d7d7b 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service_additional.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_event_snapshot_service_additional.py @@ -10,14 +10,14 @@ from typing import Any, cast from unittest.mock import MagicMock import pytest -from graphon.enums import WorkflowExecutionStatus -from graphon.runtime import GraphRuntimeState, VariablePool from sqlalchemy.orm import Session, sessionmaker from core.app.app_config.entities import WorkflowUIBasedAppConfig from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerateEntity from core.app.entities.task_entities import StreamEvent from core.app.layers.pause_state_persist_layer import WorkflowResumptionContext, _WorkflowGenerateEntityWrapper +from graphon.enums import WorkflowExecutionStatus +from graphon.runtime import GraphRuntimeState, VariablePool from models.enums import CreatorUserRole from models.model import AppMode from models.workflow import WorkflowRun diff --git a/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py b/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py index 98d057e41f..d7192994b2 100644 --- a/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py +++ b/api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py @@ -3,9 +3,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock import pytest -from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter -from graphon.enums import BuiltinNodeTypes -from graphon.nodes.human_input.entities import HumanInputNodeData from sqlalchemy.orm import sessionmaker from core.workflow.human_input_compat import ( @@ -15,6 +12,9 @@ from core.workflow.human_input_compat import ( ExternalRecipient, MemberRecipient, ) +from graphon.entities.graph_config import NodeConfigDict, NodeConfigDictAdapter +from graphon.enums import BuiltinNodeTypes +from graphon.nodes.human_input.entities import HumanInputNodeData from services import workflow_service as workflow_service_module from services.workflow_service import WorkflowService diff --git a/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py b/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py index 7119217e94..591da56f49 100644 --- a/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py +++ b/api/tests/unit_tests/tasks/test_human_input_timeout_tasks.py @@ -5,8 +5,8 @@ from types import SimpleNamespace from typing import Any import pytest -from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus +from graphon.nodes.human_input.enums import HumanInputFormKind, HumanInputFormStatus from tasks import human_input_timeout_tasks as task_module diff --git a/api/tests/unit_tests/tools/test_mcp_tool.py b/api/tests/unit_tests/tools/test_mcp_tool.py index 544e89fcee..689b973097 100644 --- a/api/tests/unit_tests/tools/test_mcp_tool.py +++ b/api/tests/unit_tests/tools/test_mcp_tool.py @@ -4,7 +4,6 @@ from typing import Any from unittest.mock import Mock, patch import pytest -from graphon.model_runtime.entities.llm_entities import LLMUsage from core.mcp.types import ( AudioContent, @@ -19,6 +18,7 @@ from core.tools.__base.tool_runtime import ToolRuntime from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolEntity, ToolIdentity, ToolInvokeMessage from core.tools.mcp_tool.tool import MCPTool +from graphon.model_runtime.entities.llm_entities import LLMUsage def _make_mcp_tool(output_schema: dict[str, Any] | None = None) -> MCPTool: diff --git a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py index ffa6833524..c166a946d9 100644 --- a/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py +++ b/api/tests/unit_tests/utils/structured_output_parser/test_structured_output_parser.py @@ -2,6 +2,9 @@ from decimal import Decimal from unittest.mock import MagicMock, patch import pytest + +from core.llm_generator.output_parser.errors import OutputParserError +from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output from graphon.model_runtime.entities.llm_entities import ( LLMResult, LLMResultChunk, @@ -18,9 +21,6 @@ from graphon.model_runtime.entities.message_entities import ( ) from graphon.model_runtime.entities.model_entities import AIModelEntity, ModelType -from core.llm_generator.output_parser.errors import OutputParserError -from core.llm_generator.output_parser.structured_output import invoke_llm_with_structured_output - def create_mock_usage(prompt_tokens: int = 10, completion_tokens: int = 5) -> LLMUsage: """Create a mock LLMUsage with all required fields""" diff --git a/api/tests/workflow_test_utils.py b/api/tests/workflow_test_utils.py index d33ac2c710..1415bb1d52 100644 --- a/api/tests/workflow_test_utils.py +++ b/api/tests/workflow_test_utils.py @@ -1,13 +1,12 @@ from collections.abc import Mapping from typing import Any +from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, build_dify_run_context +from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from graphon.entities import GraphInitParams from graphon.runtime import VariablePool from graphon.variables.variables import Variable -from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, build_dify_run_context -from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool - def build_test_run_context( *, diff --git a/api/uv.lock b/api/uv.lock index d0f1ed826e..7ed1b9960c 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -380,14 +380,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.9" +version = "1.6.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/10/b325d58ffe86815b399334a101e63bc6fa4e1953921cb23703b48a0a0220/authlib-1.6.11.tar.gz", hash = "sha256:64db35b9b01aeccb4715a6c9a6613a06f2bd7be2ab9d2eb89edd1dfc7580a38f", size = 165359, upload-time = "2026-04-16T07:22:50.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/57/2f/55fca558f925a51db046e5b929deb317ddb05afed74b22d89f4eca578980/authlib-1.6.11-py2.py3-none-any.whl", hash = "sha256:c8687a9a26451c51a34a06fa17bb97cb15bba46a6a626755e2d7f50da8bff3e3", size = 244469, upload-time = "2026-04-16T07:22:48.413Z" }, ] [[package]] @@ -546,6 +546,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] +[[package]] +name = "bidict" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, +] + [[package]] name = "billiard" version = "4.2.3" @@ -652,24 +661,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/b7/f4a051cefaf76930c77558b31646bcce7e9b3fbdcbc89e4073783e961519/botocore_stubs-1.41.3-py3-none-any.whl", hash = "sha256:6ab911bd9f7256f1dcea2e24a4af7ae0f9f07e83d0a760bba37f028f4a2e5589", size = 66749, upload-time = "2025-11-24T20:29:26.142Z" }, ] -[[package]] -name = "bottleneck" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/72/7e3593a2a3dd69ec831a9981a7b1443647acb66a5aec34c1620a5f7f8498/bottleneck-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bb16a16a86a655fdbb34df672109a8a227bb5f9c9cf5bb8ae400a639bc52fa3", size = 100515, upload-time = "2025-09-08T16:29:55.141Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d4/e7bbea08f4c0f0bab819d38c1a613da5f194fba7b19aae3e2b3a27e78886/bottleneck-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fbf5d0787af9aee6cef4db9cdd14975ce24bd02e0cc30155a51411ebe2ff35f", size = 377451, upload-time = "2025-09-08T16:29:56.718Z" }, - { url = "https://files.pythonhosted.org/packages/fe/80/a6da430e3b1a12fd85f9fe90d3ad8fe9a527ecb046644c37b4b3f4baacfc/bottleneck-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d08966f4a22384862258940346a72087a6f7cebb19038fbf3a3f6690ee7fd39f", size = 368303, upload-time = "2025-09-08T16:29:57.834Z" }, - { url = "https://files.pythonhosted.org/packages/30/11/abd30a49f3251f4538430e5f876df96f2b39dabf49e05c5836820d2c31fe/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:604f0b898b43b7bc631c564630e936a8759d2d952641c8b02f71e31dbcd9deaa", size = 361232, upload-time = "2025-09-08T16:29:59.104Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ac/1c0e09d8d92b9951f675bd42463ce76c3c3657b31c5bf53ca1f6dd9eccff/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d33720bad761e642abc18eda5f188ff2841191c9f63f9d0c052245decc0faeb9", size = 373234, upload-time = "2025-09-08T16:30:00.488Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ea/382c572ae3057ba885d484726bb63629d1f63abedf91c6cd23974eb35a9b/bottleneck-1.6.0-cp312-cp312-win32.whl", hash = "sha256:a1e5907ec2714efbe7075d9207b58c22ab6984a59102e4ecd78dced80dab8374", size = 108020, upload-time = "2025-09-08T16:30:01.773Z" }, - { url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493, upload-time = "2025-09-08T16:30:02.872Z" }, -] - [[package]] name = "brotli" version = "1.2.0" @@ -704,18 +695,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, ] -[[package]] -name = "bs4" -version = "0.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, -] - [[package]] name = "build" version = "1.3.0" @@ -1393,91 +1372,49 @@ version = "1.13.3" source = { virtual = "." } dependencies = [ { name = "aliyun-log-python-sdk" }, - { name = "apscheduler" }, { name = "arize-phoenix-otel" }, { name = "azure-identity" }, - { name = "beautifulsoup4" }, { name = "bleach" }, { name = "boto3" }, - { name = "bs4" }, - { name = "cachetools" }, { name = "celery" }, - { name = "charset-normalizer" }, { name = "croniter" }, { name = "fastopenapi", extra = ["flask"] }, - { name = "flask" }, { name = "flask-compress" }, { name = "flask-cors" }, { name = "flask-login" }, { name = "flask-migrate" }, { name = "flask-orjson" }, { name = "flask-restx" }, - { name = "flask-sqlalchemy" }, { name = "gevent" }, + { name = "gevent-websocket" }, { name = "gmpy2" }, - { name = "google-api-core" }, { name = "google-api-python-client" }, - { name = "google-auth" }, - { name = "google-auth-httplib2" }, { name = "google-cloud-aiplatform" }, - { name = "googleapis-common-protos" }, { name = "graphon" }, { name = "gunicorn" }, { name = "httpx", extra = ["socks"] }, { name = "httpx-sse" }, - { name = "jieba" }, { name = "json-repair" }, { name = "langfuse" }, { name = "langsmith" }, - { name = "litellm" }, - { name = "markdown" }, { name = "mlflow-skinny" }, - { name = "numpy" }, - { name = "openpyxl" }, - { name = "opentelemetry-api" }, { name = "opentelemetry-distro" }, - { name = "opentelemetry-exporter-otlp" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-instrumentation-celery" }, { name = "opentelemetry-instrumentation-flask" }, { name = "opentelemetry-instrumentation-httpx" }, { name = "opentelemetry-instrumentation-redis" }, { name = "opentelemetry-instrumentation-sqlalchemy" }, { name = "opentelemetry-propagator-b3" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, { name = "opik" }, - { name = "packaging" }, - { name = "pandas", extra = ["excel", "output-formatting", "performance"] }, { name = "psycogreen" }, { name = "psycopg2-binary" }, - { name = "pycryptodome" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt" }, - { name = "pypandoc" }, - { name = "pypdfium2" }, - { name = "python-docx" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, + { name = "python-socketio" }, { name = "readabilipy" }, { name = "redis", extra = ["hiredis"] }, { name = "resend" }, { name = "sendgrid" }, - { name = "sentry-sdk", extra = ["flask"] }, - { name = "sqlalchemy" }, { name = "sseclient-py" }, - { name = "starlette" }, - { name = "tiktoken" }, - { name = "transformers" }, - { name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] }, { name = "weave" }, - { name = "yarl" }, ] [package.dev-dependencies] @@ -1503,7 +1440,6 @@ dev = [ { name = "pytest-xdist" }, { name = "ruff" }, { name = "scipy-stubs" }, - { name = "sseclient-py" }, { name = "testcontainers" }, { name = "types-aiofiles" }, { name = "types-beautifulsoup4" }, @@ -1693,178 +1629,135 @@ vdb-xinference = [ [package.metadata] requires-dist = [ - { name = "aliyun-log-python-sdk", specifier = "~=0.9.44" }, - { name = "apscheduler", specifier = ">=3.11.2" }, + { name = "aliyun-log-python-sdk", specifier = ">=0.9.44,<1.0.0" }, { name = "arize-phoenix-otel", specifier = "~=0.15.0" }, - { name = "azure-identity", specifier = "==1.25.3" }, - { name = "beautifulsoup4", specifier = "==4.14.3" }, - { name = "bleach", specifier = "~=6.3.0" }, - { name = "boto3", specifier = "==1.42.88" }, - { name = "bs4", specifier = "~=0.0.1" }, - { name = "cachetools", specifier = "~=7.0.5" }, - { name = "celery", specifier = "~=5.6.3" }, - { name = "charset-normalizer", specifier = ">=3.4.7" }, + { name = "azure-identity", specifier = ">=1.25.3,<2.0.0" }, + { name = "bleach", specifier = ">=6.3.0" }, + { name = "boto3", specifier = ">=1.42.88" }, + { name = "celery", specifier = ">=5.6.3" }, { name = "croniter", specifier = ">=6.2.2" }, - { name = "fastopenapi", extras = ["flask"], specifier = ">=0.7.0" }, - { name = "flask", specifier = "~=3.1.3" }, - { name = "flask-compress", specifier = ">=1.24,<1.25" }, - { name = "flask-cors", specifier = "~=6.0.2" }, - { name = "flask-login", specifier = "~=0.6.3" }, - { name = "flask-migrate", specifier = "~=4.1.0" }, - { name = "flask-orjson", specifier = "~=2.0.0" }, - { name = "flask-restx", specifier = "~=1.3.2" }, - { name = "flask-sqlalchemy", specifier = "~=3.1.1" }, - { name = "gevent", specifier = "~=26.4.0" }, - { name = "gmpy2", specifier = "~=2.3.0" }, - { name = "google-api-core", specifier = ">=2.30.3" }, - { name = "google-api-python-client", specifier = "==2.194.0" }, - { name = "google-auth", specifier = ">=2.49.2" }, - { name = "google-auth-httplib2", specifier = "==0.3.1" }, - { name = "google-cloud-aiplatform", specifier = ">=1.147.0" }, - { name = "googleapis-common-protos", specifier = ">=1.74.0" }, - { name = "graphon", specifier = ">=0.1.2" }, - { name = "gunicorn", specifier = "~=25.3.0" }, - { name = "httpx", extras = ["socks"], specifier = "~=0.28.1" }, + { name = "fastopenapi", extras = ["flask"], specifier = "~=0.7.0" }, + { name = "flask-compress", specifier = ">=1.24,<2.0.0" }, + { name = "flask-cors", specifier = ">=6.0.2" }, + { name = "flask-login", specifier = ">=0.6.3,<1.0.0" }, + { name = "flask-migrate", specifier = ">=4.1.0,<5.0.0" }, + { name = "flask-orjson", specifier = ">=2.0.0,<3.0.0" }, + { name = "flask-restx", specifier = ">=1.3.2,<2.0.0" }, + { name = "gevent", specifier = ">=26.4.0" }, + { name = "gevent-websocket", specifier = ">=0.10.1" }, + { name = "gmpy2", specifier = ">=2.3.0" }, + { name = "google-api-python-client", specifier = ">=2.194.0" }, + { name = "google-cloud-aiplatform", specifier = ">=1.147.0,<2.0.0" }, + { name = "graphon", specifier = "~=0.1.2" }, + { name = "gunicorn", specifier = ">=25.3.0" }, + { name = "httpx", extras = ["socks"], specifier = ">=0.28.1,<1.0.0" }, { name = "httpx-sse", specifier = "~=0.4.0" }, - { name = "jieba", specifier = "==0.42.1" }, - { name = "json-repair", specifier = ">=0.59.2" }, + { name = "json-repair", specifier = "~=0.59.2" }, { name = "langfuse", specifier = ">=4.2.0,<5.0.0" }, - { name = "langsmith", specifier = "~=0.7.30" }, - { name = "litellm", specifier = "==1.83.0" }, - { name = "markdown", specifier = "~=3.10.2" }, - { name = "mlflow-skinny", specifier = ">=3.11.1" }, - { name = "numpy", specifier = "~=2.4.4" }, - { name = "openpyxl", specifier = "~=3.1.5" }, - { name = "opentelemetry-api", specifier = "==1.41.0" }, - { name = "opentelemetry-distro", specifier = "==0.62b0" }, - { name = "opentelemetry-exporter-otlp", specifier = "==1.41.0" }, - { name = "opentelemetry-exporter-otlp-proto-common", specifier = "==1.41.0" }, - { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = "==1.41.0" }, - { name = "opentelemetry-exporter-otlp-proto-http", specifier = "==1.41.0" }, - { name = "opentelemetry-instrumentation", specifier = "==0.62b0" }, - { name = "opentelemetry-instrumentation-celery", specifier = "==0.62b0" }, - { name = "opentelemetry-instrumentation-flask", specifier = "==0.62b0" }, - { name = "opentelemetry-instrumentation-httpx", specifier = "==0.62b0" }, - { name = "opentelemetry-instrumentation-redis", specifier = "==0.62b0" }, - { name = "opentelemetry-instrumentation-sqlalchemy", specifier = "==0.62b0" }, - { name = "opentelemetry-propagator-b3", specifier = "==1.41.0" }, - { name = "opentelemetry-proto", specifier = "==1.41.0" }, - { name = "opentelemetry-sdk", specifier = "==1.41.0" }, - { name = "opentelemetry-semantic-conventions", specifier = "==0.62b0" }, - { name = "opentelemetry-util-http", specifier = "==0.62b0" }, + { name = "langsmith", specifier = ">=0.7.31,<1.0.0" }, + { name = "mlflow-skinny", specifier = ">=3.11.1,<4.0.0" }, + { name = "opentelemetry-distro", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-instrumentation-celery", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-instrumentation-flask", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-instrumentation-redis", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.62b0,<1.0.0" }, + { name = "opentelemetry-propagator-b3", specifier = ">=1.41.0,<2.0.0" }, { name = "opik", specifier = "~=1.11.2" }, - { name = "packaging", specifier = "~=26.0" }, - { name = "pandas", extras = ["excel", "output-formatting", "performance"], specifier = "~=3.0.2" }, - { name = "psycogreen", specifier = "~=1.0.2" }, - { name = "psycopg2-binary", specifier = "~=2.9.11" }, - { name = "pycryptodome", specifier = "==3.23.0" }, - { name = "pydantic", specifier = "~=2.12.5" }, - { name = "pydantic-settings", specifier = "~=2.13.1" }, - { name = "pyjwt", specifier = "~=2.12.1" }, - { name = "pypandoc", specifier = "~=1.13" }, - { name = "pypdfium2", specifier = "==5.6.0" }, - { name = "python-docx", specifier = "~=1.2.0" }, - { name = "python-dotenv", specifier = "==1.2.2" }, - { name = "pyyaml", specifier = "~=6.0.1" }, - { name = "readabilipy", specifier = "~=0.3.0" }, - { name = "redis", extras = ["hiredis"], specifier = "~=7.4.0" }, - { name = "resend", specifier = "~=2.27.0" }, - { name = "sendgrid", specifier = "~=6.12.5" }, - { name = "sentry-sdk", extras = ["flask"], specifier = "~=2.57.0" }, - { name = "sqlalchemy", specifier = "~=2.0.49" }, - { name = "sseclient-py", specifier = "~=1.9.0" }, - { name = "starlette", specifier = "==1.0.0" }, - { name = "tiktoken", specifier = "~=0.12.0" }, - { name = "transformers", specifier = "~=5.3.0" }, - { name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.21.5" }, - { name = "weave", specifier = ">=0.52.36" }, - { name = "yarl", specifier = "~=1.23.0" }, + { name = "psycogreen", specifier = ">=1.0.2" }, + { name = "psycopg2-binary", specifier = ">=2.9.11" }, + { name = "python-socketio", specifier = ">=5.13.0" }, + { name = "readabilipy", specifier = ">=0.3.0,<1.0.0" }, + { name = "redis", extras = ["hiredis"], specifier = ">=7.4.0" }, + { name = "resend", specifier = ">=2.27.0,<3.0.0" }, + { name = "sendgrid", specifier = ">=6.12.5" }, + { name = "sseclient-py", specifier = ">=1.8.0" }, + { name = "weave", specifier = ">=0.52.36,<1.0.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "basedpyright", specifier = "~=1.39.0" }, + { name = "basedpyright", specifier = ">=1.39.0" }, { name = "boto3-stubs", specifier = ">=1.42.88" }, { name = "celery-types", specifier = ">=0.23.0" }, - { name = "coverage", specifier = "~=7.13.4" }, - { name = "dotenv-linter", specifier = "~=0.7.0" }, - { name = "faker", specifier = "~=40.13.0" }, + { name = "coverage", specifier = ">=7.13.4" }, + { name = "dotenv-linter", specifier = ">=0.7.0" }, + { name = "faker", specifier = ">=20.1.0" }, { name = "hypothesis", specifier = ">=6.151.12" }, { name = "import-linter", specifier = ">=2.3" }, - { name = "lxml-stubs", specifier = "~=0.5.1" }, - { name = "mypy", specifier = "~=1.20.1" }, - { name = "pandas-stubs", specifier = "~=3.0.0" }, + { name = "lxml-stubs", specifier = ">=0.5.1" }, + { name = "mypy", specifier = ">=1.20.1" }, + { name = "pandas-stubs", specifier = ">=3.0.0" }, { name = "pyrefly", specifier = ">=0.60.0" }, - { name = "pytest", specifier = "~=9.0.3" }, - { name = "pytest-benchmark", specifier = "~=5.2.3" }, - { name = "pytest-cov", specifier = "~=7.1.0" }, - { name = "pytest-env", specifier = "~=1.6.0" }, - { name = "pytest-mock", specifier = "~=3.15.1" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-benchmark", specifier = ">=5.2.3" }, + { name = "pytest-cov", specifier = ">=7.1.0" }, + { name = "pytest-env", specifier = ">=1.6.0" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = "~=0.15.10" }, + { name = "ruff", specifier = ">=0.15.10" }, { name = "scipy-stubs", specifier = ">=1.15.3.0" }, - { name = "sseclient-py", specifier = ">=1.8.0" }, - { name = "testcontainers", specifier = "~=4.14.2" }, - { name = "types-aiofiles", specifier = "~=25.1.0" }, - { name = "types-beautifulsoup4", specifier = "~=4.12.0" }, - { name = "types-cachetools", specifier = "~=6.2.0" }, + { name = "testcontainers", specifier = ">=4.14.2" }, + { name = "types-aiofiles", specifier = ">=25.1.0" }, + { name = "types-beautifulsoup4", specifier = ">=4.12.0" }, + { name = "types-cachetools", specifier = ">=6.2.0" }, { name = "types-cffi", specifier = ">=2.0.0.20260408" }, - { name = "types-colorama", specifier = "~=0.4.15" }, - { name = "types-defusedxml", specifier = "~=0.7.0" }, - { name = "types-deprecated", specifier = "~=1.3.1" }, - { name = "types-docutils", specifier = "~=0.22.3" }, - { name = "types-flask-cors", specifier = "~=6.0.0" }, - { name = "types-flask-migrate", specifier = "~=4.1.0" }, - { name = "types-gevent", specifier = "~=26.4.0" }, - { name = "types-greenlet", specifier = "~=3.4.0" }, - { name = "types-html5lib", specifier = "~=1.1.11" }, + { name = "types-colorama", specifier = ">=0.4.15" }, + { name = "types-defusedxml", specifier = ">=0.7.0" }, + { name = "types-deprecated", specifier = ">=1.3.1" }, + { name = "types-docutils", specifier = ">=0.22.3" }, + { name = "types-flask-cors", specifier = ">=6.0.0" }, + { name = "types-flask-migrate", specifier = ">=4.1.0" }, + { name = "types-gevent", specifier = ">=26.4.0" }, + { name = "types-greenlet", specifier = ">=3.4.0" }, + { name = "types-html5lib", specifier = ">=1.1.11" }, { name = "types-jmespath", specifier = ">=1.1.0.20260408" }, - { name = "types-markdown", specifier = "~=3.10.2" }, - { name = "types-oauthlib", specifier = "~=3.3.0" }, - { name = "types-objgraph", specifier = "~=3.6.0" }, - { name = "types-olefile", specifier = "~=0.47.0" }, - { name = "types-openpyxl", specifier = "~=3.1.5" }, - { name = "types-pexpect", specifier = "~=4.9.0" }, - { name = "types-protobuf", specifier = "~=7.34.1" }, - { name = "types-psutil", specifier = "~=7.2.2" }, - { name = "types-psycopg2", specifier = "~=2.9.21" }, - { name = "types-pygments", specifier = "~=2.20.0" }, - { name = "types-pymysql", specifier = "~=1.1.0" }, + { name = "types-markdown", specifier = ">=3.10.2" }, + { name = "types-oauthlib", specifier = ">=3.3.0" }, + { name = "types-objgraph", specifier = ">=3.6.0" }, + { name = "types-olefile", specifier = ">=0.47.0" }, + { name = "types-openpyxl", specifier = ">=3.1.5" }, + { name = "types-pexpect", specifier = ">=4.9.0" }, + { name = "types-protobuf", specifier = ">=7.34.1" }, + { name = "types-psutil", specifier = ">=7.2.2" }, + { name = "types-psycopg2", specifier = ">=2.9.21" }, + { name = "types-pygments", specifier = ">=2.20.0" }, + { name = "types-pymysql", specifier = ">=1.1.0" }, { name = "types-pyopenssl", specifier = ">=24.1.0" }, - { name = "types-python-dateutil", specifier = "~=2.9.0" }, + { name = "types-python-dateutil", specifier = ">=2.9.0" }, { name = "types-python-http-client", specifier = ">=3.3.7.20260408" }, - { name = "types-pywin32", specifier = "~=311.0.0" }, - { name = "types-pyyaml", specifier = "~=6.0.12" }, + { name = "types-pywin32", specifier = ">=311.0.0" }, + { name = "types-pyyaml", specifier = ">=6.0.12" }, { name = "types-redis", specifier = ">=4.6.0.20241004" }, - { name = "types-regex", specifier = "~=2026.4.4" }, + { name = "types-regex", specifier = ">=2026.4.4" }, { name = "types-setuptools", specifier = ">=82.0.0.20260408" }, - { name = "types-shapely", specifier = "~=2.1.0" }, + { name = "types-shapely", specifier = ">=2.1.0" }, { name = "types-simplejson", specifier = ">=3.20.0.20260408" }, { name = "types-six", specifier = ">=1.17.0.20260408" }, { name = "types-tensorflow", specifier = ">=2.18.0.20260408" }, { name = "types-tqdm", specifier = ">=4.67.3.20260408" }, { name = "types-ujson", specifier = ">=5.10.0" }, - { name = "xinference-client", specifier = "~=2.4.0" }, + { name = "xinference-client", specifier = ">=2.4.0" }, ] evaluation = [ { name = "deepeval", specifier = ">=2.0.0" }, { name = "ragas", specifier = ">=0.2.0" }, ] storage = [ - { name = "azure-storage-blob", specifier = "==12.28.0" }, - { name = "bce-python-sdk", specifier = "~=0.9.69" }, - { name = "cos-python-sdk-v5", specifier = "==1.9.41" }, - { name = "esdk-obs-python", specifier = "==3.26.2" }, + { name = "azure-storage-blob", specifier = ">=12.28.0" }, + { name = "bce-python-sdk", specifier = ">=0.9.69" }, + { name = "cos-python-sdk-v5", specifier = ">=1.9.41" }, + { name = "esdk-obs-python", specifier = ">=3.22.2" }, { name = "google-cloud-storage", specifier = ">=3.10.1" }, - { name = "opendal", specifier = "~=0.46.0" }, - { name = "oss2", specifier = "==2.19.1" }, - { name = "supabase", specifier = "~=2.18.1" }, - { name = "tos", specifier = "~=2.9.0" }, + { name = "opendal", specifier = ">=0.46.0" }, + { name = "oss2", specifier = ">=2.19.1" }, + { name = "supabase", specifier = ">=2.18.1" }, + { name = "tos", specifier = ">=2.9.0" }, ] tools = [ - { name = "cloudscraper", specifier = "~=1.2.71" }, - { name = "nltk", specifier = "~=3.9.1" }, + { name = "cloudscraper", specifier = ">=1.2.71" }, + { name = "nltk", specifier = ">=3.9.1" }, ] vdb-alibabacloud-mysql = [{ name = "dify-vdb-alibabacloud-mysql", editable = "providers/vdb/vdb-alibabacloud-mysql" }] vdb-all = [ @@ -1928,7 +1821,7 @@ vdb-upstash = [{ name = "dify-vdb-upstash", editable = "providers/vdb/vdb-upstas vdb-vastbase = [{ name = "dify-vdb-vastbase", editable = "providers/vdb/vdb-vastbase" }] vdb-vikingdb = [{ name = "dify-vdb-vikingdb", editable = "providers/vdb/vdb-vikingdb" }] vdb-weaviate = [{ name = "dify-vdb-weaviate", editable = "providers/vdb/vdb-weaviate" }] -vdb-xinference = [{ name = "xinference-client", specifier = "~=2.4.0" }] +vdb-xinference = [{ name = "xinference-client", specifier = ">=2.4.0" }] [[package]] name = "dify-vdb-alibabacloud-mysql" @@ -2702,6 +2595,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/df/7875e08b06a95f4577b71708ec470d029fadf873a66eb813a2861d79dfb5/gevent-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c737e6ac6ce1398df0e3f41c58d982e397c993cbe73ac05b7edbe39e128c9cb", size = 1680530, upload-time = "2026-04-08T23:15:38.714Z" }, ] +[[package]] +name = "gevent-websocket" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gevent" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/d2/6fa19239ff1ab072af40ebf339acd91fb97f34617c2ee625b8e34bf42393/gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0", size = 18366, upload-time = "2017-03-12T22:46:05.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/84/2dc373eb6493e00c884cc11e6c059ec97abae2678d42f06bf780570b0193/gevent_websocket-0.10.1-py3-none-any.whl", hash = "sha256:17b67d91282f8f4c973eba0551183fc84f56f1c90c8f6b6b30256f31f66f5242", size = 22987, upload-time = "2017-03-12T22:46:03.611Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -3862,7 +3767,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.7.30" +version = "0.7.31" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3875,9 +3780,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/e7/d27d952ce9824d684a3bb500a06541a2d55734bc4d849cdfcca2dfd4d93a/langsmith-0.7.30.tar.gz", hash = "sha256:d9df7ba5e42f818b63bda78776c8f2fc853388be3ae77b117e5d183a149321a2", size = 1106040, upload-time = "2026-04-09T21:12:01.892Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/11/696019490992db5c87774dc20515529ef42a01e1d770fb754ed6d9b12fb0/langsmith-0.7.31.tar.gz", hash = "sha256:331ee4f7c26bb5be4022b9859b7d7b122cbf8c9d01d9f530114c1914b0349ffb", size = 1178480, upload-time = "2026-04-14T17:55:41.242Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/19/96250cf58070c5563446651b03bb76c2eb5afbf08e754840ab639532d8c6/langsmith-0.7.30-py3-none-any.whl", hash = "sha256:43dd9f8d290e4d406606d6cc0bd62f5d1050963f05fe0ab6ffe50acf41f2f55a", size = 372682, upload-time = "2026-04-09T21:12:00.481Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a1/a013cf458c301cda86a213dd153ce0a01c93f1ab5833f951e6a44c9763ce/langsmith-0.7.31-py3-none-any.whl", hash = "sha256:0291d49203f6e80dda011af1afda61eb0595a4d697adb684590a8805e1d61fb6", size = 373276, upload-time = "2026-04-14T17:55:39.677Z" }, ] [[package]] @@ -3998,14 +3903,14 @@ wheels = [ [[package]] name = "mako" -version = "1.3.10" +version = "1.3.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" }, ] [[package]] @@ -4350,25 +4255,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7d/86/db87a5393f1b1fabef53ac3ba4e6b938bb27e40a04ad7cc512098fcae032/numba-0.65.0-cp312-cp312-win_amd64.whl", hash = "sha256:59bb9f2bb9f1238dfd8e927ba50645c18ae769fef4f3d58ea0ea22a2683b91f5", size = 2749979, upload-time = "2026-04-01T03:51:37.88Z" }, ] -[[package]] -name = "numexpr" -version = "2.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, - { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, - { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, - { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, - { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, -] - [[package]] name = "numpy" version = "2.4.4" @@ -5000,15 +4886,6 @@ excel = [ { name = "xlrd" }, { name = "xlsxwriter" }, ] -output-formatting = [ - { name = "jinja2" }, - { name = "tabulate" }, -] -performance = [ - { name = "bottleneck" }, - { name = "numba" }, - { name = "numexpr" }, -] [[package]] name = "pandas-stubs" @@ -5624,11 +5501,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.10.0" +version = "6.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/9f/ca96abf18683ca12602065e4ed2bec9050b672c87d317f1079abc7b6d993/pypdf-6.10.0.tar.gz", hash = "sha256:4c5a48ba258c37024ec2505f7e8fd858525f5502784a2e1c8d415604af29f6ef", size = 5314833, upload-time = "2026-04-10T09:34:57.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/3f/9f2167401c2e94833ca3b69535bad89e533b5de75fefe4197a2c224baec2/pypdf-6.10.2.tar.gz", hash = "sha256:7d09ce108eff6bf67465d461b6ef352dcb8d84f7a91befc02f904455c6eea11d", size = 5315679, upload-time = "2026-04-15T16:37:36.978Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/f2/7ebe366f633f30a6ad105f650f44f24f98cb1335c4157d21ae47138b3482/pypdf-6.10.0-py3-none-any.whl", hash = "sha256:90005e959e1596c6e6c84c8b0ad383285b3e17011751cedd17f2ce8fcdfc86de", size = 334459, upload-time = "2026-04-10T09:34:54.966Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl", hash = "sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa", size = 336308, upload-time = "2026-04-15T16:37:34.851Z" }, ] [[package]] @@ -5887,6 +5764,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-engineio" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "simple-websocket" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, +] + [[package]] name = "python-http-client" version = "3.3.7" @@ -5943,6 +5832,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, ] +[[package]] +name = "python-socketio" +version = "5.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bidict" }, + { name = "python-engineio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, +] + [[package]] name = "pytz" version = "2025.2" @@ -6334,13 +6236,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" }, ] -[package.optional-dependencies] -flask = [ - { name = "blinker" }, - { name = "flask" }, - { name = "markupsafe" }, -] - [[package]] name = "setuptools" version = "80.9.0" @@ -6359,6 +6254,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] +[[package]] +name = "simple-websocket" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -6655,15 +6562,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/3f/48af1e72e59d60481724b326317bd311615bdedc31f8f81f9508fb84cda6/tablestore-6.4.4-py3-none-any.whl", hash = "sha256:984f086fa7acabaa3558da93205ad6df562b266b85fd249bc5891f2dd1d65814", size = 5118758, upload-time = "2026-04-09T09:40:17.209Z" }, ] -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, -] - [[package]] name = "tcvdb-text" version = "1.1.2" @@ -7829,6 +7727,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362, upload-time = "2023-11-09T06:33:28.271Z" }, ] +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +] + [[package]] name = "xinference-client" version = "2.4.0" diff --git a/docker/.env.example b/docker/.env.example index 856b04a3df..8176155698 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -132,6 +132,10 @@ MIGRATION_ENABLED=true # The default value is 300 seconds. FILES_ACCESS_TIMEOUT=300 +# Collaboration mode toggle +# To open collaboration features, you also need to set SERVER_WORKER_CLASS=geventwebsocket.gunicorn.workers.GeventWebSocketWorker +ENABLE_COLLABORATION_MODE=false + # Access token expiration time in minutes ACCESS_TOKEN_EXPIRE_MINUTES=60 @@ -167,6 +171,7 @@ SERVER_WORKER_AMOUNT=1 # Modifying it may also decrease throughput. # # It is strongly discouraged to change this parameter. +# If enable collaboration mode, it must be set to geventwebsocket.gunicorn.workers.GeventWebSocketWorker SERVER_WORKER_CLASS=gevent # Default number of worker connections, the default is 10. @@ -428,6 +433,8 @@ CONSOLE_CORS_ALLOW_ORIGINS=* COOKIE_DOMAIN= # When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. NEXT_PUBLIC_COOKIE_DOMAIN= +# WebSocket server URL. +NEXT_PUBLIC_SOCKET_URL=ws://localhost NEXT_PUBLIC_BATCH_CONCURRENCY=5 # ------------------------------ diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index 4f4b3851f6..888f96332c 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -159,6 +159,7 @@ services: APP_API_URL: ${APP_API_URL:-} AMPLITUDE_API_KEY: ${AMPLITUDE_API_KEY:-} NEXT_PUBLIC_COOKIE_DOMAIN: ${NEXT_PUBLIC_COOKIE_DOMAIN:-} + NEXT_PUBLIC_SOCKET_URL: ${NEXT_PUBLIC_SOCKET_URL:-ws://localhost} SENTRY_DSN: ${WEB_SENTRY_DSN:-} NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0} EXPERIMENTAL_ENABLE_VINEXT: ${EXPERIMENTAL_ENABLE_VINEXT:-false} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index c1ddba4f80..a10fdf77c6 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -34,6 +34,7 @@ x-shared-env: &shared-api-worker-env OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1} MIGRATION_ENABLED: ${MIGRATION_ENABLED:-true} FILES_ACCESS_TIMEOUT: ${FILES_ACCESS_TIMEOUT:-300} + ENABLE_COLLABORATION_MODE: ${ENABLE_COLLABORATION_MODE:-false} ACCESS_TOKEN_EXPIRE_MINUTES: ${ACCESS_TOKEN_EXPIRE_MINUTES:-60} REFRESH_TOKEN_EXPIRE_DAYS: ${REFRESH_TOKEN_EXPIRE_DAYS:-30} APP_DEFAULT_ACTIVE_REQUESTS: ${APP_DEFAULT_ACTIVE_REQUESTS:-0} @@ -119,6 +120,7 @@ x-shared-env: &shared-api-worker-env CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} COOKIE_DOMAIN: ${COOKIE_DOMAIN:-} NEXT_PUBLIC_COOKIE_DOMAIN: ${NEXT_PUBLIC_COOKIE_DOMAIN:-} + NEXT_PUBLIC_SOCKET_URL: ${NEXT_PUBLIC_SOCKET_URL:-ws://localhost} NEXT_PUBLIC_BATCH_CONCURRENCY: ${NEXT_PUBLIC_BATCH_CONCURRENCY:-5} STORAGE_TYPE: ${STORAGE_TYPE:-opendal} OPENDAL_SCHEME: ${OPENDAL_SCHEME:-fs} @@ -878,6 +880,7 @@ services: APP_API_URL: ${APP_API_URL:-} AMPLITUDE_API_KEY: ${AMPLITUDE_API_KEY:-} NEXT_PUBLIC_COOKIE_DOMAIN: ${NEXT_PUBLIC_COOKIE_DOMAIN:-} + NEXT_PUBLIC_SOCKET_URL: ${NEXT_PUBLIC_SOCKET_URL:-ws://localhost} SENTRY_DSN: ${WEB_SENTRY_DSN:-} NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0} EXPERIMENTAL_ENABLE_VINEXT: ${EXPERIMENTAL_ENABLE_VINEXT:-false} diff --git a/docker/nginx/conf.d/default.conf.template b/docker/nginx/conf.d/default.conf.template index 1d63c1b97d..94a748290f 100644 --- a/docker/nginx/conf.d/default.conf.template +++ b/docker/nginx/conf.d/default.conf.template @@ -14,6 +14,14 @@ server { include proxy.conf; } + location /socket.io/ { + proxy_pass http://api:5001; + include proxy.conf; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_cache_bypass $http_upgrade; + } + location /v1 { proxy_pass http://api:5001; include proxy.conf; diff --git a/e2e/AGENTS.md b/e2e/AGENTS.md index da3d76210d..e56aab20a7 100644 --- a/e2e/AGENTS.md +++ b/e2e/AGENTS.md @@ -171,10 +171,10 @@ open cucumber-report/report.html ### Workflow 1. Create a `.feature` file under `features//` -2. Add step definitions under `features/step-definitions//` -3. Reuse existing steps from `common/` and other definition files before writing new ones -4. Run with `pnpm -C e2e e2e -- --tags @your-tag` to verify -5. Run `pnpm -C e2e check` before committing +1. Add step definitions under `features/step-definitions//` +1. Reuse existing steps from `common/` and other definition files before writing new ones +1. Run with `pnpm -C e2e e2e -- --tags @your-tag` to verify +1. Run `pnpm -C e2e check` before committing ### Feature file conventions @@ -202,9 +202,9 @@ Keep scenarios short and declarative. Each step should describe **what** the use ### Step definition conventions ```typescript -import { When, Then } from '@cucumber/cucumber' -import { expect } from '@playwright/test' import type { DifyWorld } from '../../support/world' +import { Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' When('I open the datasets page', async function (this: DifyWorld) { await this.getPage().goto('/datasets') diff --git a/e2e/README.md b/e2e/README.md index 9b4046eaff..feca0cb419 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -1,3 +1,5 @@ # E2E -Canonical documentation for this package lives in [AGENTS.md](./AGENTS.md). +Canonical documentation for this package lives in [AGENTS.md]. + +[AGENTS.md]: ./AGENTS.md diff --git a/e2e/features/apps/create-agent-app.feature b/e2e/features/apps/create-agent-app.feature new file mode 100644 index 0000000000..75d8dc3a77 --- /dev/null +++ b/e2e/features/apps/create-agent-app.feature @@ -0,0 +1,11 @@ +@apps @authenticated @core @mode-matrix +Feature: Create Agent app + Scenario: Create a new Agent app and redirect to the configuration page + Given I am signed in as the default E2E admin + When I open the apps console + And I start creating a blank app + And I expand the beginner app types + And I select the "Agent" app type + And I enter a unique E2E app name + And I confirm app creation + Then I should land on the app configuration page diff --git a/e2e/features/apps/create-chatflow-app.feature b/e2e/features/apps/create-chatflow-app.feature new file mode 100644 index 0000000000..364cccc494 --- /dev/null +++ b/e2e/features/apps/create-chatflow-app.feature @@ -0,0 +1,10 @@ +@apps @authenticated @core @mode-matrix +Feature: Create Chatflow app + Scenario: Create a new Chatflow app and redirect to the workflow editor + Given I am signed in as the default E2E admin + When I open the apps console + And I start creating a blank app + And I select the "Chatflow" app type + And I enter a unique E2E app name + And I confirm app creation + Then I should land on the workflow editor diff --git a/e2e/features/apps/create-text-generator-app.feature b/e2e/features/apps/create-text-generator-app.feature new file mode 100644 index 0000000000..aec0436644 --- /dev/null +++ b/e2e/features/apps/create-text-generator-app.feature @@ -0,0 +1,11 @@ +@apps @authenticated @core @mode-matrix +Feature: Create Text Generator app + Scenario: Create a new Text Generator app and redirect to the configuration page + Given I am signed in as the default E2E admin + When I open the apps console + And I start creating a blank app + And I expand the beginner app types + And I select the "Text Generator" app type + And I enter a unique E2E app name + And I confirm app creation + Then I should land on the app configuration page diff --git a/e2e/features/apps/delete-app.feature b/e2e/features/apps/delete-app.feature new file mode 100644 index 0000000000..49326ba098 --- /dev/null +++ b/e2e/features/apps/delete-app.feature @@ -0,0 +1,11 @@ +@apps @authenticated @core +Feature: Delete app + Scenario: Delete an existing app from the apps console + Given I am signed in as the default E2E admin + And there is an existing E2E app available for testing + When I open the apps console + And I open the options menu for the last created E2E app + And I click "Delete" in the app options menu + And I type the app name in the deletion confirmation + And I confirm the deletion + Then the app should no longer appear in the apps console diff --git a/e2e/features/apps/duplicate-app.feature b/e2e/features/apps/duplicate-app.feature new file mode 100644 index 0000000000..3645a7d172 --- /dev/null +++ b/e2e/features/apps/duplicate-app.feature @@ -0,0 +1,10 @@ +@apps @authenticated @core +Feature: Duplicate app + Scenario: Duplicate an existing app and open the copy in the editor + Given I am signed in as the default E2E admin + And there is an existing E2E app available for testing + When I open the apps console + And I open the options menu for the last created E2E app + And I click "Duplicate" in the app options menu + And I confirm the app duplication + Then I should land on the app editor diff --git a/e2e/features/apps/export-app.feature b/e2e/features/apps/export-app.feature new file mode 100644 index 0000000000..d6d040fb00 --- /dev/null +++ b/e2e/features/apps/export-app.feature @@ -0,0 +1,9 @@ +@apps @authenticated @core +Feature: Export app DSL + Scenario: Export the DSL file for an existing app + Given I am signed in as the default E2E admin + And there is an existing E2E completion app available for testing + When I open the apps console + And I open the options menu for the last created E2E app + And I click "Export DSL" in the app options menu + Then a YAML file named after the app should be downloaded diff --git a/e2e/features/apps/switch-app-mode.feature b/e2e/features/apps/switch-app-mode.feature new file mode 100644 index 0000000000..5cdc6341fb --- /dev/null +++ b/e2e/features/apps/switch-app-mode.feature @@ -0,0 +1,10 @@ +@apps @authenticated @core +Feature: Switch app mode + Scenario: Switch a Completion app to Workflow Orchestrate + Given I am signed in as the default E2E admin + And there is an existing E2E completion app available for testing + When I open the apps console + And I open the options menu for the last created E2E app + And I click "Switch to Workflow Orchestrate" in the app options menu + And I confirm the app switch + Then I should land on the switched app diff --git a/e2e/features/step-definitions/apps/create-app.steps.ts b/e2e/features/step-definitions/apps/create-app.steps.ts index 6bc9ae30b6..931d4662a2 100644 --- a/e2e/features/step-definitions/apps/create-app.steps.ts +++ b/e2e/features/step-definitions/apps/create-app.steps.ts @@ -1,6 +1,6 @@ +import type { DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import type { DifyWorld } from '../../support/world' When('I start creating a blank app', async function (this: DifyWorld) { const page = this.getPage() @@ -11,7 +11,7 @@ When('I start creating a blank app', async function (this: DifyWorld) { When('I enter a unique E2E app name', async function (this: DifyWorld) { const appName = `E2E App ${Date.now()}` - + this.lastCreatedAppName = appName await this.getPage().getByPlaceholder('Give your app a name').fill(appName) }) @@ -26,10 +26,15 @@ When('I confirm app creation', async function (this: DifyWorld) { When('I select the {string} app type', async function (this: DifyWorld, appType: string) { const dialog = this.getPage().getByRole('dialog') - const appTypeTitle = dialog.getByText(appType, { exact: true }) + // The modal defaults to ADVANCED_CHAT, so the preview panel immediately renders + //

Chatflow

alongside the card's
Chatflow
. + // locator('div').getByText(...) would still match the

because getByText + // searches inside each div for any descendant. Use :text-is() instead, which + // targets only
elements whose own normalised text equals appType exactly. + const appTypeCard = dialog.locator(`div:text-is("${appType}")`) - await expect(appTypeTitle).toBeVisible() - await appTypeTitle.click() + await expect(appTypeCard).toBeVisible() + await appTypeCard.click() }) When('I expand the beginner app types', async function (this: DifyWorld) { diff --git a/e2e/features/step-definitions/apps/delete-app.steps.ts b/e2e/features/step-definitions/apps/delete-app.steps.ts new file mode 100644 index 0000000000..e5da626645 --- /dev/null +++ b/e2e/features/step-definitions/apps/delete-app.steps.ts @@ -0,0 +1,35 @@ +import type { DifyWorld } from '../../support/world' +import { Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' + +When('I type the app name in the deletion confirmation', async function (this: DifyWorld) { + const appName = this.lastCreatedAppName + if (!appName) { + throw new Error( + 'No app name stored. Run "there is an existing E2E app available for testing" first.', + ) + } + + const page = this.getPage() + const dialog = page.getByRole('alertdialog') + await expect(dialog).toBeVisible() + await dialog.getByPlaceholder('Enter app name…').fill(appName) +}) + +When('I confirm the deletion', async function (this: DifyWorld) { + const dialog = this.getPage().getByRole('alertdialog') + await dialog.getByRole('button', { name: 'Confirm' }).click() +}) + +Then('the app should no longer appear in the apps console', async function (this: DifyWorld) { + const appName = this.lastCreatedAppName + if (!appName) { + throw new Error( + 'No app name stored. Run "there is an existing E2E app available for testing" first.', + ) + } + + await expect(this.getPage().getByTitle(appName)).not.toBeVisible({ + timeout: 10_000, + }) +}) diff --git a/e2e/features/step-definitions/apps/duplicate-app.steps.ts b/e2e/features/step-definitions/apps/duplicate-app.steps.ts new file mode 100644 index 0000000000..e5e3694e4d --- /dev/null +++ b/e2e/features/step-definitions/apps/duplicate-app.steps.ts @@ -0,0 +1,36 @@ +import type { DifyWorld } from '../../support/world' +import { Given, When } from '@cucumber/cucumber' +import { createTestApp } from '../../../support/api' + +Given('there is an existing E2E app available for testing', async function (this: DifyWorld) { + const name = `E2E Test App ${Date.now()}` + const app = await createTestApp(name, 'completion') + this.lastCreatedAppName = app.name + this.createdAppIds.push(app.id) +}) + +When('I open the options menu for the last created E2E app', async function (this: DifyWorld) { + const appName = this.lastCreatedAppName + if (!appName) + throw new Error('No app name stored. Run "I enter a unique E2E app name" first.') + + const page = this.getPage() + // Scope to the specific card: the card root is the innermost div that contains + // both the unique app name text and a More button (they are in separate branches, + // so no child div satisfies both). .last() picks the deepest match in DOM order. + const appCard = page + .locator('div') + .filter({ has: page.getByText(appName, { exact: true }) }) + .filter({ has: page.getByRole('button', { name: 'More' }) }) + .last() + await appCard.hover() + await appCard.getByRole('button', { name: 'More' }).click() +}) + +When('I click {string} in the app options menu', async function (this: DifyWorld, label: string) { + await this.getPage().getByRole('menuitem', { name: label }).click() +}) + +When('I confirm the app duplication', async function (this: DifyWorld) { + await this.getPage().getByRole('button', { name: 'Duplicate' }).click() +}) diff --git a/e2e/features/step-definitions/apps/export-app.steps.ts b/e2e/features/step-definitions/apps/export-app.steps.ts new file mode 100644 index 0000000000..4ebeecb507 --- /dev/null +++ b/e2e/features/step-definitions/apps/export-app.steps.ts @@ -0,0 +1,19 @@ +import type { DifyWorld } from '../../support/world' +import { Then } from '@cucumber/cucumber' +import { expect } from '@playwright/test' + +Then('a YAML file named after the app should be downloaded', async function (this: DifyWorld) { + const appName = this.lastCreatedAppName + if (!appName) { + throw new Error( + 'No app name stored. Run "there is an existing E2E app available for testing" first.', + ) + } + + // The export triggers an async API call before the blob download fires. + // Poll until the download event is captured by the page listener in DifyWorld. + await expect.poll(() => this.capturedDownloads.length, { timeout: 10_000 }).toBeGreaterThan(0) + + const download = this.capturedDownloads.at(-1)! + expect(download.suggestedFilename()).toBe(`${appName}.yml`) +}) diff --git a/e2e/features/step-definitions/apps/switch-app-mode.steps.ts b/e2e/features/step-definitions/apps/switch-app-mode.steps.ts new file mode 100644 index 0000000000..55ad1ab02c --- /dev/null +++ b/e2e/features/step-definitions/apps/switch-app-mode.steps.ts @@ -0,0 +1,28 @@ +import type { DifyWorld } from '../../support/world' +import { Given, Then, When } from '@cucumber/cucumber' +import { expect } from '@playwright/test' +import { createTestApp } from '../../../support/api' + +Given( + 'there is an existing E2E completion app available for testing', + async function (this: DifyWorld) { + const name = `E2E Test App ${Date.now()}` + const app = await createTestApp(name, 'completion') + this.lastCreatedAppName = app.name + this.createdAppIds.push(app.id) + }, +) + +When('I confirm the app switch', async function (this: DifyWorld) { + await this.getPage().getByRole('button', { name: 'Start switch' }).click() +}) + +Then('I should land on the switched app', async function (this: DifyWorld) { + const page = this.getPage() + await expect(page).toHaveURL(/\/app\/[^/]+\/workflow(?:\?.*)?$/, { timeout: 15_000 }) + + // Capture the new app's ID so the After hook can clean it up + const match = page.url().match(/\/app\/([^/]+)\/workflow/) + if (match?.[1]) + this.createdAppIds.push(match[1]) +}) diff --git a/e2e/features/step-definitions/auth/sign-out.steps.ts b/e2e/features/step-definitions/auth/sign-out.steps.ts index 935b73c3af..0cc5f76ccc 100644 --- a/e2e/features/step-definitions/auth/sign-out.steps.ts +++ b/e2e/features/step-definitions/auth/sign-out.steps.ts @@ -1,6 +1,6 @@ +import type { DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import type { DifyWorld } from '../../support/world' When('I open the account menu', async function (this: DifyWorld) { const page = this.getPage() diff --git a/e2e/features/step-definitions/common/auth.steps.ts b/e2e/features/step-definitions/common/auth.steps.ts index bed35244c5..67c18dfe6c 100644 --- a/e2e/features/step-definitions/common/auth.steps.ts +++ b/e2e/features/step-definitions/common/auth.steps.ts @@ -1,5 +1,5 @@ -import { Given } from '@cucumber/cucumber' import type { DifyWorld } from '../../support/world' +import { Given } from '@cucumber/cucumber' Given('I am signed in as the default E2E admin', async function (this: DifyWorld) { const session = await this.getAuthSession() diff --git a/e2e/features/step-definitions/common/navigation.steps.ts b/e2e/features/step-definitions/common/navigation.steps.ts index 28e6953d65..9bec34c224 100644 --- a/e2e/features/step-definitions/common/navigation.steps.ts +++ b/e2e/features/step-definitions/common/navigation.steps.ts @@ -1,6 +1,6 @@ +import type { DifyWorld } from '../../support/world' import { Then, When } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import type { DifyWorld } from '../../support/world' When('I open the apps console', async function (this: DifyWorld) { await this.getPage().goto('/apps') diff --git a/e2e/features/step-definitions/smoke/install.steps.ts b/e2e/features/step-definitions/smoke/install.steps.ts index 857e01a971..3f2f8b5199 100644 --- a/e2e/features/step-definitions/smoke/install.steps.ts +++ b/e2e/features/step-definitions/smoke/install.steps.ts @@ -1,6 +1,6 @@ +import type { DifyWorld } from '../../support/world' import { Given } from '@cucumber/cucumber' import { expect } from '@playwright/test' -import type { DifyWorld } from '../../support/world' Given( 'the last authentication bootstrap came from a fresh install', diff --git a/e2e/features/support/hooks.ts b/e2e/features/support/hooks.ts index 7a8319463b..c1a535ee2c 100644 --- a/e2e/features/support/hooks.ts +++ b/e2e/features/support/hooks.ts @@ -1,11 +1,13 @@ -import { After, AfterAll, Before, BeforeAll, Status, setDefaultTimeout } from '@cucumber/cucumber' -import { chromium, type Browser } from '@playwright/test' +import type { Browser } from '@playwright/test' +import type { DifyWorld } from './world' import { mkdir, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { After, AfterAll, Before, BeforeAll, setDefaultTimeout, Status } from '@cucumber/cucumber' +import { chromium } from '@playwright/test' import { AUTH_BOOTSTRAP_TIMEOUT_MS, ensureAuthenticatedState } from '../../fixtures/auth' +import { deleteTestApp } from '../../support/api' import { baseURL, cucumberHeadless, cucumberSlowMo } from '../../test-env' -import type { DifyWorld } from './world' const e2eRoot = fileURLToPath(new URL('../..', import.meta.url)) const artifactsDir = path.join(e2eRoot, 'cucumber-report', 'artifacts') @@ -15,7 +17,7 @@ let browser: Browser | undefined setDefaultTimeout(60_000) const sanitizeForPath = (value: string) => - value.replaceAll(/[^a-zA-Z0-9_-]+/g, '-').replaceAll(/^-+|-+$/g, '') + value.replaceAll(/[^\w-]+/g, '-').replaceAll(/^-+|-+$/g, '') const writeArtifact = async ( scenarioName: string, @@ -44,16 +46,18 @@ BeforeAll({ timeout: AUTH_BOOTSTRAP_TIMEOUT_MS }, async () => { }) Before(async function (this: DifyWorld, { pickle }) { - if (!browser) throw new Error('Shared Playwright browser is not available.') + if (!browser) + throw new Error('Shared Playwright browser is not available.') - const isUnauthenticatedScenario = pickle.tags.some((tag) => tag.name === '@unauthenticated') + const isUnauthenticatedScenario = pickle.tags.some(tag => tag.name === '@unauthenticated') - if (isUnauthenticatedScenario) await this.startUnauthenticatedSession(browser) + if (isUnauthenticatedScenario) + await this.startUnauthenticatedSession(browser) else await this.startAuthenticatedSession(browser) this.scenarioStartedAt = Date.now() - const tags = pickle.tags.map((tag) => tag.name).join(' ') + const tags = pickle.tags.map(tag => tag.name).join(' ') console.log(`[e2e] start ${pickle.name}${tags ? ` ${tags}` : ''}`) }) @@ -85,6 +89,8 @@ After(async function (this: DifyWorld, { pickle, result }) { `[e2e] end ${pickle.name} status=${status}${elapsedMs ? ` durationMs=${elapsedMs}` : ''}`, ) + for (const id of this.createdAppIds) await deleteTestApp(id).catch(() => {}) + await this.closeSession() }) diff --git a/e2e/features/support/world.ts b/e2e/features/support/world.ts index bf63199107..986f79c8f9 100644 --- a/e2e/features/support/world.ts +++ b/e2e/features/support/world.ts @@ -1,10 +1,8 @@ -import { type IWorldOptions, World, setWorldConstructor } from '@cucumber/cucumber' -import type { Browser, BrowserContext, ConsoleMessage, Page } from '@playwright/test' -import { - authStatePath, - readAuthSessionMetadata, - type AuthSessionMetadata, -} from '../../fixtures/auth' +import type { IWorldOptions } from '@cucumber/cucumber' +import type { Browser, BrowserContext, ConsoleMessage, Download, Page } from '@playwright/test' +import type { AuthSessionMetadata } from '../../fixtures/auth' +import { setWorldConstructor, World } from '@cucumber/cucumber' +import { authStatePath, readAuthSessionMetadata } from '../../fixtures/auth' import { baseURL, defaultLocale } from '../../test-env' export class DifyWorld extends World { @@ -14,6 +12,9 @@ export class DifyWorld extends World { pageErrors: string[] = [] scenarioStartedAt: number | undefined session: AuthSessionMetadata | undefined + lastCreatedAppName: string | undefined + createdAppIds: string[] = [] + capturedDownloads: Download[] = [] constructor(options: IWorldOptions) { super(options) @@ -23,6 +24,9 @@ export class DifyWorld extends World { resetScenarioState() { this.consoleErrors = [] this.pageErrors = [] + this.lastCreatedAppName = undefined + this.createdAppIds = [] + this.capturedDownloads = [] } async startSession(browser: Browser, authenticated: boolean) { @@ -37,11 +41,15 @@ export class DifyWorld extends World { this.page.setDefaultTimeout(30_000) this.page.on('console', (message: ConsoleMessage) => { - if (message.type() === 'error') this.consoleErrors.push(message.text()) + if (message.type() === 'error') + this.consoleErrors.push(message.text()) }) this.page.on('pageerror', (error) => { this.pageErrors.push(error.message) }) + this.page.on('download', (dl) => { + this.capturedDownloads.push(dl) + }) } async startAuthenticatedSession(browser: Browser) { @@ -53,7 +61,8 @@ export class DifyWorld extends World { } getPage() { - if (!this.page) throw new Error('Playwright page has not been initialized for this scenario.') + if (!this.page) + throw new Error('Playwright page has not been initialized for this scenario.') return this.page } diff --git a/e2e/fixtures/auth.ts b/e2e/fixtures/auth.ts index 14aee52634..cc54a6d47b 100644 --- a/e2e/fixtures/auth.ts +++ b/e2e/fixtures/auth.ts @@ -1,8 +1,8 @@ import type { Browser, Page } from '@playwright/test' -import { expect } from '@playwright/test' import { mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { expect } from '@playwright/test' import { defaultBaseURL, defaultLocale } from '../test-env' export type AuthSessionMetadata = { @@ -60,7 +60,8 @@ const waitForPageState = async (page: Page, deadline: number): Promise 'init'), ]) - } catch { + } + catch { throw new Error(`Unable to determine auth page state for ${page.url()}`) } } @@ -73,7 +74,8 @@ const completeInitPasswordIfNeeded = async (page: Page, deadline: number) => { .then(() => true) .catch(() => false) - if (!needsInitPassword) return false + if (!needsInitPassword) + return false await initPasswordField.fill(initPassword) await page.getByRole('button', { name: 'Validate' }).click() @@ -143,7 +145,8 @@ export const ensureAuthenticatedState = async (browser: Browser, configuredBaseU pageState = await waitForPageState(page, deadline) } - if (pageState === 'install') await completeInstall(page, baseURL, deadline) + if (pageState === 'install') + await completeInstall(page, baseURL, deadline) else await completeLogin(page, baseURL, deadline) await expect(page.getByRole('button', { name: 'Create from Blank' })).toBeVisible({ @@ -160,7 +163,8 @@ export const ensureAuthenticatedState = async (browser: Browser, configuredBaseU } await writeFile(authMetadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8') - } finally { + } + finally { await context.close() } } diff --git a/e2e/package.json b/e2e/package.json index 925418f223..94fc857c0b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,7 +1,7 @@ { "name": "dify-e2e", - "private": true, "type": "module", + "private": true, "scripts": { "check": "vp check --fix", "e2e": "tsx ./scripts/run-cucumber.ts", @@ -11,10 +11,12 @@ "e2e:install": "playwright install --with-deps chromium", "e2e:middleware:down": "tsx ./scripts/setup.ts middleware-down", "e2e:middleware:up": "tsx ./scripts/setup.ts middleware-up", - "e2e:reset": "tsx ./scripts/setup.ts reset" + "e2e:reset": "tsx ./scripts/setup.ts reset", + "type-check": "tsc" }, "devDependencies": { "@cucumber/cucumber": "catalog:", + "@dify/tsconfig": "workspace:*", "@playwright/test": "catalog:", "@types/node": "catalog:", "tsx": "catalog:", diff --git a/e2e/scripts/common.ts b/e2e/scripts/common.ts index 5bca7fb4c9..ea6c897b2d 100644 --- a/e2e/scripts/common.ts +++ b/e2e/scripts/common.ts @@ -1,4 +1,5 @@ -import { spawn, type ChildProcess } from 'node:child_process' +import type { ChildProcess } from 'node:child_process' +import { spawn } from 'node:child_process' import { createHash } from 'node:crypto' import { access, copyFile, readFile, writeFile } from 'node:fs/promises' import net from 'node:net' @@ -48,7 +49,8 @@ const formatCommand = (command: string, args: string[]) => [command, ...args].jo export const isMainModule = (metaUrl: string) => { const entrypoint = process.argv[1] - if (!entrypoint) return false + if (!entrypoint) + return false return pathToFileURL(entrypoint).href === metaUrl } @@ -107,7 +109,8 @@ export const runCommandOrThrow = async (options: RunCommandOptions) => { const forwardSignalsToChild = (childProcess: ChildProcess) => { const handleSignal = (signal: NodeJS.Signals) => { - if (childProcess.exitCode === null) childProcess.kill(signal) + if (childProcess.exitCode === null) + childProcess.kill(signal) } const onSigint = () => handleSignal('SIGINT') @@ -152,7 +155,8 @@ export const runForegroundProcess = async ({ export const ensureFileExists = async (filePath: string, exampleFilePath: string) => { try { await access(filePath) - } catch { + } + catch { await copyFile(exampleFilePath, filePath) } } @@ -162,9 +166,10 @@ export const ensureLineInFile = async (filePath: string, line: string) => { const lines = fileContent.split(/\r?\n/) const assignmentPrefix = line.includes('=') ? `${line.slice(0, line.indexOf('='))}=` : null - if (lines.includes(line)) return + if (lines.includes(line)) + return - if (assignmentPrefix && lines.some((existingLine) => existingLine.startsWith(assignmentPrefix))) + if (assignmentPrefix && lines.some(existingLine => existingLine.startsWith(assignmentPrefix))) return const normalizedContent = fileContent.endsWith('\n') ? fileContent : `${fileContent}\n` @@ -187,16 +192,16 @@ export const readSimpleDotenv = async (filePath: string) => { const fileContent = await readFile(filePath, 'utf8') const entries = fileContent .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#')) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) .map<[string, string]>((line) => { const separatorIndex = line.indexOf('=') const key = separatorIndex === -1 ? line : line.slice(0, separatorIndex).trim() const rawValue = separatorIndex === -1 ? '' : line.slice(separatorIndex + 1).trim() if ( - (rawValue.startsWith('"') && rawValue.endsWith('"')) || - (rawValue.startsWith("'") && rawValue.endsWith("'")) + (rawValue.startsWith('"') && rawValue.endsWith('"')) + || (rawValue.startsWith('\'') && rawValue.endsWith('\'')) ) { return [key, rawValue.slice(1, -1)] } @@ -221,7 +226,8 @@ export const waitForCondition = async ({ const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { - if (await check()) return + if (await check()) + return await sleep(intervalMs) } diff --git a/e2e/scripts/run-cucumber.ts b/e2e/scripts/run-cucumber.ts index 39e9157916..d7778e65e2 100644 --- a/e2e/scripts/run-cucumber.ts +++ b/e2e/scripts/run-cucumber.ts @@ -1,7 +1,7 @@ import { mkdir, rm } from 'node:fs/promises' import path from 'node:path' +import { startLoggedProcess, stopManagedProcess, waitForUrl } from '../support/process' import { startWebServer, stopWebServer } from '../support/web-server' -import { waitForUrl, startLoggedProcess, stopManagedProcess } from '../support/process' import { apiURL, baseURL, reuseExistingWebServer } from '../test-env' import { e2eDir, isMainModule, runCommand } from './common' import { resetState, startMiddleware, stopMiddleware } from './setup' @@ -17,12 +17,10 @@ const parseArgs = (argv: string[]): RunOptions => { let headed = false const forwardArgs: string[] = [] - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index] - + for (const [index, arg] of argv.entries()) { if (arg === '--') { forwardArgs.push(...argv.slice(index + 1)) - break + return { forwardArgs, full, headed } } if (arg === '--full') { @@ -38,24 +36,22 @@ const parseArgs = (argv: string[]): RunOptions => { forwardArgs.push(arg) } - return { - forwardArgs, - full, - headed, - } + return { forwardArgs, full, headed } } const hasCustomTags = (forwardArgs: string[]) => - forwardArgs.some((arg) => arg === '--tags' || arg.startsWith('--tags=')) + forwardArgs.some(arg => arg === '--tags' || arg.startsWith('--tags=')) const main = async () => { const { forwardArgs, full, headed } = parseArgs(process.argv.slice(2)) const startMiddlewareForRun = full const resetStateForRun = full - if (resetStateForRun) await resetState() + if (resetStateForRun) + await resetState() - if (startMiddlewareForRun) await startMiddleware() + if (startMiddlewareForRun) + await startMiddleware() const cucumberReportDir = path.join(e2eDir, 'cucumber-report') const logDir = path.join(e2eDir, '.logs') @@ -81,7 +77,8 @@ const main = async () => { if (startMiddlewareForRun) { try { await stopMiddleware() - } catch { + } + catch { // Cleanup should continue even if middleware shutdown fails. } } @@ -103,7 +100,8 @@ const main = async () => { try { try { await waitForUrl(`${apiURL}/health`, 180_000, 1_000) - } catch { + } + catch { throw new Error(`API did not become ready at ${apiURL}/health.`) } @@ -139,7 +137,8 @@ const main = async () => { }) process.exitCode = result.exitCode - } finally { + } + finally { process.off('SIGINT', onTerminate) process.off('SIGTERM', onTerminate) await cleanup() diff --git a/e2e/scripts/setup.ts b/e2e/scripts/setup.ts index 4bd9de09d2..ba4c011b04 100644 --- a/e2e/scripts/setup.ts +++ b/e2e/scripts/setup.ts @@ -5,8 +5,8 @@ import { apiDir, apiEnvExampleFile, dockerDir, - e2eWebEnvOverrides, e2eDir, + e2eWebEnvOverrides, ensureFileExists, ensureLineInFile, getWebEnvLocalHash, @@ -79,7 +79,8 @@ const getContainerHealth = async (containerId: string) => { stdio: 'pipe', }) - if (result.exitCode !== 0) return '' + if (result.exitCode !== 0) + return '' return result.stdout.trim() } @@ -105,7 +106,8 @@ const waitForDependency = async ({ try { await wait() - } catch (error) { + } + catch (error) { await printComposeLogs(services) throw error } @@ -134,7 +136,7 @@ export const ensureWebBuild = async () => { .then(() => true) .catch(() => false), readFile(webBuildEnvStampPath, 'utf8') - .then((value) => value.trim()) + .then(value => value.trim()) .catch(() => ''), ]) @@ -142,7 +144,8 @@ export const ensureWebBuild = async () => { console.log('Reusing existing web build artifact.') return } - } catch { + } + catch { // Fall through to rebuild when the existing build cannot be verified. } @@ -211,7 +214,8 @@ export const resetState = async () => { console.log('Stopping middleware services...') try { await stopMiddleware() - } catch { + } + catch { // Reset should continue even if middleware is already stopped. } @@ -225,7 +229,7 @@ export const resetState = async () => { console.log('Removing E2E local state...') await Promise.all( - e2eStatePaths.map((targetPath) => rm(targetPath, { force: true, recursive: true })), + e2eStatePaths.map(targetPath => rm(targetPath, { force: true, recursive: true })), ) console.log('E2E state reset complete.') diff --git a/e2e/support/api.ts b/e2e/support/api.ts new file mode 100644 index 0000000000..c6d6c98bde --- /dev/null +++ b/e2e/support/api.ts @@ -0,0 +1,54 @@ +import { readFile } from 'node:fs/promises' +import { request } from '@playwright/test' +import { authStatePath } from '../fixtures/auth' +import { apiURL } from '../test-env' + +type StorageState = { + cookies: Array<{ name: string, value: string }> +} + +async function createApiContext() { + const state = JSON.parse(await readFile(authStatePath, 'utf8')) as StorageState + const csrfToken = state.cookies.find(c => c.name.endsWith('csrf_token'))?.value ?? '' + + return request.newContext({ + baseURL: apiURL, + extraHTTPHeaders: { 'X-CSRF-Token': csrfToken }, + storageState: authStatePath, + }) +} + +export type AppSeed = { + id: string + name: string +} + +export async function createTestApp(name: string, mode = 'workflow'): Promise { + const ctx = await createApiContext() + try { + const response = await ctx.post('/console/api/apps', { + data: { + name, + mode, + icon_type: 'emoji', + icon: '🤖', + icon_background: '#FFEAD5', + }, + }) + const body = (await response.json()) as AppSeed + return body + } + finally { + await ctx.dispose() + } +} + +export async function deleteTestApp(id: string): Promise { + const ctx = await createApiContext() + try { + await ctx.delete(`/console/api/apps/${id}`) + } + finally { + await ctx.dispose() + } +} diff --git a/e2e/support/process.ts b/e2e/support/process.ts index 96273ef931..4de1161b08 100644 --- a/e2e/support/process.ts +++ b/e2e/support/process.ts @@ -1,6 +1,7 @@ import type { ChildProcess } from 'node:child_process' +import type { WriteStream } from 'node:fs' import { spawn } from 'node:child_process' -import { createWriteStream, type WriteStream } from 'node:fs' +import { createWriteStream } from 'node:fs' import { mkdir } from 'node:fs/promises' import net from 'node:net' import { dirname } from 'node:path' @@ -63,11 +64,14 @@ export const waitForUrl = async ( const response = await fetch(url, { signal: controller.signal, }) - if (response.ok) return - } finally { + if (response.ok) + return + } + finally { clearTimeout(timeout) } - } catch { + } + catch { // Keep polling until timeout. } @@ -138,7 +142,8 @@ const waitForProcessExit = (childProcess: ChildProcess, timeoutMs: number) => const signalManagedProcess = (childProcess: ChildProcess, signal: NodeJS.Signals) => { const { pid } = childProcess - if (!pid) return + if (!pid) + return try { if (process.platform !== 'win32') { @@ -147,13 +152,15 @@ const signalManagedProcess = (childProcess: ChildProcess, signal: NodeJS.Signals } childProcess.kill(signal) - } catch { + } + catch { // Best-effort shutdown. Cleanup continues even when the process is already gone. } } export const stopManagedProcess = async (managedProcess?: ManagedProcess) => { - if (!managedProcess) return + if (!managedProcess) + return const { childProcess, logStream } = managedProcess diff --git a/e2e/support/web-server.ts b/e2e/support/web-server.ts index ad5d5d916a..819f7effe3 100644 --- a/e2e/support/web-server.ts +++ b/e2e/support/web-server.ts @@ -34,7 +34,8 @@ export const startWebServer = async ({ }: WebServerStartOptions) => { const { host, port } = getUrlHostAndPort(baseURL) - if (reuseExistingServer && (await isPortReachable(host, port))) return + if (reuseExistingServer && (await isPortReachable(host, port))) + return activeProcess = await startLoggedProcess({ command, @@ -49,7 +50,8 @@ export const startWebServer = async ({ startupError = error }) activeProcess.childProcess.once('exit', (code, signal) => { - if (startupError) return + if (startupError) + return startupError = new Error( `Web server exited before readiness (code: ${code ?? 'unknown'}, signal: ${signal ?? 'none'}).`, @@ -67,7 +69,8 @@ export const startWebServer = async ({ try { await waitForUrl(baseURL, 1_000, 250, 1_000) return - } catch { + } + catch { // Continue polling until timeout or child exit. } } diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 3976c12b66..3e72e790cf 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -1,16 +1,9 @@ { + "extends": "@dify/tsconfig/node.json", "compilerOptions": { - "target": "ES2023", "lib": ["ES2023", "DOM"], - "module": "ESNext", - "moduleResolution": "Bundler", - "allowJs": false, - "resolveJsonModule": true, - "noEmit": true, - "strict": true, - "skipLibCheck": true, "types": ["node", "@playwright/test", "@cucumber/cucumber"], - "isolatedModules": true, + "allowJs": false, "verbatimModuleSyntax": true }, "include": ["./**/*.ts"], diff --git a/e2e/vite.config.ts b/e2e/vite.config.ts index 98400d5b9b..f3dd7bbb0b 100644 --- a/e2e/vite.config.ts +++ b/e2e/vite.config.ts @@ -1,15 +1,3 @@ import { defineConfig } from 'vite-plus' -export default defineConfig({ - lint: { - options: { - typeAware: true, - typeCheck: true, - denyWarnings: true, - }, - }, - fmt: { - singleQuote: true, - semi: false, - }, -}) +export default defineConfig({}) diff --git a/eslint-suppressions.json b/eslint-suppressions.json new file mode 100644 index 0000000000..763d94af9a --- /dev/null +++ b/eslint-suppressions.json @@ -0,0 +1,6885 @@ +{ + "e2e/features/support/hooks.ts": { + "no-console": { + "count": 3 + }, + "node/prefer-global/buffer": { + "count": 1 + } + }, + "e2e/scripts/common.ts": { + "node/prefer-global/buffer": { + "count": 2 + } + }, + "e2e/support/process.ts": { + "ts/no-use-before-define": { + "count": 2 + } + }, + "packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts": { + "no-console": { + "count": 11 + } + }, + "packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts": { + "no-console": { + "count": 1 + } + }, + "packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts": { + "no-console": { + "count": 9 + } + }, + "web/.storybook/main.ts": { + "storybook/no-uninstalled-addons": { + "count": 3 + } + }, + "web/__mocks__/zustand.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/__tests__/document-detail-navigation-fix.test.tsx": { + "no-console": { + "count": 10 + } + }, + "web/__tests__/document-list-sorting.test.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/__tests__/embedded-user-id-auth.test.tsx": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/__tests__/embedded-user-id-store.test.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/__tests__/goto-anything/command-selector.test.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/__tests__/i18n-upload-features.test.ts": { + "no-console": { + "count": 3 + } + }, + "web/__tests__/navigation-utils.test.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/__tests__/plugin-tool-workflow-error.test.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/__tests__/real-browser-flicker.test.tsx": { + "no-console": { + "count": 16 + }, + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/__tests__/unified-tags-logic.test.ts": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/__tests__/workflow-onboarding-integration.test.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/__tests__/svg-attribute-error-reproduction.spec.tsx": { + "no-console": { + "count": 19 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/(humanInputLayout)/form/[token]/form.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/(shareLayout)/components/splash.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/(shareLayout)/webapp-reset-password/layout.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/account/(commonLayout)/account-page/email-change-modal.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/account/(commonLayout)/account-page/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/account/(commonLayout)/delete-account/components/feed-back.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/account/(commonLayout)/delete-account/components/verify-email.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/account/(commonLayout)/delete-account/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/account/oauth/authorize/layout.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/account/oauth/authorize/page.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app-sidebar/app-info/app-operations.tsx": { + "react/set-state-in-effect": { + "count": 4 + } + }, + "web/app/components/app-sidebar/basic.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app-sidebar/dataset-info/dropdown.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app-sidebar/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app-sidebar/toggle-button.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/app/annotation/batch-add-annotation-modal/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/annotation/header-opts/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/annotation/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/app/annotation/type.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/app/annotation/view-annotation-modal/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 5 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/app-access-control/specific-groups-or-members.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/app-publisher/features-wrapper.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/app/app-publisher/index.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/app/app-publisher/version-info-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/base/var-highlight/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/configuration/config-prompt/conversation-history/edit-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/app/configuration/config-var/__tests__/index.spec.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/configuration/config-var/config-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/app/configuration/config-var/config-modal/type-select.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-var/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-var/select-var-type.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-vision/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config-vision/param-config-content.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/agent/agent-setting/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/agent/agent-tools/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 9 + } + }, + "web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx": { + "react-hooks/exhaustive-deps": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/app/configuration/config/assistant-type-picker/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/automatic/get-automatic-res.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/automatic/instruction-editor.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/automatic/prompt-res.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/automatic/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/automatic/version-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/configuration/config/config-audio.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/config/config-document.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/context-var/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/params-config/config-content.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/params-config/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/select-dataset/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/dataset-config/settings-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/debug/__tests__/index.spec.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/debug/chat-user-input.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/app/configuration/debug/debug-with-single-model/index.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/app/configuration/debug/hooks.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/app/configuration/debug/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 11 + } + }, + "web/app/components/app/configuration/debug/types.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/configuration/prompt-value-panel/index.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/app/configuration/prompt-value-panel/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/create-app-dialog/app-list/sidebar.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/app/create-app-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/create-from-dsl-modal/dsl-confirm-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/create-from-dsl-modal/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/app/duplicate-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/log/filter.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/app/log/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/log/list.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 6 + }, + "style/multiline-ternary": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/app/log/model-info.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/overview/app-card.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/overview/customize/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/overview/embedded/index.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/overview/settings/index.tsx": { + "no-restricted-imports": { + "count": 3 + }, + "react/set-state-in-effect": { + "count": 3 + }, + "regexp/no-unused-capturing-group": { + "count": 1 + } + }, + "web/app/components/app/overview/trigger-card.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/app/switch-app-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/app/text-generate/item/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/app/text-generate/item/result-tab.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/app/workflow-log/detail.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/app/workflow-log/filter.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/app/workflow-log/list.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/app/workflow-log/trigger-by-display.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/apps/app-card.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/apps/list.tsx": { + "react-hooks/exhaustive-deps": { + "count": 1 + }, + "react/unsupported-syntax": { + "count": 2 + } + }, + "web/app/components/apps/new-app-card.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/action-button/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/base/agent-log-modal/detail.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/agent-log-modal/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/agent-log-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/agent-log-modal/result.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/agent-log-modal/tool-call.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/amplitude/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/amplitude/utils.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/app-icon-picker/ImageInput.tsx": { + "react/no-create-ref": { + "count": 1 + } + }, + "web/app/components/base/audio-btn/audio.ts": { + "node/prefer-global/buffer": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/audio-btn/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/audio-gallery/AudioPlayer.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/auto-height-textarea/index.stories.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/auto-height-textarea/index.tsx": { + "react-hooks/rules-of-hooks": { + "count": 1 + }, + "react/rules-of-hooks": { + "count": 1 + } + }, + "web/app/components/base/badge/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/base/block-input/index.stories.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/block-input/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + }, + "react/component-hook-factories": { + "count": 1 + }, + "react/no-nested-component-definitions": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/carousel/index.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 3 + } + }, + "web/app/components/base/chat/chat-with-history/chat-wrapper.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/base/chat/chat-with-history/context.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/base/chat/chat-with-history/header-in-mobile.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/chat/chat-with-history/header/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/chat/chat-with-history/hooks.tsx": { + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 18 + } + }, + "web/app/components/base/chat/chat-with-history/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/chat/chat-with-history/inputs-form/content.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/chat/chat-with-history/sidebar/operation.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/answer/agent-content.tsx": { + "style/multiline-ternary": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/answer/human-input-content/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/answer/index.tsx": { + "react/set-state-in-effect": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/answer/operation.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/base/chat/chat/answer/workflow-process.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/chat-input-area/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/chat/chat/check-input-forms-hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/citation/index.tsx": { + "react-hooks/exhaustive-deps": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/hooks.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 17 + } + }, + "web/app/components/base/chat/chat/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + }, + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/base/chat/chat/type.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/base/chat/chat/utils.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/base/chat/embedded-chatbot/context.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/base/chat/embedded-chatbot/header/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/chat/embedded-chatbot/hooks.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 3 + }, + "react/set-state-in-effect": { + "count": 6 + } + }, + "web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/chat/types.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/chat/utils.ts": { + "ts/no-explicit-any": { + "count": 10 + } + }, + "web/app/components/base/checkbox/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/chip/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/content-dialog/index.stories.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/copy-feedback/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/date-and-time-picker/date-picker/index.tsx": { + "react/set-state-in-effect": { + "count": 4 + } + }, + "web/app/components/base/date-and-time-picker/hooks.ts": { + "react/no-unnecessary-use-prefix": { + "count": 2 + } + }, + "web/app/components/base/date-and-time-picker/time-picker/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/base/date-and-time-picker/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/base/date-and-time-picker/utils/dayjs.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/dialog/index.stories.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/drawer-plus/index.stories.tsx": { + "react/component-hook-factories": { + "count": 1 + } + }, + "web/app/components/base/emoji-picker/Inner.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/emoji-picker/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/error-boundary/index.tsx": { + "react-refresh/only-export-components": { + "count": 3 + }, + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/features/context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/base/features/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/config-param.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/type.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/feature-bar.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/feature-card.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/base/features/new-feature-panel/file-upload/setting-modal.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/moderation/form-generation.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/moderation/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/features/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/file-uploader/dynamic-pdf-preview.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/file-uploader/file-list-in-log.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/no-missing-key": { + "count": 1 + } + }, + "web/app/components/base/file-uploader/hooks.ts": { + "react/no-unnecessary-use-prefix": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/file-uploader/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/base/file-uploader/pdf-highlighter-adapter.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/file-uploader/pdf-preview.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/file-uploader/store.tsx": { + "react-refresh/only-export-components": { + "count": 4 + } + }, + "web/app/components/base/file-uploader/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/base/file-uploader/utils.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/form/components/base/base-field.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/form/components/base/base-form.tsx": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/base/form/components/base/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/form/components/field/mixed-variable-text-input/placeholder.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/form/components/field/variable-or-constant-input.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/form/components/field/variable-selector.tsx": { + "no-console": { + "count": 1 + } + }, + "web/app/components/base/form/form-scenarios/base/field.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/form/form-scenarios/base/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/form/form-scenarios/demo/index.tsx": { + "no-console": { + "count": 2 + } + }, + "web/app/components/base/form/form-scenarios/input-field/field.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/form/form-scenarios/input-field/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/form/form-scenarios/node-panel/field.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/form/form-scenarios/node-panel/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/form/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/form/hooks/use-check-validated.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/form/hooks/use-get-validators.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/form/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 15 + } + }, + "web/app/components/base/form/utils/secret-input/index.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/ga/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/base/icons/src/public/avatar/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/public/billing/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/base/icons/src/public/common/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/public/education/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/public/files/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 11 + } + }, + "web/app/components/base/icons/src/public/knowledge/dataset-card/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/base/icons/src/public/knowledge/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/public/knowledge/online-drive/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/public/llm/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 9 + } + }, + "web/app/components/base/icons/src/public/other/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/public/thought/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/public/tracing/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 21 + } + }, + "web/app/components/base/icons/src/vender/features/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 10 + } + }, + "web/app/components/base/icons/src/vender/knowledge/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 15 + } + }, + "web/app/components/base/icons/src/vender/line/alertsAndFeedback/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/vender/line/arrows/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/vender/line/communication/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/line/development/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/vender/line/editor/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/line/education/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/line/files/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/vender/line/financeAndECommerce/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/base/icons/src/vender/line/general/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 12 + } + }, + "web/app/components/base/icons/src/vender/line/images/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/line/layout/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/line/mediaAndDevices/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/vender/line/others/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 7 + } + }, + "web/app/components/base/icons/src/vender/line/time/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/vender/other/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 8 + } + }, + "web/app/components/base/icons/src/vender/pipeline/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/plugin/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/solid/FinanceAndECommerce/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/solid/alertsAndFeedback/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/solid/arrows/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/solid/communication/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/base/icons/src/vender/solid/development/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/base/icons/src/vender/solid/editor/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/solid/education/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/solid/files/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/icons/src/vender/solid/general/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 11 + } + }, + "web/app/components/base/icons/src/vender/solid/mediaAndDevices/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/base/icons/src/vender/solid/security/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/solid/shapes/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/solid/users/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/system/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/icons/src/vender/workflow/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 30 + } + }, + "web/app/components/base/icons/utils.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx": { + "erasable-syntax-only/parameter-properties": { + "count": 1 + } + }, + "web/app/components/base/image-uploader/__tests__/utils.spec.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/image-uploader/hooks.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/image-uploader/image-link-input.tsx": { + "regexp/no-unused-capturing-group": { + "count": 1 + } + }, + "web/app/components/base/image-uploader/image-list.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/image-uploader/image-preview.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/image-uploader/utils.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/inline-delete-confirm/index.stories.tsx": { + "no-console": { + "count": 2 + } + }, + "web/app/components/base/input-with-copy/index.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/base/input/index.stories.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/input/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/base/logo/dify-logo.tsx": { + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/markdown-blocks/audio-block.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/base/markdown-blocks/button.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/markdown-blocks/code-block.tsx": { + "react/set-state-in-effect": { + "count": 7 + }, + "ts/no-explicit-any": { + "count": 9 + } + }, + "web/app/components/base/markdown-blocks/form.tsx": { + "erasable-syntax-only/enums": { + "count": 3 + } + }, + "web/app/components/base/markdown-blocks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 10 + } + }, + "web/app/components/base/markdown-blocks/link.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/markdown-blocks/paragraph.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/markdown-blocks/plugin-img.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/base/markdown-blocks/plugin-paragraph.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/base/markdown-blocks/think-block.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/markdown-blocks/video-block.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/base/markdown/error-boundary.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/markdown/markdown-utils.ts": { + "regexp/no-unused-capturing-group": { + "count": 1 + } + }, + "web/app/components/base/mermaid/index.tsx": { + "react/set-state-in-effect": { + "count": 7 + }, + "regexp/no-super-linear-backtracking": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/mermaid/utils.ts": { + "regexp/no-unused-capturing-group": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/message-log-modal/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/message-log-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/modal-like-wrap/index.stories.tsx": { + "no-console": { + "count": 3 + } + }, + "web/app/components/base/modal/index.stories.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/modal/modal.stories.tsx": { + "no-console": { + "count": 4 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/new-audio-button/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/node-status/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/base/notion-connector/index.stories.tsx": { + "no-console": { + "count": 1 + } + }, + "web/app/components/base/notion-page-selector/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/base/pagination/index.tsx": { + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/base/pagination/type.ts": { + "ts/no-empty-object-type": { + "count": 1 + } + }, + "web/app/components/base/param-item/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/portal-to-follow-elem/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/index.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/component-picker-block/menu.tsx": { + "erasable-syntax-only/parameter-properties": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/context-block/component.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/context-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/current-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/draggable-plugin/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/history-block/component.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/history-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx": { + "react-hooks/exhaustive-deps": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/hitl-input-block/hitl-input-block-replacement-block.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/hitl-input-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/query-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/request-url-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/update-block.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/variable-block/index.tsx": { + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/plugins/workflow-variable-block/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "react-refresh/only-export-components": { + "count": 3 + } + }, + "web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/prompt-editor/utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/prompt-log-modal/index.stories.tsx": { + "no-console": { + "count": 1 + } + }, + "web/app/components/base/prompt-log-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/qrcode/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/radio-card/index.stories.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/radio/component/group/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/radio/context/index.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/radio/index.stories.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/search-input/index.stories.tsx": { + "no-console": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/select/index.stories.tsx": { + "no-console": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/select/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "style/multiline-ternary": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/sort/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/svg-gallery/index.tsx": { + "node/prefer-global/buffer": { + "count": 1 + } + }, + "web/app/components/base/switch/index.stories.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/tab-slider/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/base/tag-input/index.stories.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/tag-management/__tests__/panel.spec.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/base/tag-management/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/tag-management/tag-item-editor.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/tag-management/tag-remove-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/base/text-generation/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/text-generation/types.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/base/textarea/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/textarea/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/base/video-gallery/VideoPlayer.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/base/voice-input/__tests__/index.spec.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/base/voice-input/index.stories.tsx": { + "no-console": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/voice-input/utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/base/with-input-validation/index.stories.tsx": { + "no-console": { + "count": 1 + }, + "react/component-hook-factories": { + "count": 1 + } + }, + "web/app/components/base/with-input-validation/index.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/base/zendesk/utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/billing/annotation-full/modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/billing/billing-page/__tests__/index.spec.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/billing/plan-upgrade-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/billing/plan/assets/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/billing/plan/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/billing/pricing/assets/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 12 + } + }, + "web/app/components/billing/pricing/plan-switcher/plan-range-switcher.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/billing/pricing/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/billing/priority-label/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/billing/type.ts": { + "erasable-syntax-only/enums": { + "count": 4 + } + }, + "web/app/components/billing/upgrade-btn/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/billing/usage-info/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/common/document-picker/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/common/document-picker/preview-document-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/common/image-previewer/index.tsx": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/app/components/datasets/common/image-uploader/__tests__/store.spec.tsx": { + "react/error-boundaries": { + "count": 1 + } + }, + "web/app/components/datasets/common/image-uploader/hooks/use-upload.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/image-input.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/common/image-uploader/store.tsx": { + "react-refresh/only-export-components": { + "count": 3 + } + }, + "web/app/components/datasets/common/retrieval-method-info/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/datasets/common/retrieval-param-config/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/list/template-card/details/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/list/template-card/details/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/create-from-pipeline/list/template-card/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/embedding-process/indexing-progress-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/empty-dataset-creation-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/file-preview/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/create/notion-page-preview/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/create/step-one/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/datasets/create/step-one/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/datasets/create/step-two/components/general-chunking-options.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/step-two/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/datasets/create/step-two/components/indexing-mode-section.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/datasets/create/step-two/components/inputs.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/step-two/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/datasets/create/step-two/hooks/use-indexing-config.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 3 + } + }, + "web/app/components/datasets/create/step-two/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + }, + "react-hooks/exhaustive-deps": { + "count": 1 + } + }, + "web/app/components/datasets/create/step-two/preview-item/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/datasets/create/stop-embedding-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/website/base/checkbox-with-label.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/website/base/field.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/create/website/firecrawl/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-console": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/datasets/create/website/firecrawl/options.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/create/website/jina-reader/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-console": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/datasets/create/website/jina-reader/options.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/create/website/watercrawl/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-console": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/datasets/create/website/watercrawl/options.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/documents/components/document-list/components/document-table-row.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/components/document-list/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/datasets/documents/components/document-list/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/datasets/documents/components/documents-header.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/components/operations.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/components/rename-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/__tests__/index.spec.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/base/credential-selector/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/header/breadcrumbs/bucket.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/file-list/list/item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.tsx": { + "react/set-state-in-effect": { + "count": 5 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/utils.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/store/provider.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/store/slices/online-drive.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/checkbox-with-label.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/process-documents/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/steps/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/datasets/documents/create-from-pipeline/types.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/batch-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/common/summary-status.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/datasets/documents/detail/completed/components/segment-list-content.tsx": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/display-toggle.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/datasets/documents/detail/completed/hooks/use-search-filter.ts": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "react/use-memo": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/segment-card/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/completed/status-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/context.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/embedding/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/datasets/documents/detail/embedding/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/metadata/components/doc-type-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/metadata/components/field-info.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/metadata/components/metadata-field-list.tsx": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/metadata/hooks/use-metadata-state.ts": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 4 + }, + "react/set-state-in-effect": { + "count": 4 + } + }, + "web/app/components/datasets/documents/detail/metadata/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/segment-add/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/datasets/documents/detail/settings/pipeline-settings/process-documents/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/datasets/documents/status-item/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/external-api/external-api-modal/index.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/extra-info/statistics.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/formatted-text/flavours/type.ts": { + "ts/no-empty-object-type": { + "count": 1 + } + }, + "web/app/components/datasets/hit-testing/components/chunk-detail-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/hit-testing/components/query-input/textarea.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/hit-testing/components/result-item-external.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/hit-testing/components/score.tsx": { + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/datasets/hit-testing/index.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/datasets/list/dataset-card/components/dataset-card-footer.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/edit-metadata-batch/edited-beacon.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/edit-metadata-batch/input-combined.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/datasets/metadata/edit-metadata-batch/modal.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/datasets/metadata/hooks/use-edit-dataset-metadata.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/hooks/use-metadata-document.ts": { + "ts/no-explicit-any": { + "count": 1 + }, + "ts/no-non-null-asserted-optional-chain": { + "count": 2 + } + }, + "web/app/components/datasets/metadata/metadata-dataset/create-content.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/metadata-dataset/create-metadata-modal.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/datasets/metadata/metadata-dataset/select-metadata-modal.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/metadata-document/info-group.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/metadata/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/datasets/rename-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/settings/chunk-structure/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/datasets/settings/index-method/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/settings/index-method/keyword-number.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/datasets/settings/permission-selector/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/no-missing-key": { + "count": 1 + } + }, + "web/app/components/datasets/settings/summary-index-setting.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/develop/code.tsx": { + "ts/no-empty-object-type": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 9 + } + }, + "web/app/components/develop/md.tsx": { + "ts/no-empty-object-type": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/develop/secret-key/input-copy.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/develop/secret-key/secret-key-generate.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/develop/secret-key/secret-key-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/explore/banner/banner-item.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 3 + } + }, + "web/app/components/explore/banner/indicator-button.tsx": { + "react-hooks-extra/no-direct-set-state-in-use-effect": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/explore/create-app-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + }, + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/explore/item-operation/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/explore/try-app/app/chat.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/explore/try-app/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/explore/try-app/tab.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/goto-anything/actions/commands/command-bus.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/goto-anything/actions/commands/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/goto-anything/actions/commands/registry.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/goto-anything/actions/commands/slash.tsx": { + "react-refresh/only-export-components": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/goto-anything/actions/commands/types.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/goto-anything/actions/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/goto-anything/actions/types.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/goto-anything/command-selector.tsx": { + "react/unsupported-syntax": { + "count": 2 + } + }, + "web/app/components/goto-anything/components/footer.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/goto-anything/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/goto-anything/context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 4 + } + }, + "web/app/components/goto-anything/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 4 + } + }, + "web/app/components/goto-anything/hooks/use-goto-anything-results.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + } + }, + "web/app/components/header/account-about/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-dropdown/compliance.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/header/account-setting/api-based-extension-page/modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/api-based-extension-page/selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/data-source-page-new/card.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/data-source-page-new/configure.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/data-source-page-new/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/header/account-setting/data-source-page-new/hooks/use-marketplace-all-plugins.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/account-setting/data-source-page-new/install-from-marketplace.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/account-setting/data-source-page-new/item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/account-setting/data-source-page-new/types.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/key-validator/declarations.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/account-setting/language-page/index.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/header/account-setting/members-page/invite-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 3 + } + }, + "web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/declarations.ts": { + "erasable-syntax-only/enums": { + "count": 11 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/header/account-setting/model-provider-page/hooks.ts": { + "react/no-unnecessary-use-prefix": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/add-credential-in-load-balancing.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/add-custom-model.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/config-provider.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 6 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-auth.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-custom-models.ts": { + "react/no-unnecessary-use-prefix": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/hooks/use-model-form-schemas.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 7 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx": { + "unicorn/prefer-number-properties": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-parameter-modal/configuration-button.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-parameter-modal/status-indicators.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/model-selector/feature-icon.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-configs.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/header/account-setting/model-provider-page/provider-added-card/priority-use-tip.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/header/account-setting/model-provider-page/utils.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/header/account-setting/plugin-page/utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/header/app-nav/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/header/header-wrapper.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/base/badges/icon-with-tooltip.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/base/key-value-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/card/index.tsx": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/hooks.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/plugins/install-plugin/hooks/use-fold-anim-into.ts": { + "react/no-unnecessary-use-prefix": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-bundle/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-bundle/item/github-item.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-bundle/steps/hooks/use-install-multi-state.ts": { + "react-hooks/exhaustive-deps": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-from-github/index.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-from-local-package/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/marketplace/hooks.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + } + }, + "web/app/components/plugins/marketplace/search-box/tags-filter.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-auth/authorize/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-auth/authorized-in-node.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/authorized/index.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-auth/authorized/item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/hooks/use-get-api.ts": { + "react/no-unnecessary-use-prefix": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 12 + }, + "react-refresh/only-export-components": { + "count": 3 + } + }, + "web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "no-barrel-files/no-barrel-files": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-auth/utils.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/agent-strategy-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/app-selector/app-inputs-form.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/plugins/plugin-detail-panel/app-selector/app-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/app-selector/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/detail-header.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/detail-header/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/detail-header/components/plugin-source-badge.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/detail-header/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/plugins/plugin-detail-panel/model-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/model-selector/index.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/plugins/plugin-detail-panel/model-selector/tts-params-panel.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/strategy-detail.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/index.spec.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + }, + "no-restricted-imports": { + "count": 3 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/create/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/list-view.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/log-viewer.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/components/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 7 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/components/schema-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-detail-panel/trigger/event-detail-drawer.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/plugins/plugin-item/action.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-item/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-mutation-model/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/context.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/debug-info.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/empty/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-page/filter-management/category-filter.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/filter-management/tag-filter.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/plugins/plugin-page/plugin-info.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/readme-panel/index.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/plugins/readme-panel/store.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/plugins/reference-setting-modal/auto-update-setting/strategy-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/plugins/reference-setting-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/plugins/types.ts": { + "erasable-syntax-only/enums": { + "count": 7 + }, + "ts/no-explicit-any": { + "count": 25 + } + }, + "web/app/components/plugins/update-plugin/from-market-place.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/editor/form/hidden-fields.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/editor/form/hooks.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/editor/form/initial-fields.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/editor/form/show-all-settings.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/editor/form/types.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/hooks.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/input-field/label-right-content/global-inputs.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/result/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/rag-pipeline-children.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/rag-pipeline-main.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/components/update-dsl-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/components/version-mismatch-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 9 + } + }, + "web/app/components/rag-pipeline/hooks/use-DSL.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/hooks/use-input-fields.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/hooks/use-pipeline-config.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/rag-pipeline/hooks/use-pipeline-init.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/hooks/use-pipeline-run.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/store/index.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/rag-pipeline/utils/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/rag-pipeline/utils/nodes.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/share/text-generation/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/share/text-generation/info-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/share/text-generation/menu-dropdown.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/share/text-generation/no-data/index.tsx": { + "ts/no-empty-object-type": { + "count": 1 + } + }, + "web/app/components/share/text-generation/result/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/share/text-generation/run-batch/csv-reader/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/share/text-generation/run-once/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/share/text-generation/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/share/utils.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/edit-custom-collection-modal/get-schema.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/edit-custom-collection-modal/index.tsx": { + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/tools/edit-custom-collection-modal/test-api.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/labels/selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/mcp/create-card.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/mcp/detail/content.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/tools/mcp/detail/tool-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/mcp/mcp-server-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/tools/mcp/mcp-server-param-item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/mcp/mcp-service-card.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/mcp/modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/mcp/provider-card.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/tools/provider-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/provider/empty.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/tools/setting/build-in/config-credentials.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/tools/types.ts": { + "erasable-syntax-only/enums": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/tools/workflow-tool/confirm-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/workflow-tool/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/tools/workflow-tool/method-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow-app/components/workflow-children.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow-app/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 13 + } + }, + "web/app/components/workflow-app/hooks/use-DSL.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow-app/hooks/use-workflow-init.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow-app/hooks/use-workflow-run.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow-app/hooks/use-workflow-template.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow-app/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow-app/store/workflow/workflow-slice.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/block-selector/all-start-blocks.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/blocks.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/featured-tools.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/block-selector/featured-triggers.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/block-selector/hooks.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/index-bar.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/main.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/market-place-plugin/action.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/market-place-plugin/item.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/start-blocks.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/tabs.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/tool-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/tool/action-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/tool/tool.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/trigger-plugin/item.tsx": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/types.ts": { + "erasable-syntax-only/enums": { + "count": 4 + } + }, + "web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/use-sticky-scroll.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/block-selector/view-type-select.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/candidate-node-main.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/datasets-detail-store/provider.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/dsl-export-confirm-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/header/run-mode.tsx": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/header/test-run-menu.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/header/version-history-button.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/header/view-history.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/workflow/header/view-workflow-history.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks-store/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/hooks-store/provider.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/hooks-store/store.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts": { + "react/error-boundaries": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 27 + } + }, + "web/app/components/workflow/hooks/use-checklist.ts": { + "ts/no-empty-object-type": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/hooks/use-dynamic-test-run-options.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/hooks/use-helpline.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/use-nodes-interactions.ts": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/hooks/use-serial-async-callback.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/use-workflow-interactions.ts": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/workflow/hooks/use-workflow-run-event/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 19 + } + }, + "web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-agent-log.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-finished.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/hooks/use-workflow-search.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/hooks/use-workflow-variables.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/add-variable-popup-with-position.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx": { + "no-restricted-imports": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/_base/components/agent-strategy.tsx": { + "ts/no-empty-object-type": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/_base/components/before-run-form/form-item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 11 + } + }, + "web/app/components/workflow/nodes/_base/components/before-run-form/form.tsx": { + "ts/no-explicit-any": { + "count": 3 + }, + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/before-run-form/index.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/_base/components/collapse/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/config-vision.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx": { + "react-refresh/only-export-components": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/_base/components/entry-node-container.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/error-handle/default-value.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/error-handle/error-handle-on-panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/error-handle/error-handle-type-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/error-handle/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/field.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/form-input-item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/help-link.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/layout/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 7 + }, + "react-refresh/only-export-components": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/_base/components/mcp-tool-availability.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/memory-config.tsx": { + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/node-control.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/node-handle.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/option-card.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/prompt/editor.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/_base/components/readonly-input-with-select-var.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/selector.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/_base/components/setting-item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/switch-plugin-version.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/constant-field.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker/field.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/utils.ts": { + "ts/no-explicit-any": { + "count": 32 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/var-list.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts": { + "react/no-unnecessary-use-prefix": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/_base/components/variable/variable-label/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": { + "react/set-state-in-effect": { + "count": 7 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts": { + "react/no-unnecessary-use-prefix": { + "count": 2 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/_base/components/workflow-panel/tab.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 22 + } + }, + "web/app/components/workflow/nodes/_base/hooks/use-output-var-list.ts": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/nodes/_base/hooks/use-toggle-expend.ts": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/hooks/use-var-list.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/_base/node.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/_base/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/agent/components/model-bar.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-empty-object-type": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/agent/components/tool-icon.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/agent/default.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/agent/node.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/agent/panel.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/agent/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/agent/use-config.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/agent/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/answer/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/assigner/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/assigner/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/assigner/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/assigner/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/assigner/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/code/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/code/dependency-picker.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/code/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/code/use-config.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "regexp/no-useless-assertions": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/code/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/components.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/data-source-empty/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/data-source/default.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/data-source/hooks/use-before-run-form.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/data-source/hooks/use-config.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/data-source/panel.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/data-source/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-barrel-files/no-barrel-files": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/data-source/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/document-extractor/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/document-extractor/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/end/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/end/node.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/components/authorization/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/components/curl-panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/http/components/key-value/key-value-edit/item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/types.ts": { + "erasable-syntax-only/enums": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/http/use-config.ts": { + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/http/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/human-input/components/button-style-dropdown.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/email-configure-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/method-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + }, + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/delivery-method/upgrade-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/form-content-preview.tsx": { + "react/unsupported-syntax": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/components/form-content.tsx": { + "react/component-hook-factories": { + "count": 1 + }, + "react/no-nested-component-definitions": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/human-input/components/variable-in-markdown.tsx": { + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/human-input/panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/human-input/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-add.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/if-else/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/if-else/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/iteration-start/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/iteration/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/iteration/node.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/iteration/panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/iteration/use-config.ts": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/iteration/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/chunk-structure/selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/index-method.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/hooks.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/search-method-option.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/top-k-and-score-threshold.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/type.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/knowledge-base/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-barrel-files/no-barrel-files": { + "count": 8 + } + }, + "web/app/components/workflow/nodes/knowledge-base/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/knowledge-base/utils.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/metadata-filter-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-trigger.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/node.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/types.ts": { + "erasable-syntax-only/enums": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/list-operator/components/sub-variable-picker.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/list-operator/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/list-operator/types.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/llm/components/config-prompt.tsx": { + "react/unsupported-syntax": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-importer.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-config.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/assets/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/generated-result.tsx": { + "style/multiline-ternary": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/index.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/json-schema-generator/prompt-editor.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx": { + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/actions.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/auto-width-input.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/type-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/components/structure-output.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/llm/use-config.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/llm/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 9 + } + }, + "web/app/components/workflow/nodes/llm/utils.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/loop-start/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-add.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-list/condition-input.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-number-input.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/condition-wrap.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/loop/components/loop-variables/input-mode-selec.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/loop/components/loop-variables/variable-type-select.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/use-config.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/loop/use-single-run-form-params.helpers.ts": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/loop/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/list.spec.tsx": { + "unused-imports/no-unused-vars": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/panel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/types.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/use-config.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 9 + } + }, + "web/app/components/workflow/nodes/question-classifier/components/advanced-setting.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/question-classifier/components/class-item.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/question-classifier/components/class-list.tsx": { + "react/set-state-in-effect": { + "count": 1 + }, + "react/unsupported-syntax": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/question-classifier/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/question-classifier/node.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/question-classifier/use-config.ts": { + "react/set-state-in-effect": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/nodes/start/panel.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/start/use-config.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/start/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/template-transform/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/template-transform/use-config.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/template-transform/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/nodes/tool/components/copy-id.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/tool/components/input-var-list.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/tool/components/tool-form/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/tool/components/tool-form/item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/tool/default.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/tool/hooks/use-config.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/tool/hooks/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/nodes/tool/output-schema-utils.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/tool/panel.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/tool/types.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/index.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/components/trigger-form/item.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/default.ts": { + "ts/no-explicit-any": { + "count": 11 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/node.tsx": { + "react/unsupported-syntax": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/panel.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/types.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/use-config.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-plugin/utils/form-helpers.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/nodes/trigger-schedule/components/frequency-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-schedule/default.ts": { + "regexp/no-unused-capturing-group": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 10 + } + }, + "web/app/components/workflow/nodes/trigger-webhook/components/generic-table.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-webhook/components/parameter-table.tsx": { + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-webhook/default.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/trigger-webhook/panel.tsx": { + "no-restricted-imports": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/utils.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/nodes/variable-assigner/default.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/nodes/variable-assigner/use-single-run-form-params.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/note-node/note-editor/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 3 + } + }, + "web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/components/workflow/note-node/note-editor/toolbar/command.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/note-node/note-editor/utils.ts": { + "regexp/no-useless-quantifier": { + "count": 1 + } + }, + "web/app/components/workflow/operator/add-block.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/operator/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/operator/tip-popup.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/operator/zoom-in-out.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/panel/chat-record/index.tsx": { + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/panel/chat-record/user-input.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/components/array-bool-list.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/components/object-value-item.tsx": { + "react-refresh/only-export-components": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 5 + }, + "unicorn/prefer-number-properties": { + "count": 2 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/components/object-value-list.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/components/variable-type-select.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/panel/chat-variable-panel/type.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/panel/debug-and-preview/hooks.ts": { + "ts/no-explicit-any": { + "count": 12 + } + }, + "web/app/components/workflow/panel/debug-and-preview/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/panel/env-panel/variable-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/panel/human-input-form-list.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/panel/inputs-panel.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/panel/version-history-panel/delete-confirm-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/panel/version-history-panel/filter/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/panel/version-history-panel/restore-confirm-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/panel/workflow-preview.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/run/agent-log/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/run/hooks.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/run/index.tsx": { + "react/set-state-in-effect": { + "count": 2 + } + }, + "web/app/components/workflow/run/iteration-log/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/run/iteration-log/iteration-log-trigger.tsx": { + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/workflow/run/loop-log/__tests__/loop-log-trigger.spec.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/run/loop-log/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/run/loop-log/loop-log-trigger.tsx": { + "unicorn/prefer-number-properties": { + "count": 1 + } + }, + "web/app/components/workflow/run/node.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/components/workflow/run/output-panel.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/run/result-panel.tsx": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/app/components/workflow/run/result-text.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/run/retry-log/index.tsx": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/run/utils/format-log/agent/index.ts": { + "ts/no-explicit-any": { + "count": 11 + } + }, + "web/app/components/workflow/run/utils/format-log/graph-to-log-struct.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/components/workflow/run/utils/format-log/index.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/run/utils/format-log/iteration/index.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/run/utils/format-log/loop/index.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/run/utils/format-log/parallel/index.ts": { + "no-console": { + "count": 4 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/store/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/app/components/workflow/store/workflow/debug/inspect-vars-slice.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/store/workflow/workflow-draft-slice.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/store/workflow/workflow-slice.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/types.ts": { + "erasable-syntax-only/enums": { + "count": 16 + }, + "ts/no-empty-object-type": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/app/components/workflow/update-dsl-modal.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/utils/__tests__/node-navigation.spec.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/utils/data-source.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/utils/debug.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/utils/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 9 + } + }, + "web/app/components/workflow/utils/node-navigation.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/utils/node.ts": { + "regexp/no-super-linear-backtracking": { + "count": 1 + } + }, + "web/app/components/workflow/utils/tool.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/utils/workflow-init.ts": { + "ts/no-explicit-any": { + "count": 12 + } + }, + "web/app/components/workflow/utils/workflow.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/variable-inspect/display-content.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/variable-inspect/group.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/variable-inspect/left.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/variable-inspect/listening.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/variable-inspect/panel.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/variable-inspect/right.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/app/components/workflow/variable-inspect/trigger.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/variable-inspect/utils.tsx": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/app/components/workflow/variable-inspect/value-content.tsx": { + "react/set-state-in-effect": { + "count": 5 + }, + "regexp/no-super-linear-backtracking": { + "count": 1 + }, + "regexp/no-unused-capturing-group": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/components/workflow/workflow-history-store.tsx": { + "react-refresh/only-export-components": { + "count": 2 + } + }, + "web/app/components/workflow/workflow-preview/components/nodes/base.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/workflow-preview/components/nodes/constants.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/components/workflow/workflow-preview/components/nodes/iteration-start/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/workflow-preview/components/nodes/loop-start/index.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/components/workflow/workflow-preview/components/zoom-in-out.tsx": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/app/education-apply/expire-notice-modal.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/education-apply/hooks.ts": { + "react/set-state-in-effect": { + "count": 5 + } + }, + "web/app/education-apply/search-input.tsx": { + "no-restricted-imports": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/education-apply/verify-state-modal.tsx": { + "react/set-state-in-effect": { + "count": 1 + } + }, + "web/app/forgot-password/ForgotPasswordForm.spec.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/app/init/InitPasswordPopup.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/install/installForm.spec.tsx": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/app/layout.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/app/reset-password/layout.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/signin/_header.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/signin/components/mail-and-password-auth.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/signin/invite-settings/page.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "web/app/signin/layout.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/signin/one-more-step.tsx": { + "no-restricted-imports": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/app/signup/layout.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/context/external-api-panel-context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/context/external-knowledge-api-context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/context/global-public-context.tsx": { + "react-refresh/only-export-components": { + "count": 3 + } + }, + "web/context/hooks/use-trigger-events-limit-modal.ts": { + "react/set-state-in-effect": { + "count": 3 + } + }, + "web/context/modal-context-provider.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/context/modal-context.test.tsx": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/context/modal-context.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/context/provider-context-provider.tsx": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/context/web-app-context.tsx": { + "react-refresh/only-export-components": { + "count": 1 + } + }, + "web/hooks/use-async-window-open.spec.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/hooks/use-format-time-from-now.spec.ts": { + "regexp/no-dupe-disjunctions": { + "count": 5 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/hooks/use-metadata.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/hooks/use-mitt.ts": { + "react/component-hook-factories": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/hooks/use-oauth.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/hooks/use-pay.tsx": { + "react/set-state-in-effect": { + "count": 4 + } + }, + "web/i18n-config/index.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/i18n-config/lib.client.ts": { + "no-barrel-files/no-barrel-files": { + "count": 1 + } + }, + "web/i18n/de-DE/billing.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/en-US/app-debug.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/fr-FR/app-debug.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/fr-FR/app.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/fr-FR/plugin-trigger.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/fr-FR/tools.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/fr-FR/workflow.json": { + "no-irregular-whitespace": { + "count": 2 + } + }, + "web/i18n/pt-BR/common.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/i18n/ru-RU/common.json": { + "no-irregular-whitespace": { + "count": 2 + } + }, + "web/i18n/uk-UA/app-debug.json": { + "no-irregular-whitespace": { + "count": 1 + } + }, + "web/models/access-control.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/models/app.ts": { + "erasable-syntax-only/enums": { + "count": 2 + } + }, + "web/models/common.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/models/datasets.ts": { + "erasable-syntax-only/enums": { + "count": 8 + }, + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/models/debug.ts": { + "erasable-syntax-only/enums": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/models/log.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/models/pipeline.ts": { + "erasable-syntax-only/enums": { + "count": 3 + }, + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/models/share.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/plugins/dev-proxy/server.spec.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/scripts/component-analyzer.js": { + "regexp/no-unused-capturing-group": { + "count": 6 + } + }, + "web/service/access-control.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + } + }, + "web/service/annotation.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/service/apps.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/service/base.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/service/client.spec.ts": { + "next/no-assign-module-variable": { + "count": 1 + } + }, + "web/service/common.ts": { + "ts/no-explicit-any": { + "count": 29 + } + }, + "web/service/datasets.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/service/debug.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/service/fetch.ts": { + "regexp/no-unused-capturing-group": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/service/knowledge/use-dataset.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + } + }, + "web/service/share.ts": { + "erasable-syntax-only/enums": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/service/try-app.ts": { + "no-barrel-files/no-barrel-files": { + "count": 2 + } + }, + "web/service/use-apps.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/service/use-common.ts": { + "ts/no-empty-object-type": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/service/use-endpoints.ts": { + "ts/no-explicit-any": { + "count": 7 + } + }, + "web/service/use-flow.ts": { + "react/no-unnecessary-use-prefix": { + "count": 1 + } + }, + "web/service/use-pipeline.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + } + }, + "web/service/use-plugins-auth.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/service/use-plugins.ts": { + "react/set-state-in-effect": { + "count": 1 + }, + "regexp/no-unused-capturing-group": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + }, + "ts/no-non-null-asserted-optional-chain": { + "count": 1 + } + }, + "web/service/use-tools.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/service/use-workflow.ts": { + "@tanstack/query/exhaustive-deps": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/service/utils.spec.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/types/app.ts": { + "erasable-syntax-only/enums": { + "count": 9 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/types/assets.d.ts": { + "ts/no-explicit-any": { + "count": 5 + } + }, + "web/types/common.ts": { + "erasable-syntax-only/enums": { + "count": 1 + } + }, + "web/types/feature.ts": { + "erasable-syntax-only/enums": { + "count": 3 + } + }, + "web/types/lamejs.d.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/types/pipeline.tsx": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/types/react-18-input-autosize.d.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/types/workflow.ts": { + "ts/no-explicit-any": { + "count": 17 + } + }, + "web/utils/clipboard.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/utils/completion-params.spec.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/utils/completion-params.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/utils/context.ts": { + "react/component-hook-factories": { + "count": 1 + } + }, + "web/utils/error-parser.ts": { + "no-console": { + "count": 1 + }, + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/utils/get-icon.spec.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/utils/gtag.ts": { + "ts/no-explicit-any": { + "count": 2 + } + }, + "web/utils/index.spec.ts": { + "test/no-identical-title": { + "count": 2 + }, + "ts/no-explicit-any": { + "count": 8 + } + }, + "web/utils/index.ts": { + "ts/no-explicit-any": { + "count": 3 + } + }, + "web/utils/mcp.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/utils/model-config.spec.ts": { + "ts/no-explicit-any": { + "count": 13 + } + }, + "web/utils/model-config.ts": { + "ts/no-explicit-any": { + "count": 6 + } + }, + "web/utils/navigation.spec.ts": { + "ts/no-explicit-any": { + "count": 4 + } + }, + "web/utils/tool-call.spec.ts": { + "ts/no-explicit-any": { + "count": 1 + } + }, + "web/utils/validators.ts": { + "ts/no-explicit-any": { + "count": 2 + } + } +} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..5e81e95f2f --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,65 @@ +// @ts-check + +import antfu, { GLOB_MARKDOWN } from '@antfu/eslint-config' +import md from 'eslint-markdown' +import markdownPreferences from 'eslint-plugin-markdown-preferences' + +export default antfu( + { + ignores: original => [ + '**', + '!packages/**', + '!web/**', + '!e2e/**', + '!eslint.config.mjs', + '!package.json', + '!vite.config.ts', + ...original, + ], + typescript: { + overrides: { + 'ts/consistent-type-definitions': ['error', 'type'], + 'ts/no-explicit-any': 'error', + 'ts/no-redeclare': 'off', + }, + erasableOnly: true, + }, + test: { + overrides: { + 'test/prefer-lowercase-title': 'off', + }, + }, + stylistic: { + overrides: { + 'antfu/top-level-function': 'off', + }, + }, + e18e: false, + pnpm: false, + }, + markdownPreferences.configs.standard, + { + files: [GLOB_MARKDOWN], + plugins: { md }, + rules: { + 'md/no-url-trailing-slash': 'error', + 'markdown-preferences/prefer-link-reference-definitions': [ + 'error', + { + minLinks: 1, + }, + ], + 'markdown-preferences/ordered-list-marker-sequence': [ + 'error', + { increment: 'never' }, + ], + 'markdown-preferences/definitions-last': 'error', + 'markdown-preferences/sort-definitions': 'error', + }, + }, + { + rules: { + 'node/prefer-global/process': 'off', + }, + }, +) diff --git a/package.json b/package.json index 736a354ef7..5a67b66a9c 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,26 @@ { "name": "dify", + "type": "module", "private": true, - "scripts": { - "prepare": "vp config" - }, - "devDependencies": { - "vite": "catalog:", - "vite-plus": "catalog:" - }, + "packageManager": "pnpm@10.33.0", "engines": { "node": "^22.22.1" }, - "packageManager": "pnpm@10.33.0" + "scripts": { + "prepare": "vp config", + "type-check": "vp run -r type-check", + "lint": "eslint --cache --concurrency=auto", + "lint:ci": "eslint --cache --cache-strategy content --concurrency 2", + "lint:fix": "vp run lint --fix", + "lint:quiet": "vp run lint --quiet" + }, + "devDependencies": { + "@antfu/eslint-config": "catalog:", + "eslint": "catalog:", + "eslint-markdown": "catalog:", + "eslint-plugin-markdown-preferences": "catalog:", + "eslint-plugin-no-barrel-files": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" + } } diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md new file mode 100644 index 0000000000..ecc968e130 --- /dev/null +++ b/packages/dify-ui/AGENTS.md @@ -0,0 +1,27 @@ +# @langgenius/dify-ui + +This package provides shared design tokens (colors, shadows, typography), the `cn()` utility, and a Tailwind CSS preset consumed by `web/`. + +## Border Radius: Figma Token → Tailwind Class Mapping + +The Figma design system uses `--radius/*` tokens whose scale is **offset by one step** from Tailwind CSS v4 defaults. When translating Figma specs to code, always use this mapping — never use `radius-*` as a CSS class, and never extend `borderRadius` in the preset. + +| Figma Token | Value | Tailwind Class | +| --------------- | ----- | ---------------- | +| `--radius/2xs` | 2px | `rounded-xs` | +| `--radius/xs` | 4px | `rounded-sm` | +| `--radius/sm` | 6px | `rounded-md` | +| `--radius/md` | 8px | `rounded-lg` | +| `--radius/lg` | 10px | `rounded-[10px]` | +| `--radius/xl` | 12px | `rounded-xl` | +| `--radius/2xl` | 16px | `rounded-2xl` | +| `--radius/3xl` | 20px | `rounded-[20px]` | +| `--radius/6xl` | 28px | `rounded-[28px]` | +| `--radius/full` | 999px | `rounded-full` | + +### Rules + +- **Do not** add custom `borderRadius` values to `tailwind-preset.ts`. We use Tailwind v4 defaults and arbitrary values (`rounded-[Npx]`) for sizes without a standard equivalent. +- **Do not** use `radius-*` as CSS class names. The old `@utility radius-*` definitions have been removed. +- When the Figma MCP returns `rounded-[var(--radius/sm, 6px)]`, convert it to the standard Tailwind class from the table above (e.g. `rounded-md`). +- For values without a standard Tailwind equivalent (10px, 20px, 28px), use arbitrary values like `rounded-[10px]`. diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json new file mode 100644 index 0000000000..b54fde9b89 --- /dev/null +++ b/packages/dify-ui/package.json @@ -0,0 +1,29 @@ +{ + "name": "@langgenius/dify-ui", + "type": "module", + "version": "0.0.1", + "private": true, + "exports": { + "./styles.css": "./src/styles/styles.css", + "./tailwind-preset": { + "types": "./src/tailwind-preset.ts", + "import": "./src/tailwind-preset.ts" + }, + "./cn": { + "types": "./src/cn.ts", + "import": "./src/cn.ts" + } + }, + "scripts": { + "type-check": "tsc" + }, + "dependencies": { + "clsx": "catalog:", + "tailwind-merge": "catalog:" + }, + "devDependencies": { + "@dify/tsconfig": "workspace:*", + "tailwindcss": "catalog:", + "typescript": "catalog:" + } +} diff --git a/web/utils/classnames.ts b/packages/dify-ui/src/cn.ts similarity index 100% rename from web/utils/classnames.ts rename to packages/dify-ui/src/cn.ts diff --git a/packages/dify-ui/src/styles/components.css b/packages/dify-ui/src/styles/components.css new file mode 100644 index 0000000000..ab02be97fb --- /dev/null +++ b/packages/dify-ui/src/styles/components.css @@ -0,0 +1,69 @@ +[data-dify-scrollbar]::before, +[data-dify-scrollbar]::after { + content: ''; + position: absolute; + z-index: 1; + border-radius: 9999px; + pointer-events: none; + opacity: 0; + transition: opacity 150ms ease; +} + +[data-dify-scrollbar][data-orientation='vertical']::before { + left: 50%; + top: 4px; + width: 4px; + height: 12px; + transform: translateX(-50%); + background: linear-gradient(to bottom, var(--color-components-panel-bg), transparent); +} + +[data-dify-scrollbar][data-orientation='vertical']::after { + left: 50%; + bottom: 4px; + width: 4px; + height: 12px; + transform: translateX(-50%); + background: linear-gradient(to top, var(--color-components-panel-bg), transparent); +} + +[data-dify-scrollbar][data-orientation='horizontal']::before { + top: 50%; + left: 4px; + width: 12px; + height: 4px; + transform: translateY(-50%); + background: linear-gradient(to right, var(--color-components-panel-bg), transparent); +} + +[data-dify-scrollbar][data-orientation='horizontal']::after { + top: 50%; + right: 4px; + width: 12px; + height: 4px; + transform: translateY(-50%); + background: linear-gradient(to left, var(--color-components-panel-bg), transparent); +} + +[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-start])::before { + opacity: 1; +} + +[data-dify-scrollbar][data-orientation='vertical']:not([data-overflow-y-end])::after { + opacity: 1; +} + +[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-start])::before { + opacity: 1; +} + +[data-dify-scrollbar][data-orientation='horizontal']:not([data-overflow-x-end])::after { + opacity: 1; +} + +@media (prefers-reduced-motion: reduce) { + [data-dify-scrollbar]::before, + [data-dify-scrollbar]::after { + transition: none; + } +} diff --git a/packages/dify-ui/src/styles/styles.css b/packages/dify-ui/src/styles/styles.css new file mode 100644 index 0000000000..fb410b2d5f --- /dev/null +++ b/packages/dify-ui/src/styles/styles.css @@ -0,0 +1,4 @@ +@import '../themes/light.css' layer(base); +@import '../themes/dark.css' layer(base); +@import './utilities.css'; +@import './components.css'; diff --git a/packages/dify-ui/src/styles/utilities.css b/packages/dify-ui/src/styles/utilities.css new file mode 100644 index 0000000000..69b15d4c10 --- /dev/null +++ b/packages/dify-ui/src/styles/utilities.css @@ -0,0 +1,272 @@ +@utility system-kbd { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility system-2xs-regular-uppercase { + font-size: 10px; + font-weight: 400; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-2xs-regular { + font-size: 10px; + font-weight: 400; + line-height: 12px; +} + +@utility system-2xs-medium { + font-size: 10px; + font-weight: 500; + line-height: 12px; +} + +@utility system-2xs-medium-uppercase { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-2xs-semibold-uppercase { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +@utility system-xs-regular-uppercase { + font-size: 12px; + font-weight: 400; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-xs-medium { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility system-xs-medium-uppercase { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-xs-semibold { + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +@utility system-xs-semibold-uppercase { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 16px; +} + +@utility system-sm-medium { + font-size: 13px; + font-weight: 500; + line-height: 16px; +} + +@utility system-sm-medium-uppercase { + font-size: 13px; + font-weight: 500; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-sm-semibold { + font-size: 13px; + font-weight: 600; + line-height: 16px; +} + +@utility system-sm-semibold-uppercase { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-md-regular { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +@utility system-md-medium { + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +@utility system-md-semibold { + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +@utility system-md-semibold-uppercase { + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + line-height: 20px; +} + +@utility system-xl-medium { + font-size: 16px; + font-weight: 500; + line-height: 24px; +} + +@utility system-xl-semibold { + font-size: 16px; + font-weight: 600; + line-height: 24px; +} + +@utility code-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 1.5; +} + +@utility code-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 1.5; +} + +@utility code-sm-semibold { + font-size: 13px; + font-weight: 600; + line-height: 1.5; +} + +@utility body-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +@utility body-xs-medium { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility body-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 16px; +} + +@utility body-sm-medium { + font-size: 13px; + font-weight: 500; + line-height: 16px; +} + +@utility body-md-regular { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +@utility body-md-medium { + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +@utility body-lg-regular { + font-size: 15px; + font-weight: 400; + line-height: 20px; +} + +@utility body-2xl-regular { + font-size: 18px; + font-weight: 400; + line-height: 1.5; +} + +@utility title-xs-semi-bold { + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +@utility title-sm-semi-bold { + font-size: 13px; + font-weight: 600; + line-height: 16px; +} + +@utility title-md-semi-bold { + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +@utility title-lg-bold { + font-size: 15px; + font-weight: 700; + line-height: 1.2; +} + +@utility title-xl-semi-bold { + font-size: 16px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-2xl-semi-bold { + font-size: 18px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-3xl-semi-bold { + font-size: 20px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-3xl-bold { + font-size: 20px; + font-weight: 700; + line-height: 1.2; +} + +@utility title-4xl-semi-bold { + font-size: 24px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-5xl-bold { + font-size: 30px; + font-weight: 700; + line-height: 1.2; +} diff --git a/packages/dify-ui/src/tailwind-preset.ts b/packages/dify-ui/src/tailwind-preset.ts new file mode 100644 index 0000000000..2dbf4781b0 --- /dev/null +++ b/packages/dify-ui/src/tailwind-preset.ts @@ -0,0 +1,87 @@ +import tailwindThemeVarDefine from './themes/tailwind-theme-var-define' + +const difyUIPreset = { + theme: { + extend: { + colors: { + gray: { + 25: '#fcfcfd', + 50: '#f9fafb', + 100: '#f2f4f7', + 200: '#eaecf0', + 300: '#d0d5dd', + 400: '#98a2b3', + 500: '#667085', + 600: '#344054', + 700: '#475467', + 800: '#1d2939', + 900: '#101828', + }, + primary: { + 25: '#f5f8ff', + 50: '#eff4ff', + 100: '#d1e0ff', + 200: '#b2ccff', + 300: '#84adff', + 400: '#528bff', + 500: '#2970ff', + 600: '#155eef', + 700: '#004eeb', + 800: '#0040c1', + 900: '#00359e', + }, + blue: { + 500: '#E1EFFE', + }, + green: { + 50: '#F3FAF7', + 100: '#DEF7EC', + 800: '#03543F', + }, + yellow: { + 100: '#FDF6B2', + 800: '#723B13', + }, + purple: { + 50: '#F6F5FF', + 200: '#DCD7FE', + }, + indigo: { + 25: '#F5F8FF', + 50: '#EEF4FF', + 100: '#E0EAFF', + 300: '#A4BCFD', + 400: '#8098F9', + 600: '#444CE7', + 800: '#2D31A6', + }, + ...tailwindThemeVarDefine, + }, + boxShadow: { + 'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)', + 'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)', + 'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)', + 'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)', + 'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)', + 'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)', + '2xl': '0px 24px 48px -12px rgba(16, 24, 40, 0.18)', + '3xl': '0px 32px 64px -12px rgba(16, 24, 40, 0.14)', + 'status-indicator-green-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-success-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', + 'status-indicator-warning-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-warning-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', + 'status-indicator-red-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-error-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', + 'status-indicator-blue-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-normal-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', + 'status-indicator-gray-shadow': '0px 1px 2px 0px var(--color-components-badge-status-light-disabled-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', + }, + opacity: { + 2: '0.02', + 8: '0.08', + }, + fontSize: { + '2xs': '0.625rem', + }, + }, + }, + plugins: [], +} + +export default difyUIPreset diff --git a/web/themes/dark.css b/packages/dify-ui/src/themes/dark.css similarity index 100% rename from web/themes/dark.css rename to packages/dify-ui/src/themes/dark.css diff --git a/web/themes/light.css b/packages/dify-ui/src/themes/light.css similarity index 100% rename from web/themes/light.css rename to packages/dify-ui/src/themes/light.css diff --git a/web/themes/tailwind-theme-var-define.ts b/packages/dify-ui/src/themes/tailwind-theme-var-define.ts similarity index 100% rename from web/themes/tailwind-theme-var-define.ts rename to packages/dify-ui/src/themes/tailwind-theme-var-define.ts diff --git a/packages/dify-ui/tsconfig.json b/packages/dify-ui/tsconfig.json new file mode 100644 index 0000000000..b31c48ead6 --- /dev/null +++ b/packages/dify-ui/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@dify/tsconfig/base.json", + "compilerOptions": { + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "noEmit": false, + "outDir": "dist", + "sourceMap": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": ["src"] +} diff --git a/packages/iconify-collections/assets/public/common/enter-key.svg b/packages/iconify-collections/assets/public/common/enter-key.svg new file mode 100644 index 0000000000..edfddfc188 --- /dev/null +++ b/packages/iconify-collections/assets/public/common/enter-key.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/iconify-collections/assets/public/other/comment.svg b/packages/iconify-collections/assets/public/other/comment.svg new file mode 100644 index 0000000000..0f0609f0b6 --- /dev/null +++ b/packages/iconify-collections/assets/public/other/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/custom-public/index.d.ts b/packages/iconify-collections/custom-public/index.d.ts index ecca5633d4..be2442726c 100644 --- a/packages/iconify-collections/custom-public/index.d.ts +++ b/packages/iconify-collections/custom-public/index.d.ts @@ -1,4 +1,4 @@ -export interface IconifyJSON { +export type IconifyJSON = { prefix: string icons: Record aliases?: Record @@ -7,7 +7,7 @@ export interface IconifyJSON { lastModified?: number } -export interface IconifyIcon { +export type IconifyIcon = { body: string left?: number top?: number @@ -18,11 +18,11 @@ export interface IconifyIcon { vFlip?: boolean } -export interface IconifyAlias extends Omit { +export type IconifyAlias = { parent: string -} +} & Omit -export interface IconifyInfo { +export type IconifyInfo = { prefix: string name: string total: number @@ -40,11 +40,11 @@ export interface IconifyInfo { palette?: boolean } -export interface IconifyMetaData { +export type IconifyMetaData = { [key: string]: unknown } -export interface IconifyChars { +export type IconifyChars = { [key: string]: string } @@ -52,4 +52,3 @@ export declare const icons: IconifyJSON export declare const info: IconifyInfo export declare const metadata: IconifyMetaData export declare const chars: IconifyChars - diff --git a/packages/iconify-collections/custom-public/index.js b/packages/iconify-collections/custom-public/index.js index 81c1d0f5c4..aa7f8d5058 100644 --- a/packages/iconify-collections/custom-public/index.js +++ b/packages/iconify-collections/custom-public/index.js @@ -1,9 +1,8 @@ 'use strict' +const chars = require('./chars.json') const icons = require('./icons.json') const info = require('./info.json') const metadata = require('./metadata.json') -const chars = require('./chars.json') module.exports = { icons, info, metadata, chars } - diff --git a/packages/iconify-collections/custom-public/index.mjs b/packages/iconify-collections/custom-public/index.mjs index 6c1108a92d..8e1c022130 100644 --- a/packages/iconify-collections/custom-public/index.mjs +++ b/packages/iconify-collections/custom-public/index.mjs @@ -1,7 +1,6 @@ +import chars from './chars.json' with { type: 'json' } import icons from './icons.json' with { type: 'json' } import info from './info.json' with { type: 'json' } import metadata from './metadata.json' with { type: 'json' } -import chars from './chars.json' with { type: 'json' } - -export { icons, info, metadata, chars } +export { chars, icons, info, metadata } diff --git a/packages/iconify-collections/custom-vender/index.d.ts b/packages/iconify-collections/custom-vender/index.d.ts index ecca5633d4..be2442726c 100644 --- a/packages/iconify-collections/custom-vender/index.d.ts +++ b/packages/iconify-collections/custom-vender/index.d.ts @@ -1,4 +1,4 @@ -export interface IconifyJSON { +export type IconifyJSON = { prefix: string icons: Record aliases?: Record @@ -7,7 +7,7 @@ export interface IconifyJSON { lastModified?: number } -export interface IconifyIcon { +export type IconifyIcon = { body: string left?: number top?: number @@ -18,11 +18,11 @@ export interface IconifyIcon { vFlip?: boolean } -export interface IconifyAlias extends Omit { +export type IconifyAlias = { parent: string -} +} & Omit -export interface IconifyInfo { +export type IconifyInfo = { prefix: string name: string total: number @@ -40,11 +40,11 @@ export interface IconifyInfo { palette?: boolean } -export interface IconifyMetaData { +export type IconifyMetaData = { [key: string]: unknown } -export interface IconifyChars { +export type IconifyChars = { [key: string]: string } @@ -52,4 +52,3 @@ export declare const icons: IconifyJSON export declare const info: IconifyInfo export declare const metadata: IconifyMetaData export declare const chars: IconifyChars - diff --git a/packages/iconify-collections/custom-vender/index.js b/packages/iconify-collections/custom-vender/index.js index 81c1d0f5c4..aa7f8d5058 100644 --- a/packages/iconify-collections/custom-vender/index.js +++ b/packages/iconify-collections/custom-vender/index.js @@ -1,9 +1,8 @@ 'use strict' +const chars = require('./chars.json') const icons = require('./icons.json') const info = require('./info.json') const metadata = require('./metadata.json') -const chars = require('./chars.json') module.exports = { icons, info, metadata, chars } - diff --git a/packages/iconify-collections/custom-vender/index.mjs b/packages/iconify-collections/custom-vender/index.mjs index 6c1108a92d..8e1c022130 100644 --- a/packages/iconify-collections/custom-vender/index.mjs +++ b/packages/iconify-collections/custom-vender/index.mjs @@ -1,7 +1,6 @@ +import chars from './chars.json' with { type: 'json' } import icons from './icons.json' with { type: 'json' } import info from './info.json' with { type: 'json' } import metadata from './metadata.json' with { type: 'json' } -import chars from './chars.json' with { type: 'json' } - -export { icons, info, metadata, chars } +export { chars, icons, info, metadata } diff --git a/packages/iconify-collections/package.json b/packages/iconify-collections/package.json index 3bd7285f1a..07c29f0a07 100644 --- a/packages/iconify-collections/package.json +++ b/packages/iconify-collections/package.json @@ -1,12 +1,12 @@ { "name": "@dify/iconify-collections", - "private": true, "version": "0.0.0-private", + "private": true, "exports": { "./custom-public": { "types": "./custom-public/index.d.ts", - "require": "./custom-public/index.js", - "import": "./custom-public/index.mjs" + "import": "./custom-public/index.mjs", + "require": "./custom-public/index.js" }, "./custom-public/icons.json": "./custom-public/icons.json", "./custom-public/info.json": "./custom-public/info.json", @@ -14,8 +14,8 @@ "./custom-public/chars.json": "./custom-public/chars.json", "./custom-vender": { "types": "./custom-vender/index.d.ts", - "require": "./custom-vender/index.js", - "import": "./custom-vender/index.mjs" + "import": "./custom-vender/index.mjs", + "require": "./custom-vender/index.js" }, "./custom-vender/icons.json": "./custom-vender/icons.json", "./custom-vender/info.json": "./custom-vender/info.json", diff --git a/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js b/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js new file mode 100755 index 0000000000..2f2b0d72a9 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const entryFile = path.join(packageRoot, 'dist', 'cli.mjs') + +if (!fs.existsSync(entryFile)) + throw new Error(`Built CLI entry not found at ${entryFile}. Run "pnpm --filter migrate-no-unchecked-indexed-access build" first.`) + +const result = spawnSync( + process.execPath, + [entryFile, ...process.argv.slice(2)], + { + cwd: process.cwd(), + env: process.env, + stdio: 'inherit', + }, +) + +if (result.error) + throw result.error + +process.exit(result.status ?? 1) diff --git a/packages/migrate-no-unchecked-indexed-access/package.json b/packages/migrate-no-unchecked-indexed-access/package.json new file mode 100644 index 0000000000..5da8d4cb50 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/package.json @@ -0,0 +1,22 @@ +{ + "name": "migrate-no-unchecked-indexed-access", + "type": "module", + "version": "0.0.0-private", + "private": true, + "bin": { + "migrate-no-unchecked-indexed-access": "./bin/migrate-no-unchecked-indexed-access.js" + }, + "scripts": { + "build": "vp pack", + "type-check": "tsc" + }, + "dependencies": { + "typescript": "catalog:" + }, + "devDependencies": { + "@dify/tsconfig": "workspace:*", + "@types/node": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/cli.ts b/packages/migrate-no-unchecked-indexed-access/src/cli.ts new file mode 100644 index 0000000000..99142c388f --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/cli.ts @@ -0,0 +1,46 @@ +import process from 'node:process' +import { runBatchMigrationCommand } from './no-unchecked-indexed-access/run' + +function printUsage() { + console.log(`Usage: + migrate-no-unchecked-indexed-access [options] + +Options: + --project + --batch-size + --batch-iterations + --max-rounds + --verbose`) +} + +async function flushStandardStreams() { + await Promise.all([ + new Promise(resolve => process.stdout.write('', () => resolve())), + new Promise(resolve => process.stderr.write('', () => resolve())), + ]) +} + +async function main() { + const argv = process.argv.slice(2) + if (argv.includes('help') || argv.includes('--help') || argv.includes('-h')) { + printUsage() + return + } + + await runBatchMigrationCommand(argv) +} + +let exitCode = 0 + +try { + await main() + const currentExitCode = process.exitCode + exitCode = typeof currentExitCode === 'number' ? currentExitCode : 0 +} +catch (error) { + console.error(error instanceof Error ? error.message : error) + exitCode = 1 +} + +await flushStandardStreams() +process.exit(exitCode) diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts new file mode 100644 index 0000000000..6b79e214bb --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts @@ -0,0 +1,1835 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import ts from 'typescript' + +const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']) +export const SUPPORTED_DIAGNOSTIC_CODES = new Set([2322, 2339, 2345, 2488, 2532, 2538, 2604, 2722, 2769, 2786, 7006, 18047, 18048]) +const DEFAULT_MAX_ITERATIONS = 10 +const ACCESS_DIAGNOSTIC_CODES = new Set([2339, 2532, 18047, 18048]) +const ASSIGNABILITY_DIAGNOSTIC_CODES = new Set([2322, 2345, 2769]) +const parsedConfigCache = new Map() + +type CliOptions = { + files: string[] + maxIterations: number + project: string + useFullProjectRoots?: boolean + verbose: boolean + write: boolean +} + +type TextEdit = { + end: number + expectedText?: string + replacement: string + start: number +} + +type EditTarget + = { expression: ts.Expression, kind: 'expression', sourceFile: ts.SourceFile } + | { end: number, kind: 'direct-edit', replacement: string, sourceFile: ts.SourceFile, start: number } + | { kind: 'shorthand-property', property: ts.ShorthandPropertyAssignment, sourceFile: ts.SourceFile } + +export function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + files: [], + maxIterations: DEFAULT_MAX_ITERATIONS, + project: 'tsconfig.json', + verbose: false, + write: false, + } + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + if (!arg) + continue + + if (arg === '--') + continue + + if (arg === '--write') { + options.write = true + continue + } + + if (arg === '--verbose') { + options.verbose = true + continue + } + + if (arg === '--project') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --project') + + options.project = value + i += 1 + continue + } + + if (arg === '--max-iterations') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --max-iterations') + + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) + throw new Error(`Invalid --max-iterations value: ${value}`) + + options.maxIterations = parsed + i += 1 + continue + } + + if (arg === '--files') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --files') + + options.files.push(...splitFilesArgument(value)) + i += 1 + continue + } + + if (arg.startsWith('--')) + throw new Error(`Unknown option: ${arg}`) + + options.files.push(...splitFilesArgument(arg)) + } + + return options +} + +function splitFilesArgument(value: string): string[] { + return value + .split(',') + .map(item => item.trim()) + .filter(Boolean) +} + +function parseTsConfig(projectPath: string): ts.ParsedCommandLine { + const cached = parsedConfigCache.get(projectPath) + if (cached) + return cached + + const configFile = ts.readConfigFile(projectPath, ts.sys.readFile) + if (configFile.error) + throw new Error(formatDiagnostic(configFile.error)) + + const configDirectory = path.dirname(projectPath) + const parsedConfig = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + configDirectory, + undefined, + projectPath, + ) + parsedConfigCache.set(projectPath, parsedConfig) + return parsedConfig +} + +function createMigrationProgram( + rootNames: string[], + parsedConfig: ts.ParsedCommandLine, + fileTexts: Map, + oldProgram?: ts.Program, +): ts.Program { + const compilerHost = ts.createCompilerHost(parsedConfig.options, true) + const originalGetSourceFile = compilerHost.getSourceFile.bind(compilerHost) + + compilerHost.readFile = (fileName) => { + return fileTexts.get(fileName) ?? ts.sys.readFile(fileName) + } + + compilerHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { + const text = fileTexts.get(fileName) + if (text !== undefined) + return ts.createSourceFile(fileName, text, languageVersion, true) + + return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) + } + + return ts.createProgram({ + oldProgram, + host: compilerHost, + options: parsedConfig.options, + projectReferences: parsedConfig.projectReferences, + rootNames, + }) +} + +function isTargetFile(fileName: string): boolean { + const extension = path.extname(fileName) + if (!SUPPORTED_EXTENSIONS.has(extension)) + return false + + if (fileName.endsWith('.d.ts')) + return false + + return !fileName.includes(`${path.sep}.next${path.sep}`) +} + +function normalizeFileName(fileName: string): string { + return path.resolve(fileName) +} + +function isDeclarationSupportFile(fileName: string): boolean { + return fileName.endsWith('.d.ts') +} + +function isSetupSupportFile(fileName: string): boolean { + const baseName = path.basename(fileName) + return baseName === 'vitest.setup.ts' + || baseName === 'vitest.setup.tsx' + || baseName === 'jest.setup.ts' + || baseName === 'jest.setup.tsx' + || baseName === 'setupTests.ts' + || baseName === 'setupTests.tsx' + || baseName === 'test.setup.ts' + || baseName === 'test.setup.tsx' +} + +function getMigrationRootNames( + parsedConfig: ts.ParsedCommandLine, + targetFiles: string[], +): string[] { + const rootNames = new Set(targetFiles) + + for (const fileName of parsedConfig.fileNames.map(normalizeFileName)) { + if (isDeclarationSupportFile(fileName) || isSetupSupportFile(fileName)) + rootNames.add(fileName) + } + + return Array.from(rootNames) +} + +function createFileMatcher(filePatterns: string[]): (fileName: string) => boolean { + if (filePatterns.length === 0) + return () => true + + const patterns = filePatterns.map(pattern => ({ + absolute: normalizeFileName(pattern), + raw: pattern.split(path.sep).join('/'), + })) + return (fileName: string) => { + const normalized = normalizeFileName(fileName) + const unixStyle = normalized.split(path.sep).join('/') + return patterns.some(pattern => normalized === pattern.absolute || unixStyle.endsWith(pattern.raw)) + } +} + +function formatDiagnostic(diagnostic: ts.Diagnostic): string { + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') + if (!diagnostic.file || diagnostic.start === undefined) + return message + + const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start) + return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} TS${diagnostic.code}: ${message}` +} + +function ensureTrailingNonNullAssertion(expression: string): string { + const trimmedExpression = expression.trimEnd() + return trimmedExpression.endsWith('!') + ? trimmedExpression + : `${trimmedExpression}!` +} + +function hasOptionalChainDescendant(node: ts.Node): boolean { + let found = false + + const visit = (current: ts.Node) => { + if (found) + return + + if (ts.isOptionalChain(current)) { + found = true + return + } + + current.forEachChild(visit) + } + + visit(node) + return found +} + +function shouldPrintInlineNonNullAssertion(expression: ts.Expression): boolean { + return ts.isOptionalChain(expression) + || (ts.isParenthesizedExpression(expression) && hasOptionalChainDescendant(expression.expression)) +} + +function normalizeOptionalChainNonNullContinuations(text: string): string { + const sourceFile = ts.createSourceFile('normalize.tsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) + const edits: TextEdit[] = [] + + const visit = (node: ts.Node) => { + if ( + ts.isNonNullExpression(node) + && ts.isParenthesizedExpression(node.expression) + && hasOptionalChainDescendant(node.expression.expression) + ) { + edits.push({ + end: node.getEnd(), + replacement: `${node.expression.expression.getText(sourceFile)}!`, + start: node.getStart(sourceFile), + }) + return + } + + node.forEachChild(visit) + } + + visit(sourceFile) + + if (edits.length === 0) + return text + + return applyEdits(text, edits).text +} + +function collapseRepeatedInlineComments(text: string): string { + return text + .split('\n') + .map((line) => { + const commentIndex = line.indexOf('//') + if (commentIndex < 0) + return line + + const prefix = line.slice(0, commentIndex).trimEnd() + const comment = line.slice(commentIndex + 2).trim() + const segments = comment + .split(/\s+\/\/\s+/) + .map(item => item.trim()) + .filter(Boolean) + + if (segments.length < 2) + return line + + const lastSegment = segments[segments.length - 1]! + const stableSegments = segments.slice(0, -1) + const repeatedSameComment = stableSegments.length > 0 + && stableSegments.every(segment => segment === segments[0]) + && (lastSegment === segments[0] || segments[0]!.startsWith(lastSegment) || lastSegment.startsWith(segments[0]!)) + + if (!repeatedSameComment) + return line.replace(/!{2,}$/g, '!') + + const normalizedComment = segments[0]!.replace(/!{2,}$/g, '!') + return prefix ? `${prefix} // ${normalizedComment}` : `// ${normalizedComment}` + }) + .join('\n') +} + +export function normalizeMalformedAssertions(text: string): string { + const normalizedText = text + .replace(/\n(\s*)! (\s*\/\/[^\n]*)\n/g, '! $2\n') + .replace(/\.not!+(?=[.(])/g, '.not') + .replace(/(\(|,\s*)([A-Za-z_$][\w$]*)\s*:\s*any\s*=>/g, '$1($2: any) =>') + .replace(/([,{]\s*)([A-Z_$][\w$]*)!=\{/g, '$1$2={') + .replace(/\b([A-Z_$][\w$]*)!!,/gi, '$1: $1!,') + .replace(/\b([A-Z_$][\w$]*)!!:/gi, '$1:') + .replace(/([,{]\s*)([A-Z_$][\w$]*)!:/gi, '$1$2:') + .replace(/\b(const|let|var)\s+\{([^=\n]+)\}\s*=\s*([^\n;]+)/g, (fullMatch, keyword: string, bindings: string, expression: string) => { + if (!bindings.includes('!')) + return fullMatch + + const normalizedBindings = bindings.replace(/!([,\s}:])/g, '$1') + return `${keyword} {${normalizedBindings}} = ${ensureTrailingNonNullAssertion(expression)}` + }) + + return collapseRepeatedInlineComments(normalizeOptionalChainNonNullContinuations(normalizedText)) +} + +function isExpressionTarget(target: EditTarget): target is Extract { + return target.kind === 'expression' +} + +function createExpressionTarget(expression: ts.Expression): EditTarget { + return { + expression, + kind: 'expression', + sourceFile: expression.getSourceFile(), + } +} + +function createShorthandPropertyTarget(property: ts.ShorthandPropertyAssignment): EditTarget { + return { + kind: 'shorthand-property', + property, + sourceFile: property.getSourceFile(), + } +} + +function createDirectEditTarget( + sourceFile: ts.SourceFile, + start: number, + end: number, + replacement: string, +): EditTarget { + return { + end, + kind: 'direct-edit', + replacement, + sourceFile, + start, + } +} + +function createIterableFallbackReplacement( + expression: ts.Expression, + sourceFile: ts.SourceFile, +): string { + return `(${expression.getText(sourceFile)} ?? [])` +} + +function createIterableFallbackTarget(expression: ts.Expression): EditTarget { + return createDirectEditTarget( + expression.getSourceFile(), + expression.getStart(expression.getSourceFile()), + expression.getEnd(), + createIterableFallbackReplacement(expression, expression.getSourceFile()), + ) +} + +function createArrayLiteralIterableFallbackTarget( + arrayLiteral: ts.ArrayLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const sourceFile = arrayLiteral.getSourceFile() + const start = arrayLiteral.getStart(sourceFile) + const end = arrayLiteral.getEnd() + const originalText = sourceFile.text.slice(start, end) + const edits: TextEdit[] = [] + + for (const element of arrayLiteral.elements) { + if (!ts.isSpreadElement(element)) + continue + + if (isAlreadyNonNull(element.expression)) + continue + + if (!typeIncludesUndefined(checker.getTypeAtLocation(element.expression))) + continue + + edits.push({ + end: element.expression.getEnd() - start, + replacement: createIterableFallbackReplacement(element.expression, sourceFile), + start: element.expression.getStart(sourceFile) - start, + }) + } + + if (edits.length === 0) + return undefined + + return createDirectEditTarget( + sourceFile, + start, + end, + applyEdits(originalText, edits).text, + ) +} + +function getTokenAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { + let current: ts.Node = sourceFile + + while (true) { + let next: ts.Node | undefined + current.forEachChild((child) => { + if (!next && position >= child.getFullStart() && position < child.getEnd()) + next = child + }) + + if (!next) + return current + + current = next + } +} + +function findAncestor( + node: ts.Node | undefined, + predicate: (candidate: ts.Node) => candidate is NodeType, +): NodeType | undefined { + let current = node + + while (current) { + if (predicate(current)) + return current + + current = current.parent + } + + return undefined +} + +function findTightestExpression(sourceFile: ts.SourceFile, start: number, end: number): ts.Expression | undefined { + let node: ts.Node | undefined = getTokenAtPosition(sourceFile, start) + + while (node) { + if (ts.isExpression(node)) { + const nodeStart = node.getStart(sourceFile) + const nodeEnd = node.getEnd() + if (nodeStart <= start && end <= nodeEnd) + return node + } + + node = node.parent + } + + return undefined +} + +function isAssignmentOperator(token: ts.SyntaxKind): boolean { + return token >= ts.SyntaxKind.FirstAssignment && token <= ts.SyntaxKind.LastAssignment +} + +function typeIncludesUndefined(type: ts.Type): boolean { + if ((type.flags & ts.TypeFlags.Undefined) !== 0) + return true + + if (!type.isUnion()) + return false + + return type.types.some(typeIncludesUndefined) +} + +function skipOuterExpressions(expression: ts.Expression): ts.Expression { + let current = expression + + while (ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current)) + current = current.expression + + return current +} + +function isAlreadyNonNull(expression: ts.Expression): boolean { + let current = expression + + while (ts.isParenthesizedExpression(current)) + current = current.expression + + return ts.isNonNullExpression(current) +} + +function findAssignmentLikeCandidate( + token: ts.Node, + sourceFile: ts.SourceFile, + start: number, + end: number, +): ts.Expression | undefined { + let current: ts.Node | undefined = token + + while (current) { + if (ts.isVariableDeclaration(current) && current.initializer) + return current.initializer + + if (ts.isPropertyDeclaration(current) && current.initializer) + return current.initializer + + if (ts.isPropertyAssignment(current)) + return current.initializer + + if (ts.isShorthandPropertyAssignment(current)) + return current.name + + if (ts.isParameter(current) && current.initializer) + return current.initializer + + if (ts.isReturnStatement(current) && current.expression) + return current.expression + + if (ts.isBinaryExpression(current) && isAssignmentOperator(current.operatorToken.kind)) + return current.right + + if (ts.isJsxAttribute(current) && current.initializer && ts.isJsxExpression(current.initializer) && current.initializer.expression) + return current.initializer.expression + + if (ts.isJsxSpreadAttribute(current)) + return current.expression + + current = current.parent + } + + return findTightestExpression(sourceFile, start, end) +} + +function findArgumentCandidate( + token: ts.Node, + sourceFile: ts.SourceFile, + start: number, + end: number, +): ts.Expression | undefined { + let current: ts.Node | undefined = token + + while (current) { + if ((ts.isCallExpression(current) || ts.isNewExpression(current)) && current.arguments) { + const argument = current.arguments.find((item) => { + const itemStart = item.getStart(sourceFile) + const itemEnd = item.getEnd() + return itemStart <= start && end <= itemEnd + }) + if (argument) + return argument + } + + current = current.parent + } + + return findTightestExpression(sourceFile, start, end) +} + +function getExpressionFromJsxAttribute(attribute: ts.JsxAttribute): ts.Expression | undefined { + return attribute.initializer && ts.isJsxExpression(attribute.initializer) + ? attribute.initializer.expression + : undefined +} + +function findTargetFromExpression( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(expression, checker) + if (referencedDeclarationTarget) + return referencedDeclarationTarget + + const nestedTarget = findNestedContainerTarget(expression, checker) + if (nestedTarget) + return nestedTarget + + const innerExpression = skipOuterExpressions(expression) + if (ts.isConditionalExpression(innerExpression)) { + return findTargetFromExpression(innerExpression.whenTrue, checker) + ?? findTargetFromExpression(innerExpression.whenFalse, checker) + } + + if ( + ts.isBinaryExpression(innerExpression) + && ( + innerExpression.operatorToken.kind === ts.SyntaxKind.BarBarToken + || innerExpression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken + || innerExpression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken + ) + ) { + return findTargetFromExpression(innerExpression.left, checker) + ?? findTargetFromExpression(innerExpression.right, checker) + } + + if (ts.isArrowFunction(innerExpression) || ts.isFunctionExpression(innerExpression)) { + const functionTarget = findFunctionLikeReturnTarget(innerExpression, checker) + if (functionTarget) + return functionTarget + } + + if (ts.isPropertyAccessExpression(innerExpression)) { + const namedPropertyTarget = findNamedPropertyTarget(innerExpression.expression, innerExpression.name.text, checker) + if (namedPropertyTarget) + return namedPropertyTarget + } + + if (ts.isCallExpression(innerExpression)) { + const collectionCallbackTarget = findCollectionCallbackTarget(innerExpression, checker) + if (collectionCallbackTarget) + return collectionCallbackTarget + + const callbackArgumentTarget = findCallbackArgumentTarget(innerExpression, checker) + if (callbackArgumentTarget) + return callbackArgumentTarget + + const callExpressionTarget = findCallExpressionDeclarationTarget(innerExpression, checker) + if (callExpressionTarget) + return callExpressionTarget + } + + if (!typeIncludesUndefined(checker.getTypeAtLocation(expression))) + return undefined + + return createExpressionTarget(expression) +} + +function findJsxSpreadAttributeTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const spreadAttribute = findAncestor(token, ts.isJsxSpreadAttribute) + if (spreadAttribute) + return findTargetFromExpression(spreadAttribute.expression, checker) + + const openingLikeElement = findAncestor(token, node => + ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) + + if (!openingLikeElement) + return undefined + + for (const attribute of openingLikeElement.attributes.properties) { + if (!ts.isJsxSpreadAttribute(attribute)) + continue + + const target = findTargetFromExpression(attribute.expression, checker) + if (target) + return target + } + + return undefined +} + +function findShorthandPropertyTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const property = findAncestor(token, ts.isShorthandPropertyAssignment) + if (!property) + return undefined + + return typeIncludesUndefined(checker.getTypeAtLocation(property.name)) + ? createShorthandPropertyTarget(property) + : undefined +} + +function findPropertyAssignmentInitializerTarget( + token: ts.Node, + start: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const propertyAssignment = findAncestor(token, ts.isPropertyAssignment) + if (!propertyAssignment) + return undefined + + const propertyNameStart = propertyAssignment.name.getStart() + const propertyNameEnd = propertyAssignment.name.getEnd() + if (start < propertyNameStart || start >= propertyNameEnd) + return undefined + + const directTarget = findTargetFromExpression(propertyAssignment.initializer, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(propertyAssignment.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(propertyAssignment.initializer))) + return undefined + + return createExpressionTarget(propertyAssignment.initializer) +} + +function findPropertyAccessExpressionTarget( + token: ts.Node, + start: number, +): EditTarget | undefined { + const propertyAccess = findAncestor(token, ts.isPropertyAccessExpression) + if (!propertyAccess) + return undefined + + if (start >= propertyAccess.name.getStart() && start < propertyAccess.name.getEnd()) + return createExpressionTarget(propertyAccess.expression) + + return undefined +} + +function findUndefinedAccessTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + let current: ts.Node | undefined = token + let bestTarget: EditTarget | undefined + + while (current) { + if (ts.isPropertyAccessExpression(current)) { + const expression = current.expression + if (typeIncludesUndefined(checker.getTypeAtLocation(expression)) && !isAlreadyNonNull(expression)) + bestTarget = createExpressionTarget(expression) + } + + if (ts.isElementAccessExpression(current)) { + const expression = current.expression + if (typeIncludesUndefined(checker.getTypeAtLocation(expression)) && !isAlreadyNonNull(expression)) + bestTarget = createExpressionTarget(expression) + } + + current = current.parent + } + + return bestTarget +} + +function findElementAccessArgumentTarget(token: ts.Node): EditTarget | undefined { + let current = token + let matchingElementAccess: ts.ElementAccessExpression | undefined + + while (current) { + if (ts.isElementAccessExpression(current) && current.argumentExpression) + matchingElementAccess = current + + current = current.parent + } + + if (!matchingElementAccess?.argumentExpression) + return undefined + + return createExpressionTarget(matchingElementAccess.argumentExpression) +} + +function findIterableTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const arrayLiteral = findAncestor(token, ts.isArrayLiteralExpression) + if (arrayLiteral) { + const arrayLiteralTarget = createArrayLiteralIterableFallbackTarget(arrayLiteral, checker) + if (arrayLiteralTarget) + return arrayLiteralTarget + } + + const spreadElement = findAncestor(token, ts.isSpreadElement) + if (spreadElement && !isAlreadyNonNull(spreadElement.expression)) + return createIterableFallbackTarget(spreadElement.expression) + + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if ( + variableDeclaration?.initializer + && typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer)) + && !isAlreadyNonNull(variableDeclaration.initializer) + ) { + return createExpressionTarget(variableDeclaration.initializer) + } + + const binaryExpression = findAncestor(token, ts.isBinaryExpression) + if ( + binaryExpression + && isAssignmentOperator(binaryExpression.operatorToken.kind) + && typeIncludesUndefined(checker.getTypeAtLocation(binaryExpression.right)) + && !isAlreadyNonNull(binaryExpression.right) + ) { + return createExpressionTarget(binaryExpression.right) + } + + return undefined +} + +function findImplicitAnyParameterTarget(token: ts.Node): EditTarget | undefined { + const parameter = findAncestor(token, ts.isParameter) + if (!parameter || parameter.type || !ts.isIdentifier(parameter.name)) + return undefined + + const sourceFile = parameter.getSourceFile() + const replacement = ts.isArrowFunction(parameter.parent) && parameter.parent.parameters.length === 1 + ? `(${parameter.name.getText(sourceFile)}: any)` + : `${parameter.name.getText(sourceFile)}: any` + + return createDirectEditTarget( + sourceFile, + parameter.getStart(sourceFile), + parameter.getEnd(), + replacement, + ) +} + +function getArrayPatternElementTypeText( + element: ts.ArrayBindingElement | ts.Expression, + checker: ts.TypeChecker, +): string { + if (ts.isOmittedExpression(element)) + return 'unknown' + + const targetNode = ts.isBindingElement(element) + ? element.name + : element + + const targetType = checker.getNonNullableType(checker.getTypeAtLocation(targetNode)) + const typeText = checker.typeToString(targetType) + return typeText === 'never' ? 'unknown' : typeText +} + +function createArrayDestructuringReplacement( + sourceFile: ts.SourceFile, + expression: ts.Expression, + elements: readonly (ts.ArrayBindingElement | ts.Expression)[], + checker: ts.TypeChecker, + options?: { + fallbackToEmptyArray?: boolean + }, +): string | undefined { + if (elements.length === 0) + return undefined + + const tupleTypes = elements.map(element => getArrayPatternElementTypeText(element, checker)) + const expressionText = options?.fallbackToEmptyArray + ? `(${expression.getText(sourceFile)} ?? [])` + : `(${expression.getText(sourceFile)})` + return `${expressionText} as [${tupleTypes.join(', ')}]` +} + +function findArrayDestructuringTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const binaryExpression = findAncestor(token, ts.isBinaryExpression) + if (binaryExpression && isAssignmentOperator(binaryExpression.operatorToken.kind) && ts.isArrayLiteralExpression(binaryExpression.left)) { + const replacement = createArrayDestructuringReplacement( + binaryExpression.getSourceFile(), + binaryExpression.right, + binaryExpression.left.elements, + checker, + { + fallbackToEmptyArray: typeIncludesUndefined(checker.getTypeAtLocation(binaryExpression.right)), + }, + ) + if (replacement) { + return createDirectEditTarget( + binaryExpression.getSourceFile(), + binaryExpression.right.getStart(binaryExpression.getSourceFile()), + binaryExpression.right.getEnd(), + replacement, + ) + } + } + + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if (variableDeclaration?.initializer && ts.isArrayBindingPattern(variableDeclaration.name)) { + const replacement = createArrayDestructuringReplacement( + variableDeclaration.getSourceFile(), + variableDeclaration.initializer, + variableDeclaration.name.elements, + checker, + { + fallbackToEmptyArray: typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer)), + }, + ) + if (replacement) { + return createDirectEditTarget( + variableDeclaration.getSourceFile(), + variableDeclaration.initializer.getStart(variableDeclaration.getSourceFile()), + variableDeclaration.initializer.getEnd(), + replacement, + ) + } + } + + return undefined +} + +function findVariableDeclarationInitializerTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if (!variableDeclaration?.initializer) + return undefined + + const nestedTarget = findNestedContainerTarget(variableDeclaration.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer))) + return undefined + + return createExpressionTarget(variableDeclaration.initializer) +} + +function getResolvedValueDeclaration( + symbol: ts.Symbol | undefined, + checker: ts.TypeChecker, +): ts.Declaration | undefined { + if (!symbol) + return undefined + + const resolvedSymbol = symbol.flags & ts.SymbolFlags.Alias + ? checker.getAliasedSymbol(symbol) + : symbol + + return resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0] +} + +function getFunctionLikeDeclaration( + declaration: ts.Declaration, +): ts.FunctionLikeDeclarationBase | undefined { + if ( + ts.isFunctionDeclaration(declaration) + || ts.isMethodDeclaration(declaration) + || ts.isFunctionExpression(declaration) + || ts.isArrowFunction(declaration) + ) { + return declaration + } + + if ( + ts.isVariableDeclaration(declaration) + && declaration.initializer + && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) + ) { + return declaration.initializer + } + + return undefined +} + +function getPropertyNameText(name: ts.PropertyName | ts.BindingName): string | undefined { + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) + return name.text + + return undefined +} + +function getCallExpressionPropertyAccess(callExpression: ts.CallExpression): ts.PropertyAccessExpression | undefined { + const callee = skipOuterExpressions(callExpression.expression) + return ts.isPropertyAccessExpression(callee) ? callee : undefined +} + +function getFunctionExpressionArgument(callExpression: ts.CallExpression, index = 0): ts.ArrowFunction | ts.FunctionExpression | undefined { + const callback = callExpression.arguments[index] + return callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? callback + : undefined +} + +function findTargetInFunctionBody( + body: ts.ConciseBody, + resolveExpression: (expression: ts.Expression) => EditTarget | undefined, +): EditTarget | undefined { + if (ts.isBlock(body)) { + for (const expression of findReturnStatementExpressions(body)) { + const target = resolveExpression(expression) + if (target) + return target + } + + return undefined + } + + return resolveExpression(body) +} + +function getParameterCollectionExpression( + declaration: ts.ParameterDeclaration, +): ts.Expression | undefined { + const functionLikeDeclaration = declaration.parent + if ( + !(ts.isArrowFunction(functionLikeDeclaration) || ts.isFunctionExpression(functionLikeDeclaration)) + || !ts.isCallExpression(functionLikeDeclaration.parent) + || functionLikeDeclaration.parent.arguments[0] !== functionLikeDeclaration + ) { + return undefined + } + + const callee = getCallExpressionPropertyAccess(functionLikeDeclaration.parent) + return callee?.expression +} + +function findObjectLiteralNamedPropertyTarget( + objectLiteral: ts.ObjectLiteralExpression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + for (const property of objectLiteral.properties) { + if (ts.isSpreadAssignment(property)) + continue + + if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) + return createShorthandPropertyTarget(property) + + if (ts.isPropertyAssignment(property)) { + const currentPropertyName = getPropertyNameText(property.name) + if (currentPropertyName !== propertyName) + continue + + return findTargetFromExpression(property.initializer, checker) + ?? createExpressionTarget(property.initializer) + } + } + + return undefined +} + +function findFunctionLikeNamedReturnTarget( + declaration: ts.FunctionLikeDeclarationBase, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + if (!declaration.body) + return undefined + + return findTargetInFunctionBody( + declaration.body, + expression => findNamedPropertyTarget(expression, propertyName, checker), + ) +} + +function findCollectionPropertyTarget( + expression: ts.Expression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + + if (ts.isIdentifier(innerExpression)) + return findNamedPropertyTarget(innerExpression, propertyName, checker) + + if (!ts.isCallExpression(innerExpression)) + return undefined + + const callee = getCallExpressionPropertyAccess(innerExpression) + if (!callee) + return undefined + + if (callee.name.text === 'map' || callee.name.text === 'flatMap') { + const callback = getFunctionExpressionArgument(innerExpression) + if (!callback) + return undefined + + return findTargetInFunctionBody( + callback.body, + returnedExpression => findNamedPropertyTarget(returnedExpression, propertyName, checker), + ) + } + + if (callee.name.text === 'filter') + return findCollectionPropertyTarget(callee.expression, propertyName, checker) + + return undefined +} + +function findNamedPropertyTarget( + expression: ts.Expression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + + if (ts.isObjectLiteralExpression(innerExpression)) + return findObjectLiteralNamedPropertyTarget(innerExpression, propertyName, checker) + + if (ts.isIdentifier(innerExpression)) { + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(innerExpression), checker) + if (!declaration) + return undefined + + if (ts.isParameter(declaration)) { + const collectionExpression = getParameterCollectionExpression(declaration) + if (collectionExpression) + return findCollectionPropertyTarget(collectionExpression, propertyName, checker) + } + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (functionLikeDeclaration) + return findFunctionLikeNamedReturnTarget(functionLikeDeclaration, propertyName, checker) + + if (ts.isVariableDeclaration(declaration) && declaration.initializer) + return findNamedPropertyTarget(declaration.initializer, propertyName, checker) + + return undefined + } + + if (ts.isCallExpression(innerExpression)) { + const collectionPropertyTarget = findCollectionPropertyTarget(innerExpression, propertyName, checker) + if (collectionPropertyTarget) + return collectionPropertyTarget + + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(skipOuterExpressions(innerExpression.expression)), checker) + if (!declaration) + return undefined + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (!functionLikeDeclaration) + return undefined + + return findFunctionLikeNamedReturnTarget(functionLikeDeclaration, propertyName, checker) + } + + return undefined +} + +function findReturnStatementExpressions(node: ts.Node): ts.Expression[] { + const expressions: ts.Expression[] = [] + + const visit = (current: ts.Node) => { + if ( + current !== node + && ( + ts.isArrowFunction(current) + || ts.isFunctionExpression(current) + || ts.isFunctionDeclaration(current) + || ts.isMethodDeclaration(current) + ) + ) { + return + } + + if (ts.isReturnStatement(current) && current.expression) + expressions.push(current.expression) + + current.forEachChild(visit) + } + + visit(node) + return expressions +} + +function findFunctionLikeReturnTarget( + declaration: ts.FunctionLikeDeclarationBase, + checker: ts.TypeChecker, +): EditTarget | undefined { + if (!declaration.body) + return undefined + + return findTargetInFunctionBody( + declaration.body, + expression => findTargetFromExpression(expression, checker), + ) +} + +function findCallExpressionDeclarationTarget( + callExpression: ts.CallExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(skipOuterExpressions(callExpression.expression)), checker) + if (!declaration) + return undefined + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (!functionLikeDeclaration) + return undefined + + return findFunctionLikeReturnTarget(functionLikeDeclaration, checker) +} + +function findCallbackArgumentTarget( + callExpression: ts.CallExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const callee = skipOuterExpressions(callExpression.expression) + const calleeName = ts.isIdentifier(callee) ? callee.text : getCallExpressionPropertyAccess(callExpression)?.name.text + + if (calleeName !== 'useCallback' && calleeName !== 'useMemo') + return undefined + + const callback = getFunctionExpressionArgument(callExpression) + if (!callback) + return undefined + + return findFunctionLikeReturnTarget(callback, checker) +} + +function findReferencedDeclarationInitializerTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (!ts.isIdentifier(innerExpression)) + return undefined + + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(innerExpression), checker) + if (!declaration) + return undefined + + if (ts.isBindingElement(declaration)) { + const propertyName = declaration.propertyName + ? getPropertyNameText(declaration.propertyName) + : getPropertyNameText(declaration.name) + + const variableDeclaration = declaration.parent.parent + if (propertyName && ts.isVariableDeclaration(variableDeclaration) && variableDeclaration.initializer) { + const namedPropertyTarget = findNamedPropertyTarget(variableDeclaration.initializer, propertyName, checker) + if (namedPropertyTarget) + return namedPropertyTarget + } + } + + if (ts.isParameter(declaration)) { + const collectionExpression = getParameterCollectionExpression(declaration) + if (collectionExpression) { + const collectionTarget = findTargetFromExpression(collectionExpression, checker) + if (collectionTarget) + return collectionTarget + } + } + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (functionLikeDeclaration) { + const functionTarget = findFunctionLikeReturnTarget(functionLikeDeclaration, checker) + if (functionTarget) + return functionTarget + } + + if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) + return undefined + + const collectionCallbackTarget = findCollectionCallbackTarget(declaration.initializer, checker) + if (collectionCallbackTarget) + return collectionCallbackTarget + + const initializerTarget = findTargetFromExpression(declaration.initializer, checker) + if (initializerTarget) + return initializerTarget + + const nestedTarget = findNestedContainerTarget(declaration.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(declaration.initializer))) + return undefined + + return createExpressionTarget(declaration.initializer) +} + +function findCollectionCallbackTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (!ts.isCallExpression(innerExpression)) + return undefined + + const callee = getCallExpressionPropertyAccess(innerExpression) + if (!callee) + return undefined + + if (callee.name.text !== 'map' && callee.name.text !== 'flatMap') + return undefined + + const callback = getFunctionExpressionArgument(innerExpression) + if (!callback) + return undefined + + return findFunctionLikeReturnTarget(callback, checker) +} + +function findJsxComponentDeclarationTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const openingLikeElement = findAncestor(token, node => + ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) + if (!openingLikeElement) + return undefined + + const tagName = openingLikeElement.tagName + if (!ts.isIdentifier(tagName)) + return undefined + + const symbol = checker.getSymbolAtLocation(tagName) + const declaration = symbol?.valueDeclaration + if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) + return undefined + + if (!typeIncludesUndefined(checker.getTypeAtLocation(declaration.initializer))) + return undefined + + return createExpressionTarget(declaration.initializer) +} + +function findObjectLiteralPropertyTarget( + objectLiteral: ts.ObjectLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + for (const property of objectLiteral.properties) { + if (ts.isSpreadAssignment(property)) { + const directTarget = findTargetFromExpression(property.expression, checker) + if (directTarget) + return directTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(property.expression))) + return createExpressionTarget(property.expression) + continue + } + + if (ts.isShorthandPropertyAssignment(property)) { + if (typeIncludesUndefined(checker.getTypeAtLocation(property.name))) + return createShorthandPropertyTarget(property) + continue + } + + if (ts.isPropertyAssignment(property)) { + const directTarget = findTargetFromExpression(property.initializer, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(property.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(property.initializer))) + return createExpressionTarget(property.initializer) + } + } + + return undefined +} + +function findArrayLiteralElementTarget( + arrayLiteral: ts.ArrayLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const iterableFallbackTarget = createArrayLiteralIterableFallbackTarget(arrayLiteral, checker) + if (iterableFallbackTarget) + return iterableFallbackTarget + + for (const element of arrayLiteral.elements) { + if (ts.isSpreadElement(element)) { + const directTarget = findTargetFromExpression(element.expression, checker) + if (directTarget) + return directTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(element.expression))) + return createExpressionTarget(element.expression) + continue + } + + const directTarget = findTargetFromExpression(element, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(element, checker) + if (nestedTarget) + return nestedTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(element))) + return createExpressionTarget(element) + } + + for (let index = arrayLiteral.elements.length - 1; index >= 0; index -= 1) { + const element = arrayLiteral.elements[index] + if (!element) + continue + + if (ts.isSpreadElement(element)) + continue + + if (!isAlreadyNonNull(element)) + return createExpressionTarget(element) + } + + return undefined +} + +function findNestedContainerTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (ts.isObjectLiteralExpression(innerExpression)) + return findObjectLiteralPropertyTarget(innerExpression, checker) + + if (ts.isArrayLiteralExpression(innerExpression)) + return findArrayLiteralElementTarget(innerExpression, checker) + + return undefined +} + +function findAccessDiagnosticTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const directExpression = findTightestExpression(sourceFile, start, end) + if (directExpression) { + if (typeIncludesUndefined(checker.getTypeAtLocation(directExpression)) && !isAlreadyNonNull(directExpression)) + return createExpressionTarget(directExpression) + + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(directExpression, checker) + if (referencedDeclarationTarget && isExpressionTarget(referencedDeclarationTarget) && !isAlreadyNonNull(referencedDeclarationTarget.expression)) + return referencedDeclarationTarget + } + + const bindingPatternTarget = findVariableDeclarationInitializerTarget(sourceFile, token, checker) + if (bindingPatternTarget && isExpressionTarget(bindingPatternTarget) && !isAlreadyNonNull(bindingPatternTarget.expression)) + return bindingPatternTarget + + const accessTarget = findUndefinedAccessTarget(token, checker) + if (accessTarget && isExpressionTarget(accessTarget) && !isAlreadyNonNull(accessTarget.expression)) + return accessTarget + + const propertyAccessTarget = findPropertyAccessExpressionTarget(token, start) + if (propertyAccessTarget && isExpressionTarget(propertyAccessTarget) && !isAlreadyNonNull(propertyAccessTarget.expression)) + return propertyAccessTarget + + return undefined +} + +function findDiagnosticCandidate( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + diagnosticCode: number, + checker: ts.TypeChecker, +): ts.Expression | undefined { + if (diagnosticCode === 2322) { + const directExpression = findTightestExpression(sourceFile, start, end) + if (directExpression && typeIncludesUndefined(checker.getTypeAtLocation(directExpression))) + return directExpression + + return findAssignmentLikeCandidate(token, sourceFile, start, end) + } + + if (diagnosticCode === 2345) + return findArgumentCandidate(token, sourceFile, start, end) + + if (diagnosticCode === 2722) { + const current = findTightestExpression(sourceFile, start, end) + if (current && ts.isCallExpression(current)) + return current.expression + + return findTightestExpression(sourceFile, start, end) + } + + return findTightestExpression(sourceFile, start, end) +} + +function resolveEditTarget( + sourceFile: ts.SourceFile, + diagnostic: ts.DiagnosticWithLocation, + checker: ts.TypeChecker, +): EditTarget | undefined { + const start = diagnostic.start + const end = diagnostic.start + diagnostic.length + const token = getTokenAtPosition(sourceFile, start) + + const shorthandTarget = findShorthandPropertyTarget(token, checker) + if (shorthandTarget) + return shorthandTarget + + const propertyAssignmentTarget = findPropertyAssignmentInitializerTarget(token, start, checker) + if (propertyAssignmentTarget) + return propertyAssignmentTarget + + const jsxSpreadTarget = findJsxSpreadAttributeTarget(token, checker) + if (jsxSpreadTarget && isExpressionTarget(jsxSpreadTarget) && !isAlreadyNonNull(jsxSpreadTarget.expression)) + return jsxSpreadTarget + + const jsxAttribute = findAncestor(token, ts.isJsxAttribute) + const jsxExpression = jsxAttribute ? getExpressionFromJsxAttribute(jsxAttribute) : undefined + + if ( + ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) + && jsxExpression + && typeIncludesUndefined(checker.getTypeAtLocation(jsxExpression)) + && !isAlreadyNonNull(jsxExpression) + ) { + return findTargetFromExpression(jsxExpression, checker) + ?? createExpressionTarget(jsxExpression) + } + + if (ACCESS_DIAGNOSTIC_CODES.has(diagnostic.code)) + return findAccessDiagnosticTarget(sourceFile, token, start, end, checker) + + if (diagnostic.code === 2322 || diagnostic.code === 2488) { + const arrayDestructuringTarget = findArrayDestructuringTarget(token, checker) + if (arrayDestructuringTarget) + return arrayDestructuringTarget + } + + if (diagnostic.code === 2538) { + const elementAccessTarget = findElementAccessArgumentTarget(token) + if (elementAccessTarget && isExpressionTarget(elementAccessTarget) && !isAlreadyNonNull(elementAccessTarget.expression)) + return elementAccessTarget + } + + if (diagnostic.code === 7006) + return findImplicitAnyParameterTarget(token) + + if (diagnostic.code === 2488) { + const iterableTarget = findIterableTarget(sourceFile, token, start, end, checker) + if (iterableTarget && (!isExpressionTarget(iterableTarget) || !isAlreadyNonNull(iterableTarget.expression))) + return iterableTarget + } + + if (diagnostic.code === 2604 || diagnostic.code === 2786) { + const jsxComponentTarget = findJsxComponentDeclarationTarget(token, checker) + if (jsxComponentTarget && isExpressionTarget(jsxComponentTarget) && !isAlreadyNonNull(jsxComponentTarget.expression)) + return jsxComponentTarget + } + + const candidate = findDiagnosticCandidate(sourceFile, token, start, end, diagnostic.code, checker) + + if (!candidate) { + return jsxExpression && !isAlreadyNonNull(jsxExpression) + ? createExpressionTarget(jsxExpression) + : undefined + } + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code)) { + if ( + diagnostic.code === 2345 + && typeIncludesUndefined(checker.getTypeAtLocation(candidate)) + && !isAlreadyNonNull(candidate) + && ( + ts.isIdentifier(candidate) + || ts.isElementAccessExpression(candidate) + || ts.isPropertyAccessExpression(candidate) + ) + ) { + return createExpressionTarget(candidate) + } + + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(candidate, checker) + if (referencedDeclarationTarget && isExpressionTarget(referencedDeclarationTarget) && !isAlreadyNonNull(referencedDeclarationTarget.expression)) + return referencedDeclarationTarget + + const collectionCallbackTarget = findCollectionCallbackTarget(candidate, checker) + if (collectionCallbackTarget && isExpressionTarget(collectionCallbackTarget) && !isAlreadyNonNull(collectionCallbackTarget.expression)) + return collectionCallbackTarget + } + + const targetFromCandidate = findTargetFromExpression(candidate, checker) + if (targetFromCandidate && (!isExpressionTarget(targetFromCandidate) || !isAlreadyNonNull(targetFromCandidate.expression))) + return targetFromCandidate + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && (ts.isArrowFunction(candidate) || ts.isFunctionExpression(candidate))) { + const functionTarget = findFunctionLikeReturnTarget(candidate, checker) + if (functionTarget && isExpressionTarget(functionTarget) && !isAlreadyNonNull(functionTarget.expression)) + return functionTarget + } + + const nestedContainerTarget = findNestedContainerTarget(candidate, checker) + if (nestedContainerTarget) + return nestedContainerTarget + + if (isAlreadyNonNull(candidate)) + return undefined + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && ts.isObjectLiteralExpression(candidate)) { + const objectLiteralTarget = findObjectLiteralPropertyTarget(candidate, checker) + if (objectLiteralTarget) + return objectLiteralTarget + } + + if (diagnostic.code === 2322) { + const declarationInitializerTarget = findVariableDeclarationInitializerTarget(sourceFile, token, checker) + if (declarationInitializerTarget && isExpressionTarget(declarationInitializerTarget) && !isAlreadyNonNull(declarationInitializerTarget.expression)) + return declarationInitializerTarget + } + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && !typeIncludesUndefined(checker.getTypeAtLocation(candidate))) + return undefined + + return createExpressionTarget(candidate) +} + +function createEditForTarget( + target: EditTarget, + printer: ts.Printer, +): TextEdit { + const sourceFile = target.sourceFile + + if (target.kind === 'direct-edit') { + return { + end: target.end, + expectedText: sourceFile.text.slice(target.start, target.end), + replacement: target.replacement, + start: target.start, + } + } + + if (target.kind === 'shorthand-property') { + const name = target.property.name + const nonNullName = printer.printNode( + ts.EmitHint.Expression, + ts.factory.createNonNullExpression(name), + sourceFile, + ) + return { + end: target.property.getEnd(), + expectedText: sourceFile.text.slice(target.property.getStart(sourceFile), target.property.getEnd()), + replacement: `${name.getText(sourceFile)}: ${nonNullName}`, + start: target.property.getStart(sourceFile), + } + } + + const replacement = shouldPrintInlineNonNullAssertion(target.expression) + ? `${target.expression.getText(sourceFile)}!` + : printer.printNode( + ts.EmitHint.Expression, + ts.factory.createNonNullExpression(target.expression), + sourceFile, + ) + + return { + end: target.expression.getEnd(), + expectedText: sourceFile.text.slice(target.expression.getStart(sourceFile), target.expression.getEnd()), + replacement, + start: target.expression.getStart(sourceFile), + } +} + +function hasOverlap(existingEdits: TextEdit[], nextEdit: TextEdit): boolean { + return existingEdits.some(edit => nextEdit.start < edit.end && edit.start < nextEdit.end) +} + +function applyEdits(text: string, edits: TextEdit[]): { appliedEditCount: number, text: string } { + let currentText = text + let appliedEditCount = 0 + + for (const edit of edits.sort((left, right) => right.start - left.start)) { + if (edit.replacement.length > currentText.length * 4) + continue + + try { + currentText = `${currentText.slice(0, edit.start)}${edit.replacement}${currentText.slice(edit.end)}` + appliedEditCount += 1 + } + catch { + continue + } + } + + return { + appliedEditCount, + text: currentText, + } +} + +function isValidEditRange(text: string, edit: TextEdit): boolean { + return Number.isInteger(edit.start) + && Number.isInteger(edit.end) + && edit.start >= 0 + && edit.end >= edit.start + && edit.end <= text.length +} + +function filterApplicableEdits(text: string, edits: TextEdit[]): TextEdit[] { + return edits.filter(edit => isValidEditRange(text, edit) && (!edit.expectedText || text.slice(edit.start, edit.end) === edit.expectedText)) +} + +export async function runMigration(options: CliOptions) { + const projectPath = path.resolve(process.cwd(), options.project) + const parsedConfig = parseTsConfig(projectPath) + const matchesRequestedFile = createFileMatcher(options.files) + const targetFiles = parsedConfig.fileNames + .map(normalizeFileName) + .filter(isTargetFile) + .filter(matchesRequestedFile) + + if (targetFiles.length === 0) { + console.error('No matching TypeScript source files found.') + process.exitCode = 1 + return { converged: false, totalEdits: 0 } + } + + const fileTexts = new Map() + const printer = ts.createPrinter() + const migrationRootNames = options.useFullProjectRoots + ? parsedConfig.fileNames.map(normalizeFileName) + : getMigrationRootNames(parsedConfig, targetFiles) + + let totalEdits = 0 + let converged = false + let previousProgram: ts.Program | undefined + + for (let iteration = 1; iteration <= options.maxIterations; iteration += 1) { + const program = createMigrationProgram(migrationRootNames, parsedConfig, fileTexts, previousProgram) + const checker = program.getTypeChecker() + const editsByFile = new Map() + + for (const fileName of targetFiles) { + const sourceFile = program.getSourceFile(fileName) + if (!sourceFile) + continue + + const diagnostics = program + .getSemanticDiagnostics(sourceFile) + .filter((diagnostic): diagnostic is ts.DiagnosticWithLocation => { + return diagnostic.file !== undefined + && diagnostic.start !== undefined + && diagnostic.length !== undefined + && SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code) + }) + + if (options.verbose && diagnostics.length > 0) + console.log(`file ${path.relative(process.cwd(), fileName)}: ${diagnostics.length} supported diagnostic(s)`) + + for (const diagnostic of diagnostics) { + const target = resolveEditTarget(sourceFile, diagnostic, checker) + if (!target) { + if (options.verbose) + console.log(`unresolved ${formatDiagnostic(diagnostic)}`) + continue + } + + const editFileName = target.sourceFile.fileName + const edit = createEditForTarget(target, printer) + const existing = editsByFile.get(editFileName) ?? [] + if (hasOverlap(existing, edit)) + continue + + existing.push(edit) + editsByFile.set(editFileName, existing) + + if (options.verbose) { + const position = target.sourceFile.getLineAndCharacterOfPosition(edit.start) + console.log(`iter ${iteration}: ${path.relative(process.cwd(), editFileName)}:${position.line + 1}:${position.character + 1} -> add !`) + } + } + } + + if (editsByFile.size === 0) { + console.log(`No more supported diagnostics after ${iteration - 1} iteration(s).`) + converged = true + break + } + + let iterationEditCount = 0 + + for (const [fileName, edits] of editsByFile) { + const currentText = fileTexts.get(fileName) ?? await fs.readFile(fileName, 'utf8') + const applicableEdits = filterApplicableEdits(currentText, edits) + if (applicableEdits.length === 0) + continue + + const { appliedEditCount, text: editedText } = applyEdits(currentText, applicableEdits) + if (appliedEditCount === 0) + continue + + const nextText = normalizeMalformedAssertions(editedText) + if (nextText === currentText) { + if (options.verbose) { + const firstEdit = applicableEdits[0] + console.log(`iter ${iteration}: no-op after normalization for ${path.relative(process.cwd(), fileName)}:${firstEdit?.start ?? 0} ${JSON.stringify(firstEdit ? currentText.slice(firstEdit.start, firstEdit.end) : '')} -> ${JSON.stringify(firstEdit?.replacement ?? '')}`) + } + continue + } + + fileTexts.set(fileName, nextText) + iterationEditCount += appliedEditCount + } + + totalEdits += iterationEditCount + console.log(`Iteration ${iteration}: ${iterationEditCount} edit(s) across ${editsByFile.size} file(s).`) + previousProgram = program + } + + if (totalEdits === 0) { + console.log('No supported noUncheckedIndexedAccess-style diagnostics were migrated.') + return { converged, totalEdits } + } + + if (!options.write) { + if (!converged) + console.log(`Stopped after reaching --max-iterations=${options.maxIterations}.`) + + console.log(`Dry run complete. ${totalEdits} edit(s) are ready. Re-run with --write to apply them.`) + return { converged, totalEdits } + } + + const changedFiles = Array.from(fileTexts.entries()) + await Promise.all(changedFiles.map(async ([fileName, text]) => { + await fs.writeFile(fileName, text) + })) + + if (!converged) + console.log(`Stopped after reaching --max-iterations=${options.maxIterations}.`) + + console.log(`Wrote ${totalEdits} edit(s) to ${changedFiles.length} file(s).`) + return { converged, totalEdits } +} + +export async function runMigrationCommand(argv: string[]) { + await runMigration(parseArgs(argv)) +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts new file mode 100644 index 0000000000..d3b88736fc --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts @@ -0,0 +1,51 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { normalizeMalformedAssertions } from './migrate' + +const ROOT = process.cwd() +const EXTENSIONS = new Set(['.ts', '.tsx']) + +async function collectFiles(directory: string): Promise { + const entries = await fs.readdir(directory, { withFileTypes: true }) + const files: string[] = [] + + for (const entry of entries) { + if (entry.name === 'node_modules' || entry.name === '.next') + continue + + const absolutePath = path.join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...await collectFiles(absolutePath)) + continue + } + + if (!EXTENSIONS.has(path.extname(entry.name))) + continue + + files.push(absolutePath) + } + + return files +} + +async function main() { + const files = await collectFiles(ROOT) + let changedFileCount = 0 + + await Promise.all(files.map(async (fileName) => { + const currentText = await fs.readFile(fileName, 'utf8') + const nextText = normalizeMalformedAssertions(currentText) + if (nextText === currentText) + return + + await fs.writeFile(fileName, nextText) + changedFileCount += 1 + })) + + console.log(`Normalized malformed assertion syntax in ${changedFileCount} file(s).`) +} + +export async function runNormalizeCommand(_argv: string[]) { + await main() +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts new file mode 100644 index 0000000000..ad655e4f11 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts @@ -0,0 +1,325 @@ +import { execFile } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { promisify } from 'node:util' +import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate' + +const execFileAsync = promisify(execFile) +const DIAGNOSTIC_PATTERN = /^(.+?\.(?:ts|tsx))\((\d+),(\d+)\): error TS(\d+): (.+)$/ +const DEFAULT_BATCH_SIZE = 100 +const DEFAULT_BATCH_ITERATIONS = 5 +const DEFAULT_MAX_ROUNDS = 20 +const TYPECHECK_CACHE_DIR = path.join(os.tmpdir(), 'migrate-no-unchecked-indexed-access') + +type CliOptions = { + batchIterations: number + batchSize: number + maxRounds: number + project: string + verbose: boolean +} + +type DiagnosticEntry = { + code: number + fileName: string + line: number + message: string +} + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + batchIterations: DEFAULT_BATCH_ITERATIONS, + batchSize: DEFAULT_BATCH_SIZE, + maxRounds: DEFAULT_MAX_ROUNDS, + project: 'tsconfig.json', + verbose: false, + } + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + + if (arg === '--') + continue + + if (arg === '--verbose') { + options.verbose = true + continue + } + + if (arg === '--project') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --project') + + options.project = value + i += 1 + continue + } + + if (arg === '--batch-size') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --batch-size') + + options.batchSize = value + i += 1 + continue + } + + if (arg === '--batch-iterations') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --batch-iterations') + + options.batchIterations = value + i += 1 + continue + } + + if (arg === '--max-rounds') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --max-rounds') + + options.maxRounds = value + i += 1 + continue + } + + throw new Error(`Unknown option: ${arg}`) + } + + return options +} + +function getTypeCheckBuildInfoPath(projectPath: string): string { + const hash = createHash('sha1') + .update(projectPath) + .digest('hex') + .slice(0, 16) + + return path.join(TYPECHECK_CACHE_DIR, `${hash}.tsbuildinfo`) +} + +async function runTypeCheck( + project: string, + options?: { + incremental?: boolean + }, +): Promise<{ diagnostics: DiagnosticEntry[], exitCode: number, rawOutput: string }> { + const projectPath = path.resolve(process.cwd(), project) + const projectDirectory = path.dirname(projectPath) + const buildInfoPath = getTypeCheckBuildInfoPath(projectPath) + const incremental = options?.incremental ?? true + + await fs.mkdir(TYPECHECK_CACHE_DIR, { recursive: true }) + + const tscArgs = ['exec', 'tsc', '--noEmit', '--pretty', 'false'] + if (incremental) { + tscArgs.push('--incremental', '--tsBuildInfoFile', buildInfoPath) + } + else { + tscArgs.push('--incremental', 'false') + } + tscArgs.push('--project', projectPath) + + try { + const { stdout, stderr } = await execFileAsync('pnpm', tscArgs, { + cwd: projectDirectory, + env: { + ...process.env, + NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192', + }, + maxBuffer: 1024 * 1024 * 32, + }) + + const rawOutput = `${stdout}${stderr}`.trim() + return { + diagnostics: parseDiagnostics(rawOutput, projectDirectory), + exitCode: 0, + rawOutput, + } + } + catch (error) { + const exitCode = typeof error === 'object' && error && 'code' in error && typeof error.code === 'number' + ? error.code + : 1 + const stdout = typeof error === 'object' && error && 'stdout' in error && typeof error.stdout === 'string' + ? error.stdout + : '' + const stderr = typeof error === 'object' && error && 'stderr' in error && typeof error.stderr === 'string' + ? error.stderr + : '' + const rawOutput = `${stdout}${stderr}`.trim() + + return { + diagnostics: parseDiagnostics(rawOutput, projectDirectory), + exitCode, + rawOutput, + } + } +} + +function parseDiagnostics(rawOutput: string, projectDirectory: string): DiagnosticEntry[] { + return rawOutput + .split('\n') + .map(line => line.trim()) + .flatMap((line) => { + const match = line.match(DIAGNOSTIC_PATTERN) + if (!match) + return [] + + return [{ + code: Number(match[4]), + fileName: path.resolve(projectDirectory, match[1]!), + line: Number(match[2]), + message: match[5] ?? '', + }] + }) +} + +function summarizeCodes(diagnostics: DiagnosticEntry[]): string { + const counts = new Map() + for (const diagnostic of diagnostics) + counts.set(diagnostic.code, (counts.get(diagnostic.code) ?? 0) + 1) + + return Array.from(counts.entries()) + .sort((left, right) => right[1] - left[1]) + .slice(0, 8) + .map(([code, count]) => `TS${code}:${count}`) + .join(', ') +} + +function chunk(items: T[], size: number): T[][] { + const batches: T[][] = [] + for (let i = 0; i < items.length; i += size) + batches.push(items.slice(i, i + size)) + + return batches +} + +async function runBatchMigration(options: CliOptions) { + for (let round = 1; round <= options.maxRounds; round += 1) { + const { diagnostics, exitCode, rawOutput } = await runTypeCheck(options.project) + if (exitCode === 0) { + const finalCheck = await runTypeCheck(options.project, { incremental: false }) + if (finalCheck.exitCode !== 0) { + const finalDiagnostics = finalCheck.diagnostics + console.log(`Final cold type check found ${finalDiagnostics.length} diagnostic(s). ${summarizeCodes(finalDiagnostics)}`) + + if (options.verbose) { + for (const diagnostic of finalDiagnostics.slice(0, 40)) + console.log(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + + const finalSupportedFiles = Array.from(new Set( + finalDiagnostics + .filter(diagnostic => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + .map(diagnostic => diagnostic.fileName), + )) + + if (finalSupportedFiles.length > 0) { + console.log(` Final pass batch: ${finalSupportedFiles.length} file(s)`) + let finalResult = await runMigration({ + files: finalSupportedFiles, + maxIterations: options.batchIterations, + project: options.project, + verbose: options.verbose, + write: true, + }) + + if (finalResult.totalEdits === 0) { + console.log(' No edits produced; retrying final pass with full project roots.') + finalResult = await runMigration({ + files: finalSupportedFiles, + maxIterations: options.batchIterations, + project: options.project, + useFullProjectRoots: true, + verbose: options.verbose, + write: true, + }) + } + + if (finalResult.totalEdits > 0) + continue + } + + if (finalCheck.rawOutput) + process.stderr.write(`${finalCheck.rawOutput}\n`) + process.exitCode = 1 + return + } + + console.log(`Type check passed after ${round - 1} migration round(s).`) + return + } + + const supportedDiagnostics = diagnostics.filter(diagnostic => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + const unsupportedDiagnostics = diagnostics.filter(diagnostic => !SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + const supportedFiles = Array.from(new Set(supportedDiagnostics.map(diagnostic => diagnostic.fileName))) + + console.log(`Round ${round}: ${diagnostics.length} diagnostic(s). ${summarizeCodes(diagnostics)}`) + + if (options.verbose) { + for (const diagnostic of diagnostics.slice(0, 40)) + console.log(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + + if (supportedFiles.length === 0) { + console.error('No supported diagnostics remain to migrate.') + if (unsupportedDiagnostics.length > 0) { + console.error('Remaining unsupported diagnostics:') + for (const diagnostic of unsupportedDiagnostics.slice(0, 40)) + console.error(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + if (rawOutput) + process.stderr.write(`${rawOutput}\n`) + process.exitCode = 1 + return + } + + let roundEdits = 0 + const batches = chunk(supportedFiles, options.batchSize) + + for (const [index, batch] of batches.entries()) { + console.log(` Batch ${index + 1}/${batches.length}: ${batch.length} file(s)`) + let result = await runMigration({ + files: batch, + maxIterations: options.batchIterations, + project: options.project, + verbose: options.verbose, + write: true, + }) + + if (result.totalEdits === 0) { + console.log(' No edits produced; retrying batch with full project roots.') + result = await runMigration({ + files: batch, + maxIterations: options.batchIterations, + project: options.project, + useFullProjectRoots: true, + verbose: options.verbose, + write: true, + }) + } + + roundEdits += result.totalEdits + } + + if (roundEdits === 0) { + console.error('Migration script made no edits in this round; stopping to avoid an infinite loop.') + process.exitCode = 1 + return + } + } + + console.error(`Reached --max-rounds=${options.maxRounds} before type check passed.`) + process.exitCode = 1 +} + +export async function runBatchMigrationCommand(argv: string[]) { + await runBatchMigration(parseArgs(argv)) +} diff --git a/packages/migrate-no-unchecked-indexed-access/tsconfig.json b/packages/migrate-no-unchecked-indexed-access/tsconfig.json new file mode 100644 index 0000000000..aeb24e1df5 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@dify/tsconfig/node.json" +} diff --git a/packages/migrate-no-unchecked-indexed-access/vite.config.ts b/packages/migrate-no-unchecked-indexed-access/vite.config.ts new file mode 100644 index 0000000000..ac4aed1a06 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite-plus' + +export default defineConfig({ + pack: { + clean: true, + deps: { + neverBundle: ['typescript'], + }, + entry: ['src/cli.ts'], + format: ['esm'], + outDir: 'dist', + platform: 'node', + sourcemap: true, + target: 'node22', + treeshake: true, + }, +}) diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json new file mode 100644 index 0000000000..707f1aff56 --- /dev/null +++ b/packages/tsconfig/base.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + "module": "preserve", + "noEmit": true + } +} diff --git a/packages/tsconfig/nextjs.json b/packages/tsconfig/nextjs.json new file mode 100644 index 0000000000..81c6436a97 --- /dev/null +++ b/packages/tsconfig/nextjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./web.json", + "compilerOptions": { + "plugins": [ + { + "name": "next" + } + ] + } +} diff --git a/packages/tsconfig/node.json b/packages/tsconfig/node.json new file mode 100644 index 0000000000..832dab2b09 --- /dev/null +++ b/packages/tsconfig/node.json @@ -0,0 +1,7 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "lib": ["es2022"], + "types": ["node"] + } +} diff --git a/packages/tsconfig/package.json b/packages/tsconfig/package.json new file mode 100644 index 0000000000..52cafc5bb3 --- /dev/null +++ b/packages/tsconfig/package.json @@ -0,0 +1,11 @@ +{ + "name": "@dify/tsconfig", + "version": "0.0.0-private", + "private": true, + "exports": { + "./base.json": "./base.json", + "./nextjs.json": "./nextjs.json", + "./node.json": "./node.json", + "./web.json": "./web.json" + } +} diff --git a/packages/tsconfig/web.json b/packages/tsconfig/web.json new file mode 100644 index 0000000000..9f3ba7c121 --- /dev/null +++ b/packages/tsconfig/web.json @@ -0,0 +1,7 @@ +{ + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["es2022", "dom", "dom.iterable"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4444981601..914bc342e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,8 +274,8 @@ catalogs: specifier: 10.6.0 version: 10.6.0 dompurify: - specifier: 3.3.3 - version: 3.3.3 + specifier: 3.4.0 + version: 3.4.0 echarts: specifier: 6.0.0 version: 6.0.0 @@ -334,8 +334,8 @@ catalogs: specifier: 2.3.6 version: 2.3.6 hono: - specifier: 4.12.12 - version: 4.12.12 + specifier: 4.12.14 + version: 4.12.14 html-entities: specifier: 2.6.0 version: 2.6.0 @@ -384,6 +384,9 @@ catalogs: lexical: specifier: 0.43.0 version: 0.43.0 + loro-crdt: + specifier: 1.10.8 + version: 1.10.8 mermaid: specifier: 11.14.0 version: 11.14.0 @@ -471,6 +474,9 @@ catalogs: shiki: specifier: 4.0.2 version: 4.0.2 + socket.io-client: + specifier: 4.8.3 + version: 4.8.3 sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -520,8 +526,8 @@ catalogs: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 vite-plus: - specifier: 0.1.16 - version: 0.1.16 + specifier: 0.1.18 + version: 0.1.18 vitest-canvas-mock: specifier: 1.1.4 version: 1.1.4 @@ -545,8 +551,8 @@ overrides: flatted@<=3.4.1: 3.4.2 glob@>=10.2.0 <10.5.0: 11.1.0 is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - lodash@>=4.0.0 <= 4.17.23: 4.18.0 lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 picomatch@<2.3.2: 2.3.2 picomatch@>=4.0.0 <4.0.4: 4.0.4 rollup@>=4.0.0 <4.59.0: 4.59.0 @@ -559,8 +565,8 @@ overrides: svgo@>=3.0.0 <3.3.3: 3.3.3 tar@<=7.5.10: 7.5.11 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.16 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 + vite: npm:@voidzero-dev/vite-plus-core@0.1.18 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 @@ -568,18 +574,36 @@ importers: .: devDependencies: + '@antfu/eslint-config': + specifier: 'catalog:' + version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)(vitest@4.1.4) + eslint: + specifier: 'catalog:' + version: 10.2.0(jiti@2.6.1) + eslint-markdown: + specifier: 'catalog:' + version: 0.6.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-markdown-preferences: + specifier: 'catalog:' + version: 0.41.1(@eslint/markdown@8.0.1)(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-no-barrel-files: + specifier: 'catalog:' + version: 1.3.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 - version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) e2e: devDependencies: '@cucumber/cucumber': specifier: 'catalog:' version: 12.8.0 + '@dify/tsconfig': + specifier: workspace:* + version: link:../packages/tsconfig '@playwright/test': specifier: 'catalog:' version: 1.59.1 @@ -593,11 +617,30 @@ importers: specifier: 'catalog:' version: 6.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 - version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + + packages/dify-ui: + dependencies: + clsx: + specifier: 'catalog:' + version: 2.1.1 + tailwind-merge: + specifier: 'catalog:' + version: 3.5.0 + devDependencies: + '@dify/tsconfig': + specifier: workspace:* + version: link:../tsconfig + tailwindcss: + specifier: 'catalog:' + version: 4.2.2 + typescript: + specifier: 'catalog:' + version: 6.0.2 packages/iconify-collections: devDependencies: @@ -605,8 +648,32 @@ importers: specifier: 'catalog:' version: 0.1.2 + packages/migrate-no-unchecked-indexed-access: + dependencies: + typescript: + specifier: 'catalog:' + version: 6.0.2 + devDependencies: + '@dify/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@types/node': + specifier: 'catalog:' + version: 25.6.0 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-plus: + specifier: 'catalog:' + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + + packages/tsconfig: {} + sdks/nodejs-client: devDependencies: + '@dify/tsconfig': + specifier: workspace:* + version: link:../../packages/tsconfig '@eslint/js': specifier: 'catalog:' version: 10.0.1(eslint@10.2.0(jiti@2.6.1)) @@ -621,7 +688,7 @@ importers: version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 4.1.4(@voidzero-dev/vite-plus-test@0.1.18) eslint: specifier: 'catalog:' version: 10.2.0(jiti@2.6.1) @@ -629,14 +696,14 @@ importers: specifier: 'catalog:' version: 6.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 - version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.16 - version: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.18 + version: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' web: dependencies: @@ -739,9 +806,6 @@ importers: client-only: specifier: 'catalog:' version: 0.0.1 - clsx: - specifier: 'catalog:' - version: 2.1.1 cmdk: specifier: 'catalog:' version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -759,7 +823,7 @@ importers: version: 10.6.0 dompurify: specifier: 'catalog:' - version: 3.3.3 + version: 3.4.0 echarts: specifier: 'catalog:' version: 6.0.0 @@ -829,6 +893,9 @@ importers: lexical: specifier: 'catalog:' version: 0.43.0 + loro-crdt: + specifier: 'catalog:' + version: 1.10.8 mermaid: specifier: 'catalog:' version: 11.14.0 @@ -910,6 +977,9 @@ importers: shiki: specifier: 'catalog:' version: 4.0.2 + socket.io-client: + specifier: 'catalog:' + version: 4.8.3 sortablejs: specifier: 'catalog:' version: 1.15.7 @@ -922,9 +992,6 @@ importers: string-ts: specifier: 'catalog:' version: 2.3.1 - tailwind-merge: - specifier: 'catalog:' - version: 3.5.0 tldts: specifier: 'catalog:' version: 7.0.28 @@ -949,13 +1016,16 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2) + version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.18)(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2) '@chromatic-com/storybook': specifier: 'catalog:' version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@dify/iconify-collections': specifier: workspace:* version: link:../packages/iconify-collections + '@dify/tsconfig': + specifier: workspace:* + version: link:../packages/tsconfig '@egoist/tailwindcss-icons': specifier: 'catalog:' version: 1.9.2(tailwindcss@4.2.2) @@ -964,16 +1034,19 @@ importers: version: 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@hono/node-server': specifier: 'catalog:' - version: 1.19.14(hono@4.12.12) + version: 1.19.14(hono@4.12.14) '@iconify-json/heroicons': specifier: 'catalog:' version: 1.2.3 '@iconify-json/ri': specifier: 'catalog:' version: 1.2.10 + '@langgenius/dify-ui': + specifier: workspace:* + version: link:../packages/dify-ui '@mdx-js/loader': specifier: 'catalog:' - version: 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) + version: 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@mdx-js/react': specifier: 'catalog:' version: 3.1.1(@types/react@19.2.14)(react@19.2.5) @@ -985,13 +1058,13 @@ importers: version: 16.2.3 '@next/mdx': specifier: 'catalog:' - version: 16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)) + version: 16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)) '@rgrove/parse-xml': specifier: 'catalog:' version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/addon-links': specifier: 'catalog:' version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) @@ -1003,7 +1076,7 @@ importers: version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) + version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': specifier: 'catalog:' version: 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) @@ -1012,7 +1085,7 @@ importers: version: 4.2.2 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' version: 5.99.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -1078,13 +1151,13 @@ importers: version: 7.0.0-dev.20260413.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.24(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5) + version: 0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.5) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 4.1.4(@voidzero-dev/vite-plus-test@0.1.18) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1099,7 +1172,7 @@ importers: version: 0.6.1(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2) + version: 4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2) eslint-plugin-hyoban: specifier: 'catalog:' version: 0.14.1(eslint@10.2.0(jiti@2.6.1)) @@ -1123,16 +1196,16 @@ importers: version: 20.9.0 hono: specifier: 'catalog:' - version: 4.12.12 + version: 4.12.14 knip: specifier: 'catalog:' - version: 6.4.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + version: 6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) postcss: specifier: 'catalog:' version: 8.5.9 react-server-dom-webpack: specifier: 'catalog:' - version: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) + version: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) storybook: specifier: 'catalog:' version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1150,22 +1223,22 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.41(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2) + version: 0.0.41(453b4e184a832f83060410b31544dc36) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 - version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) vite-plus: specifier: 'catalog:' - version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.16 - version: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.18 + version: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vitest-canvas-mock: specifier: 'catalog:' - version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.18) packages: @@ -1553,14 +1626,17 @@ packages: peerDependencies: tailwindcss: '*' - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@emoji-mart/data@1.2.1': resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} @@ -2525,15 +2601,15 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/runtime@0.123.0': - resolution: {integrity: sha512-wRf0z8saz9tHLcK3YeTeBmwISrpy4bBimvKxUmryiIhbt+ZJb0nwwJNL3D8xpeWbNfZlGSlzRBZbfcbApIGZJw==} + '@oxc-project/runtime@0.124.0': + resolution: {integrity: sha512-sSg6n37J3w3mM4odFvRqzQENf6+qxKnvStr/gU0FgRRg1VE/4MqryLd9PJmE0a7K5xlDfbrctBtSagaFH6ij9Q==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.121.0': resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} - '@oxc-project/types@0.123.0': - resolution: {integrity: sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2643,124 +2719,124 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.43.0': - resolution: {integrity: sha512-CgU2s+/9hHZgo0IxVxrbMPrMj+tJ6VM3mD7Mr/4oiz4FNTISLoCvRmB5nk4wAAle045RtRjd86m673jwPyb1OQ==} + '@oxfmt/binding-android-arm-eabi@0.45.0': + resolution: {integrity: sha512-A/UMxFob1fefCuMeGxQBulGfFE38g2Gm23ynr3u6b+b7fY7/ajGbNsa3ikMIkGMLJW/TRoQaMoP1kME7S+815w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.43.0': - resolution: {integrity: sha512-T9OfRwjA/EdYxAqbvR7TtqLv5nIrwPXuCtTwOHtS7aR9uXyn74ZYgzgTo6/ZwvTq9DY4W+DsV09hB2EXgn9EbA==} + '@oxfmt/binding-android-arm64@0.45.0': + resolution: {integrity: sha512-L63z4uZmHjgvvqvMJD7mwff8aSBkM0+X4uFr6l6U5t6+Qc9DCLVZWIunJ7Gm4fn4zHPdSq6FFQnhu9yqqobxIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.43.0': - resolution: {integrity: sha512-o3i49ZUSJWANzXMAAVY1wnqb65hn4JVzwlRQ5qfcwhRzIA8lGVaud31Q3by5ALHPrksp5QEaKCQF9aAS3TXpZA==} + '@oxfmt/binding-darwin-arm64@0.45.0': + resolution: {integrity: sha512-UV34dd623FzqT+outIGndsCA/RBB+qgB3XVQhgmmJ9PJwa37NzPC9qzgKeOhPKxVk2HW+JKldQrVL54zs4Noww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.43.0': - resolution: {integrity: sha512-vWECzzCFkb0kK6jaHjbtC5sC3adiNWtqawFCxhpvsWlzVeKmv5bNvkB4nux+o4JKWTpHCM57NDK/MeXt44txmA==} + '@oxfmt/binding-darwin-x64@0.45.0': + resolution: {integrity: sha512-pMNJv0CMa1pDefVPeNbuQxibh8ITpWDFEhMC/IBB9Zlu76EbgzYwrzI4Cb11mqX2+rIYN70UTrh3z06TM59ptQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.43.0': - resolution: {integrity: sha512-rgz8JpkKiI/umOf7fl9gwKyQasC8bs5SYHy6g7e4SunfLBY3+8ATcD5caIg8KLGEtKFm5ujKaH8EfjcmnhzTLg==} + '@oxfmt/binding-freebsd-x64@0.45.0': + resolution: {integrity: sha512-xTcRoxbbo61sW2+ZRPeH+vp/o9G8gkdhiVumFU+TpneiPm14c79l6GFlxPXlCE9bNWikigbsrvJw46zCVAQFfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': - resolution: {integrity: sha512-nWYnF3vIFzT4OM1qL/HSf1Yuj96aBuKWSaObXHSWliwAk2rcj7AWd6Lf7jowEBQMo4wCZVnueIGw/7C4u0KTBQ==} + '@oxfmt/binding-linux-arm-gnueabihf@0.45.0': + resolution: {integrity: sha512-hWL8Hdni+3U1mPFx1UtWeGp3tNb6EhBAUHRMbKUxVkOp3WwoJbpVO2bfUVbS4PfpledviXXNHSTl1veTa6FhkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.43.0': - resolution: {integrity: sha512-sFg+NWJbLfupYTF4WELHAPSnLPOn1jiDZ33Z1jfDnTaA+cC3iB35x0FMMZTFdFOz3icRIArncwCcemJFGXu6TQ==} + '@oxfmt/binding-linux-arm-musleabihf@0.45.0': + resolution: {integrity: sha512-6Blt/0OBT7vvfQpqYuYbpbFLPqSiaYpEJzUUWhinPEuADypDbtV1+LdjM0vYBNGPvnj85ex7lTerEX6JGcPt9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.43.0': - resolution: {integrity: sha512-MelWqv68tX6wZEILDrTc9yewiGXe7im62+5x0bNXlCYFOZdA+VnYiJfAihbROsZ5fm90p9C3haFrqjj43XnlAA==} + '@oxfmt/binding-linux-arm64-gnu@0.45.0': + resolution: {integrity: sha512-jLjoLfe+hGfjhA8hNBSdw85yCA8ePKq7ME4T+g6P9caQXvmt6IhE2X7iVjnVdkmYUWEzZrxlh4p6RkDmAMJY/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.43.0': - resolution: {integrity: sha512-ROaWfYh+6BSJ1Arwy5ujijTlwnZetxDxzBpDc1oBR4d7rfrPBqzeyjd5WOudowzQUgyavl2wEpzn1hw3jWcqLA==} + '@oxfmt/binding-linux-arm64-musl@0.45.0': + resolution: {integrity: sha512-XQKXZIKYJC3GQJ8FnD3iMntpw69Wd9kDDK/Xt79p6xnFYlGGxSNv2vIBvRTDg5CKByWFWWZLCRDOXoP/m6YN4g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.43.0': - resolution: {integrity: sha512-PJRs/uNxmFipJJ8+SyKHh7Y7VZIKQicqrrBzvfyM5CtKi8D7yZKTwUOZV3ffxmiC2e7l1SDJpkBEOyue5NAFsg==} + '@oxfmt/binding-linux-ppc64-gnu@0.45.0': + resolution: {integrity: sha512-+g5RiG+xOkdrCWkKodv407nTvMq4vYM18Uox2MhZBm/YoqFxxJpWKsloskFFG5NU13HGPw1wzYjjOVcyd9moCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.43.0': - resolution: {integrity: sha512-j6biGAgzIhj+EtHXlbNumvwG7XqOIdiU4KgIWRXAEj/iUbHKukKW8eXa4MIwpQwW1YkxovduKtzEAPnjlnAhVQ==} + '@oxfmt/binding-linux-riscv64-gnu@0.45.0': + resolution: {integrity: sha512-V7dXKoSyEbWAkkSF4JJNtF+NJZDmJoSarSoP30WCsB3X636Rehd3CvxBj49FIJxEBFWhvcUjGSHVeU8Erck1bQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.43.0': - resolution: {integrity: sha512-RYWxAcslKxvy7yri24Xm9cmD0RiANaiEPs007EFG6l9h1ChM69Q5SOzACaCoz4Z9dEplnhhneeBaTWMEdpgIbA==} + '@oxfmt/binding-linux-riscv64-musl@0.45.0': + resolution: {integrity: sha512-Vdelft1sAEYojVGgcODEFXSWYQYlIvoyIGWebKCuUibd1tvS1TjTx413xG2ZLuHpYj45CkN/ztMLMX6jrgqpgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.43.0': - resolution: {integrity: sha512-DT6Q8zfQQy3jxpezAsBACEHNUUixKSYTwdXeXojNHe4DQOoxjPdjr3Szu6BRNjxLykZM/xMNmp9ElOIyDppwtw==} + '@oxfmt/binding-linux-s390x-gnu@0.45.0': + resolution: {integrity: sha512-RR7xKgNpqwENnK0aYCGYg0JycY2n93J0reNjHyes+I9Gq52dH95x+CBlnlAQHCPfz6FGnKA9HirgUl14WO6o7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.43.0': - resolution: {integrity: sha512-R8Yk7iYcuZORXmCfFZClqbDxRZgZ9/HEidUuBNdoX8Ptx07cMePnMVJ/woB84lFIDjh2ROHVaOP40Ds3rBXFqg==} + '@oxfmt/binding-linux-x64-gnu@0.45.0': + resolution: {integrity: sha512-U/QQ0+BQNSHxjuXR/utvXnQ50Vu5kUuqEomZvQ1/3mhgbBiMc2WU9q5kZ5WwLp3gnFIx9ibkveoRSe2EZubkqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.43.0': - resolution: {integrity: sha512-F2YYqyvnQNvi320RWZNAvsaWEHwmW3k4OwNJ1hZxRKXupY63expbBaNp6jAgvYs7y/g546vuQnGHQuCBhslhLQ==} + '@oxfmt/binding-linux-x64-musl@0.45.0': + resolution: {integrity: sha512-o5TLOUCF0RWQjsIS06yVC+kFgp092/yLe6qBGSUvtnmTVw9gxjpdQSXc3VN5Cnive4K11HNstEZF8ROKHfDFSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.43.0': - resolution: {integrity: sha512-OE6TdietLXV3F6c7pNIhx/9YC1/2YFwjU9DPc/fbjxIX19hNIaP1rS0cFjCGJlGX+cVJwIKWe8Mos+LdQ1yAJw==} + '@oxfmt/binding-openharmony-arm64@0.45.0': + resolution: {integrity: sha512-RnGcV3HgPuOjsGx/k9oyRNKmOp+NBLGzZTdPDYbc19r7NGeYPplnUU/BfU35bX2Y/O4ejvHxcfkvW2WoYL/gsg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.43.0': - resolution: {integrity: sha512-0nWK6a7pGkbdoypfVicmV9k/N1FwjPZENoqhlTU+5HhZnAhpIO3za30nEE33u6l6tuy9OVfpdXUqxUgZ+4lbZw==} + '@oxfmt/binding-win32-arm64-msvc@0.45.0': + resolution: {integrity: sha512-v3Vj7iKKsUFwt9w5hsqIIoErKVoENC6LoqfDlteOQ5QMDCXihlqLoxpmviUhXnNncg4zV6U9BPwlBbwa+qm4wg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.43.0': - resolution: {integrity: sha512-9aokTR4Ft+tRdvgN/pKzSkVy2ksc4/dCpDm9L/xFrbIw0yhLtASLbvoG/5WOTUh/BRPPnfGTsWznEqv0dlOmhA==} + '@oxfmt/binding-win32-ia32-msvc@0.45.0': + resolution: {integrity: sha512-N8yotPBX6ph0H3toF4AEpdCeVPrdcSetj+8eGiZGsrLsng3bs/Q5HPu4bbSxip5GBPx5hGbGHrZwH4+rcrjhHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.43.0': - resolution: {integrity: sha512-4bPgdQux2ZLWn3bf2TTXXMHcJB4lenmuxrLqygPmvCJ104Yqzj1UctxSRzR31TiJ4MLaG22RK8dUsVpJtrCz5g==} + '@oxfmt/binding-win32-x64-msvc@0.45.0': + resolution: {integrity: sha512-w5MMTRCK1dpQeRA+HHqXQXyN33DlG/N2LOYxJmaT4fJjcmZrbNnqw7SmIk7I2/a2493PPLZ+2E/Ar6t2iKVMug==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2795,124 +2871,124 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.58.0': - resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} + '@oxlint/binding-android-arm-eabi@1.60.0': + resolution: {integrity: sha512-YdeJKaZckDQL1qa62a1aKq/goyq48aX3yOxaaWqWb4sau4Ee4IiLbamftNLU3zbePky6QsDj6thnSSzHRBjDfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.58.0': - resolution: {integrity: sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==} + '@oxlint/binding-android-arm64@1.60.0': + resolution: {integrity: sha512-7ANS7PpXCfq84xZQ8E5WPs14gwcuPcl+/8TFNXfpSu0CQBXz3cUo2fDpHT8v8HJN+Ut02eacvMAzTnc9s6X4tw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.58.0': - resolution: {integrity: sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==} + '@oxlint/binding-darwin-arm64@1.60.0': + resolution: {integrity: sha512-pJsgd9AfplLGBm1fIr25V6V14vMrayhx4uIQvlfH7jWs2SZwSrvi3TfgfJySB8T+hvyEH8K2zXljQiUnkgUnfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.58.0': - resolution: {integrity: sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==} + '@oxlint/binding-darwin-x64@1.60.0': + resolution: {integrity: sha512-Ue1aXHX49ivwflKqGJc7zcd/LeLgbhaTcDCQStgx5x06AXgjEAZmvrlMuIkWd4AL4FHQe6QJ9f33z04Cg448VQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.58.0': - resolution: {integrity: sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==} + '@oxlint/binding-freebsd-x64@1.60.0': + resolution: {integrity: sha512-YCyQzsQtusQw+gNRW9rRTifSO+Dt/+dtCl2NHoDMZqJlRTEZ/Oht9YnuporI9yiTx7+cB+eqzX3MtHHVHGIWhg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.58.0': - resolution: {integrity: sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==} + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': + resolution: {integrity: sha512-c7dxM2Zksa45Qw16i2iGY3Fti2NirJ38FrsBsKw+qcJ0OtqTsBgKJLF0xV+yLG56UH01Z8WRPgsw31e0MoRoGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.58.0': - resolution: {integrity: sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==} + '@oxlint/binding-linux-arm-musleabihf@1.60.0': + resolution: {integrity: sha512-ZWALoA42UYqBEP1Tbw9OWURgFGS1nWj2AAvLdY6ZcGx/Gj93qVCBKjcvwXMupZibYwFbi9s/rzqkZseb/6gVtQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.58.0': - resolution: {integrity: sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==} + '@oxlint/binding-linux-arm64-gnu@1.60.0': + resolution: {integrity: sha512-tpy+1w4p9hN5CicMCxqNy6ymfRtV5ayE573vFNjp1k1TN/qhLFgflveZoE/0++RlkHikBz2vY545NWm/hp7big==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.58.0': - resolution: {integrity: sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==} + '@oxlint/binding-linux-arm64-musl@1.60.0': + resolution: {integrity: sha512-eDYDXZGhQAXyn6GwtwiX/qcLS0HlOLPJ/+iiIY8RYr+3P8oKBmgKxADLlniL6FtWfE7pPk7IGN9/xvDEvDvFeg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.58.0': - resolution: {integrity: sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==} + '@oxlint/binding-linux-ppc64-gnu@1.60.0': + resolution: {integrity: sha512-nxehly5XYBHUWI9VJX1bqCf9j/B43DaK/aS/T1fcxCpX3PA4Rm9BB54nPD1CKayT8xg6REN1ao+01hSRNgy8OA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.58.0': - resolution: {integrity: sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==} + '@oxlint/binding-linux-riscv64-gnu@1.60.0': + resolution: {integrity: sha512-j1qf/NaUfOWQutjeoooNG1Q0zsK0XGmSu1uDLq3cctquRF3j7t9Hxqf/76ehCc5GEUAanth2W4Fa+XT1RFg/nw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.58.0': - resolution: {integrity: sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==} + '@oxlint/binding-linux-riscv64-musl@1.60.0': + resolution: {integrity: sha512-YELKPRefQ/q/h3RUmeRfPCUhh2wBvgV1RyZ/F9M9u8cDyXsQW2ojv1DeWQTt466yczDITjZnIOg/s05pk7Ve2A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.58.0': - resolution: {integrity: sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==} + '@oxlint/binding-linux-s390x-gnu@1.60.0': + resolution: {integrity: sha512-JkO3C6Gki7Y6h/MiIkFKvHFOz98/YWvQ4WYbK9DLXACMP2rjULzkeGyAzorJE5S1dzLQGFgeqvN779kSFwoV1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.58.0': - resolution: {integrity: sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==} + '@oxlint/binding-linux-x64-gnu@1.60.0': + resolution: {integrity: sha512-XjKHdFVCpZZZSWBCKyyqCq65s2AKXykMXkjLoKYODrD+f5toLhlwsMESscu8FbgnJQ4Y/dpR/zdazsahmgBJIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.58.0': - resolution: {integrity: sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==} + '@oxlint/binding-linux-x64-musl@1.60.0': + resolution: {integrity: sha512-js29ZWIuPhNWzY8NC7KoffEMEeWG105vbmm+8EOJsC+T/jHBiKIJEUF78+F/IrgEWMMP9N0kRND4Pp75+xAhKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.58.0': - resolution: {integrity: sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==} + '@oxlint/binding-openharmony-arm64@1.60.0': + resolution: {integrity: sha512-H+PUITKHk04stFpWj3x3Kg08Afp/bcXSBi0EhasR5a0Vw7StXHTzdl655PUI0fB4qdh2Wsu6Dsi+3ACxPoyQnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.58.0': - resolution: {integrity: sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==} + '@oxlint/binding-win32-arm64-msvc@1.60.0': + resolution: {integrity: sha512-WA/yc7f7ZfCefBXVzNHn1Ztulb1EFwNBb4jMZ6pjML0zz6pHujlF3Q3jySluz3XHl/GNeMTntG1seUBWVMlMag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.58.0': - resolution: {integrity: sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==} + '@oxlint/binding-win32-ia32-msvc@1.60.0': + resolution: {integrity: sha512-33YxL1sqwYNZXtn3MD/4dno6s0xeedXOJlT1WohkVD565WvohClZUr7vwKdAk954n4xiEWJkewiCr+zLeq7AeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.58.0': - resolution: {integrity: sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==} + '@oxlint/binding-win32-x64-msvc@1.60.0': + resolution: {integrity: sha512-JOro4ZcfBLamJCyfURQmOQByoorgOdx3ZjAkSqnb/CyG/i+lN3KoV5LAgk5ZAW6DPq7/Cx7n23f8DuTWXTWgyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3531,6 +3607,9 @@ packages: resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} engines: {node: '>=18'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@solid-primitives/event-listener@2.4.5': resolution: {integrity: sha512-nwRV558mIabl4yVAhZKY8cb6G+O1F0M6Z75ttTu5hk+SxdOnKSGj+eetDIu7Oax1P138ZdUU01qnBPR8rnxaEA==} peerDependencies: @@ -4468,31 +4547,54 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/pretty-format@4.1.4': resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} '@vitest/utils@4.1.4': resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - '@voidzero-dev/vite-plus-core@0.1.16': - resolution: {integrity: sha512-fOyf14CXjcXqANFs2fCXEX+0Tn9ZjmqfFV+qTnARwIF1Kzl8WquO4XtvlDgs/fTQ91H4AyoNUgkvWdKS+C4xYA==} + '@voidzero-dev/vite-plus-core@0.1.18': + resolution: {integrity: sha512-3PmXOL26yHzlw8ET9SwXCmglGzUYq2fOTYf2t0mxvVIs7ua3bnf6tOnmR+6YX5k1Ez26B0ooYzx+znc8k+CAMw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.21.7 - '@tsdown/exe': 0.21.7 + '@tsdown/css': 0.21.8 + '@tsdown/exe': 0.21.8 '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 - esbuild: ^0.28.0 + esbuild: 0.27.2 jiti: '>=1.21.0' less: ^4.0.0 publint: ^0.3.0 @@ -4543,54 +4645,56 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.16': - resolution: {integrity: sha512-InG0ZmuGh7DTrn7zWQ0UvKapElphKI6G1oYfys+jraedG70EhIIee9gtO+mTE1T0bF67SgAcLXwNyaiNda0XwA==} + '@voidzero-dev/vite-plus-darwin-arm64@0.1.18': + resolution: {integrity: sha512-bw2pWWE8RZRELWjXcdxdmRaOaYjmGmsxEm23TxvGxQXFb7k9l51W8tpjxariPGLxrEl+Cw5u601IL5LASaPJ5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.16': - resolution: {integrity: sha512-LGNrECstuhkCRKRj/dE98Xcprw8HU3VMIMJnZsnDR2C5RB2HADNIu21at/a/G3giA9eWm7uhtPp9FvUtTCK9TA==} + '@voidzero-dev/vite-plus-darwin-x64@0.1.18': + resolution: {integrity: sha512-8TFj6yJNsumoH+yFc+6zf3g2UuzvrPHq2FAAVORffaVZ29PWnDSsXjegaIBmoAtGO5Xb4lcilQx7NoF9hONrZg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.16': - resolution: {integrity: sha512-AoFKu6dIOtlkp/mwmtU8ES2uzoaxCHhIym1Tk7qMxyvke4IXnye6VDc4kPMRQwD8mwR3T3bO0HuaEEHxrIWDxw==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.18': + resolution: {integrity: sha512-xHRqncKanOZ0zNnZSufL4Yx/gWrIFkCjU6jFzCukBOOCrcemq3SrALPHrNf+Nw1RLwNptGUZn2Vx/IjRLzUQDw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.16': - resolution: {integrity: sha512-PloCsGTRIhcXIpUOJ6PqVG8gYNpq+ooJNyqy5sQ82BRnJuo8oV7uBLFvg0X9B3Bzh+vO1F8/+92+o5TiL35JMg==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.18': + resolution: {integrity: sha512-CA6XxZbkT8lYwWzS2yAj6exr7nHl3R8Sz+ZdOhYCU4yR2qvzGatdVgFr7oPnrkHLF426cHJ172rmNNj8NKie/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.16': - resolution: {integrity: sha512-nY9/2g+qjhwsW5U3MrFLlx+bOBsdOJiO2HzbxQy7jo/S3jPTnXhFlrRegQuAmqrHAXrSdNwgblgRpICKhx1xZg==} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.18': + resolution: {integrity: sha512-xBO3MtLGVASPjH/GDRxexfLCT0othVpiFMdEQ83Y+woVNbrrzcdQTGFUuFG4cAiMhtmjytyFwPBtZ76BWsDO3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.16': - resolution: {integrity: sha512-JGKEAMoXqzdr9lHT/13uRNV9uzrSYXAFhjAfIC8WEQMG2VUFksvq5/TOc26hzmzbqu+bxRmfN8h1aVTDL8KwFg==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.18': + resolution: {integrity: sha512-ADNis6SMarY7i8+b2ynUJ1PiqCHqnVwY7EQ+fSGug5zZ+W/cZq14+VWPxOvGR9LJk+iol8XuqsHy4BaV2+gjzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-test@0.1.16': - resolution: {integrity: sha512-d/rJPX/heMzoAFdnpZsp04MAa6nw1yH1tA4mVCV4m8goVcE9nAvt69mjLMzE8N/rYIQOSgenf3hDXuQRuD6OKQ==} + '@voidzero-dev/vite-plus-test@0.1.18': + resolution: {integrity: sha512-dovC2kJgiwMI8ay0i+3NvQGCDWPj8HQB2ONP/HbdJ5/XQVPq13+BihnCq8/ztz6uGhiDD8Nu4OZ3RgB14uvTfA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/ui': 4.1.2 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4601,6 +4705,10 @@ packages: optional: true '@types/node': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -4608,14 +4716,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.16': - resolution: {integrity: sha512-IugPUCLY7HmiPcCeuHKUqO1+G2vxHnYzAGhS02AixD0sJLTAIKCUANDOiVUFf/HMw+jh/UkugW7MWek8lf/JrQ==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.18': + resolution: {integrity: sha512-EcDETMHG8xgjIlMizIu/wf0UtRZLGz+lHFvYFZVCkz4vLLz93a06vZ+3Oi9xY2Kc8aOHsCf8Gj5/dox/03cscw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.16': - resolution: {integrity: sha512-tq93CIeMs92HF7rdylJknRiyzMOWMKCmpw+g8nl5Q5nmUDNLUsrL3CGfbyqjgbruuPnIr761r9MfydPqZU/cYg==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.18': + resolution: {integrity: sha512-jBgL4ZjSJJu3FDcrqj4muzbr0WKlU6Ym1ilHQnq8R+2TRvE0AtvAMMuphICDslZGi6EK3fwJ+r2Lv7GU1AipQA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -4936,6 +5044,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.1: resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} engines: {node: '>=10'} @@ -5427,8 +5539,8 @@ packages: resolution: {integrity: sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==} engines: {node: '>=20'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.0: + resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5487,6 +5599,13 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} @@ -5893,6 +6012,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -6144,8 +6267,8 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} - hono@4.12.12: - resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -6611,6 +6734,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loro-crdt@1.10.8: + resolution: {integrity: sha512-GvH8fSJST1VDHRGzlQml80pBYoFbIP4ULeV1S8fD4ffmA8m+icoPORyVUW2AkJBY3dxKIcMMn0WqaJmpCmnbkQ==} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} @@ -7078,8 +7204,8 @@ packages: oxc-resolver@11.19.1: resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} - oxfmt@0.43.0: - resolution: {integrity: sha512-KTYNG5ISfHSdmeZ25Xzb3qgz9EmQvkaGAxgBY/p38+ZiAet3uZeu7FnMwcSQJg152Qwl0wnYAxDc+Z/H6cvrwA==} + oxfmt@0.45.0: + resolution: {integrity: sha512-0o/COoN9fY50bjVeM7PQsNgbhndKurBIeTIcspW033OumksjJJmIVDKjAk5HMwU/GHTxSOdGDdhJ6BRzGPmsHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -7087,8 +7213,8 @@ packages: resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} hasBin: true - oxlint@1.58.0: - resolution: {integrity: sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==} + oxlint@1.60.0: + resolution: {integrity: sha512-tnRzTWiWJ9pg3ftRWnD0+Oqh78L6ZSwcEudvCZaER0PIqiAnNyXj5N1dPwjmNpDalkKS9m/WMLN1CTPUBPmsgw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -7730,6 +7856,9 @@ packages: resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} engines: {node: '>=20'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -7750,6 +7879,14 @@ packages: resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} + engines: {node: '>=10.0.0'} + solid-js@1.9.11: resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} @@ -7794,6 +7931,9 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -8007,6 +8147,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -8356,8 +8500,8 @@ packages: storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.16: - resolution: {integrity: sha512-sgYHc5zWLSDInaHb/abvEA7UOwh7sUWuyNt+Slphj55jPvzodT8Dqw115xyKwDARTuRFSpm1eo/t58qZ8/NylQ==} + vite-plus@0.1.18: + resolution: {integrity: sha512-RiWUoOmQiJMtd4Dfm6WD0v0Selqh/nQzmaGVIrkfnr+2s5UxGVZy7n2TCO5ZnR7w9noMIgtUAQN8GtKhwHEiOQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8387,6 +8531,47 @@ packages: peerDependencies: vitest: ^3.0.0 || ^4.0.0 + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -8466,6 +8651,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -8473,6 +8663,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.20.0: resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} @@ -8501,6 +8703,10 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -8743,17 +8949,71 @@ snapshots: idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)': + '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.18)(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.2.0 - '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0)) + '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0)) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.0(jiti@2.6.1)) '@eslint/markdown': 8.0.1 '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@vitest/eslint-plugin': 1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@vitest/eslint-plugin': 1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.18)(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + ansis: 4.2.0 + cac: 7.0.0 + eslint: 10.2.0(jiti@2.6.1) + eslint-config-flat-gitignore: 2.3.0(eslint@10.2.0(jiti@2.6.1)) + eslint-flat-config-utils: 3.1.0 + eslint-merge-processors: 2.0.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-antfu: 3.2.2(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-import-lite: 0.6.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-jsdoc: 62.9.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-jsonc: 3.1.2(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-n: 17.24.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-no-only-tests: 3.3.0 + eslint-plugin-perfectionist: 5.8.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-pnpm: 1.6.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-regexp: 3.1.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-toml: 1.3.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-unicorn: 64.0.0(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-vue: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@10.2.0(jiti@2.6.1)))(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@10.2.0(jiti@2.6.1))) + eslint-plugin-yml: 3.3.1(eslint@10.2.0(jiti@2.6.1)) + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.31)(eslint@10.2.0(jiti@2.6.1)) + globals: 17.5.0 + local-pkg: 1.1.2 + parse-gitignore: 2.0.0 + toml-eslint-parser: 1.0.3 + vue-eslint-parser: 10.4.0(eslint@10.2.0(jiti@2.6.1)) + yaml-eslint-parser: 2.0.0 + optionalDependencies: + '@eslint-react/eslint-plugin': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@next/eslint-plugin-next': 16.2.3 + eslint-plugin-react-refresh: 0.5.2(eslint@10.2.0(jiti@2.6.1)) + transitivePeerDependencies: + - '@eslint/json' + - '@typescript-eslint/rule-tester' + - '@typescript-eslint/typescript-estree' + - '@typescript-eslint/utils' + - '@vue/compiler-sfc' + - oxlint + - supports-color + - typescript + - vitest + + '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)(vitest@4.1.4)': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@clack/prompts': 1.2.0 + '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0)) + '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.0(jiti@2.6.1)) + '@eslint/markdown': 8.0.1 + '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + '@vitest/eslint-plugin': 1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.4) ansis: 4.2.0 cac: 7.0.0 eslint: 10.2.0(jiti@2.6.1) @@ -9141,21 +9401,21 @@ snapshots: '@date-fns/tz@1.4.1': {} - '@e18e/eslint-plugin@0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))': + '@e18e/eslint-plugin@0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))': dependencies: eslint-plugin-depend: 1.5.0(eslint@10.2.0(jiti@2.6.1)) optionalDependencies: eslint: 10.2.0(jiti@2.6.1) - oxlint: 1.58.0(oxlint-tsgolint@0.20.0) + oxlint: 1.60.0(oxlint-tsgolint@0.20.0) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.2)': dependencies: '@iconify/utils': 3.1.0 tailwindcss: 4.2.2 - '@emnapi/core@1.9.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.2.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true @@ -9164,7 +9424,12 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.0': + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true @@ -9527,9 +9792,9 @@ snapshots: dependencies: react: 19.2.5 - '@hono/node-server@1.19.14(hono@4.12.12)': + '@hono/node-server@1.19.14(hono@4.12.14)': dependencies: - hono: 4.12.12 + hono: 4.12.14 '@humanfs/core@0.19.1': {} @@ -9685,11 +9950,11 @@ snapshots: dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.2) - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' optionalDependencies: typescript: 6.0.2 @@ -9874,12 +10139,12 @@ snapshots: lexical: 0.43.0 yjs: 13.6.30 - '@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3))': + '@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: '@mdx-js/mdx': 3.1.1 source-map: 0.7.6 optionalDependencies: - webpack: 5.105.4(uglify-js@3.19.3) + webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) transitivePeerDependencies: - supports-color @@ -9944,10 +10209,10 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -9961,11 +10226,11 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/mdx@16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5))': + '@next/mdx@16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5))': dependencies: source-map: 0.7.6 optionalDependencies: - '@mdx-js/loader': 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) + '@mdx-js/loader': 3.1.1(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) '@next/swc-darwin-arm64@16.2.3': @@ -10120,9 +10385,9 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -10137,11 +10402,11 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-project/runtime@0.123.0': {} + '@oxc-project/runtime@0.124.0': {} '@oxc-project/types@0.121.0': {} - '@oxc-project/types@0.123.0': {} + '@oxc-project/types@0.124.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -10191,9 +10456,9 @@ snapshots: '@oxc-resolver/binding-openharmony-arm64@11.19.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -10208,61 +10473,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.19.1': optional: true - '@oxfmt/binding-android-arm-eabi@0.43.0': + '@oxfmt/binding-android-arm-eabi@0.45.0': optional: true - '@oxfmt/binding-android-arm64@0.43.0': + '@oxfmt/binding-android-arm64@0.45.0': optional: true - '@oxfmt/binding-darwin-arm64@0.43.0': + '@oxfmt/binding-darwin-arm64@0.45.0': optional: true - '@oxfmt/binding-darwin-x64@0.43.0': + '@oxfmt/binding-darwin-x64@0.45.0': optional: true - '@oxfmt/binding-freebsd-x64@0.43.0': + '@oxfmt/binding-freebsd-x64@0.45.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.43.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.45.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.43.0': + '@oxfmt/binding-linux-arm-musleabihf@0.45.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.43.0': + '@oxfmt/binding-linux-arm64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.43.0': + '@oxfmt/binding-linux-arm64-musl@0.45.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.43.0': + '@oxfmt/binding-linux-ppc64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.43.0': + '@oxfmt/binding-linux-riscv64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.43.0': + '@oxfmt/binding-linux-riscv64-musl@0.45.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.43.0': + '@oxfmt/binding-linux-s390x-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.43.0': + '@oxfmt/binding-linux-x64-gnu@0.45.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.43.0': + '@oxfmt/binding-linux-x64-musl@0.45.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.43.0': + '@oxfmt/binding-openharmony-arm64@0.45.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.43.0': + '@oxfmt/binding-win32-arm64-msvc@0.45.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.43.0': + '@oxfmt/binding-win32-ia32-msvc@0.45.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.43.0': + '@oxfmt/binding-win32-x64-msvc@0.45.0': optional: true '@oxlint-tsgolint/darwin-arm64@0.20.0': @@ -10283,61 +10548,61 @@ snapshots: '@oxlint-tsgolint/win32-x64@0.20.0': optional: true - '@oxlint/binding-android-arm-eabi@1.58.0': + '@oxlint/binding-android-arm-eabi@1.60.0': optional: true - '@oxlint/binding-android-arm64@1.58.0': + '@oxlint/binding-android-arm64@1.60.0': optional: true - '@oxlint/binding-darwin-arm64@1.58.0': + '@oxlint/binding-darwin-arm64@1.60.0': optional: true - '@oxlint/binding-darwin-x64@1.58.0': + '@oxlint/binding-darwin-x64@1.60.0': optional: true - '@oxlint/binding-freebsd-x64@1.58.0': + '@oxlint/binding-freebsd-x64@1.60.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.58.0': + '@oxlint/binding-linux-arm-gnueabihf@1.60.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.58.0': + '@oxlint/binding-linux-arm-musleabihf@1.60.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.58.0': + '@oxlint/binding-linux-arm64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.58.0': + '@oxlint/binding-linux-arm64-musl@1.60.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.58.0': + '@oxlint/binding-linux-ppc64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.58.0': + '@oxlint/binding-linux-riscv64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.58.0': + '@oxlint/binding-linux-riscv64-musl@1.60.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.58.0': + '@oxlint/binding-linux-s390x-gnu@1.60.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.58.0': + '@oxlint/binding-linux-x64-gnu@1.60.0': optional: true - '@oxlint/binding-linux-x64-musl@1.58.0': + '@oxlint/binding-linux-x64-musl@1.60.0': optional: true - '@oxlint/binding-openharmony-arm64@1.58.0': + '@oxlint/binding-openharmony-arm64@1.60.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.58.0': + '@oxlint/binding-win32-arm64-msvc@1.60.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.58.0': + '@oxlint/binding-win32-ia32-msvc@1.60.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.58.0': + '@oxlint/binding-win32-x64-msvc@1.60.0': optional: true '@parcel/watcher-android-arm64@2.5.6': @@ -10880,6 +11145,8 @@ snapshots: '@sindresorhus/base62@1.0.0': {} + '@socket.io/component-emitter@3.1.2': {} + '@solid-primitives/event-listener@2.4.5(solid-js@1.9.11)': dependencies: '@solid-primitives/utils': 6.4.0(solid-js@1.9.11) @@ -10918,10 +11185,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: 19.2.5 @@ -10951,25 +11218,26 @@ snapshots: storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) unplugin: 2.3.11 optionalDependencies: + esbuild: 0.27.2 rollup: 4.59.0 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - webpack: 5.105.4(uglify-js@3.19.3) + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) '@storybook/global@5.0.0': {} @@ -10978,18 +11246,18 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) - '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: @@ -11006,11 +11274,11 @@ snapshots: react-dom: 19.2.5(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) empathic: 2.0.0 magic-string: 0.30.21 @@ -11020,7 +11288,7 @@ snapshots: resolve: 1.22.11 storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -11159,12 +11427,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.2 - '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' '@tanstack/devtools-client@0.0.6': dependencies: @@ -11758,7 +12026,7 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: @@ -11870,12 +12138,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': dependencies: '@vitejs/devtools-rpc': 0.1.11(typescript@6.0.2)(ws@8.20.0) birpc: 4.0.0 ohash: 2.0.11 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws @@ -11892,12 +12160,12 @@ snapshots: transitivePeerDependencies: - typescript - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)': + '@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.5)': dependencies: '@rolldown/pluginutils': 1.0.0-rc.15 es-module-lexer: 2.0.0 @@ -11908,12 +12176,12 @@ snapshots: srvx: 0.11.15 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) optionalDependencies: - react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) + react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) - '@vitest/coverage-v8@4.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@vitest/coverage-v8@4.1.4(@voidzero-dev/vite-plus-test@0.1.18)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.4 @@ -11925,9 +12193,24 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': + '@vitest/coverage-v8@4.1.4(vitest@4.1.4)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.4 + ast-v8-to-istanbul: 1.0.0 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.2 + obug: 2.1.1 + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0) + optional: true + + '@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.18)(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)': dependencies: '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -11935,7 +12218,19 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) typescript: 6.0.2 - vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + transitivePeerDependencies: + - supports-color + + '@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2)(vitest@4.1.4)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.2 + '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint: 10.2.0(jiti@2.6.1) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + typescript: 6.0.2 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0) transitivePeerDependencies: - supports-color @@ -11947,6 +12242,25 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + optional: true + + '@vitest/mocker@4.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + optional: true + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -11955,10 +12269,27 @@ snapshots: dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + optional: true + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + optional: true + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 + '@vitest/spy@4.1.4': + optional: true + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -11971,14 +12302,15 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: - '@oxc-project/runtime': 0.123.0 - '@oxc-project/types': 0.123.0 + '@oxc-project/runtime': 0.124.0 + '@oxc-project/types': 0.124.0 lightningcss: 1.32.0 postcss: 8.5.9 optionalDependencies: '@types/node': 25.6.0 + esbuild: 0.27.2 fsevents: 2.3.3 jiti: 2.6.1 sass: 1.98.0 @@ -11987,29 +12319,29 @@ snapshots: typescript: 6.0.2 yaml: 2.8.3 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.16': + '@voidzero-dev/vite-plus-darwin-arm64@0.1.18': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.16': + '@voidzero-dev/vite-plus-darwin-x64@0.1.18': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.16': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.18': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.16': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.18': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.16': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.18': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.16': + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.18': optional: true - '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -12018,11 +12350,12 @@ snapshots: std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + tinyglobby: 0.2.16 + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' ws: 8.20.0 optionalDependencies: '@types/node': 25.6.0 + '@vitest/coverage-v8': 4.1.4(@voidzero-dev/vite-plus-test@0.1.18) happy-dom: 20.9.0 transitivePeerDependencies: - '@arethetypeswrong/core' @@ -12045,10 +12378,10 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.16': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.18': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.16': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.18': optional: true '@volar/language-core@2.4.28': @@ -12391,6 +12724,9 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: + optional: true + chalk@4.1.1: dependencies: ansi-styles: 4.3.0 @@ -12885,7 +13221,7 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - dompurify@3.3.3: + dompurify@3.4.0: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -12944,6 +13280,20 @@ snapshots: dependencies: once: 1.4.0 + engine.io-client@6.6.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@8.1.1) + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 @@ -13059,7 +13409,7 @@ snapshots: dependencies: eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-better-tailwindcss@4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2): + eslint-plugin-better-tailwindcss@4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2): dependencies: '@eslint/css-tree': 4.0.1 '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.2)) @@ -13072,7 +13422,7 @@ snapshots: valibot: 1.3.1(typescript@6.0.2) optionalDependencies: eslint: 10.2.0(jiti@2.6.1) - oxlint: 1.58.0(oxlint-tsgolint@0.20.0) + oxlint: 1.60.0(oxlint-tsgolint@0.20.0) transitivePeerDependencies: - '@eslint/css' - typescript @@ -13203,7 +13553,7 @@ snapshots: jsonc-eslint-parser: 3.1.0 pathe: 2.0.3 pnpm-workspace-yaml: 1.6.0 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 yaml: 2.8.3 yaml-eslint-parser: 2.0.0 @@ -13571,6 +13921,9 @@ snapshots: expand-template@2.0.3: optional: true + expect-type@1.3.0: + optional: true + exsolve@1.0.8: {} extend@3.0.2: {} @@ -13906,7 +14259,7 @@ snapshots: hex-rgb@4.3.0: {} - hono@4.12.12: {} + hono@4.12.14: {} hosted-git-info@9.0.2: dependencies: @@ -14140,7 +14493,7 @@ snapshots: khroma@2.1.0: {} - knip@6.4.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -14148,8 +14501,8 @@ snapshots: get-tsconfig: 4.13.7 jiti: 2.6.1 minimist: 1.2.8 - oxc-parser: 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - oxc-resolver: 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + oxc-parser: 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) picocolors: 1.1.1 picomatch: 4.0.4 smol-toml: 1.6.1 @@ -14288,6 +14641,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loro-crdt@1.10.8: {} + loupe@3.2.1: {} lower-case@2.0.2: @@ -15056,7 +15411,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-parser@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@oxc-project/types': 0.121.0 optionalDependencies: @@ -15076,7 +15431,7 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.121.0 '@oxc-parser/binding-linux-x64-musl': 0.121.0 '@oxc-parser/binding-openharmony-arm64': 0.121.0 - '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 '@oxc-parser/binding-win32-x64-msvc': 0.121.0 @@ -15084,7 +15439,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxc-resolver@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 '@oxc-resolver/binding-android-arm64': 11.19.1 @@ -15102,7 +15457,7 @@ snapshots: '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 '@oxc-resolver/binding-linux-x64-musl': 11.19.1 '@oxc-resolver/binding-openharmony-arm64': 11.19.1 - '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 @@ -15110,29 +15465,29 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxfmt@0.43.0: + oxfmt@0.45.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.43.0 - '@oxfmt/binding-android-arm64': 0.43.0 - '@oxfmt/binding-darwin-arm64': 0.43.0 - '@oxfmt/binding-darwin-x64': 0.43.0 - '@oxfmt/binding-freebsd-x64': 0.43.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.43.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.43.0 - '@oxfmt/binding-linux-arm64-gnu': 0.43.0 - '@oxfmt/binding-linux-arm64-musl': 0.43.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.43.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.43.0 - '@oxfmt/binding-linux-riscv64-musl': 0.43.0 - '@oxfmt/binding-linux-s390x-gnu': 0.43.0 - '@oxfmt/binding-linux-x64-gnu': 0.43.0 - '@oxfmt/binding-linux-x64-musl': 0.43.0 - '@oxfmt/binding-openharmony-arm64': 0.43.0 - '@oxfmt/binding-win32-arm64-msvc': 0.43.0 - '@oxfmt/binding-win32-ia32-msvc': 0.43.0 - '@oxfmt/binding-win32-x64-msvc': 0.43.0 + '@oxfmt/binding-android-arm-eabi': 0.45.0 + '@oxfmt/binding-android-arm64': 0.45.0 + '@oxfmt/binding-darwin-arm64': 0.45.0 + '@oxfmt/binding-darwin-x64': 0.45.0 + '@oxfmt/binding-freebsd-x64': 0.45.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.45.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.45.0 + '@oxfmt/binding-linux-arm64-gnu': 0.45.0 + '@oxfmt/binding-linux-arm64-musl': 0.45.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.45.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.45.0 + '@oxfmt/binding-linux-riscv64-musl': 0.45.0 + '@oxfmt/binding-linux-s390x-gnu': 0.45.0 + '@oxfmt/binding-linux-x64-gnu': 0.45.0 + '@oxfmt/binding-linux-x64-musl': 0.45.0 + '@oxfmt/binding-openharmony-arm64': 0.45.0 + '@oxfmt/binding-win32-arm64-msvc': 0.45.0 + '@oxfmt/binding-win32-ia32-msvc': 0.45.0 + '@oxfmt/binding-win32-x64-msvc': 0.45.0 oxlint-tsgolint@0.20.0: optionalDependencies: @@ -15143,27 +15498,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.20.0 '@oxlint-tsgolint/win32-x64': 0.20.0 - oxlint@1.58.0(oxlint-tsgolint@0.20.0): + oxlint@1.60.0(oxlint-tsgolint@0.20.0): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.58.0 - '@oxlint/binding-android-arm64': 1.58.0 - '@oxlint/binding-darwin-arm64': 1.58.0 - '@oxlint/binding-darwin-x64': 1.58.0 - '@oxlint/binding-freebsd-x64': 1.58.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.58.0 - '@oxlint/binding-linux-arm-musleabihf': 1.58.0 - '@oxlint/binding-linux-arm64-gnu': 1.58.0 - '@oxlint/binding-linux-arm64-musl': 1.58.0 - '@oxlint/binding-linux-ppc64-gnu': 1.58.0 - '@oxlint/binding-linux-riscv64-gnu': 1.58.0 - '@oxlint/binding-linux-riscv64-musl': 1.58.0 - '@oxlint/binding-linux-s390x-gnu': 1.58.0 - '@oxlint/binding-linux-x64-gnu': 1.58.0 - '@oxlint/binding-linux-x64-musl': 1.58.0 - '@oxlint/binding-openharmony-arm64': 1.58.0 - '@oxlint/binding-win32-arm64-msvc': 1.58.0 - '@oxlint/binding-win32-ia32-msvc': 1.58.0 - '@oxlint/binding-win32-x64-msvc': 1.58.0 + '@oxlint/binding-android-arm-eabi': 1.60.0 + '@oxlint/binding-android-arm64': 1.60.0 + '@oxlint/binding-darwin-arm64': 1.60.0 + '@oxlint/binding-darwin-x64': 1.60.0 + '@oxlint/binding-freebsd-x64': 1.60.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.60.0 + '@oxlint/binding-linux-arm-musleabihf': 1.60.0 + '@oxlint/binding-linux-arm64-gnu': 1.60.0 + '@oxlint/binding-linux-arm64-musl': 1.60.0 + '@oxlint/binding-linux-ppc64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-gnu': 1.60.0 + '@oxlint/binding-linux-riscv64-musl': 1.60.0 + '@oxlint/binding-linux-s390x-gnu': 1.60.0 + '@oxlint/binding-linux-x64-gnu': 1.60.0 + '@oxlint/binding-linux-x64-musl': 1.60.0 + '@oxlint/binding-openharmony-arm64': 1.60.0 + '@oxlint/binding-win32-arm64-msvc': 1.60.0 + '@oxlint/binding-win32-ia32-msvc': 1.60.0 + '@oxlint/binding-win32-x64-msvc': 1.60.0 oxlint-tsgolint: 0.20.0 p-limit@3.1.0: @@ -15533,13 +15888,13 @@ snapshots: react-draggable: 4.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tslib: 2.6.2 - react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)): + react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - webpack: 5.105.4(uglify-js@3.19.3) + webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) webpack-sources: 3.3.4 react-sortablejs@6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sortablejs@1.15.7): @@ -15958,6 +16313,9 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + siginfo@2.0.0: + optional: true + simple-concat@1.0.1: optional: true @@ -15980,6 +16338,24 @@ snapshots: smol-toml@1.6.1: {} + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@8.1.1) + engine.io-client: 6.6.4 + socket.io-parser: 4.2.6 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.6: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + solid-js@1.9.11: dependencies: csstype: 3.2.3 @@ -16022,6 +16398,9 @@ snapshots: srvx@0.11.15: {} + stackback@0.0.2: + optional: true + stackframe@1.3.4: {} state-local@1.0.7: {} @@ -16199,14 +16578,15 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.4.0(uglify-js@3.19.3)(webpack@5.105.4(uglify-js@3.19.3)): + terser-webpack-plugin@5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.105.4(uglify-js@3.19.3) + webpack: 5.105.4(esbuild@0.27.2)(uglify-js@3.19.3) optionalDependencies: + esbuild: 0.27.2 uglify-js: 3.19.3 terser@5.46.1: @@ -16241,6 +16621,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@2.1.0: {} tinyrainbow@2.0.0: {} @@ -16515,21 +16900,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.41(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2): + vinext@0.0.41(453b4e184a832f83060410b31544dc36): dependencies: '@unpic/react': 1.0.2(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) magic-string: 0.30.21 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) optionalDependencies: '@mdx-js/rollup': 3.1.1(rollup@4.59.0) - '@vitejs/plugin-rsc': 0.5.24(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5) - react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) + '@vitejs/plugin-rsc': 0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)))(react@19.2.5) + react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) transitivePeerDependencies: - next - supports-color @@ -16548,9 +16933,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0): + vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0): dependencies: - '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) ansis: 4.2.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -16559,12 +16944,12 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 @@ -16573,29 +16958,29 @@ snapshots: next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + vite-plus@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: - '@oxc-project/types': 0.123.0 - '@voidzero-dev/vite-plus-core': 0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) - oxfmt: 0.43.0 - oxlint: 1.58.0(oxlint-tsgolint@0.20.0) + '@oxc-project/types': 0.124.0 + '@voidzero-dev/vite-plus-core': 0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + oxfmt: 0.45.0 + oxlint: 1.60.0(oxlint-tsgolint@0.20.0) oxlint-tsgolint: 0.20.0 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.16 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.16 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.16 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.16 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.16 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.16 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.16 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.16 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.18 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.18 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.18 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.18 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.18 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.18 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.18 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.18 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -16604,6 +16989,8 @@ snapshots: - '@tsdown/exe' - '@types/node' - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' - '@vitest/ui' - bufferutil - esbuild @@ -16624,36 +17011,66 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.2) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.2) - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.18): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + + vitest@4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) + happy-dom: 20.9.0 + transitivePeerDependencies: + - msw + optional: true void-elements@3.1.0: {} @@ -16701,7 +17118,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.105.4(uglify-js@3.19.3): + webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -16725,7 +17142,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(uglify-js@3.19.3)(webpack@5.105.4(uglify-js@3.19.3)) + terser-webpack-plugin: 5.4.0(esbuild@0.27.2)(uglify-js@3.19.3)(webpack@5.105.4(esbuild@0.27.2)(uglify-js@3.19.3)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -16745,10 +17162,18 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + optional: true + word-wrap@1.2.5: {} wrappy@1.0.2: {} + ws@8.18.3: {} + ws@8.20.0: {} wsl-utils@0.1.0: @@ -16764,6 +17189,8 @@ snapshots: xmlbuilder@15.1.1: {} + xmlhttprequest-ssl@2.1.2: {} + yallist@3.1.1: {} yallist@5.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 98f1fcfa86..0bd1303fb3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,130 +1,96 @@ -catalogMode: prefer -trustPolicy: no-downgrade -trustPolicyExclude: - - chokidar@4.0.3 - - reselect@5.1.1 - - semver@6.3.1 -blockExoticSubdeps: true -strictDepBuilds: true -allowBuilds: - "@parcel/watcher": false - canvas: false - esbuild: false - sharp: false packages: - web - e2e - sdks/nodejs-client - packages/* -overrides: - "@lexical/code": npm:lexical-code-no-prism@0.41.0 - "@monaco-editor/loader": 1.7.0 - brace-expansion@>=2.0.0 <2.0.3: 2.0.3 - canvas: ^3.2.2 - dompurify@>=3.1.3 <=3.3.1: 3.3.2 - esbuild@<0.27.2: 0.27.2 - flatted@<=3.4.1: 3.4.2 - glob@>=10.2.0 <10.5.0: 11.1.0 - is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - lodash@>=4.0.0 <= 4.17.23: 4.18.0 - lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 - picomatch@<2.3.2: 2.3.2 - picomatch@>=4.0.0 <4.0.4: 4.0.4 - rollup@>=4.0.0 <4.59.0: 4.59.0 - safe-buffer: ^5.2.1 - safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 - side-channel: npm:@nolyfill/side-channel@^1.0.44 - smol-toml@<1.6.1: 1.6.1 - solid-js: 1.9.11 - string-width: ~8.2.0 - svgo@>=3.0.0 <3.3.3: 3.3.3 - tar@<=7.5.10: 7.5.11 - undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.16 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 - yaml@>=2.0.0 <2.8.3: 2.8.3 - yauzl@<3.2.1: 3.2.1 +allowBuilds: + '@parcel/watcher': false + canvas: false + esbuild: false + sharp: false +blockExoticSubdeps: true catalog: - "@amplitude/analytics-browser": 2.39.0 - "@amplitude/plugin-session-replay-browser": 1.27.7 - "@antfu/eslint-config": 8.2.0 - "@base-ui/react": 1.4.0 - "@date-fns/tz": 1.4.1 - "@chromatic-com/storybook": 5.1.2 - "@cucumber/cucumber": 12.8.0 - "@egoist/tailwindcss-icons": 1.9.2 - "@emoji-mart/data": 1.2.1 - "@eslint-react/eslint-plugin": 3.0.0 - "@eslint/js": 10.0.1 - "@floating-ui/react": 0.27.19 - "@formatjs/intl-localematcher": 0.8.3 - "@headlessui/react": 2.2.10 - "@heroicons/react": 2.2.0 - "@hono/node-server": 1.19.14 - "@iconify-json/heroicons": 1.2.3 - "@iconify-json/ri": 1.2.10 - "@lexical/code": 0.43.0 - "@lexical/link": 0.43.0 - "@lexical/list": 0.43.0 - "@lexical/react": 0.43.0 - "@lexical/selection": 0.43.0 - "@lexical/text": 0.43.0 - "@lexical/utils": 0.43.0 - "@mdx-js/loader": 3.1.1 - "@mdx-js/react": 3.1.1 - "@mdx-js/rollup": 3.1.1 - "@monaco-editor/react": 4.7.0 - "@next/eslint-plugin-next": 16.2.3 - "@next/mdx": 16.2.3 - "@orpc/client": 1.13.14 - "@orpc/contract": 1.13.14 - "@orpc/openapi-client": 1.13.14 - "@orpc/tanstack-query": 1.13.14 - "@playwright/test": 1.59.1 - "@remixicon/react": 4.9.0 - "@rgrove/parse-xml": 4.2.0 - "@sentry/react": 10.48.0 - "@storybook/addon-docs": 10.3.5 - "@storybook/addon-links": 10.3.5 - "@storybook/addon-onboarding": 10.3.5 - "@storybook/addon-themes": 10.3.5 - "@storybook/nextjs-vite": 10.3.5 - "@storybook/react": 10.3.5 - "@streamdown/math": 1.0.2 - "@svgdotjs/svg.js": 3.2.5 - "@t3-oss/env-nextjs": 0.13.11 - "@tailwindcss/postcss": 4.2.2 - "@tailwindcss/typography": 0.5.19 - "@tailwindcss/vite": 4.2.2 - "@tanstack/eslint-plugin-query": 5.99.0 - "@tanstack/react-devtools": 0.10.2 - "@tanstack/react-form": 1.29.0 - "@tanstack/react-form-devtools": 0.2.21 - "@tanstack/react-query": 5.99.0 - "@tanstack/react-query-devtools": 5.99.0 - "@tanstack/react-virtual": 3.13.23 - "@testing-library/dom": 10.4.1 - "@testing-library/jest-dom": 6.9.1 - "@testing-library/react": 16.3.2 - "@testing-library/user-event": 14.6.1 - "@tsslint/cli": 3.0.3 - "@tsslint/compat-eslint": 3.0.3 - "@tsslint/config": 3.0.3 - "@types/js-cookie": 3.0.6 - "@types/js-yaml": 4.0.9 - "@types/negotiator": 0.6.4 - "@types/node": 25.6.0 - "@types/postcss-js": 4.1.0 - "@types/qs": 6.15.0 - "@types/react": 19.2.14 - "@types/react-dom": 19.2.3 - "@types/sortablejs": 1.15.9 - "@typescript-eslint/eslint-plugin": 8.58.2 - "@typescript-eslint/parser": 8.58.2 - "@typescript/native-preview": 7.0.0-dev.20260413.1 - "@vitejs/plugin-react": 6.0.1 - "@vitejs/plugin-rsc": 0.5.24 - "@vitest/coverage-v8": 4.1.4 + '@amplitude/analytics-browser': 2.39.0 + '@amplitude/plugin-session-replay-browser': 1.27.7 + '@antfu/eslint-config': 8.2.0 + '@base-ui/react': 1.4.0 + '@chromatic-com/storybook': 5.1.2 + '@cucumber/cucumber': 12.8.0 + '@date-fns/tz': 1.4.1 + '@egoist/tailwindcss-icons': 1.9.2 + '@emoji-mart/data': 1.2.1 + '@eslint-react/eslint-plugin': 3.0.0 + '@eslint/js': 10.0.1 + '@floating-ui/react': 0.27.19 + '@formatjs/intl-localematcher': 0.8.3 + '@headlessui/react': 2.2.10 + '@heroicons/react': 2.2.0 + '@hono/node-server': 1.19.14 + '@iconify-json/heroicons': 1.2.3 + '@iconify-json/ri': 1.2.10 + '@lexical/code': 0.43.0 + '@lexical/link': 0.43.0 + '@lexical/list': 0.43.0 + '@lexical/react': 0.43.0 + '@lexical/selection': 0.43.0 + '@lexical/text': 0.43.0 + '@lexical/utils': 0.43.0 + '@mdx-js/loader': 3.1.1 + '@mdx-js/react': 3.1.1 + '@mdx-js/rollup': 3.1.1 + '@monaco-editor/react': 4.7.0 + '@next/eslint-plugin-next': 16.2.3 + '@next/mdx': 16.2.3 + '@orpc/client': 1.13.14 + '@orpc/contract': 1.13.14 + '@orpc/openapi-client': 1.13.14 + '@orpc/tanstack-query': 1.13.14 + '@playwright/test': 1.59.1 + '@remixicon/react': 4.9.0 + '@rgrove/parse-xml': 4.2.0 + '@sentry/react': 10.48.0 + '@storybook/addon-docs': 10.3.5 + '@storybook/addon-links': 10.3.5 + '@storybook/addon-onboarding': 10.3.5 + '@storybook/addon-themes': 10.3.5 + '@storybook/nextjs-vite': 10.3.5 + '@storybook/react': 10.3.5 + '@streamdown/math': 1.0.2 + '@svgdotjs/svg.js': 3.2.5 + '@t3-oss/env-nextjs': 0.13.11 + '@tailwindcss/postcss': 4.2.2 + '@tailwindcss/typography': 0.5.19 + '@tailwindcss/vite': 4.2.2 + '@tanstack/eslint-plugin-query': 5.99.0 + '@tanstack/react-devtools': 0.10.2 + '@tanstack/react-form': 1.29.0 + '@tanstack/react-form-devtools': 0.2.21 + '@tanstack/react-query': 5.99.0 + '@tanstack/react-query-devtools': 5.99.0 + '@tanstack/react-virtual': 3.13.23 + '@testing-library/dom': 10.4.1 + '@testing-library/jest-dom': 6.9.1 + '@testing-library/react': 16.3.2 + '@testing-library/user-event': 14.6.1 + '@tsdown/css': 0.21.8 + '@tsslint/cli': 3.0.3 + '@tsslint/compat-eslint': 3.0.3 + '@tsslint/config': 3.0.3 + '@types/js-cookie': 3.0.6 + '@types/js-yaml': 4.0.9 + '@types/negotiator': 0.6.4 + '@types/node': 25.6.0 + '@types/postcss-js': 4.1.0 + '@types/qs': 6.15.0 + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3 + '@types/sortablejs': 1.15.9 + '@typescript-eslint/eslint-plugin': 8.58.2 + '@typescript-eslint/parser': 8.58.2 + '@typescript/native-preview': 7.0.0-dev.20260413.1 + '@vitejs/plugin-react': 6.0.1 + '@vitejs/plugin-rsc': 0.5.24 + '@vitest/coverage-v8': 4.1.4 abcjs: 6.6.2 agentation: 3.0.2 ahooks: 3.9.7 @@ -139,7 +105,7 @@ catalog: date-fns: 4.1.0 dayjs: 1.11.20 decimal.js: 10.6.0 - dompurify: 3.3.3 + dompurify: 3.4.0 echarts: 6.0.0 echarts-for-react: 3.0.6 elkjs: 0.11.1 @@ -159,7 +125,7 @@ catalog: fast-deep-equal: 3.1.3 happy-dom: 20.9.0 hast-util-to-jsx-runtime: 2.3.6 - hono: 4.12.12 + hono: 4.12.14 html-entities: 2.6.0 html-to-image: 1.11.13 i18next: 26.0.4 @@ -176,6 +142,7 @@ catalog: ky: 2.0.0 lamejs: 1.2.1 lexical: 0.43.0 + loro-crdt: 1.10.8 mermaid: 11.14.0 mime: 4.1.0 mitt: 3.0.1 @@ -206,6 +173,7 @@ catalog: scheduler: 0.27.0 sharp: 0.34.5 shiki: 4.0.2 + socket.io-client: 4.8.3 sortablejs: 1.15.7 std-semver: 1.0.8 storybook: 10.3.5 @@ -222,11 +190,46 @@ catalog: use-context-selector: 2.0.0 uuid: 13.0.0 vinext: 0.0.41 - vite: npm:@voidzero-dev/vite-plus-core@0.1.16 + vite: npm:@voidzero-dev/vite-plus-core@0.1.18 vite-plugin-inspect: 12.0.0-beta.1 - vite-plus: 0.1.16 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 + vite-plus: 0.1.18 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 vitest-canvas-mock: 1.1.4 zod: 4.3.6 zundo: 2.3.0 zustand: 5.0.12 +catalogMode: prefer +overrides: + '@lexical/code': npm:lexical-code-no-prism@0.41.0 + '@monaco-editor/loader': 1.7.0 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 + canvas: ^3.2.2 + dompurify@>=3.1.3 <=3.3.1: 3.3.2 + esbuild@<0.27.2: 0.27.2 + flatted@<=3.4.1: 3.4.2 + glob@>=10.2.0 <10.5.0: 11.1.0 + is-core-module: npm:@nolyfill/is-core-module@^1.0.39 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + picomatch@<2.3.2: 2.3.2 + picomatch@>=4.0.0 <4.0.4: 4.0.4 + rollup@>=4.0.0 <4.59.0: 4.59.0 + safe-buffer: ^5.2.1 + safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 + side-channel: npm:@nolyfill/side-channel@^1.0.44 + smol-toml@<1.6.1: 1.6.1 + solid-js: 1.9.11 + string-width: ~8.2.0 + svgo@>=3.0.0 <3.3.3: 3.3.3 + tar@<=7.5.10: 7.5.11 + undici@>=7.0.0 <7.24.0: 7.24.0 + vite: npm:@voidzero-dev/vite-plus-core@0.1.18 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 + yaml@>=2.0.0 <2.8.3: 2.8.3 + yauzl@<3.2.1: 3.2.1 +strictDepBuilds: true +trustPolicy: no-downgrade +trustPolicyExclude: + - chokidar@4.0.3 + - reselect@5.1.1 + - semver@6.3.1 diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index e058edb0ca..28ebcb89c2 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -48,13 +48,14 @@ "build": "vp pack", "lint": "eslint", "lint:fix": "eslint --fix", - "type-check": "tsc -p tsconfig.json --noEmit", + "type-check": "tsc", "test": "vp test", "test:coverage": "vp test --coverage", "publish:check": "./scripts/publish.sh --dry-run", "publish:npm": "./scripts/publish.sh" }, "devDependencies": { + "@dify/tsconfig": "workspace:*", "@eslint/js": "catalog:", "@types/node": "catalog:", "@typescript-eslint/eslint-plugin": "catalog:", diff --git a/sdks/nodejs-client/src/http/sse.test.ts b/sdks/nodejs-client/src/http/sse.test.ts index 70cd11007d..83cde28de3 100644 --- a/sdks/nodejs-client/src/http/sse.test.ts +++ b/sdks/nodejs-client/src/http/sse.test.ts @@ -14,8 +14,8 @@ describe("sse parsing", () => { events.push(event); } expect(events).toHaveLength(1); - expect(events[0].event).toBe("message"); - expect(events[0].data).toEqual({ answer: "hi" }); + expect(events[0]!.event).toBe("message"); + expect(events[0]!.data).toEqual({ answer: "hi" }); }); it("handles multi-line data payloads", async () => { @@ -24,8 +24,8 @@ describe("sse parsing", () => { for await (const event of parseSseStream(stream)) { events.push(event); } - expect(events[0].raw).toBe("line1\nline2"); - expect(events[0].data).toBe("line1\nline2"); + expect(events[0]!.raw).toBe("line1\nline2"); + expect(events[0]!.data).toBe("line1\nline2"); }); it("ignores comments and flushes the last event without a trailing separator", async () => { diff --git a/sdks/nodejs-client/src/index.test.ts b/sdks/nodejs-client/src/index.test.ts index d194680379..8d56b994c4 100644 --- a/sdks/nodejs-client/src/index.test.ts +++ b/sdks/nodejs-client/src/index.test.ts @@ -99,7 +99,7 @@ describe("File uploads", () => { super(); } - _read() {} + override _read() {} append() {} diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json index 46055447be..1e55007ed0 100644 --- a/sdks/nodejs-client/tsconfig.json +++ b/sdks/nodejs-client/tsconfig.json @@ -1,18 +1,14 @@ { + "extends": "@dify/tsconfig/node.json", "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", + "lib": ["ES2023", "DOM", "DOM.Iterable"], "rootDir": ".", "outDir": "dist", + "noEmit": false, "declaration": true, "declarationMap": true, "sourceMap": true, - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, "types": ["node"] }, - "include": ["src/**/*.ts", "tests/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts", "vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index a34932a4ef..aebcaf8f73 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from 'vite-plus' export default defineConfig({ - staged: {}, + staged: { + '*': 'eslint --fix --pass-on-unpruned-suppressions', + }, }) diff --git a/web/.env.example b/web/.env.example index 93cbc22fc8..643aba482e 100644 --- a/web/.env.example +++ b/web/.env.example @@ -14,6 +14,8 @@ NEXT_PUBLIC_API_PREFIX=http://localhost:5001/console/api NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api # When the frontend and backend run on different subdomains, set NEXT_PUBLIC_COOKIE_DOMAIN=1. NEXT_PUBLIC_COOKIE_DOMAIN= +# WebSocket server URL. +NEXT_PUBLIC_SOCKET_URL=ws://localhost:5001 # Dev-only Hono proxy targets. # The frontend keeps requesting http://localhost:5001 directly, diff --git a/web/.gitignore b/web/.gitignore index a4ae324795..9de3dc83f9 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -64,5 +64,3 @@ public/fallback-*.js .vscode/settings.json .vscode/mcp.json - -.eslintcache diff --git a/web/.storybook/utils/form-story-wrapper.tsx b/web/.storybook/utils/form-story-wrapper.tsx index 90349a0325..7503e9905d 100644 --- a/web/.storybook/utils/form-story-wrapper.tsx +++ b/web/.storybook/utils/form-story-wrapper.tsx @@ -47,7 +47,7 @@ export const FormStoryWrapper = ({ {children(form)}