Compare commits

..
Author SHA1 Message Date
yyhandGitHub 9311150bd2 Merge branch 'main' into 4-2-no-global-loading 2026-04-02 19:16:40 +08:00
autofix-ci[bot]andGitHub c49201ee28 [autofix.ci] apply automated fixes 2026-04-02 09:40:01 +00:00
Stephen Zhou d13e6901cf refactor: no global loading 2026-04-02 17:36:06 +08:00
526 changed files with 7538 additions and 3788 deletions
-6
View File
@@ -89,12 +89,6 @@ if $web_modified; then
echo "No staged TypeScript changes detected, skipping type-check:tsgo"
fi
echo "Running knip"
if ! pnpm run knip; then
echo "Knip check failed. Please run 'pnpm run knip' to fix the errors."
exit 1
fi
echo "Running unit tests check"
modified_files=$(git diff --cached --name-only -- utils | grep -v '\.spec\.ts$' || true)
+2 -2
View File
@@ -1,6 +1,6 @@
from flask_restx import Resource, fields, marshal_with
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.orm import sessionmaker
from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import (
@@ -71,7 +71,7 @@ class AppImportApi(Resource):
args = AppImportPayload.model_validate(console_ns.payload)
# Create service with session
with Session(db.engine) as session:
with sessionmaker(db.engine).begin() as session:
import_service = AppDslService(session)
# Import app
account = current_user
@@ -158,11 +158,10 @@ class DataSourceApi(Resource):
@login_required
@account_initialization_required
def patch(self, binding_id, action: Literal["enable", "disable"]):
_, current_tenant_id = current_account_with_tenant()
binding_id = str(binding_id)
with sessionmaker(db.engine, expire_on_commit=False).begin() as session:
data_source_binding = session.execute(
select(DataSourceOauthBinding).filter_by(id=binding_id, tenant_id=current_tenant_id)
select(DataSourceOauthBinding).filter_by(id=binding_id)
).scalar_one_or_none()
if data_source_binding is None:
raise NotFound("Data source binding not found.")
+1 -2
View File
@@ -7,7 +7,7 @@ from werkzeug.exceptions import NotFound, RequestEntityTooLarge
from controllers.trigger import bp
from core.trigger.debug.event_bus import TriggerDebugEventBus
from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key
from services.trigger.webhook_service import RawWebhookDataDict, WebhookService
from services.trigger.webhook_service import WebhookService
logger = logging.getLogger(__name__)
@@ -23,7 +23,6 @@ def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False):
webhook_id, is_debug=is_debug
)
webhook_data: RawWebhookDataDict
try:
# Use new unified extraction and validation
webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config)
+3 -9
View File
@@ -3,19 +3,13 @@
import logging
import traceback
from datetime import UTC, datetime
from typing import Any, TypedDict
from typing import Any
import orjson
from configs import dify_config
class IdentityDict(TypedDict, total=False):
tenant_id: str
user_id: str
user_type: str
class StructuredJSONFormatter(logging.Formatter):
"""
JSON log formatter following the specified schema:
@@ -90,7 +84,7 @@ class StructuredJSONFormatter(logging.Formatter):
return log_dict
def _extract_identity(self, record: logging.LogRecord) -> IdentityDict | None:
def _extract_identity(self, record: logging.LogRecord) -> dict[str, str] | None:
tenant_id = getattr(record, "tenant_id", None)
user_id = getattr(record, "user_id", None)
user_type = getattr(record, "user_type", None)
@@ -98,7 +92,7 @@ class StructuredJSONFormatter(logging.Formatter):
if not any([tenant_id, user_id, user_type]):
return None
identity: IdentityDict = {}
identity: dict[str, str] = {}
if tenant_id:
identity["tenant_id"] = tenant_id
if user_id:
+6 -4
View File
@@ -4,7 +4,7 @@ from collections.abc import Callable
from concurrent.futures import Future, ThreadPoolExecutor, TimeoutError
from datetime import timedelta
from types import TracebackType
from typing import Any, Self
from typing import Any, Self, cast
from httpx import HTTPStatusError
from pydantic import BaseModel
@@ -338,11 +338,12 @@ class BaseSession[
validated_request = self._receive_request_type.model_validate(
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
)
validated_request = cast(ReceiveRequestT, validated_request)
responder = RequestResponder[ReceiveRequestT, SendResultT](
request_id=message.message.root.id,
request_meta=validated_request.root.params.meta if validated_request.root.params else None,
request=validated_request, # type: ignore[arg-type] # mypy can't narrow constrained TypeVar from model_validate
request=validated_request,
session=self,
on_complete=lambda r: self._in_flight.pop(r.request_id, None),
)
@@ -358,14 +359,15 @@ class BaseSession[
notification = self._receive_notification_type.model_validate(
message.message.root.model_dump(by_alias=True, mode="json", exclude_none=True)
)
notification = cast(ReceiveNotificationT, notification)
# Handle cancellation notifications
if isinstance(notification.root, CancelledNotification):
cancelled_id = notification.root.params.requestId
if cancelled_id in self._in_flight:
self._in_flight[cancelled_id].cancel()
else:
self._received_notification(notification) # type: ignore[arg-type]
self._handle_incoming(notification) # type: ignore[arg-type]
self._received_notification(notification)
self._handle_incoming(notification)
except Exception as e:
# For other validation errors, log and continue
logger.warning("Failed to validate notification: %s. Message was: %s", e, message.message.root)
+12 -20
View File
@@ -144,26 +144,22 @@ class AccountService:
@staticmethod
def load_user(user_id: str) -> None | Account:
account = db.session.get(Account, user_id)
account = db.session.query(Account).filter_by(id=user_id).first()
if not account:
return None
if account.status == AccountStatus.BANNED:
raise Unauthorized("Account is banned.")
current_tenant = db.session.scalar(
select(TenantAccountJoin)
.where(TenantAccountJoin.account_id == account.id, TenantAccountJoin.current == True)
.limit(1)
)
current_tenant = db.session.query(TenantAccountJoin).filter_by(account_id=account.id, current=True).first()
if current_tenant:
account.set_tenant_id(current_tenant.tenant_id)
else:
available_ta = db.session.scalar(
select(TenantAccountJoin)
.where(TenantAccountJoin.account_id == account.id)
available_ta = (
db.session.query(TenantAccountJoin)
.filter_by(account_id=account.id)
.order_by(TenantAccountJoin.id.asc())
.limit(1)
.first()
)
if not available_ta:
return None
@@ -199,7 +195,7 @@ class AccountService:
def authenticate(email: str, password: str, invite_token: str | None = None) -> Account:
"""authenticate account with email and password"""
account = db.session.scalar(select(Account).where(Account.email == email).limit(1))
account = db.session.query(Account).filter_by(email=email).first()
if not account:
raise AccountPasswordError("Invalid email or password.")
@@ -375,10 +371,8 @@ class AccountService:
"""Link account integrate"""
try:
# Query whether there is an existing binding record for the same provider
account_integrate: AccountIntegrate | None = db.session.scalar(
select(AccountIntegrate)
.where(AccountIntegrate.account_id == account.id, AccountIntegrate.provider == provider)
.limit(1)
account_integrate: AccountIntegrate | None = (
db.session.query(AccountIntegrate).filter_by(account_id=account.id, provider=provider).first()
)
if account_integrate:
@@ -422,9 +416,7 @@ class AccountService:
def update_account_email(account: Account, email: str) -> Account:
"""Update account email"""
account.email = email
account_integrate = db.session.scalar(
select(AccountIntegrate).where(AccountIntegrate.account_id == account.id).limit(1)
)
account_integrate = db.session.query(AccountIntegrate).filter_by(account_id=account.id).first()
if account_integrate:
db.session.delete(account_integrate)
db.session.add(account)
@@ -826,7 +818,7 @@ class AccountService:
)
)
account = db.session.scalar(select(Account).where(Account.email == email).limit(1))
account = db.session.query(Account).where(Account.email == email).first()
if not account:
return None
@@ -1026,7 +1018,7 @@ class AccountService:
@staticmethod
def check_email_unique(email: str) -> bool:
return db.session.scalar(select(Account).where(Account.email == email).limit(1)) is None
return db.session.query(Account).filter_by(email=email).first() is None
class TenantService:
+1 -6
View File
@@ -32,11 +32,6 @@ class SubscriptionPlan(TypedDict):
expiration_date: int
class KnowledgeRateLimitDict(TypedDict):
limit: int
subscription_plan: str
class BillingService:
base_url = os.environ.get("BILLING_API_URL", "BILLING_API_URL")
secret_key = os.environ.get("BILLING_API_SECRET_KEY", "BILLING_API_SECRET_KEY")
@@ -63,7 +58,7 @@ class BillingService:
return usage_info
@classmethod
def get_knowledge_rate_limit(cls, tenant_id: str) -> KnowledgeRateLimitDict:
def get_knowledge_rate_limit(cls, tenant_id: str):
params = {"tenant_id": tenant_id}
knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params)
+25 -45
View File
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
import httpx
from graphon.nodes.http_request.exc import InvalidHttpMethodError
from sqlalchemy import func, select
from sqlalchemy import select
from constants import HIDDEN_VALUE
from core.helper import ssrf_proxy
@@ -103,10 +103,8 @@ class ExternalDatasetService:
@staticmethod
def get_external_knowledge_api(external_knowledge_api_id: str, tenant_id: str) -> ExternalKnowledgeApis:
external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar(
select(ExternalKnowledgeApis)
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
.limit(1)
external_knowledge_api: ExternalKnowledgeApis | None = (
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
)
if external_knowledge_api is None:
raise ValueError("api template not found")
@@ -114,10 +112,8 @@ class ExternalDatasetService:
@staticmethod
def update_external_knowledge_api(tenant_id, user_id, external_knowledge_api_id, args) -> ExternalKnowledgeApis:
external_knowledge_api: ExternalKnowledgeApis | None = db.session.scalar(
select(ExternalKnowledgeApis)
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
.limit(1)
external_knowledge_api: ExternalKnowledgeApis | None = (
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
)
if external_knowledge_api is None:
raise ValueError("api template not found")
@@ -136,10 +132,8 @@ class ExternalDatasetService:
@staticmethod
def delete_external_knowledge_api(tenant_id: str, external_knowledge_api_id: str):
external_knowledge_api = db.session.scalar(
select(ExternalKnowledgeApis)
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
.limit(1)
external_knowledge_api = (
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
)
if external_knowledge_api is None:
raise ValueError("api template not found")
@@ -150,12 +144,9 @@ class ExternalDatasetService:
@staticmethod
def external_knowledge_api_use_check(external_knowledge_api_id: str) -> tuple[bool, int]:
count = (
db.session.scalar(
select(func.count(ExternalKnowledgeBindings.id)).where(
ExternalKnowledgeBindings.external_knowledge_api_id == external_knowledge_api_id
)
)
or 0
db.session.query(ExternalKnowledgeBindings)
.filter_by(external_knowledge_api_id=external_knowledge_api_id)
.count()
)
if count > 0:
return True, count
@@ -163,10 +154,8 @@ class ExternalDatasetService:
@staticmethod
def get_external_knowledge_binding_with_dataset_id(tenant_id: str, dataset_id: str) -> ExternalKnowledgeBindings:
external_knowledge_binding: ExternalKnowledgeBindings | None = db.session.scalar(
select(ExternalKnowledgeBindings)
.where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id)
.limit(1)
external_knowledge_binding: ExternalKnowledgeBindings | None = (
db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first()
)
if not external_knowledge_binding:
raise ValueError("external knowledge binding not found")
@@ -174,10 +163,8 @@ class ExternalDatasetService:
@staticmethod
def document_create_args_validate(tenant_id: str, external_knowledge_api_id: str, process_parameter: dict):
external_knowledge_api = db.session.scalar(
select(ExternalKnowledgeApis)
.where(ExternalKnowledgeApis.id == external_knowledge_api_id, ExternalKnowledgeApis.tenant_id == tenant_id)
.limit(1)
external_knowledge_api = (
db.session.query(ExternalKnowledgeApis).filter_by(id=external_knowledge_api_id, tenant_id=tenant_id).first()
)
if external_knowledge_api is None or external_knowledge_api.settings is None:
raise ValueError("api template not found")
@@ -251,17 +238,12 @@ class ExternalDatasetService:
@staticmethod
def create_external_dataset(tenant_id: str, user_id: str, args: dict) -> Dataset:
# check if dataset name already exists
if db.session.scalar(
select(Dataset).where(Dataset.name == args.get("name"), Dataset.tenant_id == tenant_id).limit(1)
):
if db.session.query(Dataset).filter_by(name=args.get("name"), tenant_id=tenant_id).first():
raise DatasetNameDuplicateError(f"Dataset with name {args.get('name')} already exists.")
external_knowledge_api = db.session.scalar(
select(ExternalKnowledgeApis)
.where(
ExternalKnowledgeApis.id == args.get("external_knowledge_api_id"),
ExternalKnowledgeApis.tenant_id == tenant_id,
)
.limit(1)
external_knowledge_api = (
db.session.query(ExternalKnowledgeApis)
.filter_by(id=args.get("external_knowledge_api_id"), tenant_id=tenant_id)
.first()
)
if external_knowledge_api is None:
@@ -304,18 +286,16 @@ class ExternalDatasetService:
external_retrieval_parameters: dict,
metadata_condition: MetadataCondition | None = None,
):
external_knowledge_binding = db.session.scalar(
select(ExternalKnowledgeBindings)
.where(ExternalKnowledgeBindings.dataset_id == dataset_id, ExternalKnowledgeBindings.tenant_id == tenant_id)
.limit(1)
external_knowledge_binding = (
db.session.query(ExternalKnowledgeBindings).filter_by(dataset_id=dataset_id, tenant_id=tenant_id).first()
)
if not external_knowledge_binding:
raise ValueError("external knowledge binding not found")
external_knowledge_api = db.session.scalar(
select(ExternalKnowledgeApis)
.where(ExternalKnowledgeApis.id == external_knowledge_binding.external_knowledge_api_id)
.limit(1)
external_knowledge_api = (
db.session.query(ExternalKnowledgeApis)
.filter_by(id=external_knowledge_binding.external_knowledge_api_id)
.first()
)
if external_knowledge_api is None or external_knowledge_api.settings is None:
raise ValueError("external api template not found")
+3 -13
View File
@@ -1,7 +1,7 @@
import json
import logging
import time
from typing import Any, TypedDict
from typing import Any
from graphon.model_runtime.entities import LLMMode
@@ -18,16 +18,6 @@ from models.enums import CreatorUserRole, DatasetQuerySource
logger = logging.getLogger(__name__)
class QueryDict(TypedDict):
content: str
class RetrieveResponseDict(TypedDict):
query: QueryDict
records: list[dict[str, Any]]
default_retrieval_model = {
"search_method": RetrievalMethod.SEMANTIC_SEARCH,
"reranking_enable": False,
@@ -160,7 +150,7 @@ class HitTestingService:
return dict(cls.compact_external_retrieve_response(dataset, query, all_documents))
@classmethod
def compact_retrieve_response(cls, query: str, documents: list[Document]) -> RetrieveResponseDict:
def compact_retrieve_response(cls, query: str, documents: list[Document]) -> dict[Any, Any]:
records = RetrievalService.format_retrieval_documents(documents)
return {
@@ -171,7 +161,7 @@ class HitTestingService:
}
@classmethod
def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> RetrieveResponseDict:
def compact_external_retrieve_response(cls, dataset: Dataset, query: str, documents: list) -> dict[Any, Any]:
records = []
if dataset.provider == "external":
for document in documents:
+24 -53
View File
@@ -1,8 +1,6 @@
import copy
import logging
from sqlalchemy import delete, func, select
from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource
from extensions.ext_database import db
from extensions.ext_redis import redis_client
@@ -27,14 +25,10 @@ class MetadataService:
raise ValueError("Metadata name cannot exceed 255 characters.")
current_user, current_tenant_id = current_account_with_tenant()
# check if metadata name already exists
if db.session.scalar(
select(DatasetMetadata)
.where(
DatasetMetadata.tenant_id == current_tenant_id,
DatasetMetadata.dataset_id == dataset_id,
DatasetMetadata.name == metadata_args.name,
)
.limit(1)
if (
db.session.query(DatasetMetadata)
.filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=metadata_args.name)
.first()
):
raise ValueError("Metadata name already exists.")
for field in BuiltInField:
@@ -60,14 +54,10 @@ class MetadataService:
lock_key = f"dataset_metadata_lock_{dataset_id}"
# check if metadata name already exists
current_user, current_tenant_id = current_account_with_tenant()
if db.session.scalar(
select(DatasetMetadata)
.where(
DatasetMetadata.tenant_id == current_tenant_id,
DatasetMetadata.dataset_id == dataset_id,
DatasetMetadata.name == name,
)
.limit(1)
if (
db.session.query(DatasetMetadata)
.filter_by(tenant_id=current_tenant_id, dataset_id=dataset_id, name=name)
.first()
):
raise ValueError("Metadata name already exists.")
for field in BuiltInField:
@@ -75,11 +65,7 @@ class MetadataService:
raise ValueError("Metadata name already exists in Built-in fields.")
try:
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
metadata = db.session.scalar(
select(DatasetMetadata)
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
.limit(1)
)
metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first()
if metadata is None:
raise ValueError("Metadata not found.")
old_name = metadata.name
@@ -88,9 +74,9 @@ class MetadataService:
metadata.updated_at = naive_utc_now()
# update related documents
dataset_metadata_bindings = db.session.scalars(
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
).all()
dataset_metadata_bindings = (
db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all()
)
if dataset_metadata_bindings:
document_ids = [binding.document_id for binding in dataset_metadata_bindings]
documents = DocumentService.get_document_by_ids(document_ids)
@@ -115,19 +101,15 @@ class MetadataService:
lock_key = f"dataset_metadata_lock_{dataset_id}"
try:
MetadataService.knowledge_base_metadata_lock_check(dataset_id, None)
metadata = db.session.scalar(
select(DatasetMetadata)
.where(DatasetMetadata.id == metadata_id, DatasetMetadata.dataset_id == dataset_id)
.limit(1)
)
metadata = db.session.query(DatasetMetadata).filter_by(id=metadata_id, dataset_id=dataset_id).first()
if metadata is None:
raise ValueError("Metadata not found.")
db.session.delete(metadata)
# deal related documents
dataset_metadata_bindings = db.session.scalars(
select(DatasetMetadataBinding).where(DatasetMetadataBinding.metadata_id == metadata_id)
).all()
dataset_metadata_bindings = (
db.session.query(DatasetMetadataBinding).filter_by(metadata_id=metadata_id).all()
)
if dataset_metadata_bindings:
document_ids = [binding.document_id for binding in dataset_metadata_bindings]
documents = DocumentService.get_document_by_ids(document_ids)
@@ -242,23 +224,16 @@ class MetadataService:
# deal metadata binding (in the same transaction as the doc_metadata update)
if not operation.partial_update:
db.session.execute(
delete(DatasetMetadataBinding).where(
DatasetMetadataBinding.document_id == operation.document_id
)
)
db.session.query(DatasetMetadataBinding).filter_by(document_id=operation.document_id).delete()
current_user, current_tenant_id = current_account_with_tenant()
for metadata_value in operation.metadata_list:
# check if binding already exists
if operation.partial_update:
existing_binding = db.session.scalar(
select(DatasetMetadataBinding)
.where(
DatasetMetadataBinding.document_id == operation.document_id,
DatasetMetadataBinding.metadata_id == metadata_value.id,
)
.limit(1)
existing_binding = (
db.session.query(DatasetMetadataBinding)
.filter_by(document_id=operation.document_id, metadata_id=metadata_value.id)
.first()
)
if existing_binding:
continue
@@ -300,13 +275,9 @@ class MetadataService:
"id": item.get("id"),
"name": item.get("name"),
"type": item.get("type"),
"count": db.session.scalar(
select(func.count(DatasetMetadataBinding.id)).where(
DatasetMetadataBinding.metadata_id == item.get("id"),
DatasetMetadataBinding.dataset_id == dataset.id,
)
)
or 0,
"count": db.session.query(DatasetMetadataBinding)
.filter_by(metadata_id=item.get("id"), dataset_id=dataset.id)
.count(),
}
for item in dataset.doc_metadata or []
if item.get("id") != "built-in"
+54 -53
View File
@@ -156,27 +156,27 @@ class RagPipelineService:
:param template_id: template id
:param template_info: template info
"""
customized_template: PipelineCustomizedTemplate | None = db.session.scalar(
select(PipelineCustomizedTemplate)
customized_template: PipelineCustomizedTemplate | None = (
db.session.query(PipelineCustomizedTemplate)
.where(
PipelineCustomizedTemplate.id == template_id,
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
)
.limit(1)
.first()
)
if not customized_template:
raise ValueError("Customized pipeline template not found.")
# check template name is exist
template_name = template_info.name
if template_name:
template = db.session.scalar(
select(PipelineCustomizedTemplate)
template = (
db.session.query(PipelineCustomizedTemplate)
.where(
PipelineCustomizedTemplate.name == template_name,
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
PipelineCustomizedTemplate.id != template_id,
)
.limit(1)
.first()
)
if template:
raise ValueError("Template name is already exists")
@@ -192,13 +192,13 @@ class RagPipelineService:
"""
Delete customized pipeline template.
"""
customized_template: PipelineCustomizedTemplate | None = db.session.scalar(
select(PipelineCustomizedTemplate)
customized_template: PipelineCustomizedTemplate | None = (
db.session.query(PipelineCustomizedTemplate)
.where(
PipelineCustomizedTemplate.id == template_id,
PipelineCustomizedTemplate.tenant_id == current_user.current_tenant_id,
)
.limit(1)
.first()
)
if not customized_template:
raise ValueError("Customized pipeline template not found.")
@@ -210,14 +210,14 @@ class RagPipelineService:
Get draft workflow
"""
# fetch draft workflow by rag pipeline
workflow = db.session.scalar(
select(Workflow)
workflow = (
db.session.query(Workflow)
.where(
Workflow.tenant_id == pipeline.tenant_id,
Workflow.app_id == pipeline.id,
Workflow.version == "draft",
)
.limit(1)
.first()
)
# return draft workflow
@@ -232,28 +232,28 @@ class RagPipelineService:
return None
# fetch published workflow by workflow_id
workflow = db.session.scalar(
select(Workflow)
workflow = (
db.session.query(Workflow)
.where(
Workflow.tenant_id == pipeline.tenant_id,
Workflow.app_id == pipeline.id,
Workflow.id == pipeline.workflow_id,
)
.limit(1)
.first()
)
return workflow
def get_published_workflow_by_id(self, pipeline: Pipeline, workflow_id: str) -> Workflow | None:
"""Fetch a published workflow snapshot by ID for restore operations."""
workflow = db.session.scalar(
select(Workflow)
workflow = (
db.session.query(Workflow)
.where(
Workflow.tenant_id == pipeline.tenant_id,
Workflow.app_id == pipeline.id,
Workflow.id == workflow_id,
)
.limit(1)
.first()
)
if workflow and workflow.version == Workflow.VERSION_DRAFT:
raise IsDraftWorkflowError("source workflow must be published")
@@ -974,7 +974,7 @@ class RagPipelineService:
if invoke_from.value == InvokeFrom.PUBLISHED_PIPELINE:
document_id = get_system_segment(variable_pool, SystemVariableKey.DOCUMENT_ID)
if document_id:
document = db.session.get(Document, document_id.value)
document = db.session.query(Document).where(Document.id == document_id.value).first()
if document:
document.indexing_status = IndexingStatus.ERROR
document.error = error
@@ -1178,12 +1178,12 @@ class RagPipelineService:
"""
Publish customized pipeline template
"""
pipeline = db.session.get(Pipeline, pipeline_id)
pipeline = db.session.query(Pipeline).where(Pipeline.id == pipeline_id).first()
if not pipeline:
raise ValueError("Pipeline not found")
if not pipeline.workflow_id:
raise ValueError("Pipeline workflow not found")
workflow = db.session.get(Workflow, pipeline.workflow_id)
workflow = db.session.query(Workflow).where(Workflow.id == pipeline.workflow_id).first()
if not workflow:
raise ValueError("Workflow not found")
with Session(db.engine) as session:
@@ -1194,21 +1194,21 @@ class RagPipelineService:
# check template name is exist
template_name = args.get("name")
if template_name:
template = db.session.scalar(
select(PipelineCustomizedTemplate)
template = (
db.session.query(PipelineCustomizedTemplate)
.where(
PipelineCustomizedTemplate.name == template_name,
PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id,
)
.limit(1)
.first()
)
if template:
raise ValueError("Template name is already exists")
max_position = db.session.scalar(
select(func.max(PipelineCustomizedTemplate.position)).where(
PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id
)
max_position = (
db.session.query(func.max(PipelineCustomizedTemplate.position))
.where(PipelineCustomizedTemplate.tenant_id == pipeline.tenant_id)
.scalar()
)
from services.rag_pipeline.rag_pipeline_dsl_service import RagPipelineDslService
@@ -1239,14 +1239,13 @@ class RagPipelineService:
def is_workflow_exist(self, pipeline: Pipeline) -> bool:
return (
db.session.scalar(
select(func.count(Workflow.id)).where(
Workflow.tenant_id == pipeline.tenant_id,
Workflow.app_id == pipeline.id,
Workflow.version == Workflow.VERSION_DRAFT,
)
db.session.query(Workflow)
.where(
Workflow.tenant_id == pipeline.tenant_id,
Workflow.app_id == pipeline.id,
Workflow.version == Workflow.VERSION_DRAFT,
)
or 0
.count()
) > 0
def get_node_last_run(
@@ -1354,11 +1353,11 @@ class RagPipelineService:
def get_recommended_plugins(self, type: str) -> dict:
# Query active recommended plugins
stmt = select(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
query = db.session.query(PipelineRecommendedPlugin).where(PipelineRecommendedPlugin.active == True)
if type and type != "all":
stmt = stmt.where(PipelineRecommendedPlugin.type == type)
query = query.where(PipelineRecommendedPlugin.type == type)
pipeline_recommended_plugins = db.session.scalars(stmt.order_by(PipelineRecommendedPlugin.position.asc())).all()
pipeline_recommended_plugins = query.order_by(PipelineRecommendedPlugin.position.asc()).all()
if not pipeline_recommended_plugins:
return {
@@ -1397,12 +1396,14 @@ class RagPipelineService:
"""
Retry error document
"""
document_pipeline_execution_log = db.session.scalar(
select(DocumentPipelineExecutionLog).where(DocumentPipelineExecutionLog.document_id == document.id).limit(1)
document_pipeline_execution_log = (
db.session.query(DocumentPipelineExecutionLog)
.where(DocumentPipelineExecutionLog.document_id == document.id)
.first()
)
if not document_pipeline_execution_log:
raise ValueError("Document pipeline execution log not found")
pipeline = db.session.get(Pipeline, document_pipeline_execution_log.pipeline_id)
pipeline = db.session.query(Pipeline).where(Pipeline.id == document_pipeline_execution_log.pipeline_id).first()
if not pipeline:
raise ValueError("Pipeline not found")
# convert to app config
@@ -1431,23 +1432,23 @@ class RagPipelineService:
"""
Get datasource plugins
"""
dataset: Dataset | None = db.session.scalar(
select(Dataset)
dataset: Dataset | None = (
db.session.query(Dataset)
.where(
Dataset.id == dataset_id,
Dataset.tenant_id == tenant_id,
)
.limit(1)
.first()
)
if not dataset:
raise ValueError("Dataset not found")
pipeline: Pipeline | None = db.session.scalar(
select(Pipeline)
pipeline: Pipeline | None = (
db.session.query(Pipeline)
.where(
Pipeline.id == dataset.pipeline_id,
Pipeline.tenant_id == tenant_id,
)
.limit(1)
.first()
)
if not pipeline:
raise ValueError("Pipeline not found")
@@ -1529,23 +1530,23 @@ class RagPipelineService:
"""
Get pipeline
"""
dataset: Dataset | None = db.session.scalar(
select(Dataset)
dataset: Dataset | None = (
db.session.query(Dataset)
.where(
Dataset.id == dataset_id,
Dataset.tenant_id == tenant_id,
)
.limit(1)
.first()
)
if not dataset:
raise ValueError("Dataset not found")
pipeline: Pipeline | None = db.session.scalar(
select(Pipeline)
pipeline: Pipeline | None = (
db.session.query(Pipeline)
.where(
Pipeline.id == dataset.pipeline_id,
Pipeline.tenant_id == tenant_id,
)
.limit(1)
.first()
)
if not pipeline:
raise ValueError("Pipeline not found")
@@ -3,7 +3,7 @@ import logging
from datetime import datetime
from graphon.model_runtime.utils.encoders import jsonable_encoder
from sqlalchemy import delete, or_, select
from sqlalchemy import or_, select
from sqlalchemy.orm import Session
from core.tools.__base.tool_provider import ToolProviderController
@@ -42,22 +42,20 @@ class WorkflowToolManageService:
labels: list[str] | None = None,
):
# check if the name is unique
existing_workflow_tool_provider = db.session.scalar(
select(WorkflowToolProvider)
existing_workflow_tool_provider = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == tenant_id,
# name or app_id
or_(WorkflowToolProvider.name == name, WorkflowToolProvider.app_id == workflow_app_id),
)
.limit(1)
.first()
)
if existing_workflow_tool_provider is not None:
raise ValueError(f"Tool with name {name} or app_id {workflow_app_id} already exists")
app: App | None = db.session.scalar(
select(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).limit(1)
)
app: App | None = db.session.query(App).where(App.id == workflow_app_id, App.tenant_id == tenant_id).first()
if app is None:
raise ValueError(f"App {workflow_app_id} not found")
@@ -124,30 +122,30 @@ class WorkflowToolManageService:
:return: the updated tool
"""
# check if the name is unique
existing_workflow_tool_provider = db.session.scalar(
select(WorkflowToolProvider)
existing_workflow_tool_provider = (
db.session.query(WorkflowToolProvider)
.where(
WorkflowToolProvider.tenant_id == tenant_id,
WorkflowToolProvider.name == name,
WorkflowToolProvider.id != workflow_tool_id,
)
.limit(1)
.first()
)
if existing_workflow_tool_provider is not None:
raise ValueError(f"Tool with name {name} already exists")
workflow_tool_provider: WorkflowToolProvider | None = db.session.scalar(
select(WorkflowToolProvider)
workflow_tool_provider: WorkflowToolProvider | None = (
db.session.query(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
.limit(1)
.first()
)
if workflow_tool_provider is None:
raise ValueError(f"Tool {workflow_tool_id} not found")
app: App | None = db.session.scalar(
select(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).limit(1)
app: App | None = (
db.session.query(App).where(App.id == workflow_tool_provider.app_id, App.tenant_id == tenant_id).first()
)
if app is None:
@@ -236,11 +234,9 @@ class WorkflowToolManageService:
:param tenant_id: the tenant id
:param workflow_tool_id: the workflow tool id
"""
db.session.execute(
delete(WorkflowToolProvider).where(
WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id
)
)
db.session.query(WorkflowToolProvider).where(
WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id
).delete()
db.session.commit()
@@ -255,10 +251,10 @@ class WorkflowToolManageService:
:param workflow_tool_id: the workflow tool id
:return: the tool
"""
db_tool: WorkflowToolProvider | None = db.session.scalar(
select(WorkflowToolProvider)
db_tool: WorkflowToolProvider | None = (
db.session.query(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
.limit(1)
.first()
)
return cls._get_workflow_tool(tenant_id, db_tool)
@@ -271,10 +267,10 @@ class WorkflowToolManageService:
:param workflow_app_id: the workflow app id
:return: the tool
"""
db_tool: WorkflowToolProvider | None = db.session.scalar(
select(WorkflowToolProvider)
db_tool: WorkflowToolProvider | None = (
db.session.query(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.app_id == workflow_app_id)
.limit(1)
.first()
)
return cls._get_workflow_tool(tenant_id, db_tool)
@@ -288,8 +284,8 @@ class WorkflowToolManageService:
if db_tool is None:
raise ValueError("Tool not found")
workflow_app: App | None = db.session.scalar(
select(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).limit(1)
workflow_app: App | None = (
db.session.query(App).where(App.id == db_tool.app_id, App.tenant_id == db_tool.tenant_id).first()
)
if workflow_app is None:
@@ -335,10 +331,10 @@ class WorkflowToolManageService:
:param workflow_tool_id: the workflow tool id
:return: the list of tools
"""
db_tool: WorkflowToolProvider | None = db.session.scalar(
select(WorkflowToolProvider)
db_tool: WorkflowToolProvider | None = (
db.session.query(WorkflowToolProvider)
.where(WorkflowToolProvider.tenant_id == tenant_id, WorkflowToolProvider.id == workflow_tool_id)
.limit(1)
.first()
)
if db_tool is None:
+8 -16
View File
@@ -3,7 +3,7 @@ import logging
import mimetypes
import secrets
from collections.abc import Callable, Mapping, Sequence
from typing import Any, TypedDict
from typing import Any
import orjson
from flask import request
@@ -50,14 +50,6 @@ logger = logging.getLogger(__name__)
_file_access_controller = DatabaseFileAccessController()
class RawWebhookDataDict(TypedDict):
method: str
headers: dict[str, str]
query_params: dict[str, str]
body: dict[str, Any]
files: dict[str, Any]
class WebhookService:
"""Service for handling webhook operations."""
@@ -153,7 +145,7 @@ class WebhookService:
@classmethod
def extract_and_validate_webhook_data(
cls, webhook_trigger: WorkflowWebhookTrigger, node_config: NodeConfigDict
) -> RawWebhookDataDict:
) -> dict[str, Any]:
"""Extract and validate webhook data in a single unified process.
Args:
@@ -181,7 +173,7 @@ class WebhookService:
return processed_data
@classmethod
def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> RawWebhookDataDict:
def extract_webhook_data(cls, webhook_trigger: WorkflowWebhookTrigger) -> dict[str, Any]:
"""Extract raw data from incoming webhook request without type conversion.
Args:
@@ -197,7 +189,7 @@ class WebhookService:
"""
cls._validate_content_length()
data: RawWebhookDataDict = {
data = {
"method": request.method,
"headers": dict(request.headers),
"query_params": dict(request.args),
@@ -231,7 +223,7 @@ class WebhookService:
return data
@classmethod
def _process_and_validate_data(cls, raw_data: RawWebhookDataDict, node_data: WebhookData) -> RawWebhookDataDict:
def _process_and_validate_data(cls, raw_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]:
"""Process and validate webhook data according to node configuration.
Args:
@@ -672,7 +664,7 @@ class WebhookService:
raise ValueError(f"Required header missing: {header_name}")
@classmethod
def _validate_http_metadata(cls, webhook_data: RawWebhookDataDict, node_data: WebhookData) -> dict[str, Any]:
def _validate_http_metadata(cls, webhook_data: dict[str, Any], node_data: WebhookData) -> dict[str, Any]:
"""Validate HTTP method and content-type.
Args:
@@ -737,7 +729,7 @@ class WebhookService:
return False
@classmethod
def build_workflow_inputs(cls, webhook_data: RawWebhookDataDict) -> dict[str, Any]:
def build_workflow_inputs(cls, webhook_data: dict[str, Any]) -> dict[str, Any]:
"""Construct workflow inputs payload from webhook data.
Args:
@@ -755,7 +747,7 @@ class WebhookService:
@classmethod
def trigger_workflow_execution(
cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: RawWebhookDataDict, workflow: Workflow
cls, webhook_trigger: WorkflowWebhookTrigger, webhook_data: dict[str, Any], workflow: Workflow
) -> None:
"""Trigger workflow execution via AsyncWorkflowService.
@@ -401,7 +401,10 @@ class TestMetadataServiceCreateMetadata:
metadata_args = MetadataTestDataFactory.create_metadata_args_mock(name="category", metadata_type="string")
# Mock query to return None (no existing metadata with same name)
mock_db_session.scalar.return_value = None
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
mock_db_session.query.return_value = mock_query
# Mock BuiltInField enum iteration
with patch("services.metadata_service.BuiltInField") as mock_builtin:
@@ -414,6 +417,10 @@ class TestMetadataServiceCreateMetadata:
assert result is not None
assert isinstance(result, DatasetMetadata)
# Verify query was made to check for duplicates
mock_db_session.query.assert_called()
mock_query.filter_by.assert_called()
# Verify metadata was added and committed
mock_db_session.add.assert_called_once()
mock_db_session.commit.assert_called_once()
@@ -461,7 +468,10 @@ class TestMetadataServiceCreateMetadata:
# Mock existing metadata with same name
existing_metadata = MetadataTestDataFactory.create_metadata_mock(name="category")
mock_db_session.scalar.return_value = existing_metadata
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_metadata
mock_db_session.query.return_value = mock_query
# Act & Assert
with pytest.raises(ValueError, match="Metadata name already exists"):
@@ -490,7 +500,10 @@ class TestMetadataServiceCreateMetadata:
)
# Mock query to return None (no duplicate in database)
mock_db_session.scalar.return_value = None
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
mock_db_session.query.return_value = mock_query
# Mock BuiltInField to include the conflicting name
with patch("services.metadata_service.BuiltInField") as mock_builtin:
@@ -584,11 +597,27 @@ class TestMetadataServiceUpdateMetadataName:
existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category")
# Mock scalar calls: first for duplicate check (None), second for metadata retrieval
mock_db_session.scalar.side_effect = [None, existing_metadata]
# Mock query for duplicate check (no duplicate)
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
mock_db_session.query.return_value = mock_query
# Mock metadata retrieval
def query_side_effect(model):
if model == DatasetMetadata:
mock_meta_query = Mock()
mock_meta_query.filter_by.return_value = mock_meta_query
mock_meta_query.first.return_value = existing_metadata
return mock_meta_query
return mock_query
mock_db_session.query.side_effect = query_side_effect
# Mock no metadata bindings (no documents to update)
mock_db_session.scalars.return_value.all.return_value = []
mock_binding_query = Mock()
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.all.return_value = []
# Mock BuiltInField enum
with patch("services.metadata_service.BuiltInField") as mock_builtin:
@@ -626,8 +655,22 @@ class TestMetadataServiceUpdateMetadataName:
metadata_id = "non-existent-metadata"
new_name = "updated_category"
# Mock scalar calls: first for duplicate check (None), second for metadata retrieval (None = not found)
mock_db_session.scalar.side_effect = [None, None]
# Mock query for duplicate check (no duplicate)
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
mock_db_session.query.return_value = mock_query
# Mock metadata retrieval to return None
def query_side_effect(model):
if model == DatasetMetadata:
mock_meta_query = Mock()
mock_meta_query.filter_by.return_value = mock_meta_query
mock_meta_query.first.return_value = None # Not found
return mock_meta_query
return mock_query
mock_db_session.query.side_effect = query_side_effect
# Mock BuiltInField enum
with patch("services.metadata_service.BuiltInField") as mock_builtin:
@@ -703,10 +746,15 @@ class TestMetadataServiceDeleteMetadata:
existing_metadata = MetadataTestDataFactory.create_metadata_mock(metadata_id=metadata_id, name="category")
# Mock metadata retrieval
mock_db_session.scalar.return_value = existing_metadata
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_metadata
mock_db_session.query.return_value = mock_query
# Mock no metadata bindings (no documents to update)
mock_db_session.scalars.return_value.all.return_value = []
mock_binding_query = Mock()
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.all.return_value = []
# Act
result = MetadataService.delete_metadata(dataset_id, metadata_id)
@@ -740,7 +788,10 @@ class TestMetadataServiceDeleteMetadata:
metadata_id = "non-existent-metadata"
# Mock metadata retrieval to return None
mock_db_session.scalar.return_value = None
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
mock_db_session.query.return_value = mock_query
# Act & Assert
with pytest.raises(ValueError, match="Metadata not found"):
@@ -962,7 +1013,10 @@ class TestMetadataServiceGetDatasetMetadatas:
)
# Mock usage count queries
mock_db_session.scalar.return_value = 5 # 5 documents use this metadata
mock_query = Mock()
mock_query.filter_by.return_value = mock_query
mock_query.count.return_value = 5 # 5 documents use this metadata
mock_db_session.query.return_value = mock_query
# Act
result = MetadataService.get_dataset_metadatas(dataset)
@@ -292,7 +292,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
"""
api = Mock(spec=ExternalKnowledgeApis)
mock_db_session.scalar.return_value = api
mock_db_session.query.return_value.filter_by.return_value.first.return_value = api
result = ExternalDatasetService.get_external_knowledge_api("api-id", "tenant-id")
assert result is api
@@ -302,7 +302,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
When the record is absent, a ``ValueError`` is raised.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="api template not found"):
ExternalDatasetService.get_external_knowledge_api("missing-id", "tenant-id")
@@ -320,7 +320,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
existing_api = Mock(spec=ExternalKnowledgeApis)
existing_api.settings_dict = {"api_key": "stored-key"}
existing_api.settings = '{"api_key":"stored-key"}'
mock_db_session.scalar.return_value = existing_api
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_api
args = {
"name": "New Name",
@@ -340,7 +340,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
Updating a nonexistent API template should raise ``ValueError``.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="api template not found"):
ExternalDatasetService.update_external_knowledge_api(
@@ -356,7 +356,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
"""
api = Mock(spec=ExternalKnowledgeApis)
mock_db_session.scalar.return_value = api
mock_db_session.query.return_value.filter_by.return_value.first.return_value = api
ExternalDatasetService.delete_external_knowledge_api("tenant-1", "api-1")
@@ -368,7 +368,7 @@ class TestExternalDatasetServiceCrudExternalKnowledgeApi:
Deletion of a missing template should raise ``ValueError``.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="api template not found"):
ExternalDatasetService.delete_external_knowledge_api("tenant-1", "missing")
@@ -394,7 +394,7 @@ class TestExternalDatasetServiceUsageAndBindings:
When there are bindings, ``external_knowledge_api_use_check`` returns True and count.
"""
mock_db_session.scalar.return_value = 3
mock_db_session.query.return_value.filter_by.return_value.count.return_value = 3
in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1")
@@ -406,7 +406,7 @@ class TestExternalDatasetServiceUsageAndBindings:
Zero bindings should return ``(False, 0)``.
"""
mock_db_session.scalar.return_value = 0
mock_db_session.query.return_value.filter_by.return_value.count.return_value = 0
in_use, count = ExternalDatasetService.external_knowledge_api_use_check("api-1")
@@ -419,7 +419,7 @@ class TestExternalDatasetServiceUsageAndBindings:
"""
binding = Mock(spec=ExternalKnowledgeBindings)
mock_db_session.scalar.return_value = binding
mock_db_session.query.return_value.filter_by.return_value.first.return_value = binding
result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1")
assert result is binding
@@ -429,7 +429,7 @@ class TestExternalDatasetServiceUsageAndBindings:
Missing binding should result in a ``ValueError``.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="external knowledge binding not found"):
ExternalDatasetService.get_external_knowledge_binding_with_dataset_id("tenant-1", "ds-1")
@@ -460,7 +460,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
'[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]'
)
# Raw string; the service itself calls json.loads on it
mock_db_session.scalar.return_value = external_api
mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api
process_parameter = {"foo": "value", "bar": "optional"}
@@ -474,7 +474,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
When the referenced API template is missing, a ``ValueError`` is raised.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="api template not found"):
ExternalDatasetService.document_create_args_validate("tenant-1", "missing", {})
@@ -488,7 +488,7 @@ class TestExternalDatasetServiceDocumentCreateArgsValidate:
external_api.settings = (
'[{"document_process_setting":[{"name":"foo","required":true},{"name":"bar","required":false}]}]'
)
mock_db_session.scalar.return_value = external_api
mock_db_session.query.return_value.filter_by.return_value.first.return_value = external_api
process_parameter = {"bar": "present"} # missing "foo"
@@ -702,7 +702,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
}
# No existing dataset with same name.
mock_db_session.scalar.side_effect = [
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
None, # duplicatename check
Mock(spec=ExternalKnowledgeApis), # external knowledge api
]
@@ -724,7 +724,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
"""
existing_dataset = Mock(spec=Dataset)
mock_db_session.scalar.return_value = existing_dataset
mock_db_session.query.return_value.filter_by.return_value.first.return_value = existing_dataset
args = {
"name": "Existing",
@@ -744,7 +744,7 @@ class TestExternalDatasetServiceCreateExternalDataset:
"""
# First call: duplicate name check not found.
mock_db_session.scalar.side_effect = [
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
None,
None, # external knowledge api lookup
]
@@ -763,10 +763,8 @@ class TestExternalDatasetServiceCreateExternalDataset:
``external_knowledge_id`` and ``external_knowledge_api_id`` are mandatory.
"""
# duplicate name check — two calls to create_external_dataset, each does 2 scalar calls
mock_db_session.scalar.side_effect = [
None,
Mock(spec=ExternalKnowledgeApis),
# duplicate name check
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
None,
Mock(spec=ExternalKnowledgeApis),
]
@@ -828,7 +826,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
api.settings = '{"endpoint":"https://example.com","api_key":"secret"}'
# First query: binding; second query: api.
mock_db_session.scalar.side_effect = [
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
binding,
api,
]
@@ -863,7 +861,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
Missing binding should raise ``ValueError``.
"""
mock_db_session.scalar.return_value = None
mock_db_session.query.return_value.filter_by.return_value.first.return_value = None
with pytest.raises(ValueError, match="external knowledge binding not found"):
ExternalDatasetService.fetch_external_knowledge_retrieval(
@@ -880,7 +878,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
"""
binding = ExternalDatasetTestDataFactory.create_external_binding()
mock_db_session.scalar.side_effect = [
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
binding,
None,
]
@@ -903,7 +901,7 @@ class TestExternalDatasetServiceFetchExternalKnowledgeRetrieval:
api = Mock(spec=ExternalKnowledgeApis)
api.settings = '{"endpoint":"https://example.com","api_key":"secret"}'
mock_db_session.scalar.side_effect = [
mock_db_session.query.return_value.filter_by.return_value.first.side_effect = [
binding,
api,
]
@@ -117,7 +117,9 @@ def test_get_all_published_workflow_applies_limit_and_has_more(rag_pipeline_serv
def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
first_query = mocker.Mock()
first_query.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=first_query)
with pytest.raises(ValueError, match="Dataset not found"):
rag_pipeline_service.get_pipeline("tenant-1", "dataset-1")
@@ -129,8 +131,12 @@ def test_get_pipeline_raises_when_dataset_not_found(mocker, rag_pipeline_service
def test_update_customized_pipeline_template_success(mocker) -> None:
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
# First scalar finds the template, second scalar (duplicate check) returns None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, None])
# First query finds the template, second query (duplicate check) returns None
query_mock_1 = mocker.Mock()
query_mock_1.where.return_value.first.return_value = template
query_mock_2 = mocker.Mock()
query_mock_2.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", side_effect=[query_mock_1, query_mock_2])
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
@@ -146,7 +152,9 @@ def test_update_customized_pipeline_template_success(mocker) -> None:
def test_update_customized_pipeline_template_not_found(mocker) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
query_mock = mocker.Mock()
query_mock.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
info = PipelineTemplateInfoEntity(name="x", description="d", icon_info=IconInfo(icon="i"))
@@ -158,7 +166,9 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None:
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
duplicate = SimpleNamespace(name="dup")
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[template, duplicate])
query_mock = mocker.Mock()
query_mock.where.return_value.first.side_effect = [template, duplicate]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
info = PipelineTemplateInfoEntity(name="dup", description="d", icon_info=IconInfo(icon="i"))
@@ -171,7 +181,9 @@ def test_update_customized_pipeline_template_duplicate_name(mocker) -> None:
def test_delete_customized_pipeline_template_success(mocker) -> None:
template = SimpleNamespace(id="tpl-1")
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template)
query_mock = mocker.Mock()
query_mock.where.return_value.first.return_value = template
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
delete_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.delete")
commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
@@ -184,7 +196,9 @@ def test_delete_customized_pipeline_template_success(mocker) -> None:
def test_delete_customized_pipeline_template_not_found(mocker) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
query_mock = mocker.Mock()
query_mock.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
with pytest.raises(ValueError, match="Customized pipeline template not found"):
@@ -383,14 +397,18 @@ def test_get_rag_pipeline_workflow_run_delegates(mocker, rag_pipeline_service) -
def test_is_workflow_exist_returns_true_when_draft_exists(mocker, rag_pipeline_service) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=1)
query_mock = mocker.Mock()
query_mock.where.return_value.count.return_value = 1
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
pipeline = SimpleNamespace(tenant_id="t1", id="p1")
assert rag_pipeline_service.is_workflow_exist(pipeline) is True
def test_is_workflow_exist_returns_false_when_no_draft(mocker, rag_pipeline_service) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=0)
query_mock = mocker.Mock()
query_mock.where.return_value.count.return_value = 0
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query_mock)
pipeline = SimpleNamespace(tenant_id="t1", id="p1")
assert rag_pipeline_service.is_workflow_exist(pipeline) is False
@@ -720,7 +738,8 @@ def test_get_second_step_parameters_success(mocker, rag_pipeline_service) -> Non
def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_service) -> None:
from models.dataset import Pipeline
from models.dataset import Dataset, Pipeline, PipelineCustomizedTemplate
from models.workflow import Workflow
# 1. Setup mocks
pipeline = mocker.Mock(spec=Pipeline)
@@ -735,15 +754,36 @@ def test_publish_customized_pipeline_template_success(mocker, rag_pipeline_servi
# Mock db itself to avoid app context errors
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
# Mock get() for Pipeline and Workflow PK lookups
mock_db.session.get.side_effect = [pipeline, workflow]
# Mock scalar() for template name check (None) and max position (5)
mock_db.session.scalar.side_effect = [None, 5]
# Improved mocking for session.query
def mock_query_side_effect(model):
m = mocker.Mock()
if model == Pipeline:
m.where.return_value.first.return_value = pipeline
elif model == Workflow:
m.where.return_value.first.return_value = workflow
elif model == PipelineCustomizedTemplate:
m.where.return_value.first.return_value = None
elif model == Dataset:
m.where.return_value.first.return_value = mocker.Mock()
else:
# For func.max cases
m.where.return_value.scalar.return_value = 5
m.where.return_value.first.return_value = mocker.Mock()
return m
mock_db.session.query.side_effect = mock_query_side_effect
# Mock retrieve_dataset
dataset = mocker.Mock()
pipeline.retrieve_dataset.return_value = dataset
# Mock max position
mocker.patch("services.rag_pipeline.rag_pipeline.func.max", return_value=1)
mocker.patch(
"services.rag_pipeline.rag_pipeline.db.session.query.return_value.where.return_value.scalar",
return_value=5,
)
# Mock RagPipelineDslService
mock_dsl_service = mocker.Mock()
mock_dsl_service.export_rag_pipeline_dsl.return_value = {"dsl": "content"}
@@ -799,7 +839,9 @@ def test_get_datasource_plugins_success(mocker, rag_pipeline_service) -> None:
workflow.rag_pipeline_variables = []
# Mock queries
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
mock_query = mocker.Mock()
mock_query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
@@ -839,9 +881,11 @@ def test_retry_error_document_success(mocker, rag_pipeline_service) -> None:
workflow = mocker.Mock()
# Mock queries: Log lookup via scalar, Pipeline lookup via get
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=log)
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
# Mock queries
mock_query = mocker.Mock()
# Log lookup, then Pipeline lookup
mock_query.where.return_value.first.side_effect = [log, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=mock_query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
@@ -869,7 +913,7 @@ def test_set_datasource_variables_success(mocker, rag_pipeline_service) -> None:
# Mock db aggressively
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.engine = mocker.Mock()
mock_db.session.scalar.return_value = mocker.Mock()
mock_db.session.query.return_value.where.return_value.first.return_value = mocker.Mock()
pipeline = mocker.Mock(spec=Pipeline)
pipeline.id = "p-1"
@@ -932,7 +976,7 @@ def test_get_draft_workflow_success(mocker, rag_pipeline_service) -> None:
workflow = mocker.Mock(spec=Workflow)
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.session.scalar.return_value = workflow
mock_db.session.query.return_value.where.return_value.first.return_value = workflow
# 2. Run test
result = rag_pipeline_service.get_draft_workflow(pipeline)
@@ -954,7 +998,7 @@ def test_get_published_workflow_success(mocker, rag_pipeline_service) -> None:
workflow = mocker.Mock(spec=Workflow)
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.session.scalar.return_value = workflow
mock_db.session.query.return_value.where.return_value.first.return_value = workflow
# 2. Run test
result = rag_pipeline_service.get_published_workflow(pipeline)
@@ -1275,8 +1319,11 @@ def test_get_rag_pipeline_workflow_run_node_executions_returns_sorted_executions
def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, rag_pipeline_service) -> None:
query = mocker.Mock()
query.where.return_value = query
query.order_by.return_value.all.return_value = []
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.session.scalars.return_value.all.return_value = []
mock_db.session.query.return_value = query
result = rag_pipeline_service.get_recommended_plugins("all")
@@ -1289,8 +1336,11 @@ def test_get_recommended_plugins_returns_empty_when_no_active_plugins(mocker, ra
def test_get_recommended_plugins_returns_installed_and_uninstalled(mocker, rag_pipeline_service) -> None:
plugin_a = SimpleNamespace(plugin_id="plugin-a")
plugin_b = SimpleNamespace(plugin_id="plugin-b")
query = mocker.Mock()
query.where.return_value = query
query.order_by.return_value.all.return_value = [plugin_a, plugin_b]
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.session.scalars.return_value.all.return_value = [plugin_a, plugin_b]
mock_db.session.query.return_value = query
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
mocker.patch(
"services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools",
@@ -1518,7 +1568,9 @@ def test_get_second_step_parameters_filters_first_step_variables(mocker, rag_pip
def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pipeline_service) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
query = mocker.Mock()
query.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Document pipeline execution log not found"):
rag_pipeline_service.retry_error_document(
@@ -1529,7 +1581,9 @@ def test_retry_error_document_raises_when_execution_log_not_found(mocker, rag_pi
def test_get_datasource_plugins_raises_when_workflow_not_found(mocker, rag_pipeline_service) -> None:
dataset = SimpleNamespace(pipeline_id="p1")
pipeline = SimpleNamespace(id="p1", tenant_id="t1")
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
query = mocker.Mock()
query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None)
with pytest.raises(ValueError, match="Pipeline or workflow not found"):
@@ -1602,7 +1656,8 @@ def test_handle_node_run_result_marks_document_error_for_published_invoke(mocker
document = SimpleNamespace(indexing_status="waiting", error=None)
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=document)
query.where.return_value.first.return_value = document
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
add_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.add")
commit_mock = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
@@ -1657,7 +1712,9 @@ def test_run_datasource_node_preview_raises_for_unsupported_provider(mocker, rag
def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker, rag_pipeline_service) -> None:
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None)
query = mocker.Mock()
query.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Pipeline not found"):
rag_pipeline_service.publish_customized_pipeline_template("p1", {})
@@ -1665,7 +1722,9 @@ def test_publish_customized_pipeline_template_raises_for_missing_pipeline(mocker
def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(mocker, rag_pipeline_service) -> None:
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id=None)
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
query = mocker.Mock()
query.where.return_value.first.return_value = pipeline
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Pipeline workflow not found"):
rag_pipeline_service.publish_customized_pipeline_template("p1", {"name": "template-name"})
@@ -1673,7 +1732,8 @@ def test_publish_customized_pipeline_template_raises_for_missing_workflow_id(moc
def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None:
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=None)
query.where.return_value.first.return_value = None
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Dataset not found"):
rag_pipeline_service.get_pipeline("t1", "d1")
@@ -1682,7 +1742,8 @@ def test_get_pipeline_raises_when_dataset_missing(mocker, rag_pipeline_service)
def test_get_pipeline_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None:
dataset = SimpleNamespace(pipeline_id="p1")
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, None])
query.where.return_value.first.side_effect = [dataset, None]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Pipeline not found"):
rag_pipeline_service.get_pipeline("t1", "d1")
@@ -1722,7 +1783,8 @@ def test_get_pipeline_templates_builtin_en_us_no_fallback(mocker) -> None:
def test_update_customized_pipeline_template_commits_when_name_empty(mocker) -> None:
template = SimpleNamespace(name="old", description="old", icon={}, updated_by=None)
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=template)
query.where.return_value.first.return_value = template
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
commit = mocker.patch("services.rag_pipeline.rag_pipeline.db.session.commit")
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
@@ -1949,7 +2011,8 @@ def test_run_free_workflow_node_delegates_to_handle_result(mocker, rag_pipeline_
def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocker, rag_pipeline_service) -> None:
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1")
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", side_effect=[pipeline, None])
query.where.return_value.first.side_effect = [pipeline, None]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Workflow not found"):
rag_pipeline_service.publish_customized_pipeline_template("p1", {})
@@ -1958,9 +2021,11 @@ def test_publish_customized_pipeline_template_raises_when_workflow_missing(mocke
def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker, rag_pipeline_service) -> None:
pipeline = SimpleNamespace(id="p1", tenant_id="t1", workflow_id="wf-1")
workflow = SimpleNamespace(id="wf-1")
query = mocker.Mock()
query.where.return_value.first.side_effect = [pipeline, workflow]
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.engine = mocker.Mock()
mock_db.session.get.side_effect = [pipeline, workflow]
mock_db.session.query.return_value = query
session_ctx = mocker.MagicMock()
session_ctx.__enter__.return_value = SimpleNamespace()
session_ctx.__exit__.return_value = False
@@ -1973,8 +2038,11 @@ def test_publish_customized_pipeline_template_raises_when_dataset_missing(mocker
def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipeline_service) -> None:
plugin = SimpleNamespace(plugin_id="plugin-a")
query = mocker.Mock()
query.where.return_value = query
query.order_by.return_value.all.return_value = [plugin]
mock_db = mocker.patch("services.rag_pipeline.rag_pipeline.db")
mock_db.session.scalars.return_value.all.return_value = [plugin]
mock_db.session.query.return_value = query
mocker.patch("services.rag_pipeline.rag_pipeline.current_user", SimpleNamespace(id="u1", current_tenant_id="t1"))
mocker.patch("services.rag_pipeline.rag_pipeline.BuiltinToolManageService.list_builtin_tools", return_value=[])
mocker.patch("services.rag_pipeline.rag_pipeline.marketplace.batch_fetch_plugin_by_ids", return_value=[])
@@ -1988,8 +2056,8 @@ def test_get_recommended_plugins_skips_manifest_when_missing(mocker, rag_pipelin
def test_retry_error_document_raises_when_pipeline_missing(mocker, rag_pipeline_service) -> None:
exec_log = SimpleNamespace(pipeline_id="p1")
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log)
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=None)
query.where.return_value.first.side_effect = [exec_log, None]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
with pytest.raises(ValueError, match="Pipeline not found"):
rag_pipeline_service.retry_error_document(
@@ -2001,8 +2069,8 @@ def test_retry_error_document_raises_when_workflow_missing(mocker, rag_pipeline_
exec_log = SimpleNamespace(pipeline_id="p1")
pipeline = SimpleNamespace(id="p1")
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", return_value=exec_log)
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.get", return_value=pipeline)
query.where.return_value.first.side_effect = [exec_log, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=None)
with pytest.raises(ValueError, match="Workflow not found"):
@@ -2018,7 +2086,8 @@ def test_get_datasource_plugins_returns_empty_for_non_datasource_nodes(mocker, r
graph_dict={"nodes": [{"id": "n1", "data": {"type": "start"}}]}, rag_pipeline_variables=[]
)
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
assert rag_pipeline_service.get_datasource_plugins("t1", "d1", True) == []
@@ -2181,7 +2250,8 @@ def test_get_datasource_plugins_handles_empty_datasource_data_and_non_published(
rag_pipeline_variables=[{"variable": "v1", "belong_to_node_id": "shared"}],
)
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
mocker.patch.object(rag_pipeline_service, "get_draft_workflow", return_value=workflow)
mocker.patch(
"services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials", return_value=[]
@@ -2221,7 +2291,8 @@ def test_get_datasource_plugins_extracts_user_inputs_and_credentials(mocker, rag
],
)
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
mocker.patch.object(rag_pipeline_service, "get_published_workflow", return_value=workflow)
mocker.patch(
"services.rag_pipeline.rag_pipeline.DatasourceProviderService.list_datasource_credentials",
@@ -2239,7 +2310,8 @@ def test_get_pipeline_returns_pipeline_when_found(mocker, rag_pipeline_service)
dataset = SimpleNamespace(pipeline_id="p1")
pipeline = SimpleNamespace(id="p1")
query = mocker.Mock()
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.scalar", side_effect=[dataset, pipeline])
query.where.return_value.first.side_effect = [dataset, pipeline]
mocker.patch("services.rag_pipeline.rag_pipeline.db.session.query", return_value=query)
result = rag_pipeline_service.get_pipeline("t1", "d1")
@@ -173,7 +173,9 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
mock_db_dependencies["db"].session.scalar.return_value = mock_account
# Setup smart database query mock
query_results = {("Account", "email", "[email protected]"): mock_account}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
mock_password_dependencies["compare_password"].return_value = True
@@ -186,7 +188,9 @@ class TestAccountService:
def test_authenticate_account_not_found(self, mock_db_dependencies):
"""Test authentication when account does not exist."""
mock_db_dependencies["db"].session.scalar.return_value = None
# Setup smart database query mock - no matching results
query_results = {("Account", "email", "[email protected]"): None}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Execute test and verify exception
self._assert_exception_raised(
@@ -198,7 +202,9 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned")
mock_db_dependencies["db"].session.scalar.return_value = mock_account
# Setup smart database query mock
query_results = {("Account", "email", "[email protected]"): mock_account}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Execute test and verify exception
self._assert_exception_raised(AccountLoginError, AccountService.authenticate, "[email protected]", "password")
@@ -208,7 +214,9 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
mock_db_dependencies["db"].session.scalar.return_value = mock_account
# Setup smart database query mock
query_results = {("Account", "email", "[email protected]"): mock_account}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
mock_password_dependencies["compare_password"].return_value = False
@@ -222,7 +230,9 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="pending")
mock_db_dependencies["db"].session.scalar.return_value = mock_account
# Setup smart database query mock
query_results = {("Account", "email", "[email protected]"): mock_account}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
mock_password_dependencies["compare_password"].return_value = True
@@ -412,8 +422,12 @@ class TestAccountService:
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
mock_tenant_join = TestAccountAssociatedDataFactory.create_tenant_join_mock()
mock_db_dependencies["db"].session.get.return_value = mock_account
mock_db_dependencies["db"].session.scalar.return_value = mock_tenant_join
# Setup smart database query mock
query_results = {
("Account", "id", "user-123"): mock_account,
("TenantAccountJoin", "account_id", "user-123"): mock_tenant_join,
}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Mock datetime
with patch("services.account_service.datetime") as mock_datetime:
@@ -430,7 +444,9 @@ class TestAccountService:
def test_load_user_not_found(self, mock_db_dependencies):
"""Test user loading when user does not exist."""
mock_db_dependencies["db"].session.get.return_value = None
# Setup smart database query mock - no matching results
query_results = {("Account", "id", "non-existent-user"): None}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Execute test
result = AccountService.load_user("non-existent-user")
@@ -443,7 +459,9 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock(status="banned")
mock_db_dependencies["db"].session.get.return_value = mock_account
# Setup smart database query mock
query_results = {("Account", "id", "user-123"): mock_account}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Execute test and verify exception
self._assert_exception_raised(
@@ -458,9 +476,13 @@ class TestAccountService:
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
mock_available_tenant = TestAccountAssociatedDataFactory.create_tenant_join_mock(current=False)
mock_db_dependencies["db"].session.get.return_value = mock_account
# First scalar: current tenant (None), second scalar: available tenant
mock_db_dependencies["db"].session.scalar.side_effect = [None, mock_available_tenant]
# Setup smart database query mock for complex scenario
query_results = {
("Account", "id", "user-123"): mock_account,
("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant
("TenantAccountJoin", "order_by", "first_available"): mock_available_tenant, # First available tenant
}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Mock datetime
with patch("services.account_service.datetime") as mock_datetime:
@@ -481,9 +503,13 @@ class TestAccountService:
# Setup test data
mock_account = TestAccountAssociatedDataFactory.create_account_mock()
mock_db_dependencies["db"].session.get.return_value = mock_account
# First scalar: current tenant (None), second scalar: available tenant (None)
mock_db_dependencies["db"].session.scalar.side_effect = [None, None]
# Setup smart database query mock for no tenants scenario
query_results = {
("Account", "id", "user-123"): mock_account,
("TenantAccountJoin", "account_id", "user-123"): None, # No current tenant
("TenantAccountJoin", "order_by", "first_available"): None, # No available tenants
}
ServiceDbTestHelper.setup_db_query_filter_by_mock(mock_db_dependencies["db"], query_results)
# Mock datetime
with patch("services.account_service.datetime") as mock_datetime:
@@ -799,7 +799,10 @@ class TestExternalDatasetServiceGetAPI:
api_id = "api-123"
expected_api = factory.create_external_knowledge_api_mock(api_id=api_id)
mock_db.session.scalar.return_value = expected_api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = expected_api
# Act
tenant_id = "tenant-123"
@@ -807,12 +810,16 @@ class TestExternalDatasetServiceGetAPI:
# Assert
assert result.id == api_id
mock_query.filter_by.assert_called_once_with(id=api_id, tenant_id=tenant_id)
@patch("services.external_knowledge_service.db")
def test_get_external_knowledge_api_not_found(self, mock_db, factory):
"""Test error when API is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="api template not found"):
@@ -841,7 +848,10 @@ class TestExternalDatasetServiceUpdateAPI:
"settings": {"endpoint": "https://new.example.com", "api_key": "new-key"},
}
mock_db.session.scalar.return_value = existing_api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_api
# Act
result = ExternalDatasetService.update_external_knowledge_api(tenant_id, user_id, api_id, args)
@@ -871,7 +881,10 @@ class TestExternalDatasetServiceUpdateAPI:
"settings": {"endpoint": "https://api.example.com", "api_key": HIDDEN_VALUE},
}
mock_db.session.scalar.return_value = existing_api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_api
# Act
result = ExternalDatasetService.update_external_knowledge_api(tenant_id, "user-123", api_id, args)
@@ -884,7 +897,10 @@ class TestExternalDatasetServiceUpdateAPI:
def test_update_external_knowledge_api_not_found(self, mock_db, factory):
"""Test error when API is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
args = {"name": "Updated API"}
@@ -896,7 +912,10 @@ class TestExternalDatasetServiceUpdateAPI:
def test_update_external_knowledge_api_tenant_mismatch(self, mock_db, factory):
"""Test error when tenant ID doesn't match."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
args = {"name": "Updated API"}
@@ -915,7 +934,10 @@ class TestExternalDatasetServiceUpdateAPI:
args = {"name": "New Name Only"}
mock_db.session.scalar.return_value = existing_api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_api
# Act
result = ExternalDatasetService.update_external_knowledge_api("tenant-123", "user-123", "api-123", args)
@@ -936,7 +958,10 @@ class TestExternalDatasetServiceDeleteAPI:
existing_api = factory.create_external_knowledge_api_mock(api_id=api_id, tenant_id=tenant_id)
mock_db.session.scalar.return_value = existing_api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_api
# Act
ExternalDatasetService.delete_external_knowledge_api(tenant_id, api_id)
@@ -949,7 +974,10 @@ class TestExternalDatasetServiceDeleteAPI:
def test_delete_external_knowledge_api_not_found(self, mock_db, factory):
"""Test error when API is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="api template not found"):
@@ -959,7 +987,10 @@ class TestExternalDatasetServiceDeleteAPI:
def test_delete_external_knowledge_api_tenant_mismatch(self, mock_db, factory):
"""Test error when tenant ID doesn't match."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="api template not found"):
@@ -975,7 +1006,10 @@ class TestExternalDatasetServiceAPIUseCheck:
# Arrange
api_id = "api-123"
mock_db.session.scalar.return_value = 1
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.count.return_value = 1
# Act
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
@@ -990,7 +1024,10 @@ class TestExternalDatasetServiceAPIUseCheck:
# Arrange
api_id = "api-123"
mock_db.session.scalar.return_value = 10
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.count.return_value = 10
# Act
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
@@ -1005,7 +1042,10 @@ class TestExternalDatasetServiceAPIUseCheck:
# Arrange
api_id = "api-123"
mock_db.session.scalar.return_value = 0
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.count.return_value = 0
# Act
in_use, count = ExternalDatasetService.external_knowledge_api_use_check(api_id)
@@ -1027,7 +1067,10 @@ class TestExternalDatasetServiceGetBinding:
expected_binding = factory.create_external_knowledge_binding_mock(tenant_id=tenant_id, dataset_id=dataset_id)
mock_db.session.scalar.return_value = expected_binding
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = expected_binding
# Act
result = ExternalDatasetService.get_external_knowledge_binding_with_dataset_id(tenant_id, dataset_id)
@@ -1040,7 +1083,10 @@ class TestExternalDatasetServiceGetBinding:
def test_get_external_knowledge_binding_not_found(self, mock_db, factory):
"""Test error when binding is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="external knowledge binding not found"):
@@ -1067,7 +1113,10 @@ class TestExternalDatasetServiceDocumentValidate:
api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings])
mock_db.session.scalar.return_value = api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = api
process_parameter = {"param1": "value1", "param2": "value2"}
@@ -1085,7 +1134,10 @@ class TestExternalDatasetServiceDocumentValidate:
api = factory.create_external_knowledge_api_mock(api_id=api_id, settings=[settings])
mock_db.session.scalar.return_value = api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = api
process_parameter = {}
@@ -1097,7 +1149,10 @@ class TestExternalDatasetServiceDocumentValidate:
def test_document_create_args_validate_api_not_found(self, mock_db, factory):
"""Test validation fails when API is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="api template not found"):
@@ -1110,7 +1165,10 @@ class TestExternalDatasetServiceDocumentValidate:
settings = {}
api = factory.create_external_knowledge_api_mock(settings=[settings])
mock_db.session.scalar.return_value = api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = api
# Act & Assert - should not raise
ExternalDatasetService.document_create_args_validate("tenant-123", "api-123", {})
@@ -1128,7 +1186,10 @@ class TestExternalDatasetServiceDocumentValidate:
api = factory.create_external_knowledge_api_mock(settings=[settings])
mock_db.session.scalar.return_value = api
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = api
process_parameter = {"required_param": "value"}
@@ -1437,7 +1498,24 @@ class TestExternalDatasetServiceCreateDataset:
api = factory.create_external_knowledge_api_mock(api_id="api-123")
mock_db.session.scalar.side_effect = [None, api]
# Mock database queries
mock_dataset_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == Dataset:
return mock_dataset_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_dataset_query.filter_by.return_value = mock_dataset_query
mock_dataset_query.first.return_value = None
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
# Act
result = ExternalDatasetService.create_external_dataset(tenant_id, user_id, args)
@@ -1456,7 +1534,10 @@ class TestExternalDatasetServiceCreateDataset:
# Arrange
existing_dataset = factory.create_dataset_mock(name="Duplicate Dataset")
mock_db.session.scalar.return_value = existing_dataset
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = existing_dataset
args = {"name": "Duplicate Dataset"}
@@ -1468,7 +1549,23 @@ class TestExternalDatasetServiceCreateDataset:
def test_create_external_dataset_api_not_found_error(self, mock_db, factory):
"""Test error when external knowledge API is not found."""
# Arrange
mock_db.session.scalar.side_effect = [None, None]
mock_dataset_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == Dataset:
return mock_dataset_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_dataset_query.filter_by.return_value = mock_dataset_query
mock_dataset_query.first.return_value = None
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = None
args = {"name": "Test Dataset", "external_knowledge_api_id": "nonexistent-api"}
@@ -1482,7 +1579,23 @@ class TestExternalDatasetServiceCreateDataset:
# Arrange
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [None, api]
mock_dataset_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == Dataset:
return mock_dataset_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_dataset_query.filter_by.return_value = mock_dataset_query
mock_dataset_query.first.return_value = None
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
args = {"name": "Test Dataset", "external_knowledge_api_id": "api-123"}
@@ -1496,7 +1609,23 @@ class TestExternalDatasetServiceCreateDataset:
# Arrange
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [None, api]
mock_dataset_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == Dataset:
return mock_dataset_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_dataset_query.filter_by.return_value = mock_dataset_query
mock_dataset_query.first.return_value = None
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
args = {"name": "Test Dataset", "external_knowledge_id": "knowledge-123"}
@@ -1522,7 +1651,23 @@ class TestExternalDatasetServiceFetchRetrieval:
)
api = factory.create_external_knowledge_api_mock(api_id="api-123")
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = 200
@@ -1550,7 +1695,10 @@ class TestExternalDatasetServiceFetchRetrieval:
def test_fetch_external_knowledge_retrieval_binding_not_found_error(self, mock_db, factory):
"""Test error when external knowledge binding is not found."""
# Arrange
mock_db.session.scalar.return_value = None
mock_query = MagicMock()
mock_db.session.query.return_value = mock_query
mock_query.filter_by.return_value = mock_query
mock_query.first.return_value = None
# Act & Assert
with pytest.raises(ValueError, match="external knowledge binding not found"):
@@ -1564,7 +1712,23 @@ class TestExternalDatasetServiceFetchRetrieval:
binding = factory.create_external_knowledge_binding_mock()
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = 200
@@ -1587,7 +1751,23 @@ class TestExternalDatasetServiceFetchRetrieval:
binding = factory.create_external_knowledge_binding_mock()
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = 200
@@ -1619,7 +1799,23 @@ class TestExternalDatasetServiceFetchRetrieval:
binding = factory.create_external_knowledge_binding_mock()
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = 500
@@ -1660,7 +1856,23 @@ class TestExternalDatasetServiceFetchRetrieval:
)
api = factory.create_external_knowledge_api_mock(api_id="api-123")
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = status_code
@@ -1679,7 +1891,23 @@ class TestExternalDatasetServiceFetchRetrieval:
binding = factory.create_external_knowledge_binding_mock()
api = factory.create_external_knowledge_api_mock()
mock_db.session.scalar.side_effect = [binding, api]
mock_binding_query = MagicMock()
mock_api_query = MagicMock()
def query_side_effect(model):
if model == ExternalKnowledgeBindings:
return mock_binding_query
elif model == ExternalKnowledgeApis:
return mock_api_query
return MagicMock()
mock_db.session.query.side_effect = query_side_effect
mock_binding_query.filter_by.return_value = mock_binding_query
mock_binding_query.first.return_value = binding
mock_api_query.filter_by.return_value = mock_api_query
mock_api_query.first.return_value = api
mock_response = MagicMock()
mock_response.status_code = 503
-2
View File
@@ -1,2 +0,0 @@
dist/
node_modules/
-82
View File
@@ -1,82 +0,0 @@
{
"name": "@langgenius/dify-ui",
"private": true,
"version": "0.0.0-private",
"type": "module",
"files": [
"dist"
],
"sideEffects": [
"**/*.css"
],
"exports": {
"./context-menu": {
"types": "./dist/context-menu/index.d.ts",
"import": "./dist/context-menu/index.js",
"default": "./dist/context-menu/index.js"
},
"./dropdown-menu": {
"types": "./dist/dropdown-menu/index.d.ts",
"import": "./dist/dropdown-menu/index.js",
"default": "./dist/dropdown-menu/index.js"
},
"./tailwind-preset": {
"types": "./dist/tailwind-preset.d.ts",
"import": "./dist/tailwind-preset.js",
"default": "./dist/tailwind-preset.js"
},
"./styles.css": "./dist/styles.css",
"./markdown.css": "./dist/markdown.css",
"./themes/light.css": "./dist/themes/light.css",
"./themes/dark.css": "./dist/themes/dark.css",
"./themes/manual-light.css": "./dist/themes/manual-light.css",
"./themes/manual-dark.css": "./dist/themes/manual-dark.css",
"./themes/markdown-light.css": "./dist/themes/markdown-light.css",
"./themes/markdown-dark.css": "./dist/themes/markdown-dark.css",
"./tokens/tailwind-theme-var-define": {
"types": "./dist/tokens/tailwind-theme-var-define.d.ts",
"import": "./dist/tokens/tailwind-theme-var-define.js",
"default": "./dist/tokens/tailwind-theme-var-define.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "node ./scripts/build.mjs",
"prepack": "pnpm build",
"test": "vp test",
"test:watch": "vp test --watch",
"type-check": "tsc -p tsconfig.json --noEmit"
},
"peerDependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"dependencies": {
"@base-ui/react": "catalog:",
"@dify/iconify-collections": "workspace:*",
"@egoist/tailwindcss-icons": "catalog:",
"@iconify-json/heroicons": "catalog:",
"@iconify-json/ri": "catalog:",
"@remixicon/react": "catalog:",
"@tailwindcss/typography": "catalog:",
"clsx": "catalog:",
"tailwind-merge": "catalog:"
},
"devDependencies": {
"@storybook/react": "catalog:",
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"happy-dom": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
}
}
-31
View File
@@ -1,31 +0,0 @@
import { cp, mkdir, rm } from 'node:fs/promises'
import { spawnSync } from 'node:child_process'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const distDir = resolve(packageRoot, 'dist')
await rm(distDir, { recursive: true, force: true })
const tsc = spawnSync('pnpm', ['exec', 'tsc', '-p', 'tsconfig.build.json'], {
cwd: packageRoot,
stdio: 'inherit',
})
if (tsc.status !== 0)
process.exit(tsc.status ?? 1)
await mkdir(distDir, { recursive: true })
await cp(resolve(packageRoot, 'src/styles.css'), resolve(packageRoot, 'dist/styles.css'))
await cp(resolve(packageRoot, 'src/markdown.css'), resolve(packageRoot, 'dist/markdown.css'))
await cp(resolve(packageRoot, 'src/styles'), resolve(packageRoot, 'dist/styles'), {
force: true,
recursive: true,
})
await cp(resolve(packageRoot, 'src/themes'), resolve(packageRoot, 'dist/themes'), {
force: true,
recursive: true,
})
-7
View File
@@ -1,7 +0,0 @@
import type { ClassValue } from 'clsx'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
@@ -1,25 +0,0 @@
type Side = 'top' | 'bottom' | 'left' | 'right'
type Align = 'start' | 'center' | 'end'
export type Placement
= 'top'
| 'top-start'
| 'top-end'
| 'right'
| 'right-start'
| 'right-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end'
export function parsePlacement(placement: Placement): { side: Side, align: Align } {
const [side, align] = placement.split('-') as [Side, Align | undefined]
return {
side,
align: align ?? 'center',
}
}
-2
View File
@@ -1,2 +0,0 @@
@import './themes/markdown-light.css';
@import './themes/markdown-dark.css';
-7
View File
@@ -1,7 +0,0 @@
@import './themes/light.css' layer(base);
@import './themes/dark.css' layer(base);
@import './themes/manual-light.css' layer(base);
@import './themes/manual-dark.css' layer(base);
@import './styles/tokens.css';
@source './**/*.{js,mjs}';
-713
View File
@@ -1,713 +0,0 @@
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@utility system-kbd {
/* font define start */
font-size: 12px;
font-weight: 500;
line-height: 16px;
/* border radius end */
}
@utility system-2xs-regular-uppercase {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
line-height: 12px;
/* border radius end */
}
@utility system-2xs-regular {
font-size: 10px;
font-weight: 400;
line-height: 12px;
/* border radius end */
}
@utility system-2xs-medium {
font-size: 10px;
font-weight: 500;
line-height: 12px;
/* border radius end */
}
@utility system-2xs-medium-uppercase {
font-size: 10px;
font-weight: 500;
text-transform: uppercase;
line-height: 12px;
/* border radius end */
}
@utility system-2xs-semibold-uppercase {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
line-height: 12px;
/* border radius end */
}
@utility system-xs-regular {
font-size: 12px;
font-weight: 400;
line-height: 16px;
/* border radius end */
}
@utility system-xs-regular-uppercase {
font-size: 12px;
font-weight: 400;
text-transform: uppercase;
line-height: 16px;
/* border radius end */
}
@utility system-xs-medium {
font-size: 12px;
font-weight: 500;
line-height: 16px;
/* border radius end */
}
@utility system-xs-medium-uppercase {
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
line-height: 16px;
/* border radius end */
}
@utility system-xs-semibold {
font-size: 12px;
font-weight: 600;
line-height: 16px;
/* border radius end */
}
@utility system-xs-semibold-uppercase {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
line-height: 16px;
/* border radius end */
}
@utility system-sm-regular {
font-size: 13px;
font-weight: 400;
line-height: 16px;
/* border radius end */
}
@utility system-sm-medium {
font-size: 13px;
font-weight: 500;
line-height: 16px;
/* border radius end */
}
@utility system-sm-medium-uppercase {
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
line-height: 16px;
/* border radius end */
}
@utility system-sm-semibold {
font-size: 13px;
font-weight: 600;
line-height: 16px;
/* border radius end */
}
@utility system-sm-semibold-uppercase {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
line-height: 16px;
/* border radius end */
}
@utility system-md-regular {
font-size: 14px;
font-weight: 400;
line-height: 20px;
/* border radius end */
}
@utility system-md-medium {
font-size: 14px;
font-weight: 500;
line-height: 20px;
/* border radius end */
}
@utility system-md-semibold {
font-size: 14px;
font-weight: 600;
line-height: 20px;
/* border radius end */
}
@utility system-md-semibold-uppercase {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
line-height: 20px;
/* border radius end */
}
@utility system-xl-regular {
font-size: 16px;
font-weight: 400;
line-height: 24px;
/* border radius end */
}
@utility system-xl-medium {
font-size: 16px;
font-weight: 500;
line-height: 24px;
/* border radius end */
}
@utility system-xl-semibold {
font-size: 16px;
font-weight: 600;
line-height: 24px;
/* border radius end */
}
@utility code-xs-regular {
font-size: 12px;
font-weight: 400;
line-height: 1.5;
/* border radius end */
}
@utility code-xs-semibold {
font-size: 12px;
font-weight: 600;
line-height: 1.5;
/* border radius end */
}
@utility code-sm-regular {
font-size: 13px;
font-weight: 400;
line-height: 1.5;
/* border radius end */
}
@utility code-sm-semibold {
font-size: 13px;
font-weight: 600;
line-height: 1.5;
/* border radius end */
}
@utility code-md-regular {
font-size: 14px;
font-weight: 400;
line-height: 1.5;
/* border radius end */
}
@utility code-md-semibold {
font-size: 14px;
font-weight: 600;
line-height: 1.5;
/* border radius end */
}
@utility body-xs-light {
font-size: 12px;
font-weight: 300;
line-height: 16px;
/* border radius end */
}
@utility body-xs-regular {
font-size: 12px;
font-weight: 400;
line-height: 16px;
/* border radius end */
}
@utility body-xs-medium {
font-size: 12px;
font-weight: 500;
line-height: 16px;
/* border radius end */
}
@utility body-sm-light {
font-size: 13px;
font-weight: 300;
line-height: 16px;
/* border radius end */
}
@utility body-sm-regular {
font-size: 13px;
font-weight: 400;
line-height: 16px;
/* border radius end */
}
@utility body-sm-medium {
font-size: 13px;
font-weight: 500;
line-height: 16px;
/* border radius end */
}
@utility body-md-light {
font-size: 14px;
font-weight: 300;
line-height: 20px;
/* border radius end */
}
@utility body-md-regular {
font-size: 14px;
font-weight: 400;
line-height: 20px;
/* border radius end */
}
@utility body-md-medium {
font-size: 14px;
font-weight: 500;
line-height: 20px;
/* border radius end */
}
@utility body-lg-light {
font-size: 15px;
font-weight: 300;
line-height: 20px;
/* border radius end */
}
@utility body-lg-regular {
font-size: 15px;
font-weight: 400;
line-height: 20px;
/* border radius end */
}
@utility body-lg-medium {
font-size: 15px;
font-weight: 500;
line-height: 20px;
/* border radius end */
}
@utility body-xl-regular {
font-size: 16px;
font-weight: 400;
line-height: 24px;
/* border radius end */
}
@utility body-xl-medium {
font-size: 16px;
font-weight: 500;
line-height: 24px;
/* border radius end */
}
@utility body-xl-light {
font-size: 16px;
font-weight: 300;
line-height: 24px;
/* border radius end */
}
@utility body-2xl-light {
font-size: 18px;
font-weight: 300;
line-height: 1.5;
/* border radius end */
}
@utility body-2xl-regular {
font-size: 18px;
font-weight: 400;
line-height: 1.5;
/* border radius end */
}
@utility body-2xl-medium {
font-size: 18px;
font-weight: 500;
line-height: 1.5;
/* border radius end */
}
@utility title-xs-semi-bold {
font-size: 12px;
font-weight: 600;
line-height: 16px;
/* border radius end */
}
@utility title-xs-bold {
font-size: 12px;
font-weight: 700;
line-height: 16px;
/* border radius end */
}
@utility title-sm-semi-bold {
font-size: 13px;
font-weight: 600;
line-height: 16px;
/* border radius end */
}
@utility title-sm-bold {
font-size: 13px;
font-weight: 700;
line-height: 16px;
/* border radius end */
}
@utility title-md-semi-bold {
font-size: 14px;
font-weight: 600;
line-height: 20px;
/* border radius end */
}
@utility title-md-bold {
font-size: 14px;
font-weight: 700;
line-height: 20px;
/* border radius end */
}
@utility title-lg-semi-bold {
font-size: 15px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-lg-bold {
font-size: 15px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-xl-semi-bold {
font-size: 16px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-xl-bold {
font-size: 16px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-2xl-semi-bold {
font-size: 18px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-2xl-bold {
font-size: 18px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-3xl-semi-bold {
font-size: 20px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-3xl-bold {
font-size: 20px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-4xl-semi-bold {
font-size: 24px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-4xl-bold {
font-size: 24px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-5xl-semi-bold {
font-size: 30px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-5xl-bold {
font-size: 30px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-6xl-semi-bold {
font-size: 36px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-6xl-bold {
font-size: 36px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-7xl-semi-bold {
font-size: 48px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-7xl-bold {
font-size: 48px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility title-8xl-semi-bold {
font-size: 60px;
font-weight: 600;
line-height: 1.2;
/* border radius end */
}
@utility title-8xl-bold {
font-size: 60px;
font-weight: 700;
line-height: 1.2;
/* border radius end */
}
@utility radius-2xs {
/* font define end */
/* border radius start */
border-radius: 2px;
/* border radius end */
}
@utility radius-xs {
border-radius: 4px;
/* border radius end */
}
@utility radius-sm {
border-radius: 6px;
/* border radius end */
}
@utility radius-md {
border-radius: 8px;
/* border radius end */
}
@utility radius-lg {
border-radius: 10px;
/* border radius end */
}
@utility radius-xl {
border-radius: 12px;
/* border radius end */
}
@utility radius-2xl {
border-radius: 16px;
/* border radius end */
}
@utility radius-3xl {
border-radius: 20px;
/* border radius end */
}
@utility radius-4xl {
border-radius: 24px;
/* border radius end */
}
@utility radius-5xl {
border-radius: 24px;
/* border radius end */
}
@utility radius-6xl {
border-radius: 28px;
/* border radius end */
}
@utility radius-7xl {
border-radius: 32px;
/* border radius end */
}
@utility radius-8xl {
border-radius: 40px;
/* border radius end */
}
@utility radius-9xl {
border-radius: 48px;
/* border radius end */
}
@utility radius-full {
border-radius: 64px;
/* border radius end */
}
@utility no-scrollbar {
/* Hide scrollbar for Chrome, Safari and Opera */
&::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none;
scrollbar-width: none;
}
@utility no-spinner {
/* Hide arrows from number input */
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
-moz-appearance: textfield;
}
-3
View File
@@ -1,3 +0,0 @@
declare const typography: (helpers: { theme: (path: string) => unknown }) => Record<string, unknown>
export default typography
-8
View File
@@ -1,8 +0,0 @@
import difyUiTailwindPreset from './src/tailwind-preset'
const config = {
content: [],
...difyUiTailwindPreset,
}
export default config
-21
View File
@@ -1,21 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true,
"noEmit": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js"
],
"exclude": [
"src/**/*.stories.tsx",
"src/**/__tests__/**"
]
}
-38
View File
@@ -1,38 +0,0 @@
{
"compilerOptions": {
"target": "es2022",
"jsx": "react-jsx",
"lib": [
"dom",
"dom.iterable",
"es2022"
],
"module": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"types": [
"node",
"vitest/globals",
"@testing-library/jest-dom"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js",
"scripts/**/*.mjs",
"vite.config.ts",
"vitest.setup.ts"
]
}
-11
View File
@@ -1,11 +0,0 @@
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite-plus'
export default defineConfig({
plugins: [react()],
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
},
})
-39
View File
@@ -1,39 +0,0 @@
import { cleanup } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
import { afterEach } from 'vitest'
if (typeof globalThis.ResizeObserver === 'undefined') {
globalThis.ResizeObserver = class {
observe() {
return undefined
}
unobserve() {
return undefined
}
disconnect() {
return undefined
}
}
}
if (typeof globalThis.IntersectionObserver === 'undefined') {
globalThis.IntersectionObserver = class {
readonly root: Element | Document | null = null
readonly rootMargin = ''
readonly scrollMargin = ''
readonly thresholds: ReadonlyArray<number> = []
constructor(_callback: IntersectionObserverCallback, _options?: IntersectionObserverInit) {}
observe(_target: Element) {}
unobserve(_target: Element) {}
disconnect() {}
takeRecords(): IntersectionObserverEntry[] {
return []
}
}
}
afterEach(() => {
cleanup()
})
+1340 -1350
View File
File diff suppressed because it is too large Load Diff
+47 -50
View File
@@ -1,9 +1,5 @@
catalogMode: prefer
trustPolicy: no-downgrade
trustPolicyExclude:
- [email protected]
- [email protected]
- [email protected]
minimumReleaseAge: 2880
blockExoticSubdeps: true
strictDepBuilds: true
allowBuilds:
@@ -27,7 +23,7 @@ overrides:
array.prototype.flatmap: npm:@nolyfill/array.prototype.flatmap@^1.0.44
array.prototype.tosorted: npm:@nolyfill/array.prototype.tosorted@^1.0.44
assert: npm:@nolyfill/assert@^1.0.26
brace-expansion@>=2.0.0 <2.0.3: 2.0.3
brace-expansion@<2.0.2: 2.0.2
canvas: ^3.2.2
devalue@<5.3.2: 5.3.2
dompurify@>=3.1.3 <=3.3.1: 3.3.2
@@ -41,7 +37,6 @@ overrides:
is-generator-function: npm:@nolyfill/is-generator-function@^1.0.44
is-typed-array: npm:@nolyfill/is-typed-array@^1.0.44
isarray: npm:@nolyfill/isarray@^1.0.44
lodash-es@>=4.0.0 <= 4.17.23: 4.18.0
object.assign: npm:@nolyfill/object.assign@^1.0.44
object.entries: npm:@nolyfill/object.entries@^1.0.44
object.fromentries: npm:@nolyfill/object.fromentries@^1.0.44
@@ -69,15 +64,15 @@ overrides:
tar@<=7.5.10: 7.5.11
typed-array-buffer: npm:@nolyfill/typed-array-buffer@^1.0.44
undici@>=7.0.0 <7.24.0: 7.24.0
vite: npm:@voidzero-dev/[email protected]5
vitest: npm:@voidzero-dev/[email protected]5
vite: npm:@voidzero-dev/[email protected]4
vitest: npm:@voidzero-dev/[email protected]4
which-typed-array: npm:@nolyfill/which-typed-array@^1.0.44
yaml@>=2.0.0 <2.8.3: 2.8.3
yauzl@<3.2.1: 3.2.1
catalog:
"@amplitude/analytics-browser": 2.38.1
"@amplitude/plugin-session-replay-browser": 1.27.6
"@antfu/eslint-config": 8.0.0
"@amplitude/analytics-browser": 2.38.0
"@amplitude/plugin-session-replay-browser": 1.27.5
"@antfu/eslint-config": 7.7.3
"@base-ui/react": 1.3.0
"@chromatic-com/storybook": 5.1.1
"@cucumber/cucumber": 12.7.0
@@ -89,7 +84,7 @@ catalog:
"@formatjs/intl-localematcher": 0.8.2
"@headlessui/react": 2.2.9
"@heroicons/react": 2.2.0
"@hono/node-server": 1.19.12
"@hono/node-server": 1.19.11
"@iconify-json/heroicons": 1.2.3
"@iconify-json/ri": 1.2.10
"@lexical/code": 0.42.0
@@ -103,34 +98,34 @@ catalog:
"@mdx-js/react": 3.1.1
"@mdx-js/rollup": 3.1.1
"@monaco-editor/react": 4.7.0
"@next/eslint-plugin-next": 16.2.2
"@next/mdx": 16.2.2
"@next/eslint-plugin-next": 16.2.1
"@next/mdx": 16.2.1
"@orpc/client": 1.13.13
"@orpc/contract": 1.13.13
"@orpc/openapi-client": 1.13.13
"@orpc/tanstack-query": 1.13.13
"@playwright/test": 1.59.1
"@playwright/test": 1.58.2
"@remixicon/react": 4.9.0
"@rgrove/parse-xml": 4.2.0
"@sentry/react": 10.47.0
"@storybook/addon-docs": 10.3.4
"@storybook/addon-links": 10.3.4
"@storybook/addon-onboarding": 10.3.4
"@storybook/addon-themes": 10.3.4
"@storybook/nextjs-vite": 10.3.4
"@storybook/react": 10.3.4
"@sentry/react": 10.46.0
"@storybook/addon-docs": 10.3.3
"@storybook/addon-links": 10.3.3
"@storybook/addon-onboarding": 10.3.3
"@storybook/addon-themes": 10.3.3
"@storybook/nextjs-vite": 10.3.3
"@storybook/react": 10.3.3
"@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.96.1
"@tanstack/react-devtools": 0.10.1
"@tanstack/react-form": 1.28.6
"@tanstack/react-form-devtools": 0.2.20
"@tanstack/react-query": 5.96.1
"@tanstack/react-query-devtools": 5.96.1
"@tanstack/eslint-plugin-query": 5.95.2
"@tanstack/react-devtools": 0.10.0
"@tanstack/react-form": 1.28.5
"@tanstack/react-form-devtools": 0.2.19
"@tanstack/react-query": 5.95.2
"@tanstack/react-query-devtools": 5.95.2
"@testing-library/dom": 10.4.1
"@testing-library/jest-dom": 6.9.1
"@testing-library/react": 16.3.2
@@ -149,12 +144,12 @@ catalog:
"@types/react-syntax-highlighter": 15.5.13
"@types/react-window": 1.8.8
"@types/sortablejs": 1.15.9
"@typescript-eslint/eslint-plugin": 8.58.0
"@typescript-eslint/parser": 8.58.0
"@typescript/native-preview": 7.0.0-dev.20260401.1
"@typescript-eslint/eslint-plugin": 8.57.2
"@typescript-eslint/parser": 8.57.2
"@typescript/native-preview": 7.0.0-dev.20260329.1
"@vitejs/plugin-react": 6.0.1
"@vitejs/plugin-rsc": 0.5.21
"@vitest/coverage-v8": 4.1.2
"@vitest/coverage-v8": 4.1.1
abcjs: 6.6.2
agentation: 3.0.2
ahooks: 3.9.7
@@ -162,7 +157,7 @@ catalog:
class-variance-authority: 0.7.1
clsx: 2.1.1
cmdk: 1.1.1
code-inspector-plugin: 1.5.0
code-inspector-plugin: 1.4.5
copy-to-clipboard: 3.3.3
cron-parser: 5.5.0
dayjs: 1.11.20
@@ -179,18 +174,19 @@ catalog:
eslint-markdown: 0.6.0
eslint-plugin-better-tailwindcss: 4.3.2
eslint-plugin-hyoban: 0.14.1
eslint-plugin-markdown-preferences: 0.41.0
eslint-plugin-markdown-preferences: 0.40.3
eslint-plugin-no-barrel-files: 1.2.2
eslint-plugin-react-hooks: 7.0.1
eslint-plugin-react-refresh: 0.5.2
eslint-plugin-sonarjs: 4.0.2
eslint-plugin-storybook: 10.3.4
eslint-plugin-storybook: 10.3.3
fast-deep-equal: 3.1.3
foxact: 0.3.0
happy-dom: 20.8.9
hono: 4.12.10
hono: 4.12.9
html-entities: 2.6.0
html-to-image: 1.11.13
i18next: 26.0.3
i18next: 25.10.10
i18next-resources-to-backend: 1.2.1
iconify-import-svg: 0.1.2
immer: 11.1.4
@@ -200,15 +196,15 @@ catalog:
js-yaml: 4.1.1
jsonschema: 1.5.0
katex: 0.16.44
knip: 6.2.0
knip: 6.1.0
ky: 1.14.3
lamejs: 1.2.1
lexical: 0.42.0
mermaid: 11.14.0
mermaid: 11.13.0
mime: 4.1.0
mitt: 3.0.1
negotiator: 1.0.0
next: 16.2.2
next: 16.2.1
next-themes: 0.4.6
nuqs: 2.8.9
pinyin-pro: 3.28.0
@@ -221,7 +217,7 @@ catalog:
react-dom: 19.2.4
react-easy-crop: 5.5.7
react-hotkeys-hook: 5.2.4
react-i18next: 17.0.2
react-i18next: 16.6.6
react-multi-email: 1.0.25
react-papaparse: 4.4.0
react-pdf-highlighter: 8.0.0-rc.0
@@ -233,29 +229,30 @@ catalog:
reactflow: 11.11.4
remark-breaks: 4.0.0
remark-directive: 4.0.0
sass: 1.98.0
scheduler: 0.27.0
sharp: 0.34.5
sortablejs: 1.15.7
std-semver: 1.0.8
storybook: 10.3.4
storybook: 10.3.3
streamdown: 2.5.0
string-ts: 2.3.1
tailwind-merge: 3.5.0
tailwindcss: 4.2.2
taze: 19.11.0
taze: 19.10.0
tldts: 7.0.27
tsdown: 0.21.7
tsup: ^8.5.1
tsx: 4.21.0
typescript: 6.0.2
typescript: 5.9.3
uglify-js: 3.19.3
unist-util-visit: 5.1.0
use-context-selector: 2.0.0
uuid: 13.0.0
vinext: 0.0.39
vite: npm:@voidzero-dev/[email protected]5
vinext: 0.0.38
vite: npm:@voidzero-dev/[email protected]4
vite-plugin-inspect: 12.0.0-beta.1
vite-plus: 0.1.15
vitest: npm:@voidzero-dev/[email protected]5
vite-plus: 0.1.14
vitest: npm:@voidzero-dev/[email protected]4
vitest-canvas-mock: 1.1.4
zod: 4.3.6
zundo: 2.3.0
+4 -4
View File
@@ -45,12 +45,12 @@
"homepage": "https://dify.ai",
"license": "MIT",
"scripts": {
"build": "vp pack",
"build": "tsup",
"lint": "eslint",
"lint:fix": "eslint --fix",
"type-check": "tsc -p tsconfig.json --noEmit",
"test": "vp test",
"test:coverage": "vp test --coverage",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"publish:check": "./scripts/publish.sh --dry-run",
"publish:npm": "./scripts/publish.sh"
},
@@ -61,8 +61,8 @@
"@typescript-eslint/parser": "catalog:",
"@vitest/coverage-v8": "catalog:",
"eslint": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
}
}
+1 -2
View File
@@ -11,8 +11,7 @@
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"types": ["node"]
"skipLibCheck": true
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: true,
splitting: false,
treeshake: true,
outDir: "dist",
});
@@ -1,17 +1,6 @@
import { defineConfig } from "vite-plus";
import { defineConfig } from "vitest/config";
export default defineConfig({
pack: {
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: true,
// splitting: false,
treeshake: true,
outDir: "dist",
target: false,
},
test: {
environment: "node",
include: ["**/*.test.ts"],
+5
View File
@@ -6,5 +6,10 @@ export default defineConfig({
'react-syntax-highlighter',
'react-window',
'@types/react-window',
// We can not upgrade these yet
'typescript',
],
maturityPeriod: 2,
})
+1 -4
View File
@@ -1,10 +1,7 @@
import type { StorybookConfig } from '@storybook/nextjs-vite'
const config: StorybookConfig = {
stories: [
'../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)',
'../../packages/dify-ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
stories: ['../app/components/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
// Not working with Storybook Vite framework
// '@storybook/addon-onboarding',
@@ -35,7 +35,7 @@ const TagManagementModal = dynamic(() => import('@/app/components/base/tag-manag
ssr: false,
})
type IAppDetailLayoutProps = {
export type IAppDetailLayoutProps = {
children: React.ReactNode
appId: string
}
@@ -25,7 +25,7 @@ import { useAppWorkflow } from '@/service/use-workflow'
import { AppModeEnum } from '@/types/app'
import { asyncRunSafe } from '@/utils'
type ICardViewProps = {
export type ICardViewProps = {
appId: string
isInPanel?: boolean
className?: string
@@ -27,7 +27,7 @@ const TIME_PERIOD_MAPPING: { value: number, name: TimePeriodName }[] = [
const queryDateFormat = 'YYYY-MM-DD HH:mm'
type IChartViewProps = {
export type IChartViewProps = {
appId: string
headerRight: React.ReactNode
}
@@ -1,3 +1,5 @@
@reference "../../../../styles/globals.css";
.app {
flex-grow: 1;
height: 0;
@@ -0,0 +1,11 @@
@reference "../../../../../styles/globals.css";
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}
@@ -26,7 +26,7 @@ import { usePathname } from '@/next/navigation'
import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset'
import { cn } from '@/utils/classnames'
type IAppDetailLayoutProps = {
export type IAppDetailLayoutProps = {
children: React.ReactNode
datasetId: string
}
-3
View File
@@ -1,5 +1,4 @@
import type { ReactNode } from 'react'
import * as React from 'react'
import { AppInitializer } from '@/app/components/app-initializer'
import InSiteMessageNotification from '@/app/components/app/in-site-message/notification'
import AmplitudeProvider from '@/app/components/base/amplitude'
@@ -14,7 +13,6 @@ import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
import { ModalContextProvider } from '@/context/modal-context-provider'
import { ProviderContextProvider } from '@/context/provider-context-provider'
import PartnerStack from '../components/billing/partner-stack'
import Splash from '../components/splash'
import RoleRouteGuard from './role-route-guard'
const Layout = ({ children }: { children: ReactNode }) => {
@@ -37,7 +35,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
<PartnerStack />
<ReadmePanel />
<GotoAnything />
<Splash />
</ModalContextProvider>
</ProviderContextProvider>
</EventEmitterContextProvider>
+2 -14
View File
@@ -1,10 +1,8 @@
'use client'
import type { ReactNode } from 'react'
import { useEffect } from 'react'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { usePathname, useRouter } from '@/next/navigation'
import { redirect, usePathname } from '@/next/navigation'
const datasetOperatorRedirectRoutes = ['/apps', '/app', '/explore', '/tools'] as const
@@ -13,21 +11,11 @@ const isPathUnderRoute = (pathname: string, route: string) => pathname === route
export default function RoleRouteGuard({ children }: { children: ReactNode }) {
const { isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
const pathname = usePathname()
const router = useRouter()
const shouldGuardRoute = datasetOperatorRedirectRoutes.some(route => isPathUnderRoute(pathname, route))
const shouldRedirect = shouldGuardRoute && !isLoadingCurrentWorkspace && isCurrentWorkspaceDatasetOperator
useEffect(() => {
if (shouldRedirect)
router.replace('/datasets')
}, [shouldRedirect, router])
// Block rendering only for guarded routes to avoid permission flicker.
if (shouldGuardRoute && isLoadingCurrentWorkspace)
return <Loading type="app" />
if (shouldRedirect)
return null
return redirect('/datasets')
return <>{children}</>
}
@@ -13,6 +13,10 @@ import { useProviderContext } from '@/context/provider-context'
import { useRouter } from '@/next/navigation'
import { useLogout, useUserProfile } from '@/service/use-common'
export type IAppSelector = {
isMobile: boolean
}
export default function AppSelector() {
const router = useRouter()
const { t } = useTranslation()
+2 -6
View File
@@ -3,7 +3,7 @@
import type { ReactNode } from 'react'
import Cookies from 'js-cookie'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect } from 'react'
import {
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
@@ -25,7 +25,6 @@ export const AppInitializer = ({
const searchParams = useSearchParams()
// Tokens are now stored in cookies, no need to check localStorage
const pathname = usePathname()
const [init, setInit] = useState(false)
const [oauthNewUser] = useQueryState(
'oauth_new_user',
parseAsBoolean.withOptions({ history: 'replace' }),
@@ -87,10 +86,7 @@ export const AppInitializer = ({
const redirectUrl = resolvePostLoginRedirect()
if (redirectUrl) {
location.replace(redirectUrl)
return
}
setInit(true)
}
catch {
router.replace('/signin')
@@ -98,5 +94,5 @@ export const AppInitializer = ({
})()
}, [isSetupFinished, router, pathname, searchParams, oauthNewUser])
return init ? children : null
return children
}
@@ -5,7 +5,7 @@ import AppInfoModals from './app-info-modals'
import AppInfoTrigger from './app-info-trigger'
import { useAppInfoActions } from './use-app-info-actions'
type IAppInfoProps = {
export type IAppInfoProps = {
expand: boolean
onlyShowDetail?: boolean
openState?: boolean
+1 -1
View File
@@ -7,7 +7,7 @@ import {
import Tooltip from '@/app/components/base/tooltip'
import AppIcon from '../base/app-icon'
type IAppBasicProps = {
export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
icon?: string
icon_background?: string | null
+1 -1
View File
@@ -17,7 +17,7 @@ import DatasetSidebarDropdown from './dataset-sidebar-dropdown'
import NavLink from './nav-link'
import ToggleButton from './toggle-button'
type IAppDetailNavProps = {
export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset'
navigation: Array<{
name: string
@@ -39,5 +39,7 @@ export enum AnnotationEnableStatus {
}
export enum JobStatus {
waiting = 'waiting',
processing = 'processing',
completed = 'completed',
}
@@ -2,7 +2,7 @@ import type { HTMLProps, PropsWithChildren } from 'react'
import { RiArrowRightUpLine } from '@remixicon/react'
import { cn } from '@/utils/classnames'
type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement> & {
icon?: React.ReactNode
link?: string
disabled?: boolean
@@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react'
import * as React from 'react'
import { cn } from '@/utils/classnames'
type IFeaturePanelProps = {
export type IFeaturePanelProps = {
className?: string
headerIcon?: ReactNode
title: ReactNode
@@ -2,7 +2,7 @@
import type { FC } from 'react'
import * as React from 'react'
type IGroupNameProps = {
export type IGroupNameProps = {
name: string
}
@@ -9,7 +9,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type IOperationBtnProps = {
export type IOperationBtnProps = {
className?: string
type: 'add' | 'edit'
actionName?: string
@@ -4,7 +4,7 @@ import * as React from 'react'
import s from './style.module.css'
type IVarHighlightProps = {
export type IVarHighlightProps = {
name: string
className?: string
}
@@ -1,3 +1,5 @@
@reference "../../../../../styles/globals.css";
.item {
background-color: rgba(21, 94, 239, 0.05);
}
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import WarningMask from '.'
type IFormattingChangedProps = {
export type IFormattingChangedProps = {
onConfirm: () => void
}
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import WarningMask from '.'
type IFormattingChangedProps = {
export type IFormattingChangedProps = {
onConfirm: () => void
onCancel: () => void
}
@@ -3,7 +3,7 @@ import type { FC } from 'react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
type IHasNotSetAPIProps = {
export type IHasNotSetAPIProps = {
onSetting: () => void
}
@@ -4,7 +4,7 @@ import * as React from 'react'
import s from './style.module.css'
type IWarningMaskProps = {
export type IWarningMaskProps = {
title: string
description: string
footer: React.ReactNode
@@ -1,3 +1,5 @@
@reference "../../../../../styles/globals.css";
.mask {
backdrop-filter: blur(2px);
}
@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import VarHighlight from '../../base/var-highlight'
type IConfirmAddVarProps = {
export type IConfirmAddVarProps = {
varNameArr: string[]
onConfirm: () => void
onCancel: () => void
@@ -33,7 +33,7 @@ import { getNewVar, getVars } from '@/utils/var'
import ConfirmAddVar from './confirm-add-var'
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
type ISimplePromptInput = {
export type ISimplePromptInput = {
mode: AppModeEnum
promptTemplate: string
promptVariables: PromptVariable[]
@@ -1,3 +1,5 @@
@reference "../../../../styles/globals.css";
.gradientBorder {
background: radial-gradient(circle at 100% 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 0% 0%/12px 12px no-repeat,
radial-gradient(circle at 0 100%, #fcfcfd 0, #fcfcfd 10px, transparent 10px) 100% 0%/12px 12px no-repeat,
@@ -1,3 +1,10 @@
export const jsonObjectWrap = {
type: 'object',
properties: {},
required: [],
additionalProperties: true,
}
export const jsonConfigPlaceHolder = JSON.stringify(
{
type: 'object',
@@ -52,7 +52,7 @@ const normalizeSelectDefaultValue = (inputVar: InputVar) => {
return inputVar
}
type IConfigModalProps = {
export type IConfigModalProps = {
isCreate?: boolean
payload?: InputVar
isShow: boolean
@@ -8,7 +8,7 @@ import { ReactSortable } from 'react-sortablejs'
import { cn } from '@/utils/classnames'
export type Options = string[]
type IConfigSelectProps = {
export type IConfigSelectProps = {
options: Options
onChange: (options: Options) => void
}
@@ -4,7 +4,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type IModalFootProps = {
export type IModalFootProps = {
onConfirm: () => void
onCancel: () => void
}
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
import { cn } from '@/utils/classnames'
type ISelectTypeItemProps = {
export type ISelectTypeItemProps = {
type: InputVarType
selected: boolean
onClick: () => void
@@ -0,0 +1,152 @@
'use client'
import type { FC } from 'react'
import type { ExternalDataTool } from '@/models/common'
import copy from 'copy-to-clipboard'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import s from '@/app/components/app/configuration/config-prompt/style.module.css'
import {
Copy,
CopyCheck,
} from '@/app/components/base/icons/src/vender/line/files'
import PromptEditor from '@/app/components/base/prompt-editor'
import { toast } from '@/app/components/base/ui/toast'
import ConfigContext from '@/context/debug-configuration'
import { useModalContext } from '@/context/modal-context'
import { cn } from '@/utils/classnames'
type Props = {
className?: string
type: 'first-prompt' | 'next-iteration'
value: string
onChange: (value: string) => void
}
const Editor: FC<Props> = ({
className,
type,
value,
onChange,
}) => {
const { t } = useTranslation()
const [isCopied, setIsCopied] = React.useState(false)
const {
modelConfig,
hasSetBlockStatus,
dataSets,
showSelectDataSet,
externalDataToolsConfig,
setExternalDataToolsConfig,
} = useContext(ConfigContext)
const promptVariables = modelConfig.configs.prompt_variables
const { setShowExternalDataToolModal } = useModalContext()
const isFirstPrompt = type === 'first-prompt'
const editorHeight = isFirstPrompt ? 'h-[336px]' : 'h-[52px]'
const handleOpenExternalDataToolModal = () => {
setShowExternalDataToolModal({
payload: {},
onSaveCallback: (newExternalDataTool?: ExternalDataTool) => {
if (!newExternalDataTool)
return
setExternalDataToolsConfig([...externalDataToolsConfig, newExternalDataTool])
},
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
for (let i = 0; i < promptVariables.length; i++) {
if (promptVariables[i].key === newExternalDataTool.variable) {
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
return false
}
}
for (let i = 0; i < externalDataToolsConfig.length; i++) {
if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) {
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }))
return false
}
}
return true
},
})
}
return (
<div className={cn(className, s.gradientBorder, 'relative')}>
<div className="rounded-xl bg-white">
<div className={cn(s.boxHeader, 'flex h-11 items-center justify-between rounded-tl-xl rounded-tr-xl bg-white pb-1 pl-4 pr-3 pt-2 hover:shadow-xs')}>
<div className="text-sm font-semibold uppercase text-indigo-800">{t(`agent.${isFirstPrompt ? 'firstPrompt' : 'nextIteration'}`, { ns: 'appDebug' })}</div>
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
{!isCopied
? (
<Copy
className="h-6 w-6 cursor-pointer p-1 text-gray-500"
onClick={() => {
copy(value)
setIsCopied(true)
}}
/>
)
: (
<CopyCheck className="h-6 w-6 p-1 text-gray-500" />
)}
</div>
</div>
<div className={cn(editorHeight, 'min-h-[102px] overflow-y-auto px-4 text-sm text-gray-700')}>
<PromptEditor
className={editorHeight}
value={value}
contextBlock={{
show: true,
selectable: !hasSetBlockStatus.context,
datasets: dataSets.map(item => ({
id: item.id,
name: item.name,
type: item.data_source_type,
})),
onAddContext: showSelectDataSet,
}}
variableBlock={{
show: true,
variables: modelConfig.configs.prompt_variables.filter(item => item.key && item.key.trim() && item.name && item.name.trim()).map(item => ({
name: item.name,
value: item.key,
})),
}}
externalToolBlock={{
show: true,
externalTools: externalDataToolsConfig.map(item => ({
name: item.label!,
variableName: item.variable!,
icon: item.icon,
icon_background: item.icon_background,
})),
onAddExternalTool: handleOpenExternalDataToolModal,
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: '',
},
onEditRole: noop,
}}
queryBlock={{
show: false,
selectable: false,
}}
onChange={onChange}
onBlur={noop}
/>
</div>
<div className="flex pb-2 pl-4">
<div className="h-[18px] rounded-md bg-gray-100 px-1 text-xs leading-[18px] text-gray-500">{value.length}</div>
</div>
</div>
</div>
)
}
export default React.memo(Editor)
@@ -7,7 +7,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type IAutomaticBtnProps = {
export type IAutomaticBtnProps = {
onClick: () => void
}
const AutomaticBtn: FC<IAutomaticBtnProps> = ({
@@ -43,7 +43,7 @@ import useGenData from './use-gen-data'
const i18nPrefix = 'generate'
type IGetAutomaticResProps = {
export type IGetAutomaticResProps = {
mode: AppModeEnum
isShow: boolean
onClose: () => void
@@ -1,3 +1,5 @@
@reference "../../../../../styles/globals.css";
.textGradient {
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
-webkit-background-clip: text;
@@ -31,7 +31,7 @@ import { GeneratorType } from '../automatic/types'
import useGenData from '../automatic/use-gen-data'
const i18nPrefix = 'generate'
type IGetCodeGeneratorResProps = {
export type IGetCodeGeneratorResProps = {
flowId: string
nodeId: string
currentCode?: string
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import s from './style.module.css'
type IContrlBtnGroupProps = {
export type IContrlBtnGroupProps = {
onSave: () => void
onReset: () => void
}
@@ -1,3 +1,5 @@
@reference "../../../../styles/globals.css";
.ctrlBtn {
left: -16px;
right: -16px;
@@ -17,7 +17,7 @@ import Link from '@/next/link'
import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
import { cn } from '@/utils/classnames'
type ISelectDataSetProps = {
export type ISelectDataSetProps = {
isShow: boolean
onClose: () => void
selectedIds: string[]
@@ -18,3 +18,5 @@ export const DebugWithMultipleModelContext = createContext<DebugWithMultipleMode
})
export const useDebugWithMultipleModelContext = () => useContext(DebugWithMultipleModelContext)
export default DebugWithMultipleModelContext
@@ -0,0 +1,16 @@
@reference "../../../styles/globals.css";
.advancedPromptMode {
position: relative;
}
.advancedPromptMode::before {
content: '';
position: absolute;
bottom: 0;
left: -1px;
width: 100%;
height: 3px;
background-color: rgba(68, 76, 231, 0.18);
transform: skewX(-30deg);
}
@@ -0,0 +1,204 @@
import type { ExternalDataTool } from '@/models/common'
import {
RiAddLine,
RiArrowDownSLine,
RiDeleteBinLine,
} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import AppIcon from '@/app/components/base/app-icon'
import {
Settings01,
} from '@/app/components/base/icons/src/vender/line/general'
import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general'
import Switch from '@/app/components/base/switch'
import { toast } from '@/app/components/base/ui/toast'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/app/components/base/ui/tooltip'
import ConfigContext from '@/context/debug-configuration'
import { useModalContext } from '@/context/modal-context'
const Tools = () => {
const { t } = useTranslation()
const { setShowExternalDataToolModal } = useModalContext()
const {
externalDataToolsConfig,
setExternalDataToolsConfig,
modelConfig,
} = useContext(ConfigContext)
const [expanded, setExpanded] = useState(true)
const [copied, setCopied] = useState(false)
const handleSaveExternalDataToolModal = (externalDataTool: ExternalDataTool, index: number) => {
if (index > -1) {
setExternalDataToolsConfig([
...externalDataToolsConfig.slice(0, index),
externalDataTool,
...externalDataToolsConfig.slice(index + 1),
])
}
else {
setExternalDataToolsConfig([...externalDataToolsConfig, externalDataTool])
}
}
const handleValidateBeforeSaveExternalDataToolModal = (newExternalDataTool: ExternalDataTool, index: number) => {
const promptVariables = modelConfig?.configs?.prompt_variables || []
for (let i = 0; i < promptVariables.length; i++) {
if (promptVariables[i].key === newExternalDataTool.variable) {
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }))
return false
}
}
let existedExternalDataTools = []
if (index > -1) {
existedExternalDataTools = [
...externalDataToolsConfig.slice(0, index),
...externalDataToolsConfig.slice(index + 1),
]
}
else {
existedExternalDataTools = [...externalDataToolsConfig]
}
for (let i = 0; i < existedExternalDataTools.length; i++) {
if (existedExternalDataTools[i].variable === newExternalDataTool.variable) {
toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }))
return false
}
}
return true
}
const handleOpenExternalDataToolModal = (payload: ExternalDataTool, index: number) => {
setShowExternalDataToolModal({
payload,
onSaveCallback: (externalDataTool?: ExternalDataTool) => {
if (!externalDataTool)
return
handleSaveExternalDataToolModal(externalDataTool, index)
},
onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => handleValidateBeforeSaveExternalDataToolModal(newExternalDataTool, index),
})
}
return (
<div className="mt-3 rounded-xl bg-gray-50 px-3">
<div className="flex h-12 items-center">
<div className="flex grow items-center">
<div
className={`
group mr-1 flex h-6 w-6 items-center justify-center rounded-md
${externalDataToolsConfig.length && 'hover:bg-white hover:shadow-xs'}
`}
onClick={() => setExpanded(v => !v)}
>
{
externalDataToolsConfig.length
? <Tool03 className="h-4 w-4 text-[#444CE7] group-hover:hidden" />
: <Tool03 className="h-4 w-4 text-[#444CE7]" />
}
{
!!externalDataToolsConfig.length && (
<RiArrowDownSLine className={`hidden h-4 w-4 cursor-pointer text-primary-600 group-hover:block ${expanded ? 'rotate-180' : 'rotate-0'}`} />
)
}
</div>
<div className="mr-1 text-sm font-semibold text-gray-800">
{t('feature.tools.title', { ns: 'appDebug' })}
</div>
<Tooltip>
<TooltipTrigger render={<span className="i-ri-question-line ml-1 h-4 w-4 shrink-0 text-text-quaternary" />} />
<TooltipContent>
<div className="max-w-[160px]">
{t('feature.tools.tips', { ns: 'appDebug' })}
</div>
</TooltipContent>
</Tooltip>
</div>
{
!expanded && !!externalDataToolsConfig.length && (
<>
<div className="mr-3 text-xs text-gray-500">{t('feature.tools.toolsInUse', { ns: 'appDebug', count: externalDataToolsConfig.length })}</div>
<div className="mr-1 h-3.5 w-px bg-gray-200" />
</>
)
}
<div
className="flex h-7 cursor-pointer items-center px-3 text-xs font-medium text-gray-700"
onClick={() => handleOpenExternalDataToolModal({}, -1)}
>
<RiAddLine className="mr-[5px] h-3.5 w-3.5" />
{t('operation.add', { ns: 'common' })}
</div>
</div>
{
expanded && !!externalDataToolsConfig.length && (
<div className="pb-3">
{
externalDataToolsConfig.map((item, index: number) => (
<div
key={`${index}-${item.type}-${item.label}-${item.variable}`}
className="group mb-1 flex items-center rounded-lg border-[0.5px] border-gray-200 bg-white px-2.5 py-2 shadow-xs last-of-type:mb-0"
>
<div className="flex grow items-center">
<AppIcon
size="large"
className="mr-2 h-6! w-6! rounded-md border-[0.5px] border-black/5"
icon={item.icon}
background={item.icon_background}
/>
<div className="mr-2 text-[13px] font-medium text-gray-800">{item.label}</div>
<Tooltip>
<TooltipTrigger
render={(
<div
className="text-xs text-gray-500"
onClick={() => {
copy(item.variable || '')
setCopied(true)
}}
>
{item.variable}
</div>
)}
/>
<TooltipContent>
{copied ? t('copied', { ns: 'appApi' }) : `${item.variable}, ${t('copy', { ns: 'appApi' })}`}
</TooltipContent>
</Tooltip>
</div>
<div
className="mr-1 hidden h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-black/5 group-hover:flex"
onClick={() => handleOpenExternalDataToolModal(item, index)}
>
<Settings01 className="h-4 w-4 text-gray-500" />
</div>
<div
className="group/action hidden h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-[#FEE4E2] group-hover:flex"
onClick={() => setExternalDataToolsConfig([...externalDataToolsConfig.slice(0, index), ...externalDataToolsConfig.slice(index + 1)])}
>
<RiDeleteBinLine className="h-4 w-4 text-gray-500 group-hover/action:text-[#D92D20]" />
</div>
<div className="ml-2 mr-3 hidden h-3.5 w-px bg-gray-200 group-hover:block" />
<Switch
size="lg"
value={item.enabled ?? false}
onChange={(enabled: boolean) => handleSaveExternalDataToolModal({ ...item, enabled }, index)}
/>
</div>
))
}
</div>
)
}
</div>
)
}
export default Tools
@@ -12,7 +12,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { cn } from '@/utils/classnames'
import { AppTypeIcon, AppTypeLabel } from '../../type-selector'
type AppCardProps = {
export type AppCardProps = {
app: App
canCreate: boolean
onCreate: () => void
@@ -13,7 +13,7 @@ import { toast } from '@/app/components/base/ui/toast'
import { cn } from '@/utils/classnames'
import { formatFileSize } from '@/utils/format'
type Props = {
export type Props = {
file: File | undefined
updateFile: (file?: File) => void
className?: string
+1 -1
View File
@@ -17,7 +17,7 @@ import EmptyElement from './empty-element'
import Filter, { TIME_PERIOD_MAPPING } from './filter'
import List from './list'
type ILogsProps = {
export type ILogsProps = {
appDetail: App
}
@@ -72,17 +72,17 @@ const defaultModalContext: ModalContextState = {
setShowTriggerEventsLimitModal: noop,
}
type MockOverrides = {
export type MockOverrides = {
providerContext?: Partial<typeof defaultProviderContext>
modalContext?: Partial<typeof defaultModalContext>
}
type APIKeyInfoPanelRenderOptions = {
export type APIKeyInfoPanelRenderOptions = {
mockOverrides?: MockOverrides
} & Omit<RenderOptions, 'wrapper'>
// Setup function to configure mocks
function setupMocks(overrides: MockOverrides = {}) {
export function setupMocks(overrides: MockOverrides = {}) {
mockUseProviderContext.mockReturnValue({
...defaultProviderContext,
...overrides.providerContext,
@@ -95,7 +95,7 @@ function setupMocks(overrides: MockOverrides = {}) {
}
// Custom render function
function renderAPIKeyInfoPanel(options: APIKeyInfoPanelRenderOptions = {}) {
export function renderAPIKeyInfoPanel(options: APIKeyInfoPanelRenderOptions = {}) {
const { mockOverrides, ...renderOptions } = options
setupMocks(mockOverrides)
@@ -210,4 +210,4 @@ export function clearAllMocks() {
}
// Export mock functions for external access
export { defaultModalContext, mockUseModalContext }
export { defaultModalContext, mockUseModalContext, mockUseProviderContext }
@@ -102,12 +102,12 @@ export type PeriodParamsWithTimeRange = {
query?: TimeRange
}
type IBizChartProps = {
export type IBizChartProps = {
period: PeriodParams
id: string
}
type IChartProps = {
export type IChartProps = {
className?: string
basicInfo: { title: string, explanation: string, timePeriod: string }
valueKey?: string
@@ -508,3 +508,5 @@ export const AvgUserInteractions: FC<IBizChartProps> = ({ id, period }) => {
/>
)
}
export default Chart
@@ -29,7 +29,7 @@ import Link from '@/next/link'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
type ISettingsModalProps = {
export type ISettingsModalProps = {
isChat: boolean
appInfo: AppDetailResponse & Partial<AppSSO>
isShow: boolean
@@ -22,7 +22,7 @@ import {
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { canFindTool } from '@/utils'
type ITriggerCardProps = {
export type ITriggerCardProps = {
appInfo: AppDetailResponse & Partial<AppSSO>
onToggleResult?: (err: Error | null, message?: I18nKeysByPrefix<'common', 'actionMsg.'>) => void
}
@@ -38,7 +38,7 @@ import ResultTab from './result-tab'
const MAX_DEPTH = 3
type IGenerationItemProps = {
export type IGenerationItemProps = {
isWorkflow?: boolean
workflowProcessData?: WorkflowProcess
className?: string
@@ -67,6 +67,12 @@ type IGenerationItemProps = {
inSidePanel?: boolean
}
export const copyIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const GenerationItem: FC<IGenerationItemProps> = ({
isWorkflow,
workflowProcessData,
@@ -8,7 +8,7 @@ import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type INoDataProps = {
export type INoDataProps = {
onStartCreateContent: () => void
}
@@ -11,7 +11,7 @@ import {
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
type AppSelectorProps = {
export type AppSelectorProps = {
value: Array<AppModeEnum>
onChange: (value: AppSelectorProps['value']) => void
}
+1 -1
View File
@@ -62,7 +62,7 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont
ssr: false,
})
type AppCardProps = {
export type AppCardProps = {
app: App
onRefresh?: () => void
}
+1 -1
View File
@@ -25,7 +25,7 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
ssr: false,
})
type CreateAppCardProps = {
export type CreateAppCardProps = {
className?: string
isLoading?: boolean
onSuccess?: () => void
@@ -15,7 +15,7 @@ import { cn } from '@/utils/classnames'
import ResultPanel from './result'
import TracingPanel from './tracing'
type AgentLogDetailProps = {
export type AgentLogDetailProps = {
activeTab?: 'DETAIL' | 'TRACING'
conversationID: string
log: IChatItem

Some files were not shown because too many files have changed in this diff Show More