From 8fafc8d129909bb499070068bc3eb09efbaa8e8b Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Thu, 8 Oct 2026 18:34:56 +0800 Subject: [PATCH 1/5] refactor(api): inject retrieval dependencies into APIs and annotation tasks --- .../console/datasets/hit_testing_base.py | 8 +- .../inner_api/knowledge/retrieval.py | 9 +- .../application_services/knowledge.py | 7 + api/extensions/ext_application_services.py | 5 +- api/services/hit_testing_service.py | 8 +- api/services/knowledge/segments/indexing.py | 2 +- .../knowledge_retrieval_inner_service.py | 52 +--- api/tasks/annotation/__init__.py | 1 + .../add_annotation_to_index_task.py | 8 +- .../batch_import_annotations_task.py | 8 +- .../delete_annotation_index_task.py | 8 +- .../enable_annotation_reply_task.py | 6 +- .../update_annotation_to_index_task.py | 8 +- .../test_dataset_retrieval_integration.py | 64 ++--- .../services/test_hit_testing_service.py | 22 +- .../console/datasets/test_hit_testing_base.py | 8 +- .../test_index_tool_callback_handler.py | 258 +----------------- .../extensions/otel/test_retrieval_tracing.py | 35 +-- .../services/annotation/test_reply_service.py | 25 ++ api/tests/unit_tests/services/hit_service.py | 54 +++- .../test_indexing_execution_adapters.py | 14 +- .../test_retrieval_attachment_signing.py | 27 +- .../knowledge/test_segment_adapters.py | 5 +- .../test_knowledge_retrieval_inner_service.py | 56 ++-- 24 files changed, 258 insertions(+), 440 deletions(-) create mode 100644 api/tasks/annotation/__init__.py diff --git a/api/controllers/console/datasets/hit_testing_base.py b/api/controllers/console/datasets/hit_testing_base.py index a1a59cbd40107b..377bd900adf323 100644 --- a/api/controllers/console/datasets/hit_testing_base.py +++ b/api/controllers/console/datasets/hit_testing_base.py @@ -3,9 +3,9 @@ from pydantic import BaseModel, Field from sqlalchemy.orm import Session -from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services.errors.base +from controllers.common.errors import AccessDeniedError, InternalServerError, NotFoundError from controllers.console.app.error import ( CompletionRequestError, ProviderModelCurrentlyNotSupportError, @@ -19,6 +19,7 @@ ProviderTokenNotInitError, QuotaExceededError, ) +from extensions.ext_application_services import application_services from graphon.model_runtime.errors.invoke import InvokeError from libs.login import resolve_account_fallback from models.account import Account @@ -93,12 +94,12 @@ def get_and_validate_dataset( current_user, _ = resolve_account_fallback(current_user, current_tenant_id) dataset = DatasetService.get_dataset(dataset_id, session) if dataset is None: - raise NotFound("Dataset not found.") + raise NotFoundError("Dataset not found.") try: DatasetService.check_dataset_permission(dataset, current_user, session) except services.errors.base.NoPermissionError as e: - raise Forbidden(str(e)) + raise AccessDeniedError(str(e)) from e return dataset @@ -123,6 +124,7 @@ def perform_hit_testing( try: current_user, _ = resolve_account_fallback(current_user, current_tenant_id) response = HitTestingService.retrieve( + retrieval=application_services().workflow_runtime.retrieval, session=session, dataset=dataset, query=cast(str, args.get("query")), diff --git a/api/controllers/inner_api/knowledge/retrieval.py b/api/controllers/inner_api/knowledge/retrieval.py index 9d83007aa3d727..dc48b3dee3497d 100644 --- a/api/controllers/inner_api/knowledge/retrieval.py +++ b/api/controllers/inner_api/knowledge/retrieval.py @@ -9,17 +9,15 @@ from flask_restx import Resource from pydantic import ValidationError -from sqlalchemy.orm import Session from controllers.common.schema import register_response_schema_models, register_schema_models -from controllers.console.app.wraps import with_session from controllers.inner_api import inner_api_ns from controllers.inner_api.wraps import plugin_inner_api_only from core.workflow.nodes.knowledge_retrieval import exc as retrieval_exc +from extensions.ext_application_services import application_services from libs.exception import BaseHTTPException from services.entities.knowledge_retrieval_inner import InnerKnowledgeRetrieveRequest, InnerKnowledgeRetrieveResponse from services.errors.knowledge_retrieval import ExternalKnowledgeRetrievalError, InnerKnowledgeRetrievalServiceError -from services.knowledge_retrieval_inner_service import InnerKnowledgeRetrievalService class InnerKnowledgeRetrievalHttpError(BaseHTTPException): @@ -71,8 +69,7 @@ class InnerKnowledgeRetrieveApi(Resource): 500: "Unexpected knowledge retrieval failure", } ) - @with_session - def post(self, session: Session) -> dict[str, object]: + def post(self) -> dict[str, object]: """Validate the payload, run retrieval, and return workflow-style sources.""" try: payload = InnerKnowledgeRetrieveRequest.model_validate(inner_api_ns.payload or {}) @@ -84,7 +81,7 @@ def post(self, session: Session) -> dict[str, object]: ) from exc try: - response = InnerKnowledgeRetrievalService().retrieve(payload, session=session) + response = application_services().knowledge.inner_retrieval.retrieve(payload) except InnerKnowledgeRetrievalServiceError as exc: raise InnerKnowledgeRetrievalHttpError( error_code=exc.error_code, diff --git a/api/extensions/application_services/knowledge.py b/api/extensions/application_services/knowledge.py index 91733434af4e53..e6562cbcaa4e1b 100644 --- a/api/extensions/application_services/knowledge.py +++ b/api/extensions/application_services/knowledge.py @@ -8,11 +8,13 @@ from configs import dify_config from core.app.apps.pipeline.pipeline_generator import PipelineGenerator from core.rag.extractor.entity.datasource_type import DatasourceType +from extensions.application_services.retrieval import build_dataset_retrieval from libs.helper import generate_text_hash from repositories.knowledge.dataset_api_key_repository import DatasetApiKeyRepository from repositories.knowledge.dataset_repository import SQLAlchemyDatasetRepository from repositories.knowledge.document_repository import SQLAlchemyDocumentRepository from repositories.knowledge.metadata_repository import SQLAlchemyMetadataRepository +from repositories.knowledge.retrieval_repository import KnowledgeRetrievalRepository from repositories.knowledge.segment_repository import SQLAlchemySegmentRepository from repositories.knowledge.upload_file_repository import SQLAlchemyKnowledgeUploadRepository from services.api_token_service import ApiTokenCache @@ -50,6 +52,7 @@ ) from services.knowledge.segments.application import DatasetSegmentApplicationService from services.knowledge.segments.indexing import SegmentIndexingGateway +from services.knowledge_retrieval_inner_service import InnerKnowledgeRetrievalService from services.tag_application_service import TagTargetQuery from tasks.batch_create_segment_to_index_task import batch_create_segment_to_index_task from tasks.delete_segment_from_index_task import delete_segment_from_index_task @@ -60,6 +63,7 @@ @dataclass(frozen=True, slots=True) class KnowledgeServices: + inner_retrieval: InnerKnowledgeRetrievalService metadata: MetadataService datasets: DatasetApplicationService external: ExternalKnowledgeApplicationService @@ -121,6 +125,9 @@ def build_knowledge_services( disable_task=disable_segments_from_index_task.delay, ) return KnowledgeServices( + inner_retrieval=InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(database_client), retrieval=build_dataset_retrieval(database_client) + ), metadata=MetadataService( store=SQLAlchemyMetadataRepository(session_factory=database_client), dataset_access=dataset_access ), diff --git a/api/extensions/ext_application_services.py b/api/extensions/ext_application_services.py index 457ed72afe19ff..8419c7cc6d9d24 100644 --- a/api/extensions/ext_application_services.py +++ b/api/extensions/ext_application_services.py @@ -36,7 +36,7 @@ build_knowledge_services, ) from extensions.application_services.trial_app import TrialAppServices, build_trial_app_services -from extensions.application_services.workflow import build_workflow_suggestions +from extensions.application_services.workflow import build_workflow_execution_dependencies, build_workflow_suggestions from extensions.application_services.workflow_variables import ( build_console_workflow_variables, build_workflow_variable_service, @@ -99,6 +99,7 @@ from services.agent.roster_package_exporter import RosterAgentPackageExporter from services.app.advanced_prompt_template_service import AdvancedPromptTemplateService from services.app.api_key_service import AppApiKeyService +from services.app.generation.runtime import AppGenerationRuntime from services.app.mcp_server_service import AppMCPServerService from services.app_audio_adapters import AppAudioRuntime from services.app_audio_service import AppAudio @@ -246,6 +247,7 @@ class AppScopedEndUserServices: @dataclass(frozen=True, slots=True) class ApplicationServices: + workflow_runtime: AppGenerationRuntime workflow_variables: WorkflowVariableService console_workflow_variables: ConsoleWorkflowVariableService workflow_suggestions: WorkflowInstructionSuggestions @@ -503,6 +505,7 @@ def build_application_services( ) workflow_variables = build_workflow_variable_service(database_client=database_client) return ApplicationServices( + workflow_runtime=build_workflow_execution_dependencies(database_client), workflow_variables=workflow_variables, console_workflow_variables=build_console_workflow_variables( database_client=database_client, variables=workflow_variables diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index 2f9d5c3677b5af..0012c791e7b29d 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -11,13 +11,13 @@ from core.rag.embedding.retrieval import RetrievalSegments from core.rag.index_processor.constant.query_type import QueryType from core.rag.models.document import Document -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.rag.retrieval.retrieval_methods import RetrievalMethod from graphon.model_runtime.entities import LLMMode from models import Account from models.dataset import Dataset, DatasetQuery from models.dataset import Document as DatasetDocument from models.enums import CreatorUserRole, DatasetQuerySource +from services.knowledge.retrieval.ports import DatasetRetrievalFactory logger = logging.getLogger(__name__) @@ -112,6 +112,7 @@ def retrieve( limit: int = 10, *, session: Session, + retrieval: DatasetRetrievalFactory, ): start = time.perf_counter() @@ -123,20 +124,19 @@ def retrieve( document_ids_filter = None metadata_filtering_conditions_raw = resolved_retrieval_model.get("metadata_filtering_conditions", {}) if metadata_filtering_conditions_raw and query: - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = retrieval() from core.rag.entities import MetadataFilteringCondition metadata_filtering_conditions = MetadataFilteringCondition.model_validate(metadata_filtering_conditions_raw) metadata_filter_document_ids, metadata_condition = dataset_retrieval.get_metadata_filter_condition( - session=session, dataset_ids=[dataset.id], query=query, metadata_filtering_mode="manual", metadata_filtering_conditions=metadata_filtering_conditions, inputs={}, - tenant_id="", + tenant_id=dataset.tenant_id, user_id="", metadata_model_config=ModelConfig(provider="", name="", mode=LLMMode.CHAT, completion_params={}), ) diff --git a/api/services/knowledge/segments/indexing.py b/api/services/knowledge/segments/indexing.py index 462e19f5ecec4c..2ea6b2be9eafb1 100644 --- a/api/services/knowledge/segments/indexing.py +++ b/api/services/knowledge/segments/indexing.py @@ -63,7 +63,7 @@ def count_tokens(self, dataset: SegmentDatasetRecord, text: str) -> int: @staticmethod def _vector(snapshot: SegmentIndexingSnapshot) -> Vector: - return Vector(snapshot.dataset, session=None, vector_type=snapshot.vector_type) + return Vector(snapshot.dataset, session=None, configuration=snapshot.vector_configuration) @staticmethod def _document(snapshot: SegmentIndexingSnapshot) -> Document: diff --git a/api/services/knowledge_retrieval_inner_service.py b/api/services/knowledge_retrieval_inner_service.py index c10c413d4db643..7a235eda3513fe 100644 --- a/api/services/knowledge_retrieval_inner_service.py +++ b/api/services/knowledge_retrieval_inner_service.py @@ -1,7 +1,7 @@ """Service wrapper for the inner knowledge retrieval API. This service keeps the internal HTTP contract small while reusing the workflow -retrieval stack in ``core.rag.retrieval.dataset_retrieval.DatasetRetrieval``. +retrieval stack in ``services.knowledge.retrieval.dataset_retrieval.DatasetRetrieval``. The only authorization enforced here is tenant ownership of the caller app and requested datasets. @@ -12,36 +12,28 @@ of a separate validation error. """ -from sqlalchemy import select -from sqlalchemy.orm import Session - from core.rag.entities.metadata_entities import Condition, MetadataFilteringCondition -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest from graphon.model_runtime.utils.encoders import jsonable_encoder from graphon.nodes.llm.entities import ModelConfig -from models.dataset import Dataset -from models.model import App from services.entities.knowledge_retrieval_inner import ( InnerKnowledgeRetrieveRequest, InnerKnowledgeRetrieveResponse, InnerKnowledgeRetrieveUsage, ) -from services.errors.knowledge_retrieval import ( - InnerKnowledgeRetrieveAppNotFoundError, - InnerKnowledgeRetrieveAppTenantMismatchError, - InnerKnowledgeRetrieveDatasetNotFoundError, - InnerKnowledgeRetrieveDatasetTenantMismatchError, -) +from services.knowledge.retrieval.ports import DatasetRetrievalFactory, RetrievalScopeQueries class InnerKnowledgeRetrievalService: """Validate inner caller scope and delegate to workflow dataset retrieval.""" + def __init__(self, *, scopes: RetrievalScopeQueries, retrieval: DatasetRetrievalFactory): + self._scopes = scopes + self._retrieval = retrieval + def retrieve( self, request: InnerKnowledgeRetrieveRequest, - session: Session, ) -> InnerKnowledgeRetrieveResponse: """Run tenant-scoped retrieval for a trusted internal caller. @@ -60,39 +52,17 @@ def retrieve( InnerKnowledgeRetrieveDatasetTenantMismatchError: At least one requested dataset is outside the caller tenant. """ - self._validate_caller_app(tenant_id=request.caller.tenant_id, app_id=request.caller.app_id, session=session) - self._validate_datasets(tenant_id=request.caller.tenant_id, dataset_ids=request.dataset_ids, session=session) + self._scopes.validate_scope( + tenant_id=request.caller.tenant_id, app_id=request.caller.app_id, dataset_ids=request.dataset_ids + ) - rag = DatasetRetrieval() - results = rag.knowledge_retrieval(session=session, request=self._to_rag_request(request)) + rag = self._retrieval() + results = rag.knowledge_retrieval(request=self._to_rag_request(request)) return InnerKnowledgeRetrieveResponse( results=results, usage=InnerKnowledgeRetrieveUsage.model_validate(jsonable_encoder(rag.llm_usage)), ) - def _validate_caller_app(self, *, tenant_id: str, app_id: str, session: Session) -> None: - app = session.scalar(select(App).where(App.id == app_id).limit(1)) - if app is None: - raise InnerKnowledgeRetrieveAppNotFoundError(f"App '{app_id}' not found") - if app.tenant_id != tenant_id: - raise InnerKnowledgeRetrieveAppTenantMismatchError( - f"App '{app_id}' does not belong to tenant '{tenant_id}'" - ) - - def _validate_datasets(self, *, tenant_id: str, dataset_ids: list[str], session: Session) -> None: - datasets = session.scalars(select(Dataset).where(Dataset.id.in_(dataset_ids))).all() - - found_ids = {dataset.id for dataset in datasets} - missing_ids = sorted(set(dataset_ids) - found_ids) - if missing_ids: - raise InnerKnowledgeRetrieveDatasetNotFoundError(f"Datasets not found: {', '.join(missing_ids)}") - - mismatched_ids = sorted(dataset.id for dataset in datasets if dataset.tenant_id != tenant_id) - if mismatched_ids: - raise InnerKnowledgeRetrieveDatasetTenantMismatchError( - f"Datasets do not belong to tenant '{tenant_id}': {', '.join(mismatched_ids)}" - ) - def _to_rag_request(self, request: InnerKnowledgeRetrieveRequest) -> KnowledgeRetrievalRequest: metadata_model_config = request.metadata_filtering.metadata_model_config metadata_conditions = request.metadata_filtering.conditions diff --git a/api/tasks/annotation/__init__.py b/api/tasks/annotation/__init__.py new file mode 100644 index 00000000000000..39cc12bf6cb876 --- /dev/null +++ b/api/tasks/annotation/__init__.py @@ -0,0 +1 @@ +"""Annotation indexing and import tasks.""" diff --git a/api/tasks/annotation/add_annotation_to_index_task.py b/api/tasks/annotation/add_annotation_to_index_task.py index 509b9a0355c234..ddf9e760dd078d 100644 --- a/api/tasks/annotation/add_annotation_to_index_task.py +++ b/api/tasks/annotation/add_annotation_to_index_task.py @@ -9,7 +9,7 @@ from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import Document from models.dataset import Dataset -from services.knowledge.dataset_service import DatasetCollectionBindingService +from repositories.knowledge.collection_binding_repository import DatasetCollectionBindingRepository logger = logging.getLogger(__name__) @@ -33,8 +33,10 @@ def add_annotation_to_index_task( try: with session_factory.create_session() as session: - dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type( - collection_binding_id, session, "annotation" + dataset_collection_binding = ( + DatasetCollectionBindingRepository.get_dataset_collection_binding_by_id_and_type( + collection_binding_id, session, "annotation" + ) ) dataset = Dataset( id=app_id, diff --git a/api/tasks/annotation/batch_import_annotations_task.py b/api/tasks/annotation/batch_import_annotations_task.py index 41656e1ab116d8..dbb410e5b14019 100644 --- a/api/tasks/annotation/batch_import_annotations_task.py +++ b/api/tasks/annotation/batch_import_annotations_task.py @@ -4,7 +4,6 @@ import click from celery import shared_task from sqlalchemy import select -from werkzeug.exceptions import NotFound from core.db.session_factory import session_factory from core.rag.datasource.vdb.vector_factory import Vector @@ -13,7 +12,8 @@ from extensions.ext_redis import redis_client from models.dataset import Dataset from models.model import App, AppAnnotationSetting, MessageAnnotation -from services.knowledge.dataset_service import DatasetCollectionBindingService +from repositories.knowledge.collection_binding_repository import DatasetCollectionBindingRepository +from services.annotation.errors import AnnotationResourceNotFoundError logger = logging.getLogger(__name__) @@ -62,12 +62,12 @@ def batch_import_annotations_task(job_id: str, content_list: list[dict], app_id: if app_annotation_setting: dataset_collection_binding = ( - DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type( + DatasetCollectionBindingRepository.get_dataset_collection_binding_by_id_and_type( app_annotation_setting.collection_binding_id, session, "annotation" ) ) if not dataset_collection_binding: - raise NotFound("App annotation setting not found") + raise AnnotationResourceNotFoundError("App annotation setting not found") dataset = Dataset( id=app_id, tenant_id=tenant_id, diff --git a/api/tasks/annotation/delete_annotation_index_task.py b/api/tasks/annotation/delete_annotation_index_task.py index f86d06e153fc7a..ada2ead3a043db 100644 --- a/api/tasks/annotation/delete_annotation_index_task.py +++ b/api/tasks/annotation/delete_annotation_index_task.py @@ -8,7 +8,7 @@ from core.rag.datasource.vdb.vector_factory import Vector from core.rag.index_processor.constant.index_type import IndexTechniqueType from models.dataset import Dataset -from services.knowledge.dataset_service import DatasetCollectionBindingService +from repositories.knowledge.collection_binding_repository import DatasetCollectionBindingRepository logger = logging.getLogger(__name__) @@ -22,8 +22,10 @@ def delete_annotation_index_task(annotation_id: str, app_id: str, tenant_id: str start_at = time.perf_counter() try: with session_factory.create_session() as session: - dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type( - collection_binding_id, session, "annotation" + dataset_collection_binding = ( + DatasetCollectionBindingRepository.get_dataset_collection_binding_by_id_and_type( + collection_binding_id, session, "annotation" + ) ) dataset = Dataset( diff --git a/api/tasks/annotation/enable_annotation_reply_task.py b/api/tasks/annotation/enable_annotation_reply_task.py index bf4410f10aae6a..d9802b7bae20fa 100644 --- a/api/tasks/annotation/enable_annotation_reply_task.py +++ b/api/tasks/annotation/enable_annotation_reply_task.py @@ -14,7 +14,7 @@ from models.dataset import Dataset from models.enums import CollectionBindingType from models.model import App, AppAnnotationSetting, MessageAnnotation -from services.knowledge.dataset_service import DatasetCollectionBindingService +from repositories.knowledge.collection_binding_repository import DatasetCollectionBindingRepository logger = logging.getLogger(__name__) @@ -50,7 +50,7 @@ def enable_annotation_reply_task( try: documents = [] - dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding( + dataset_collection_binding = DatasetCollectionBindingRepository.get_dataset_collection_binding( embedding_provider_name, embedding_model_name, session, CollectionBindingType.ANNOTATION ) annotation_setting = session.scalar( @@ -59,7 +59,7 @@ def enable_annotation_reply_task( if annotation_setting: if dataset_collection_binding.id != annotation_setting.collection_binding_id: old_dataset_collection_binding = ( - DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type( + DatasetCollectionBindingRepository.get_dataset_collection_binding_by_id_and_type( annotation_setting.collection_binding_id, session, CollectionBindingType.ANNOTATION ) ) diff --git a/api/tasks/annotation/update_annotation_to_index_task.py b/api/tasks/annotation/update_annotation_to_index_task.py index 611934c5cb1dfc..e53f657704c6cd 100644 --- a/api/tasks/annotation/update_annotation_to_index_task.py +++ b/api/tasks/annotation/update_annotation_to_index_task.py @@ -9,7 +9,7 @@ from core.rag.index_processor.constant.index_type import IndexTechniqueType from core.rag.models.document import Document from models.dataset import Dataset -from services.knowledge.dataset_service import DatasetCollectionBindingService +from repositories.knowledge.collection_binding_repository import DatasetCollectionBindingRepository logger = logging.getLogger(__name__) @@ -33,8 +33,10 @@ def update_annotation_to_index_task( try: with session_factory.create_session() as session: - dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding_by_id_and_type( - collection_binding_id, session, "annotation" + dataset_collection_binding = ( + DatasetCollectionBindingRepository.get_dataset_collection_binding_by_id_and_type( + collection_binding_id, session, "annotation" + ) ) dataset = Dataset( diff --git a/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py b/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py index 215e1e467047df..6fece8e109a2c3 100644 --- a/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py +++ b/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py @@ -6,8 +6,8 @@ from sqlalchemy.orm import Session from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest +from extensions.application_services.retrieval import build_dataset_retrieval from models.dataset import Dataset, Document from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus from tests.test_containers_integration_tests.helpers import accounts as account_fixtures @@ -16,7 +16,7 @@ class TestGetAvailableDatasetsIntegration: def test_returns_datasets_with_available_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -69,8 +69,8 @@ def test_returns_datasets_with_available_documents( db_session_with_containers.commit() # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [dataset.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 1 @@ -79,7 +79,7 @@ def test_returns_datasets_with_available_documents( assert result[0].name == dataset.name def test_filters_out_datasets_with_only_archived_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -126,14 +126,14 @@ def test_filters_out_datasets_with_only_archived_documents( db_session_with_containers.commit() # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [dataset.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_filters_out_datasets_with_only_disabled_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -180,14 +180,14 @@ def test_filters_out_datasets_with_only_disabled_documents( db_session_with_containers.commit() # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [dataset.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_filters_out_datasets_with_non_completed_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -234,14 +234,14 @@ def test_filters_out_datasets_with_non_completed_documents( db_session_with_containers.commit() # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [dataset.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_includes_external_datasets_without_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): """ Test that external datasets are returned even with no available documents. @@ -278,15 +278,17 @@ def test_includes_external_datasets_without_documents( db_session_with_containers.commit() # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [dataset.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 1 assert result[0].id == dataset.id assert result[0].provider == "external" - def test_filters_by_tenant_id(self, db_session_with_containers: Session, mock_external_service_dependencies): + def test_filters_by_tenant_id( + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + ): # Arrange fake = Faker() @@ -355,8 +357,8 @@ def test_filters_by_tenant_id(self, db_session_with_containers: Session, mock_ex db_session_with_containers.commit() # Act - request from tenant1, should only get tenant1's dataset - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant1.id, [dataset1.id, dataset2.id]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant1.id, [dataset1.id, dataset2.id]) # Assert assert len(result) == 1 @@ -364,7 +366,7 @@ def test_filters_by_tenant_id(self, db_session_with_containers: Session, mock_ex assert result[0].tenant_id == tenant1.id def test_returns_empty_list_when_no_datasets_found( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -382,14 +384,14 @@ def test_returns_empty_list_when_no_datasets_found( # Don't create any datasets # Act - dataset_retrieval = DatasetRetrieval() - result = dataset_retrieval._get_available_datasets(tenant.id, [str(uuid.uuid4())]) + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + result = dataset_retrieval._records.available_datasets(tenant.id, [str(uuid.uuid4())]) # Assert assert result == [] def test_returns_only_requested_dataset_ids( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -439,9 +441,9 @@ def test_returns_only_requested_dataset_ids( db_session_with_containers.commit() # Act - request only dataset 0 and 2, not dataset 1 - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() requested_ids = [datasets[0].id, datasets[2].id] - result = dataset_retrieval._get_available_datasets(tenant.id, requested_ids) + result = dataset_retrieval._records.available_datasets(tenant.id, requested_ids) # Assert assert len(result) == 2 @@ -451,7 +453,7 @@ def test_returns_only_requested_dataset_ids( class TestKnowledgeRetrievalIntegration: def test_knowledge_retrieval_with_available_datasets( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -507,7 +509,7 @@ def test_knowledge_retrieval_with_available_datasets( top_k=5, ) - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() # Mock rate limit check and retrieval with patch.object(dataset_retrieval, "_check_knowledge_rate_limit"): @@ -520,7 +522,7 @@ def test_knowledge_retrieval_with_available_datasets( assert isinstance(result, list) def test_knowledge_retrieval_no_available_datasets( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -558,7 +560,7 @@ def test_knowledge_retrieval_no_available_datasets( top_k=5, ) - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() # Mock rate limit check with patch.object(dataset_retrieval, "_check_knowledge_rate_limit"): @@ -569,7 +571,7 @@ def test_knowledge_retrieval_no_available_datasets( assert result == [] def test_knowledge_retrieval_rate_limit_exceeded( - self, db_session_with_containers: Session, mock_external_service_dependencies + self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory ): # Arrange fake = Faker() @@ -606,7 +608,7 @@ def test_knowledge_retrieval_rate_limit_exceeded( top_k=5, ) - dataset_retrieval = DatasetRetrieval() + dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() # Mock rate limit check to raise exception with patch.object( diff --git a/api/tests/test_containers_integration_tests/services/test_hit_testing_service.py b/api/tests/test_containers_integration_tests/services/test_hit_testing_service.py index 249a8f98cf8453..c58a01c6922c71 100644 --- a/api/tests/test_containers_integration_tests/services/test_hit_testing_service.py +++ b/api/tests/test_containers_integration_tests/services/test_hit_testing_service.py @@ -8,11 +8,12 @@ import pytest from pydantic import BaseModel, ConfigDict, TypeAdapter from sqlalchemy import func, select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from core.rag.embedding.retrieval import RetrievalSegments from core.rag.models.document import Document from core.rag.retrieval.retrieval_methods import RetrievalMethod +from extensions.application_services.retrieval import build_dataset_retrieval from models.dataset import Dataset, DatasetQuery, DocumentSegment from models.dataset import Document as DatasetDocument from models.enums import DataSourceType, DocumentCreatedFrom, SegmentStatus @@ -277,12 +278,13 @@ def test_retrieve_should_use_default_model_when_none_provided( response = _RetrieveResponse.model_validate( HitTestingService.retrieve( + session=db_session_with_containers, + retrieval=build_dataset_retrieval(sessionmaker(bind=db_session_with_containers.get_bind())), dataset=dataset, query="test query", account=account, retrieval_model=None, external_retrieval_model=external_retrieval_model, - session=db_session_with_containers, ) ) @@ -295,7 +297,7 @@ def test_retrieve_should_use_default_model_when_none_provided( assert after_count == before_count + 1 @patch("core.rag.datasource.retrieval_service.RetrievalService.retrieve") - @patch("core.rag.retrieval.dataset_retrieval.DatasetRetrieval.get_metadata_filter_condition") + @patch("services.knowledge.retrieval.dataset_retrieval.DatasetRetrieval.get_metadata_filter_condition") def test_retrieve_should_handle_metadata_filtering( self, mock_get_meta: MagicMock, mock_retrieve: MagicMock, db_session_with_containers: Session ) -> None: @@ -320,12 +322,13 @@ def test_retrieve_should_handle_metadata_filtering( mock_retrieve.return_value = retrieved_documents HitTestingService.retrieve( + session=db_session_with_containers, + retrieval=build_dataset_retrieval(sessionmaker(bind=db_session_with_containers.get_bind())), dataset=dataset, query="test query", account=account, retrieval_model=retrieval_model, external_retrieval_model=external_retrieval_model, - session=db_session_with_containers, ) mock_get_meta.assert_called_once() @@ -333,7 +336,7 @@ def test_retrieve_should_handle_metadata_filtering( assert mock_retrieve.call_args.kwargs["document_ids_filter"] == ["doc_id1"] @patch("core.rag.datasource.retrieval_service.RetrievalService.retrieve") - @patch("core.rag.retrieval.dataset_retrieval.DatasetRetrieval.get_metadata_filter_condition") + @patch("services.knowledge.retrieval.dataset_retrieval.DatasetRetrieval.get_metadata_filter_condition") def test_retrieve_should_return_empty_if_metadata_filtering_fails( self, mock_get_meta: MagicMock, mock_retrieve: MagicMock, db_session_with_containers: Session ) -> None: @@ -357,12 +360,13 @@ def test_retrieve_should_return_empty_if_metadata_filtering_fails( response = _RetrieveResponse.model_validate( HitTestingService.retrieve( + session=db_session_with_containers, + retrieval=build_dataset_retrieval(sessionmaker(bind=db_session_with_containers.get_bind())), dataset=dataset, query="test query", account=account, retrieval_model=retrieval_model, external_retrieval_model=external_retrieval_model, - session=db_session_with_containers, ) ) @@ -389,13 +393,14 @@ def test_retrieve_should_handle_attachments( mock_retrieve.return_value = retrieved_documents HitTestingService.retrieve( + session=db_session_with_containers, + retrieval=build_dataset_retrieval(sessionmaker(bind=db_session_with_containers.get_bind())), dataset=dataset, query="test query", account=account, retrieval_model=retrieval_model, external_retrieval_model=external_retrieval_model, attachment_ids=attachment_ids, - session=db_session_with_containers, ) mock_retrieve.assert_called_once_with( @@ -449,12 +454,13 @@ def test_retrieve_should_handle_reranking_and_threshold( mock_retrieve.return_value = retrieved_documents HitTestingService.retrieve( + session=db_session_with_containers, + retrieval=build_dataset_retrieval(sessionmaker(bind=db_session_with_containers.get_bind())), dataset=dataset, query="test query", account=account, retrieval_model=retrieval_model, external_retrieval_model=external_retrieval_model, - session=db_session_with_containers, ) mock_retrieve.assert_called_once() diff --git a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py index 8054d21b633559..15f5f51d656ab6 100644 --- a/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py +++ b/api/tests/unit_tests/controllers/console/datasets/test_hit_testing_base.py @@ -1,9 +1,9 @@ from unittest.mock import Mock, patch import pytest -from werkzeug.exceptions import Forbidden, InternalServerError, NotFound import services.errors.base +from controllers.common.errors import AccessDeniedError, InternalServerError, NotFoundError from controllers.console.app.error import ( CompletionRequestError, ProviderModelCurrentlyNotSupportError, @@ -26,6 +26,8 @@ from services.hit_testing_service import HitTestingService from services.knowledge.dataset_service import DatasetService +pytestmark = pytest.mark.usefixtures("workflow_application") + @pytest.fixture def account(): @@ -104,7 +106,7 @@ def test_dataset_not_found(self, account): "get_dataset", return_value=None, ): - with pytest.raises(NotFound, match="Dataset not found"): + with pytest.raises(NotFoundError, match="Dataset not found"): DatasetsHitTestingBase.get_and_validate_dataset(Mock(), "dataset-1", account, "tenant-1") def test_permission_denied(self, dataset, account): @@ -120,7 +122,7 @@ def test_permission_denied(self, dataset, account): side_effect=services.errors.base.NoPermissionError("no access"), ), ): - with pytest.raises(Forbidden, match="no access"): + with pytest.raises(AccessDeniedError, match="no access"): DatasetsHitTestingBase.get_and_validate_dataset(Mock(), "dataset-1", account, "tenant-1") diff --git a/api/tests/unit_tests/core/callback_handler/test_index_tool_callback_handler.py b/api/tests/unit_tests/core/callback_handler/test_index_tool_callback_handler.py index 2808da27236ab4..78939cc7e56495 100644 --- a/api/tests/unit_tests/core/callback_handler/test_index_tool_callback_handler.py +++ b/api/tests/unit_tests/core/callback_handler/test_index_tool_callback_handler.py @@ -1,38 +1,7 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from uuid import uuid4 - import pytest from pytest_mock import MockerFixture -from sqlalchemy import select -from sqlalchemy.engine import Engine -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session, SessionTransaction - -import core.callback_handler.index_tool_callback_handler as callback_module -from core.app.entities.app_invoke_entities import InvokeFrom -from core.callback_handler.index_tool_callback_handler import DatasetIndexToolCallbackHandler -from core.rag.index_processor.constant.index_type import IndexStructureType -from core.rag.models.document import Document -from models.dataset import ChildChunk, DatasetQuery, DocumentSegment -from models.dataset import Document as DatasetDocument -from models.enums import CreatorUserRole, DatasetQuerySource, DataSourceType, DocumentCreatedFrom - - -class _DatabaseBinding: - engine: Engine - - def __init__(self, engine: Engine) -> None: - self.engine = engine - -@dataclass(frozen=True) -class _CallerSessionBoundary: - """Caller session state that callback-owned transactions must not disturb.""" - - session: Session - transaction: SessionTransaction - pending_query: DatasetQuery +from services.knowledge.retrieval.adapters.resource_events import DatasetIndexToolCallbackHandler @pytest.fixture @@ -41,232 +10,13 @@ def mock_queue_manager(mocker: MockerFixture): @pytest.fixture -def handler(mock_queue_manager, sqlite_engine: Engine, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(callback_module, "db", _DatabaseBinding(sqlite_engine)) - return DatasetIndexToolCallbackHandler( - queue_manager=mock_queue_manager, - app_id=str(uuid4()), - message_id=str(uuid4()), - user_id=str(uuid4()), - invoke_from=InvokeFrom.DEBUGGER, - ) - - -@pytest.fixture -def caller_session_boundary(sqlite_engine: Engine) -> Iterator[_CallerSessionBoundary]: - """Keep an unflushed caller transaction open to prove callback isolation.""" - - with Session(sqlite_engine, expire_on_commit=False) as session: - pending_query = DatasetQuery( - dataset_id=str(uuid4()), - content="caller-owned pending query", - source=DatasetQuerySource.APP, - source_app_id=str(uuid4()), - created_by_role=CreatorUserRole.ACCOUNT, - created_by=str(uuid4()), - ) - session.add(pending_query) - transaction = session.get_transaction() - assert transaction is not None - - yield _CallerSessionBoundary( - session=session, - transaction=transaction, - pending_query=pending_query, - ) - - assert session.get_transaction() is transaction - assert list(session.new) == [pending_query] - assert not session.dirty - assert not session.deleted - - with Session(sqlite_engine) as observer_session: - assert observer_session.get(DatasetQuery, pending_query.id) is None - - -def _dataset_document(*, doc_form: IndexStructureType) -> DatasetDocument: - return DatasetDocument( - tenant_id=str(uuid4()), - dataset_id=str(uuid4()), - position=1, - data_source_type=DataSourceType.UPLOAD_FILE, - batch="batch", - name="Document", - created_from=DocumentCreatedFrom.API, - created_by=str(uuid4()), - doc_form=doc_form, - ) - - -def _segment(document: DatasetDocument, *, index_node_id: str) -> DocumentSegment: - return DocumentSegment( - tenant_id=document.tenant_id, - dataset_id=document.dataset_id, - document_id=document.id, - position=1, - content="content", - word_count=1, - tokens=1, - created_by=document.created_by, - index_node_id=index_node_id, - hit_count=0, - ) - - -class TestOnQuery: - @pytest.mark.parametrize( - ("invoke_from", "expected_role"), - [ - (InvokeFrom.EXPLORE, "account"), - (InvokeFrom.DEBUGGER, "account"), - (InvokeFrom.WEB_APP, "end_user"), - ], - ) - def test_on_query_success_roles( - self, - monkeypatch: pytest.MonkeyPatch, - sqlite_engine: Engine, - sqlite_session: Session, - caller_session_boundary: _CallerSessionBoundary, - mock_queue_manager, - invoke_from: InvokeFrom, - expected_role: str, - ) -> None: - monkeypatch.setattr(callback_module, "db", _DatabaseBinding(sqlite_engine)) - handler = DatasetIndexToolCallbackHandler( - queue_manager=mock_queue_manager, - app_id=str(uuid4()), - message_id=str(uuid4()), - user_id=str(uuid4()), - invoke_from=invoke_from, - ) - - handler.on_query("test query", str(uuid4()), caller_session_boundary.session) - - dataset_query = sqlite_session.scalar(select(DatasetQuery)) - assert dataset_query is not None - assert dataset_query.created_by_role == expected_role - - def test_on_query_none_values_roll_back_independent_transaction( - self, - monkeypatch: pytest.MonkeyPatch, - sqlite_engine: Engine, - sqlite_session: Session, - caller_session_boundary: _CallerSessionBoundary, - mock_queue_manager, - ) -> None: - monkeypatch.setattr(callback_module, "db", _DatabaseBinding(sqlite_engine)) - handler = DatasetIndexToolCallbackHandler( - queue_manager=mock_queue_manager, - app_id=None, - message_id=None, - user_id=None, - invoke_from=None, - ) - - with pytest.raises(IntegrityError): - handler.on_query(None, None, caller_session_boundary.session) # type: ignore[arg-type] - - assert sqlite_session.scalar(select(DatasetQuery)) is None - - -class TestOnToolEnd: - def test_on_tool_end_no_metadata( - self, - handler: DatasetIndexToolCallbackHandler, - caller_session_boundary: _CallerSessionBoundary, - ) -> None: - document = Document.model_construct(page_content="content", metadata=None, provider="dify") - - handler.on_tool_end([document], caller_session_boundary.session) - - def test_on_tool_end_dataset_document_not_found( - self, - handler: DatasetIndexToolCallbackHandler, - sqlite_session: Session, - caller_session_boundary: _CallerSessionBoundary, - ) -> None: - document = Document( - page_content="content", - metadata={"document_id": str(uuid4()), "doc_id": "node-1"}, - ) - - handler.on_tool_end([document], caller_session_boundary.session) - - assert sqlite_session.scalar(select(DatasetDocument)) is None - - def test_on_tool_end_parent_child_index_with_child( - self, - handler: DatasetIndexToolCallbackHandler, - sqlite_session: Session, - caller_session_boundary: _CallerSessionBoundary, - ) -> None: - dataset_document = _dataset_document(doc_form=IndexStructureType.PARENT_CHILD_INDEX) - sqlite_session.add(dataset_document) - sqlite_session.flush() - segment = _segment(dataset_document, index_node_id="parent-node") - sqlite_session.add(segment) - sqlite_session.flush() - child = ChildChunk( - tenant_id=dataset_document.tenant_id, - dataset_id=dataset_document.dataset_id, - document_id=dataset_document.id, - segment_id=segment.id, - position=1, - content="child", - word_count=1, - created_by=dataset_document.created_by, - index_node_id="child-node", - ) - sqlite_session.add(child) - sqlite_session.commit() - document = Document( - page_content="content", - metadata={"document_id": dataset_document.id, "doc_id": child.index_node_id}, - ) - - handler.on_tool_end([document], caller_session_boundary.session) - - sqlite_session.expire_all() - assert sqlite_session.get(DocumentSegment, segment.id).hit_count == 1 # type: ignore[union-attr] - - def test_on_tool_end_non_parent_child_index( - self, - handler: DatasetIndexToolCallbackHandler, - sqlite_session: Session, - caller_session_boundary: _CallerSessionBoundary, - ) -> None: - dataset_document = _dataset_document(doc_form=IndexStructureType.PARAGRAPH_INDEX) - sqlite_session.add(dataset_document) - sqlite_session.flush() - segment = _segment(dataset_document, index_node_id="node-1") - sqlite_session.add(segment) - sqlite_session.commit() - document = Document( - page_content="content", - metadata={ - "document_id": dataset_document.id, - "doc_id": segment.index_node_id, - "dataset_id": dataset_document.dataset_id, - }, - ) - - handler.on_tool_end([document], caller_session_boundary.session) - - sqlite_session.expire_all() - assert sqlite_session.get(DocumentSegment, segment.id).hit_count == 1 # type: ignore[union-attr] - - def test_on_tool_end_empty_documents( - self, - handler: DatasetIndexToolCallbackHandler, - caller_session_boundary: _CallerSessionBoundary, - ) -> None: - handler.on_tool_end([], caller_session_boundary.session) +def handler(mock_queue_manager): + return DatasetIndexToolCallbackHandler(mock_queue_manager) class TestReturnRetrieverResourceInfo: def test_publish_called(self, handler: DatasetIndexToolCallbackHandler, mock_queue_manager, mocker: MockerFixture): - mock_event = mocker.patch("core.callback_handler.index_tool_callback_handler.QueueRetrieverResourcesEvent") + mock_event = mocker.patch("services.knowledge.retrieval.adapters.resource_events.QueueRetrieverResourcesEvent") resources = [mocker.Mock()] handler.return_retriever_resource_info(resources) diff --git a/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py b/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py index bdd418e47e4e23..1315131e01f623 100644 --- a/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py +++ b/api/tests/unit_tests/extensions/otel/test_retrieval_tracing.py @@ -8,8 +8,8 @@ from sqlalchemy.orm import Session from core.rag.rerank.rerank_type import RerankMode -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest +from extensions.application_services.retrieval import build_dataset_retrieval from models.dataset import Dataset @@ -19,9 +19,7 @@ def _otel_enabled(config_overrides: Callable[..., None]) -> None: def test_knowledge_retrieval_creates_a_child_otel_span( - memory_span_exporter, - tracer_provider_with_memory_exporter, - sqlite_session: Session, + memory_span_exporter, tracer_provider_with_memory_exporter, sqlite_session: Session, sqlite_session_factory ) -> None: """The retrieval entry point must be visible beneath its workflow node span.""" request = KnowledgeRetrievalRequest( @@ -33,19 +31,19 @@ def test_knowledge_retrieval_creates_a_child_otel_span( retrieval_mode="multiple", query="test query", ) - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() with ( patch.object(retrieval, "_check_knowledge_rate_limit"), - patch.object(retrieval, "_get_available_datasets", return_value=[]), + patch.object(retrieval._records, "available_datasets", return_value=[]), get_tracer(__name__).start_as_current_span("knowledge-retrieval-node") as node_span, ): - assert retrieval.knowledge_retrieval(sqlite_session, request) == [] + assert retrieval.knowledge_retrieval(request) == [] retrieval_span = next( span for span in memory_span_exporter.get_finished_spans() - if span.name == "core.rag.retrieval.dataset_retrieval.DatasetRetrieval.knowledge_retrieval" + if span.name == "services.knowledge.retrieval.dataset_retrieval.DatasetRetrieval.knowledge_retrieval" ) node_span_context = node_span.get_span_context() assert retrieval_span.context.trace_id == node_span_context.trace_id @@ -54,11 +52,10 @@ def test_knowledge_retrieval_creates_a_child_otel_span( def test_multiple_retrieve_preserves_otel_context_in_dataset_thread( - app, - tracer_provider_with_memory_exporter, + app, tracer_provider_with_memory_exporter, sqlite_session_factory ) -> None: """Per-dataset retrieval spans must remain in the workflow node trace.""" - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() dataset = Dataset( id=str(uuid4()), indexing_technique="high_quality", @@ -93,11 +90,9 @@ def record_active_trace(**_kwargs: object) -> None: def test_retriever_thread_exception_sets_error_span_and_is_collected( - app, - memory_span_exporter, - tracer_provider_with_memory_exporter, + app, memory_span_exporter, tracer_provider_with_memory_exporter, sqlite_session_factory ) -> None: - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() cancel_event = threading.Event() thread_exceptions: list[Exception] = [] expected_error = RuntimeError("retrieval failed") @@ -106,7 +101,7 @@ def test_retriever_thread_exception_sets_error_span_and_is_collected( patch.object(retrieval, "_retriever", side_effect=expected_error), ): retrieval._run_retriever_thread_safely( - flask_app=app, + tenant_id="tenant-1", dataset_id=str(uuid4()), query="test query", top_k=4, @@ -129,11 +124,9 @@ def test_retriever_thread_exception_sets_error_span_and_is_collected( def test_retriever_thread_exception_emits_skip_event_when_requested( - app, - memory_span_exporter, - tracer_provider_with_memory_exporter, + app, memory_span_exporter, tracer_provider_with_memory_exporter, sqlite_session_factory ) -> None: - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() cancel_event = threading.Event() thread_exceptions: list[Exception] = [] expected_error = RuntimeError("retrieval failed") @@ -144,7 +137,7 @@ def test_retriever_thread_exception_emits_skip_event_when_requested( get_tracer(__name__).start_as_current_span("dataset-retrieval-parent") as parent_span, ): retrieval._run_retriever_thread_safely( - flask_app=app, + tenant_id="tenant-1", dataset_id=dataset_id, query="test query", top_k=4, diff --git a/api/tests/unit_tests/services/annotation/test_reply_service.py b/api/tests/unit_tests/services/annotation/test_reply_service.py index 7a272c5fbb86af..0ea158ca524e5b 100644 --- a/api/tests/unit_tests/services/annotation/test_reply_service.py +++ b/api/tests/unit_tests/services/annotation/test_reply_service.py @@ -1,8 +1,11 @@ +from dataclasses import replace + import pytest from sqlalchemy import create_engine, event, select from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import QueuePool +from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueStopEvent from core.rag.models.document import Document from models.base import TypeBase from models.dataset import DatasetCollectionBinding @@ -172,3 +175,25 @@ def fail(_conn, _cursor, statement, _params, _context, _many): with annotation_store() as session: assert session.get(MessageAnnotation, "annotation-1").hit_count == 0 assert session.scalar(select(AppAnnotationHitHistory)) is None + + +def test_chatflow_runner_search_and_publish_are_outside_transaction(build_runner, annotation_store, monkeypatch): + runner = build_runner + runner._app.id = runner.application_generate_entity.app_config.app_id = "app-1" + runner._app.tenant_id = runner.application_generate_entity.app_config.tenant_id = "tenant-1" + runner.message.id = "msg-1" + runner.application_generate_entity.query = "hi" + runner._runtime = replace(runner._runtime, annotation_replies=install_vector(monkeypatch, annotation_store)) + monkeypatch.setattr(runner, "handle_input_moderation", lambda **_kwargs: (False, {}, "hi")) + events = [] + + def publish(item): + assert annotation_store.kw["bind"].pool.checkedout() == 0 + with annotation_store() as session: + assert session.scalar(select(AppAnnotationHitHistory)) is not None + events.append(item) + + monkeypatch.setattr(runner._events, "_publish_event", publish) + runner.run() + assert isinstance(events[0], QueueAnnotationReplyEvent) + assert isinstance(events[-1], QueueStopEvent) diff --git a/api/tests/unit_tests/services/hit_service.py b/api/tests/unit_tests/services/hit_service.py index c576d6fa646256..adb62a3a6d6449 100644 --- a/api/tests/unit_tests/services/hit_service.py +++ b/api/tests/unit_tests/services/hit_service.py @@ -12,10 +12,11 @@ import pytest from sqlalchemy import select -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from core.rag.models.document import Document from core.rag.retrieval.retrieval_methods import RetrievalMethod +from extensions.application_services.retrieval import build_dataset_retrieval from models import Account, Tenant from models.dataset import Dataset, DatasetQuery from services.hit_testing_service import HitTestingService @@ -166,7 +167,13 @@ def test_retrieve_success_with_default_retrieval_model(self, sqlite_session: Ses # Act result = HitTestingService.retrieve( - dataset, query, account, retrieval_model, external_retrieval_model, session=sqlite_session + dataset, + query, + account, + retrieval_model, + external_retrieval_model, + session=sqlite_session, + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), ) # Assert @@ -217,7 +224,13 @@ def test_retrieve_success_with_custom_retrieval_model(self, sqlite_session: Sess # Act result = HitTestingService.retrieve( - dataset, query, account, retrieval_model, external_retrieval_model, session=sqlite_session + dataset, + query, + account, + retrieval_model, + external_retrieval_model, + session=sqlite_session, + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), ) # Assert @@ -265,17 +278,21 @@ def test_retrieve_with_metadata_filtering(self, sqlite_session: Session): patch( "services.hit_testing_service.RetrievalService.format_retrieval_documents", autospec=True ) as mock_format, - patch("services.hit_testing_service.DatasetRetrieval", autospec=True) as mock_dataset_retrieval_class, patch("services.hit_testing_service.time.perf_counter", autospec=True) as mock_perf_counter, ): mock_perf_counter.side_effect = [0.0, 0.1] - mock_dataset_retrieval_class.return_value = mock_dataset_retrieval mock_retrieve.return_value = documents mock_format.return_value = mock_records # Act result = HitTestingService.retrieve( - dataset, query, account, retrieval_model, external_retrieval_model, session=sqlite_session + dataset, + query, + account, + retrieval_model, + external_retrieval_model, + session=sqlite_session, + retrieval=lambda: mock_dataset_retrieval, ) # Assert @@ -310,17 +327,21 @@ def test_retrieve_with_metadata_filtering_no_documents(self, sqlite_session: Ses mock_dataset_retrieval.get_metadata_filter_condition.return_value = ({}, True) with ( - patch("services.hit_testing_service.DatasetRetrieval", autospec=True) as mock_dataset_retrieval_class, patch( "services.hit_testing_service.RetrievalService.format_retrieval_documents", autospec=True ) as mock_format, ): - mock_dataset_retrieval_class.return_value = mock_dataset_retrieval mock_format.return_value = [] # Act result = HitTestingService.retrieve( - dataset, query, account, retrieval_model, external_retrieval_model, session=sqlite_session + dataset, + query, + account, + retrieval_model, + external_retrieval_model, + session=sqlite_session, + retrieval=lambda: mock_dataset_retrieval, ) # Assert @@ -363,7 +384,13 @@ def test_retrieve_with_dataset_retrieval_model(self, sqlite_session: Session): # Act result = HitTestingService.retrieve( - dataset, query, account, retrieval_model, external_retrieval_model, session=sqlite_session + dataset, + query, + account, + retrieval_model, + external_retrieval_model, + session=sqlite_session, + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), ) # Assert @@ -450,7 +477,12 @@ def test_external_retrieve_non_external_provider(self, sqlite_session: Session): # Act result = HitTestingService.external_retrieve( - dataset, query, account, external_retrieval_model, metadata_filtering_conditions, session=sqlite_session + dataset, + query, + account, + external_retrieval_model, + metadata_filtering_conditions, + session=sqlite_session, ) # Assert diff --git a/api/tests/unit_tests/services/knowledge/test_indexing_execution_adapters.py b/api/tests/unit_tests/services/knowledge/test_indexing_execution_adapters.py index 45ec28a4ec812b..de0ed1ad3b1169 100644 --- a/api/tests/unit_tests/services/knowledge/test_indexing_execution_adapters.py +++ b/api/tests/unit_tests/services/knowledge/test_indexing_execution_adapters.py @@ -22,6 +22,7 @@ from models.dataset import Dataset, DatasetKeywordTable, DatasetProcessRule, Document, DocumentSegment from models.enums import CreatorUserRole, IndexingStatus, ProcessRuleMode, SegmentStatus from models.model import UploadFile +from models.vector import VectorConfiguration from repositories.knowledge.document_repository import SQLAlchemyDocumentRepository from repositories.knowledge.segment_repository import SQLAlchemySegmentRepository from repositories.knowledge.upload_file_repository import SQLAlchemyKnowledgeUploadRepository @@ -46,7 +47,8 @@ @pytest.fixture -def backend(sqlite_session_factory: sessionmaker[Session]) -> Iterator[BackendFixture]: +def backend(sqlite_session_factory: sessionmaker[Session], monkeypatch: pytest.MonkeyPatch) -> Iterator[BackendFixture]: + apply_config_overrides(monkeypatch, VECTOR_STORE="qdrant", VECTOR_STORE_WHITELIST_ENABLE=False) with sqlite_session_factory.begin() as session: session.add_all( [_dataset("dataset-1", "workspace-1"), _document("document-1", status=IndexingStatus.SPLITTING)] @@ -344,11 +346,11 @@ def test_hash_groups_load_worker_owned_inputs_before_completion( calls: list[tuple[Dataset, list[str]]] = [] main_thread = threading.get_ident() - def vector(dataset: Dataset, *, session: Session | None, vector_type: str) -> MagicMock: + def vector(dataset: Dataset, *, session: Session | None, configuration: VectorConfiguration) -> MagicMock: assert threading.get_ident() != main_thread assert inspect(dataset).detached assert session is None - assert vector_type == "qdrant" + assert configuration.vector_type == "qdrant" def create(group: list[IndexDocument]) -> None: with sqlite_session_factory() as reader: @@ -365,8 +367,10 @@ def create(group: list[IndexDocument]) -> None: backend.create.side_effect = create return backend - with Flask(__name__).app_context(), patch(f"{MODULE}.Vector", side_effect=vector) as factory: - factory.resolve_vector_type.return_value = "qdrant" + with ( + Flask(__name__).app_context(), + patch(f"{MODULE}.Vector", side_effect=vector), + ): adapter.load(job, chunks) assert sum(group.count("same") for _, group in calls) == 2 assert sum("same" in group for _, group in calls) == 1 diff --git a/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py b/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py index 23ce2b19dd3b57..fc1e880d3afd2d 100644 --- a/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py +++ b/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py @@ -1,3 +1,6 @@ +from extensions.application_services.retrieval import build_dataset_retrieval +from repositories.knowledge import retrieval_repository as retrieval_repository_module + """Published applications sign admin-uploaded images only through valid knowledge bindings.""" from datetime import UTC, datetime @@ -12,14 +15,13 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom from core.app.file_access import FileAccessScope, bind_file_access_scope, get_current_file_access_scope from core.rag.models.document import Document as RagDocument -from core.rag.retrieval import dataset_retrieval as retrieval_module -from core.rag.retrieval.dataset_retrieval import DatasetRetrieval from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest from extensions.storage.storage_type import StorageType from models.dataset import Dataset, Document, DocumentSegment, SegmentAttachmentBinding from models.enums import CreatorUserRole, DataSourceType, DocumentCreatedFrom, SegmentStatus from models.model import UploadFile -from services.knowledge.retrieval.attachments import authorize_retrieved_segment +from repositories.knowledge.dataset_read_repository import authorize_retrieved_segment +from services.knowledge.retrieval import dataset_retrieval as retrieval_module @pytest.fixture @@ -90,15 +92,15 @@ def graph(sqlite_session: Session) -> tuple[Dataset, Document, DocumentSegment, @pytest.mark.parametrize("vision_enabled", [False, True]) def test_published_retrieval_grants_bound_images_before_signing( graph: tuple[Dataset, Document, DocumentSegment, dict[str, str]], - sqlite_session: Session, monkeypatch: pytest.MonkeyPatch, entry: str, vision_enabled: bool, + sqlite_session_factory, ) -> None: dataset, document, segment, file_ids = graph - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() monkeypatch.setattr(retrieval, "_check_knowledge_rate_limit", MagicMock()) - monkeypatch.setattr(retrieval, "_get_available_datasets", MagicMock(return_value=[dataset])) + monkeypatch.setattr(retrieval._records, "available_datasets", MagicMock(return_value=[dataset])) monkeypatch.setattr(retrieval, "get_metadata_filter_condition", MagicMock(return_value=(None, None))) monkeypatch.setattr( retrieval, @@ -138,7 +140,6 @@ def test_published_retrieval_grants_bound_images_before_signing( with bind_file_access_scope(scope): if entry == "chat": content, files = retrieval.retrieve( - sqlite_session, app_id=str(uuid4()), user_id=scope.user_id, tenant_id=dataset.tenant_id, @@ -158,7 +159,6 @@ def test_published_retrieval_grants_bound_images_before_signing( assert [file.storage_key for file in files] == (["valid.png"] if vision_enabled else []) else: sources = retrieval.knowledge_retrieval( - sqlite_session, KnowledgeRetrievalRequest( tenant_id=dataset.tenant_id, user_id=scope.user_id, @@ -224,14 +224,15 @@ def test_workflow_retrieval_releases_owned_connections_and_preserves_caller_tran monkeypatch: pytest.MonkeyPatch, with_image: bool, signing_fails: bool, + sqlite_session_factory, ) -> None: dataset, document, segment, _ = graph if not with_image: segment.content = "Plain text" sqlite_session.commit() - retrieval = DatasetRetrieval() + retrieval = build_dataset_retrieval(sqlite_session_factory)() monkeypatch.setattr(retrieval, "_check_knowledge_rate_limit", MagicMock()) - monkeypatch.setattr(retrieval, "_get_available_datasets", MagicMock(return_value=[dataset])) + monkeypatch.setattr(retrieval._records, "available_datasets", MagicMock(return_value=[dataset])) monkeypatch.setattr( retrieval, "multiple_retrieve", @@ -247,7 +248,7 @@ def test_workflow_retrieval_releases_owned_connections_and_preserves_caller_tran ) if signing_fails: monkeypatch.setattr( - retrieval_module, "sign_segment_content", MagicMock(side_effect=RuntimeError("signing failed")) + retrieval_repository_module, "sign_segment_content", MagicMock(side_effect=RuntimeError("signing failed")) ) request = KnowledgeRetrievalRequest( tenant_id=dataset.tenant_id, @@ -286,9 +287,9 @@ def track_checkin(*_args: object) -> None: with bind_file_access_scope(scope), Session(engine) as caller_session, caller_session.begin() as caller_txn: if signing_fails: with pytest.raises(RuntimeError, match="signing failed"): - retrieval.knowledge_retrieval(caller_session, request) + retrieval.knowledge_retrieval(request) else: - sources = retrieval.knowledge_retrieval(caller_session, request) + sources = retrieval.knowledge_retrieval(request) assert len(sources) == 1 assert sources[0].metadata.segment_id == segment.id assert caller_session.get_transaction() is caller_txn diff --git a/api/tests/unit_tests/services/knowledge/test_segment_adapters.py b/api/tests/unit_tests/services/knowledge/test_segment_adapters.py index 8f15766dc6fc09..04f166d9a11779 100644 --- a/api/tests/unit_tests/services/knowledge/test_segment_adapters.py +++ b/api/tests/unit_tests/services/knowledge/test_segment_adapters.py @@ -30,6 +30,7 @@ from models.dataset import ChildChunk, Dataset, Document, DocumentSegment, SegmentAttachmentBinding from models.enums import CreatorUserRole, DocumentCreatedFrom, SegmentStatus, SummaryStatus from models.model import UploadFile +from models.vector import VectorConfiguration from repositories.knowledge.dataset_repository import SQLAlchemyDatasetRepository from repositories.knowledge.segment_repository import SQLAlchemySegmentRepository from repositories.knowledge.upload_file_repository import SQLAlchemyKnowledgeUploadRepository @@ -508,10 +509,10 @@ def indexing_probe( event.listen(engine, "rollback", finish) probe = VectorProbe(active) - def vector(dataset: Dataset, *, session: Session | None, vector_type: str) -> VectorProbe: + def vector(dataset: Dataset, *, session: Session | None, configuration: VectorConfiguration) -> VectorProbe: assert dataset.tenant_id == "workspace-1" assert session is None - assert vector_type == "qdrant" + assert configuration.vector_type == "qdrant" assert not active return probe diff --git a/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py b/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py index 66a289d24faa7a..adfaf2c3b02244 100644 --- a/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py +++ b/api/tests/unit_tests/services/test_knowledge_retrieval_inner_service.py @@ -1,6 +1,11 @@ +from sqlalchemy.orm import sessionmaker + +from extensions.application_services.retrieval import build_dataset_retrieval +from repositories.knowledge.retrieval_repository import KnowledgeRetrievalRepository + """Unit tests for the inner knowledge retrieval service.""" -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from sqlalchemy.orm import Session @@ -113,10 +118,8 @@ def _build_source() -> Source: class TestInnerKnowledgeRetrievalService: @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) - @patch("services.knowledge_retrieval_inner_service.DatasetRetrieval") def test_retrieve_maps_multiple_request_and_skips_enable_api_check( self, - mock_rag_cls, sqlite_session: Session, ): request = _build_request() @@ -145,9 +148,10 @@ def test_retrieve_maps_multiple_request_and_skips_enable_api_check( "time_to_first_token": None, "time_to_generate": None, } - mock_rag_cls.return_value = rag - response = InnerKnowledgeRetrievalService().retrieve(request, sqlite_session) + response = InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), retrieval=lambda: rag + ).retrieve(request) rag_request = rag.knowledge_retrieval.call_args.kwargs["request"] assert rag_request.tenant_id == TENANT_ID @@ -171,12 +175,11 @@ def test_retrieve_maps_multiple_request_and_skips_enable_api_check( assert rag_request.attachment_ids == ["attachment-1"] assert response.results[0].title == "FAQ.md" assert response.usage.currency == "USD" - assert rag.knowledge_retrieval.call_args.kwargs["session"] is sqlite_session - assert sqlite_session.in_transaction() + assert "session" not in rag.knowledge_retrieval.call_args.kwargs + assert not sqlite_session.in_transaction() @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) - @patch("services.knowledge_retrieval_inner_service.DatasetRetrieval") - def test_retrieve_maps_single_request(self, mock_rag_cls, sqlite_session: Session): + def test_retrieve_maps_single_request(self, sqlite_session: Session): request = _build_request( dataset_ids=[DATASET_1_ID], retrieval={ @@ -217,9 +220,10 @@ def test_retrieve_maps_single_request(self, mock_rag_cls, sqlite_session: Sessio "currency": "USD", "latency": 1, } - mock_rag_cls.return_value = rag - InnerKnowledgeRetrievalService().retrieve(request, sqlite_session) + InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), retrieval=lambda: rag + ).retrieve(request) rag_request = rag.knowledge_retrieval.call_args.kwargs["request"] assert rag_request.retrieval_mode == "single" @@ -230,29 +234,38 @@ def test_retrieve_maps_single_request(self, mock_rag_cls, sqlite_session: Sessio assert rag_request.metadata_filtering_mode == "automatic" assert rag_request.metadata_model_config is not None assert rag_request.metadata_model_config.provider == "openai" - assert sqlite_session.in_transaction() + assert not sqlite_session.in_transaction() @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) def test_retrieve_raises_when_app_missing(self, sqlite_session: Session): with pytest.raises(InnerKnowledgeRetrieveAppNotFoundError): - InnerKnowledgeRetrievalService().retrieve(_build_request(), sqlite_session) - assert sqlite_session.in_transaction() + InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), + ).retrieve(_build_request()) + assert not sqlite_session.in_transaction() @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) def test_retrieve_raises_when_app_belongs_to_other_tenant(self, sqlite_session: Session): _persist_state(sqlite_session, _app(tenant_id=OTHER_TENANT_ID)) with pytest.raises(InnerKnowledgeRetrieveAppTenantMismatchError): - InnerKnowledgeRetrievalService().retrieve(_build_request(), sqlite_session) - assert sqlite_session.in_transaction() + InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), + ).retrieve(_build_request()) + assert not sqlite_session.in_transaction() @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) def test_retrieve_raises_when_dataset_missing(self, sqlite_session: Session): _persist_state(sqlite_session, _app(), _dataset(dataset_id=DATASET_1_ID)) with pytest.raises(InnerKnowledgeRetrieveDatasetNotFoundError): - InnerKnowledgeRetrievalService().retrieve(_build_request(), sqlite_session) - assert sqlite_session.in_transaction() + InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), + ).retrieve(_build_request()) + assert not sqlite_session.in_transaction() @pytest.mark.parametrize("sqlite_session", [(App, Dataset)], indirect=True) def test_retrieve_raises_when_dataset_belongs_to_other_tenant(self, sqlite_session: Session): @@ -264,5 +277,8 @@ def test_retrieve_raises_when_dataset_belongs_to_other_tenant(self, sqlite_sessi ) with pytest.raises(InnerKnowledgeRetrieveDatasetTenantMismatchError): - InnerKnowledgeRetrievalService().retrieve(_build_request(), sqlite_session) - assert sqlite_session.in_transaction() + InnerKnowledgeRetrievalService( + scopes=KnowledgeRetrievalRepository(sessionmaker(bind=sqlite_session.get_bind())), + retrieval=build_dataset_retrieval(sessionmaker(bind=sqlite_session.get_bind())), + ).retrieve(_build_request()) + assert not sqlite_session.in_transaction() From 1e3ed19fff346d94c3257764a6bd00b88b9c11c1 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Fri, 9 Oct 2026 13:13:35 +0800 Subject: [PATCH 2/5] fix(retrieval): complete injected hit-testing contracts and migrated tests --- api/services/knowledge/retrieval/ports.py | 15 +++++++- .../inner_api/test_knowledge_retrieval.py | 37 ++++++++++++------- .../service_api/dataset/test_hit_testing.py | 25 +++++++++++-- .../services/annotation/test_reply_service.py | 21 +++++++---- .../test_retrieval_attachment_signing.py | 9 +++-- 5 files changed, 80 insertions(+), 27 deletions(-) diff --git a/api/services/knowledge/retrieval/ports.py b/api/services/knowledge/retrieval/ports.py index 2f2b090b8145e7..b7ebeeb2b4f984 100644 --- a/api/services/knowledge/retrieval/ports.py +++ b/api/services/knowledge/retrieval/ports.py @@ -6,6 +6,7 @@ from threading import Thread from typing import Any, Protocol +from core.app.app_config.entities import ModelConfig from core.app.entities.app_invoke_entities import EasyUIBasedAppGenerateEntity, InvokeFrom from core.rag.data_post_processor.data_post_processor import RerankingModelDict, WeightsDict from core.rag.entities import DocumentContext, MetadataFilteringCondition, RetrievalSourceMetadata @@ -77,7 +78,7 @@ def __call__( class DatasetRetriever(Protocol): - """Retrieval operations consumed by the workflow execution layer.""" + """Retrieval operations consumed by workflow execution and hit testing.""" @property def llm_usage(self) -> LLMUsage: ... @@ -86,6 +87,18 @@ def set_request_metadata(self, request_metadata: Mapping[str, object] | None) -> def knowledge_retrieval(self, request: KnowledgeRetrievalRequest) -> list[Source]: ... + def get_metadata_filter_condition( + self, + dataset_ids: list[str], + query: str, + tenant_id: str, + user_id: str, + metadata_filtering_mode: str, + metadata_model_config: ModelConfig | None, + metadata_filtering_conditions: MetadataFilteringCondition | None, + inputs: dict[str, Any], + ) -> tuple[dict[str, list[str]] | None, MetadataFilteringCondition | None]: ... + class DatasetRetrievalFactory(Protocol): def __call__(self, application_generate_entity: EasyUIBasedAppGenerateEntity | None = None) -> DatasetRetriever: ... diff --git a/api/tests/unit_tests/controllers/inner_api/test_knowledge_retrieval.py b/api/tests/unit_tests/controllers/inner_api/test_knowledge_retrieval.py index 805385b09b2477..7f973a73bf86f9 100644 --- a/api/tests/unit_tests/controllers/inner_api/test_knowledge_retrieval.py +++ b/api/tests/unit_tests/controllers/inner_api/test_knowledge_retrieval.py @@ -2,7 +2,8 @@ from collections.abc import Iterator from contextlib import contextmanager -from unittest.mock import patch +from types import SimpleNamespace +from unittest.mock import MagicMock, create_autospec import pytest from flask import Flask @@ -16,6 +17,7 @@ InnerKnowledgeRetrieveAppNotFoundError, InnerKnowledgeRetrieveDatasetTenantMismatchError, ) +from services.knowledge_retrieval_inner_service import InnerKnowledgeRetrievalService from tests.unit_tests.config_override import config_overrides_context @@ -27,6 +29,17 @@ def inner_api_app() -> Flask: return app +@pytest.fixture +def mock_retrieve(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + inner_retrieval = create_autospec(InnerKnowledgeRetrievalService, instance=True, spec_set=True) + services = SimpleNamespace(knowledge=SimpleNamespace(inner_retrieval=inner_retrieval)) + monkeypatch.setattr( + "controllers.inner_api.knowledge.retrieval.application_services", + lambda: services, + ) + return inner_retrieval.retrieve + + def _headers(api_key: str | None = "inner-key") -> dict[str, str]: headers = {"Content-Type": "application/json"} if api_key is not None: @@ -96,8 +109,7 @@ def test_post_returns_400_for_invalid_body(self, inner_api_app: Flask): assert response.status_code == 400 assert response.get_json()["code"] == "invalid_request" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_404_for_service_not_found_error(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_404_for_service_not_found_error(self, mock_retrieve: MagicMock, inner_api_app: Flask) -> None: mock_retrieve.side_effect = InnerKnowledgeRetrieveAppNotFoundError("app missing") with _plugin_inner_auth(): @@ -110,8 +122,7 @@ def test_post_returns_404_for_service_not_found_error(self, mock_retrieve, inner assert response.status_code == 404 assert response.get_json()["code"] == "app_not_found" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_403_for_service_forbidden_error(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_403_for_service_forbidden_error(self, mock_retrieve: MagicMock, inner_api_app: Flask) -> None: mock_retrieve.side_effect = InnerKnowledgeRetrieveDatasetTenantMismatchError("wrong tenant") with _plugin_inner_auth(): @@ -124,8 +135,9 @@ def test_post_returns_403_for_service_forbidden_error(self, mock_retrieve, inner assert response.status_code == 403 assert response.get_json()["code"] == "dataset_tenant_mismatch" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_422_for_retrieval_config_value_error(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_422_for_retrieval_config_value_error( + self, mock_retrieve: MagicMock, inner_api_app: Flask + ) -> None: mock_retrieve.side_effect = ValueError("invalid reranking config") with _plugin_inner_auth(): @@ -138,8 +150,7 @@ def test_post_returns_422_for_retrieval_config_value_error(self, mock_retrieve, assert response.status_code == 422 assert response.get_json()["code"] == "retrieval_config_invalid" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_429_for_rate_limit_error(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_429_for_rate_limit_error(self, mock_retrieve: MagicMock, inner_api_app: Flask) -> None: mock_retrieve.side_effect = RateLimitExceededError("knowledge rate limited") with _plugin_inner_auth(): @@ -180,8 +191,9 @@ def test_post_returns_400_for_automatic_metadata_without_model_config(self, inne assert response.status_code == 400 assert response.get_json()["code"] == "invalid_request" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_502_for_external_knowledge_failure(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_502_for_external_knowledge_failure( + self, mock_retrieve: MagicMock, inner_api_app: Flask + ) -> None: mock_retrieve.side_effect = ExternalKnowledgeRetrievalError("upstream failed") with _plugin_inner_auth(): @@ -194,8 +206,7 @@ def test_post_returns_502_for_external_knowledge_failure(self, mock_retrieve, in assert response.status_code == 502 assert response.get_json()["code"] == "external_knowledge_failed" - @patch("controllers.inner_api.knowledge.retrieval.InnerKnowledgeRetrievalService.retrieve") - def test_post_returns_service_response(self, mock_retrieve, inner_api_app: Flask): + def test_post_returns_service_response(self, mock_retrieve: MagicMock, inner_api_app: Flask) -> None: mock_retrieve.return_value = InnerKnowledgeRetrieveResponse( results=[ Source( diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py b/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py index 3083c9c1f60b05..ccbd00866076d5 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_hit_testing.py @@ -15,18 +15,29 @@ """ import uuid +from types import SimpleNamespace from unittest.mock import MagicMock, patch import pytest from flask import Flask, g -from werkzeug.exceptions import Forbidden, NotFound import services.errors.base +from controllers.common.errors import AccessDeniedError, NotFoundError +from controllers.console.datasets import hit_testing_base as hit_testing_base_module from controllers.service_api.dataset.hit_testing import HitTestingApi, HitTestingPayload from models.account import Account, Tenant, TenantAccountRole from models.dataset import Dataset from services.knowledge.entities.knowledge_entities import RetrievalModel + +@pytest.fixture +def workflow_retrieval(monkeypatch: pytest.MonkeyPatch) -> MagicMock: + retrieval = MagicMock(name="dataset_retrieval_factory") + services = SimpleNamespace(workflow_runtime=SimpleNamespace(retrieval=retrieval)) + monkeypatch.setattr(hit_testing_base_module, "application_services", lambda: services) + return retrieval + + # --------------------------------------------------------------------------- # HitTestingPayload Model Tests # --------------------------------------------------------------------------- @@ -150,6 +161,7 @@ def test_post_success( mock_hit_svc: MagicMock, mock_ns: MagicMock, app: Flask, + workflow_retrieval: MagicMock, ) -> None: """Test successful hit testing request.""" dataset_id = str(uuid.uuid4()) @@ -175,6 +187,7 @@ def test_post_success( assert response["query"] == {"content": "test query"} mock_hit_svc.retrieve.assert_called_once() + assert mock_hit_svc.retrieve.call_args.kwargs["retrieval"] is workflow_retrieval @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.HitTestingService") @@ -185,6 +198,7 @@ def test_post_with_retrieval_model( mock_hit_svc: MagicMock, mock_ns: MagicMock, app: Flask, + workflow_retrieval: MagicMock, ) -> None: """Test hit testing with custom retrieval model.""" dataset_id = str(uuid.uuid4()) @@ -221,6 +235,7 @@ def test_post_with_retrieval_model( assert response["query"] == {"content": "complex query"} call_kwargs = mock_hit_svc.retrieve.call_args + assert call_kwargs.kwargs["retrieval"] is workflow_retrieval # retrieval_model is serialized via model_dump, verify key fields passed_retrieval_model = call_kwargs.kwargs.get("retrieval_model") assert passed_retrieval_model is not None @@ -236,6 +251,7 @@ def test_post_preserves_retrieval_model_metadata_filtering_conditions( mock_hit_svc: MagicMock, mock_ns: MagicMock, app: Flask, + workflow_retrieval: MagicMock, ) -> None: """Service API retrieval payload should not drop metadata filters.""" dataset_id = str(uuid.uuid4()) @@ -277,6 +293,7 @@ def test_post_preserves_retrieval_model_metadata_filtering_conditions( HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) passed_retrieval_model = mock_hit_svc.retrieve.call_args.kwargs.get("retrieval_model") + assert mock_hit_svc.retrieve.call_args.kwargs["retrieval"] is workflow_retrieval assert passed_retrieval_model is not None assert passed_retrieval_model["metadata_filtering_conditions"] == metadata_filtering_conditions @@ -289,6 +306,7 @@ def test_post_prepares_nullable_list_fields( mock_hit_svc: MagicMock, mock_ns: MagicMock, app: Flask, + workflow_retrieval: MagicMock, ) -> None: """Test service API prepares nullable list fields from retrieval records.""" dataset_id = str(uuid.uuid4()) @@ -323,6 +341,7 @@ def test_post_prepares_nullable_list_fields( assert record["score"] == 0.9 assert record["tsne_position"] is None assert record["summary"] is None + assert mock_hit_svc.retrieve.call_args.kwargs["retrieval"] is workflow_retrieval @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @patch("controllers.console.datasets.hit_testing_base.DatasetService") @@ -344,7 +363,7 @@ def test_post_dataset_not_found( # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack g._login_user = account api = HitTestingApi() - with pytest.raises(NotFound): + with pytest.raises(NotFoundError): HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) @patch("controllers.service_api.dataset.hit_testing.service_api_ns") @@ -370,5 +389,5 @@ def test_post_no_dataset_permission( # TODO: the service APIs are NOT migrated yet, so we have to do the very dirty hack g._login_user = account api = HitTestingApi() - with pytest.raises(Forbidden): + with pytest.raises(AccessDeniedError): HitTestingApi.post.__wrapped__(api, tenant_id, dataset_id) diff --git a/api/tests/unit_tests/services/annotation/test_reply_service.py b/api/tests/unit_tests/services/annotation/test_reply_service.py index a59b62f414fa67..ca75a8738d1093 100644 --- a/api/tests/unit_tests/services/annotation/test_reply_service.py +++ b/api/tests/unit_tests/services/annotation/test_reply_service.py @@ -9,8 +9,9 @@ from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import QueuePool -from core.app.entities.queue_entities import QueueAnnotationReplyEvent, QueueStopEvent +from core.app.entities.queue_entities import AppQueueEvent, QueueAnnotationReplyEvent, QueueStopEvent from core.rag.models.document import Document +from extensions.application_services.workflow import WorkflowExecutionDependencies from models.annotation_reply import AnnotationReply from models.base import TypeBase from models.dataset import Dataset, DatasetCollectionBinding @@ -21,6 +22,7 @@ from services.annotation import retrieval_gateway from services.annotation.reply_service import AnnotationReplyService from services.annotation.retrieval_gateway import AnnotationVectorRetrieval +from services.workflow.execution.adapters.chatflow.app_runner import AdvancedChatAppRunner from tests.unit_tests.core.app.apps.advanced_chat.test_app_runner_input_moderation import ( build_runner as _runner_fixture, ) @@ -250,19 +252,24 @@ def fail( assert session.scalar(select(AppAnnotationHitHistory)) is None -def test_chatflow_runner_search_and_publish_are_outside_transaction(build_runner, annotation_store, monkeypatch): +def test_chatflow_runner_search_and_publish_are_outside_transaction( + build_runner: AdvancedChatAppRunner, + annotation_store: AnnotationStore, + monkeypatch: pytest.MonkeyPatch, +) -> None: runner = build_runner runner._app.id = runner.application_generate_entity.app_config.app_id = "app-1" runner._app.tenant_id = runner.application_generate_entity.app_config.tenant_id = "tenant-1" runner.message.id = "msg-1" runner.application_generate_entity.query = "hi" - runner._runtime = replace(runner._runtime, annotation_replies=install_vector(monkeypatch, annotation_store)) + runtime = cast(WorkflowExecutionDependencies, runner._runtime) + runner._runtime = replace(runtime, annotation_replies=install_vector(monkeypatch, annotation_store)) monkeypatch.setattr(runner, "handle_input_moderation", lambda **_kwargs: (False, {}, "hi")) - events = [] + events: list[AppQueueEvent] = [] - def publish(item): - assert annotation_store.kw["bind"].pool.checkedout() == 0 - with annotation_store() as session: + def publish(item: AppQueueEvent) -> None: + assert annotation_store.pool.checkedout() == 0 + with annotation_store.sessions() as session: assert session.scalar(select(AppAnnotationHitHistory)) is not None events.append(item) diff --git a/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py b/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py index fc1e880d3afd2d..11e26ecfc75b34 100644 --- a/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py +++ b/api/tests/unit_tests/services/knowledge/test_retrieval_attachment_signing.py @@ -9,7 +9,7 @@ import pytest from sqlalchemy import Engine, event -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from core.app.app_config.entities import DatasetEntity, DatasetRetrieveConfigEntity from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom @@ -22,6 +22,7 @@ from models.model import UploadFile from repositories.knowledge.dataset_read_repository import authorize_retrieved_segment from services.knowledge.retrieval import dataset_retrieval as retrieval_module +from services.knowledge.retrieval.dataset_retrieval import DatasetRetrieval @pytest.fixture @@ -95,10 +96,11 @@ def test_published_retrieval_grants_bound_images_before_signing( monkeypatch: pytest.MonkeyPatch, entry: str, vision_enabled: bool, - sqlite_session_factory, + sqlite_session_factory: sessionmaker[Session], ) -> None: dataset, document, segment, file_ids = graph retrieval = build_dataset_retrieval(sqlite_session_factory)() + assert isinstance(retrieval, DatasetRetrieval) monkeypatch.setattr(retrieval, "_check_knowledge_rate_limit", MagicMock()) monkeypatch.setattr(retrieval._records, "available_datasets", MagicMock(return_value=[dataset])) monkeypatch.setattr(retrieval, "get_metadata_filter_condition", MagicMock(return_value=(None, None))) @@ -224,13 +226,14 @@ def test_workflow_retrieval_releases_owned_connections_and_preserves_caller_tran monkeypatch: pytest.MonkeyPatch, with_image: bool, signing_fails: bool, - sqlite_session_factory, + sqlite_session_factory: sessionmaker[Session], ) -> None: dataset, document, segment, _ = graph if not with_image: segment.content = "Plain text" sqlite_session.commit() retrieval = build_dataset_retrieval(sqlite_session_factory)() + assert isinstance(retrieval, DatasetRetrieval) monkeypatch.setattr(retrieval, "_check_knowledge_rate_limit", MagicMock()) monkeypatch.setattr(retrieval._records, "available_datasets", MagicMock(return_value=[dataset])) monkeypatch.setattr( From 93d840535679e7a0c38ddd11220cce487c02c4b7 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Fri, 9 Oct 2026 13:24:49 +0800 Subject: [PATCH 3/5] test: bind retrieval integration factories to container database --- .../test_dataset_retrieval_integration.py | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py b/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py index 6fece8e109a2c3..2545bfad728d09 100644 --- a/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py +++ b/api/tests/test_containers_integration_tests/core/rag/retrieval/test_dataset_retrieval_integration.py @@ -3,20 +3,27 @@ import pytest from faker import Faker -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, sessionmaker from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType from core.workflow.nodes.knowledge_retrieval.retrieval import KnowledgeRetrievalRequest from extensions.application_services.retrieval import build_dataset_retrieval from models.dataset import Dataset, Document from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus +from services.knowledge.retrieval.dataset_retrieval import DatasetRetrieval from tests.test_containers_integration_tests.helpers import accounts as account_fixtures from tests.test_containers_integration_tests.helpers import generate_valid_password +def _build_dataset_retrieval(session: Session) -> DatasetRetrieval: + retrieval = build_dataset_retrieval(sessionmaker(bind=session.get_bind()))() + assert isinstance(retrieval, DatasetRetrieval) + return retrieval + + class TestGetAvailableDatasetsIntegration: def test_returns_datasets_with_available_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -69,7 +76,7 @@ def test_returns_datasets_with_available_documents( db_session_with_containers.commit() # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert @@ -79,7 +86,7 @@ def test_returns_datasets_with_available_documents( assert result[0].name == dataset.name def test_filters_out_datasets_with_only_archived_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -126,14 +133,14 @@ def test_filters_out_datasets_with_only_archived_documents( db_session_with_containers.commit() # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_filters_out_datasets_with_only_disabled_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -180,14 +187,14 @@ def test_filters_out_datasets_with_only_disabled_documents( db_session_with_containers.commit() # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_filters_out_datasets_with_non_completed_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -234,14 +241,14 @@ def test_filters_out_datasets_with_non_completed_documents( db_session_with_containers.commit() # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert assert len(result) == 0 def test_includes_external_datasets_without_documents( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): """ Test that external datasets are returned even with no available documents. @@ -278,7 +285,7 @@ def test_includes_external_datasets_without_documents( db_session_with_containers.commit() # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [dataset.id]) # Assert @@ -286,9 +293,7 @@ def test_includes_external_datasets_without_documents( assert result[0].id == dataset.id assert result[0].provider == "external" - def test_filters_by_tenant_id( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory - ): + def test_filters_by_tenant_id(self, db_session_with_containers: Session, mock_external_service_dependencies): # Arrange fake = Faker() @@ -357,7 +362,7 @@ def test_filters_by_tenant_id( db_session_with_containers.commit() # Act - request from tenant1, should only get tenant1's dataset - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant1.id, [dataset1.id, dataset2.id]) # Assert @@ -366,7 +371,7 @@ def test_filters_by_tenant_id( assert result[0].tenant_id == tenant1.id def test_returns_empty_list_when_no_datasets_found( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -384,14 +389,14 @@ def test_returns_empty_list_when_no_datasets_found( # Don't create any datasets # Act - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) result = dataset_retrieval._records.available_datasets(tenant.id, [str(uuid.uuid4())]) # Assert assert result == [] def test_returns_only_requested_dataset_ids( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -441,7 +446,7 @@ def test_returns_only_requested_dataset_ids( db_session_with_containers.commit() # Act - request only dataset 0 and 2, not dataset 1 - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) requested_ids = [datasets[0].id, datasets[2].id] result = dataset_retrieval._records.available_datasets(tenant.id, requested_ids) @@ -453,7 +458,7 @@ def test_returns_only_requested_dataset_ids( class TestKnowledgeRetrievalIntegration: def test_knowledge_retrieval_with_available_datasets( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -509,20 +514,20 @@ def test_knowledge_retrieval_with_available_datasets( top_k=5, ) - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) # Mock rate limit check and retrieval with patch.object(dataset_retrieval, "_check_knowledge_rate_limit"): with patch.object(dataset_retrieval, "get_metadata_filter_condition", return_value=(None, None)): with patch.object(dataset_retrieval, "multiple_retrieve", return_value=[]): # Act - result = dataset_retrieval.knowledge_retrieval(db_session_with_containers, request) + result = dataset_retrieval.knowledge_retrieval(request) # Assert assert isinstance(result, list) def test_knowledge_retrieval_no_available_datasets( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -560,18 +565,18 @@ def test_knowledge_retrieval_no_available_datasets( top_k=5, ) - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) # Mock rate limit check with patch.object(dataset_retrieval, "_check_knowledge_rate_limit"): # Act - result = dataset_retrieval.knowledge_retrieval(db_session_with_containers, request) + result = dataset_retrieval.knowledge_retrieval(request) # Assert assert result == [] def test_knowledge_retrieval_rate_limit_exceeded( - self, db_session_with_containers: Session, mock_external_service_dependencies, sqlite_session_factory + self, db_session_with_containers: Session, mock_external_service_dependencies ): # Arrange fake = Faker() @@ -608,7 +613,7 @@ def test_knowledge_retrieval_rate_limit_exceeded( top_k=5, ) - dataset_retrieval = build_dataset_retrieval(sqlite_session_factory)() + dataset_retrieval = _build_dataset_retrieval(db_session_with_containers) # Mock rate limit check to raise exception with patch.object( @@ -618,7 +623,7 @@ def test_knowledge_retrieval_rate_limit_exceeded( ): # Act & Assert with pytest.raises(Exception, match="Rate limit exceeded"): - dataset_retrieval.knowledge_retrieval(db_session_with_containers, request) + dataset_retrieval.knowledge_retrieval(request) @pytest.fixture From f40970a1f529a6f26dfb6b017b319ae604fd5708 Mon Sep 17 00:00:00 2001 From: Byron Wang Date: Tue, 20 Oct 2026 10:44:43 +0800 Subject: [PATCH 4/5] fix(api): keep extracted execution types compatible with ORM callers --- .../services/annotation/test_reply_service.py | 2 +- .../services/test_hit_testing_service.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/tests/unit_tests/services/annotation/test_reply_service.py b/api/tests/unit_tests/services/annotation/test_reply_service.py index ca75a8738d1093..dc8d9e217e8345 100644 --- a/api/tests/unit_tests/services/annotation/test_reply_service.py +++ b/api/tests/unit_tests/services/annotation/test_reply_service.py @@ -274,6 +274,6 @@ def publish(item: AppQueueEvent) -> None: events.append(item) monkeypatch.setattr(runner._events, "_publish_event", publish) - runner.run() + assert runner.prepare() is None assert isinstance(events[0], QueueAnnotationReplyEvent) assert isinstance(events[-1], QueueStopEvent) diff --git a/api/tests/unit_tests/services/test_hit_testing_service.py b/api/tests/unit_tests/services/test_hit_testing_service.py index e3b2739068a08c..6897d0e7d01a5c 100644 --- a/api/tests/unit_tests/services/test_hit_testing_service.py +++ b/api/tests/unit_tests/services/test_hit_testing_service.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import Session from core.rag.models.document import Document +from extensions.application_services.workflow import WorkflowExecutionDependencies from models import Account from models.dataset import Dataset, DatasetQuery from services.hit_testing_service import HitTestingService @@ -66,7 +67,12 @@ def model_dump(self) -> dict[str, str | float]: class TestHitTestingServiceRetrieveTransaction: """Regression tests for https://github.com/langgenius/dify/issues/38998.""" - def test_retrieve_should_not_close_caller_managed_transaction(self, sqlite_session: Session) -> None: + def test_retrieve_should_not_close_caller_managed_transaction( + self, + sqlite_session: Session, + *, + workflow_runtime: WorkflowExecutionDependencies, + ) -> None: """retrieve() must not commit the caller-managed session mid-flow. When the caller manages the session inside a ``session.begin()`` block, an @@ -92,7 +98,15 @@ def test_retrieve_should_not_close_caller_managed_transaction(self, sqlite_sessi mock_format.return_value = [record] with sqlite_session.begin(): - result = HitTestingService.retrieve(dataset, query, account, None, {}, session=sqlite_session) + result = HitTestingService.retrieve( + dataset, + query, + account, + None, + {}, + session=sqlite_session, + retrieval=workflow_runtime.retrieval, + ) assert result["query"]["content"] == query query_log = sqlite_session.scalar(select(DatasetQuery)) From 9d5cc4e992967865138dea5f5cec85078346889d Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:31:36 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- .../generated/api/console/apps/errors.gen.ts | 26 ------------------- .../generated/api/console/errors.gen.ts | 20 -------------- .../generated/api/console/rag/errors.gen.ts | 18 ------------- 3 files changed, 64 deletions(-) diff --git a/packages/contracts/generated/api/console/apps/errors.gen.ts b/packages/contracts/generated/api/console/apps/errors.gen.ts index 1ca81cfa7c000b..4cd4d5e228e674 100644 --- a/packages/contracts/generated/api/console/apps/errors.gen.ts +++ b/packages/contracts/generated/api/console/apps/errors.gen.ts @@ -605,8 +605,6 @@ export const get15Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, } -export const get16Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post13Errors = { no_file_uploaded: { status: 400, data: zNoFileUploadedError }, too_many_files: { status: 400, data: zTooManyFilesError }, @@ -919,17 +917,10 @@ export const get65Errors = { app_not_found: { status: 404, data: zAppNotFoundErr export const get66Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } -export const get67Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const post44Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const get68Errors = { - app_not_found: { status: 404, data: zAppNotFoundError }, draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, } -export const post45Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post46Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } export const post47Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } @@ -993,10 +984,6 @@ export const post56Errors = { app_not_found: { status: 404, data: zAppNotFoundEr export const post57Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } -export const delete11Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const get72Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post58Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, rate_limit_error: { status: 429, data: zRateLimitError }, @@ -1025,8 +1012,6 @@ export const get76Errors = { workflow_run_not_found: { status: 404, data: zWorkflowRunNotFoundError }, } -export const get77Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const post59Errors = { app_not_found: { status: 404, data: zAppNotFoundError }, rate_limit_error: { status: 429, data: zRateLimitError }, @@ -1037,18 +1022,7 @@ export const post60Errors = { rate_limit_error: { status: 429, data: zRateLimitError }, } -export const put6Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const delete12Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const get78Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const patch2Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - -export const delete13Errors = { app_not_found: { status: 404, data: zAppNotFoundError } } - export const get79Errors = { - app_not_found: { status: 404, data: zAppNotFoundError }, draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, } diff --git a/packages/contracts/generated/api/console/errors.gen.ts b/packages/contracts/generated/api/console/errors.gen.ts index 34e0844610ffc4..e3098084a0a65e 100644 --- a/packages/contracts/generated/api/console/errors.gen.ts +++ b/packages/contracts/generated/api/console/errors.gen.ts @@ -1,11 +1,8 @@ // This file is auto-generated by scripts/add-orpc-error-maps.ts export { delete10Errors as 'DELETE /apps/{app_id}/workflow/comments/{comment_id}' } from './apps/errors.gen.ts' export { delete10Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/research-tasks/{task_id}' } from './knowledge-fs/errors.gen.ts' -export { delete11Errors as 'DELETE /apps/{app_id}/workflows/draft/nodes/{node_id}/variables' } from './apps/errors.gen.ts' export { delete11Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/sources/{source_id}' } from './knowledge-fs/errors.gen.ts' -export { delete12Errors as 'DELETE /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { delete12Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}' } from './knowledge-fs/errors.gen.ts' -export { delete13Errors as 'DELETE /apps/{app_id}/workflows/draft/variables' } from './apps/errors.gen.ts' export { delete13Errors as 'DELETE /knowledge-fs/uploads/{upload_id}' } from './knowledge-fs/errors.gen.ts' export { delete14Errors as 'DELETE /apps/{app_id}/workflows/{workflow_id}' } from './apps/errors.gen.ts' export { delete16Errors as 'DELETE /workspaces/current/skills/{skill_id}' } from './workspaces/errors.gen.ts' @@ -14,15 +11,12 @@ export { delete2Errors as 'DELETE /apps/{app_id}/agent/config/skills/{name}' } f export { delete2Errors as 'DELETE /datasets/external-knowledge-api/{external_knowledge_api_id}' } from './datasets/errors.gen.ts' export { delete2Errors as 'DELETE /installed-apps/{installed_app_id}/saved-messages/{message_id}' } from './installed-apps/errors.gen.ts' export { delete2Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/app-bindings/{caller_kind}/{app_id}' } from './knowledge-fs/errors.gen.ts' -export { delete2Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables' } from './rag/errors.gen.ts' export { delete3Errors as 'DELETE /agent/{agent_id}/config/files/{name}' } from './agent/errors.gen.ts' export { delete3Errors as 'DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}' } from './datasets/errors.gen.ts' export { delete3Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/documents/bulk' } from './knowledge-fs/errors.gen.ts' -export { delete3Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { delete4Errors as 'DELETE /agent/{agent_id}/config/skills/{name}' } from './agent/errors.gen.ts' export { delete4Errors as 'DELETE /apps/{app_id}/annotations' } from './apps/errors.gen.ts' export { delete4Errors as 'DELETE /knowledge-fs/spaces/{control_space_id}/documents/{document_id}' } from './knowledge-fs/errors.gen.ts' -export { delete4Errors as 'DELETE /rag/pipelines/{pipeline_id}/workflows/draft/variables' } from './rag/errors.gen.ts' export { delete4Errors as 'DELETE /workspaces/current/members/{member_id}' } from './workspaces/errors.gen.ts' export { delete5Errors as 'DELETE /agent/{agent_id}/trace-config' } from './agent/errors.gen.ts' export { delete5Errors as 'DELETE /apps/{app_id}/chat-conversations/{conversation_id}' } from './apps/errors.gen.ts' @@ -63,7 +57,6 @@ export { get13Errors as 'GET /trial-apps/{app_id}/messages/{message_id}/suggeste export { get14Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/content' } from './agent/errors.gen.ts' export { get14Errors as 'GET /apps/{app_id}/agent/config/skills/{name}/inspect' } from './apps/errors.gen.ts' export { get14Errors as 'GET /knowledge-fs/spaces/{control_space_id}/documents/{document_id}/revisions' } from './knowledge-fs/errors.gen.ts' -export { get14Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/nodes/{node_id}/variables' } from './rag/errors.gen.ts' export { get14Errors as 'GET /snippets/{snippet_id}/workflows/draft/variables' } from './snippets/errors.gen.ts' export { get14Errors as 'GET /trial-apps/{app_id}/parameters' } from './trial-apps/errors.gen.ts' export { get15Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/download' } from './agent/errors.gen.ts' @@ -73,19 +66,16 @@ export { get15Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/pre-pr export { get15Errors as 'GET /snippets/{snippet_id}/workflows/draft' } from './snippets/errors.gen.ts' export { get15Errors as 'GET /trial-apps/{app_id}/site' } from './trial-apps/errors.gen.ts' export { get16Errors as 'GET /agent/{agent_id}/config/skills/{name}/files/preview' } from './agent/errors.gen.ts' -export { get16Errors as 'GET /apps/{app_id}/agent/logs' } from './apps/errors.gen.ts' export { get16Errors as 'GET /datasets/{dataset_id}/documents/{document_id}/indexing-estimate' } from './datasets/errors.gen.ts' export { get16Errors as 'GET /knowledge-fs/spaces/{control_space_id}/documents' } from './knowledge-fs/errors.gen.ts' export { get16Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/processing/parameters' } from './rag/errors.gen.ts' export { get16Errors as 'GET /trial-apps/{app_id}/workflows' } from './trial-apps/errors.gen.ts' export { get17Errors as 'GET /agent/{agent_id}/config/skills/{name}/inspect' } from './agent/errors.gen.ts' export { get17Errors as 'GET /knowledge-fs/spaces/{control_space_id}/external-access' } from './knowledge-fs/errors.gen.ts' -export { get17Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/system-variables' } from './rag/errors.gen.ts' export { get17Errors as 'GET /trial-apps/{app_id}' } from './trial-apps/errors.gen.ts' export { get18Errors as 'GET /agent/{agent_id}/config/skills' } from './agent/errors.gen.ts' export { get18Errors as 'GET /datasets/{dataset_id}/documents/{document_id}/notion/sync' } from './datasets/errors.gen.ts' export { get18Errors as 'GET /knowledge-fs/spaces/{control_space_id}/golden-questions' } from './knowledge-fs/errors.gen.ts' -export { get18Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { get19Errors as 'GET /agent/{agent_id}/log-sources' } from './agent/errors.gen.ts' export { get19Errors as 'GET /knowledge-fs/spaces/{control_space_id}/jobs/{job_id}' } from './knowledge-fs/errors.gen.ts' export { get19Errors as 'GET /rag/pipelines/{pipeline_id}/workflows/draft/variables' } from './rag/errors.gen.ts' @@ -228,7 +218,6 @@ export { get63Errors as 'GET /apps/{app_id}/workflow/statistics/daily-terminals' export { get64Errors as 'GET /apps/{app_id}/workflow/statistics/token-costs' } from './apps/errors.gen.ts' export { get65Errors as 'GET /apps/{app_id}/workflows/default-workflow-block-configs/{block_type}' } from './apps/errors.gen.ts' export { get66Errors as 'GET /apps/{app_id}/workflows/default-workflow-block-configs' } from './apps/errors.gen.ts' -export { get67Errors as 'GET /apps/{app_id}/workflows/draft/conversation-variables' } from './apps/errors.gen.ts' export { get68Errors as 'GET /apps/{app_id}/workflows/draft/environment-variables' } from './apps/errors.gen.ts' export { get69Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer/candidates' } from './apps/errors.gen.ts' export { get6Errors as 'GET /agent/{agent_id}/chat-messages' } from './agent/errors.gen.ts' @@ -239,13 +228,10 @@ export { get6Errors as 'GET /rag/pipelines/{pipeline_id}/exports' } from './rag/ export { get6Errors as 'GET /trial-apps/{app_id}/agent/config/skills/{name}/files/content' } from './trial-apps/errors.gen.ts' export { get70Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './apps/errors.gen.ts' export { get71Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/last-run' } from './apps/errors.gen.ts' -export { get72Errors as 'GET /apps/{app_id}/workflows/draft/nodes/{node_id}/variables' } from './apps/errors.gen.ts' export { get73Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/events' } from './apps/errors.gen.ts' export { get74Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}/{output_name}/preview' } from './apps/errors.gen.ts' export { get75Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs/{node_id}' } from './apps/errors.gen.ts' export { get76Errors as 'GET /apps/{app_id}/workflows/draft/runs/{run_id}/node-outputs' } from './apps/errors.gen.ts' -export { get77Errors as 'GET /apps/{app_id}/workflows/draft/system-variables' } from './apps/errors.gen.ts' -export { get78Errors as 'GET /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { get79Errors as 'GET /apps/{app_id}/workflows/draft/variables' } from './apps/errors.gen.ts' export { get7Errors as 'GET /account/profile' } from './account/errors.gen.ts' export { get7Errors as 'GET /agent/{agent_id}/composer/candidates' } from './agent/errors.gen.ts' @@ -287,11 +273,9 @@ export { getErrors as 'GET /installed-apps/{installed_app_id}/conversations' } f export { getErrors as 'GET /remote-files/{url}' } from './remote-files/errors.gen.ts' export { getErrors as 'GET /trial-apps/{app_id}/agent-composer' } from './trial-apps/errors.gen.ts' export { getErrors as 'GET /website/crawl/status/{job_id}' } from './website/errors.gen.ts' -export { patch2Errors as 'PATCH /apps/{app_id}/workflows/draft/variables/{variable_id}' } from './apps/errors.gen.ts' export { patch2Errors as 'PATCH /datasets/{dataset_id}/documents/status/{action}/batch' } from './datasets/errors.gen.ts' export { patch2Errors as 'PATCH /installed-apps/{installed_app_id}/conversations/{c_id}/unpin' } from './installed-apps/errors.gen.ts' export { patch2Errors as 'PATCH /knowledge-fs/spaces/{control_space_id}/golden-questions/{question_id}' } from './knowledge-fs/errors.gen.ts' -export { patch2Errors as 'PATCH /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}' } from './rag/errors.gen.ts' export { patch3Errors as 'PATCH /apps/{app_id}/workflows/{workflow_id}' } from './apps/errors.gen.ts' export { patch3Errors as 'PATCH /datasets/{dataset_id}/documents/{document_id}/processing/pause' } from './datasets/errors.gen.ts' export { patch3Errors as 'PATCH /installed-apps/{installed_app_id}' } from './installed-apps/errors.gen.ts' @@ -452,9 +436,7 @@ export { post42Errors as 'POST /apps/{app_id}/workflow/comments/{comment_id}/res export { post42Errors as 'POST /knowledge-fs/uploads' } from './knowledge-fs/errors.gen.ts' export { post43Errors as 'POST /apps/{app_id}/workflow/comments' } from './apps/errors.gen.ts' export { post43Errors as 'POST /workspaces/current/plugin/upload/github' } from './workspaces/errors.gen.ts' -export { post44Errors as 'POST /apps/{app_id}/workflows/draft/conversation-variables' } from './apps/errors.gen.ts' export { post44Errors as 'POST /workspaces/current/plugin/upload/pkg' } from './workspaces/errors.gen.ts' -export { post45Errors as 'POST /apps/{app_id}/workflows/draft/environment-variables' } from './apps/errors.gen.ts' export { post46Errors as 'POST /apps/{app_id}/workflows/draft/features' } from './apps/errors.gen.ts' export { post47Errors as 'POST /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/delivery-test' } from './apps/errors.gen.ts' export { post48Errors as 'POST /apps/{app_id}/workflows/draft/human-input/nodes/{node_id}/form/preview' } from './apps/errors.gen.ts' @@ -559,10 +541,8 @@ export { put4Errors as 'PUT /apps/{app_id}/workflow/comments/{comment_id}' } fro export { put4Errors as 'PUT /knowledge-fs/spaces/{control_space_id}/sources/{source_id}/sync-policy' } from './knowledge-fs/errors.gen.ts' export { put5Errors as 'PUT /apps/{app_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './apps/errors.gen.ts' export { put5Errors as 'PUT /knowledge-fs/spaces/{control_space_id}/tags' } from './knowledge-fs/errors.gen.ts' -export { put6Errors as 'PUT /apps/{app_id}/workflows/draft/variables/{variable_id}/reset' } from './apps/errors.gen.ts' export { putErrors as 'PUT /agent/{agent_id}/build-draft' } from './agent/errors.gen.ts' export { putErrors as 'PUT /apps/{app_id}/network-access-group' } from './apps/errors.gen.ts' export { putErrors as 'PUT /knowledge-fs/spaces/{control_space_id}/app-bindings' } from './knowledge-fs/errors.gen.ts' -export { putErrors as 'PUT /rag/pipelines/{pipeline_id}/workflows/draft/variables/{variable_id}/reset' } from './rag/errors.gen.ts' export { putErrors as 'PUT /snippets/{snippet_id}/workflows/draft/nodes/{node_id}/agent-composer' } from './snippets/errors.gen.ts' export { putErrors as 'PUT /workspaces/current/agents/{agent_id}/skills' } from './workspaces/errors.gen.ts' diff --git a/packages/contracts/generated/api/console/rag/errors.gen.ts b/packages/contracts/generated/api/console/rag/errors.gen.ts index f87498e70afbc6..501d339d7f2b47 100644 --- a/packages/contracts/generated/api/console/rag/errors.gen.ts +++ b/packages/contracts/generated/api/console/rag/errors.gen.ts @@ -78,7 +78,6 @@ export const post10Errors = { pipeline_not_found: { status: 404, data: zPipeline export const get12Errors = { draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, - pipeline_not_found: { status: 404, data: zPipelineNotFoundError }, } export const post11Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } @@ -89,10 +88,6 @@ export const get13Errors = { pipeline_not_found: { status: 404, data: zPipelineN export const post13Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } -export const delete2Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const get14Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - export const get15Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } export const get16Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } @@ -102,21 +97,8 @@ export const post14Errors = { rate_limit_error: { status: 429, data: zRateLimitError }, } -export const get17Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const putErrors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const delete3Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const get18Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const patch2Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - -export const delete4Errors = { pipeline_not_found: { status: 404, data: zPipelineNotFoundError } } - export const get19Errors = { draft_workflow_not_exist: { status: 404, data: zDraftWorkflowNotExistError }, - pipeline_not_found: { status: 404, data: zPipelineNotFoundError }, } export const get20Errors = {