diff --git a/api/controllers/console/knowledge_fs/resources.py b/api/controllers/console/knowledge_fs/resources.py index afb0c7ddd51..83362e3a39f 100644 --- a/api/controllers/console/knowledge_fs/resources.py +++ b/api/controllers/console/knowledge_fs/resources.py @@ -146,6 +146,7 @@ from services.knowledge_fs.product_dto import ( KnowledgeFSIdempotencyHeader, KnowledgeFSInitialSourcePreviewJobCreateResponse, KnowledgeFSInitialSourcePreviewJobResponse, + KnowledgeFSInitialSourcePreviewPageResponse, KnowledgeFSInitialSourcePreviewPayload, KnowledgeFSInitialSourcePreviewResponse, KnowledgeFSInitialWebsiteSourcePreviewPayload, @@ -893,7 +894,7 @@ class KnowledgeFSInitialSourcePreviewJobsApi(Resource): ) return dump_response( KnowledgeFSInitialSourcePreviewJobCreateResponse, - KnowledgeFSInitialSourcePreviewJobCreateResponse(jobId=result.job_id), + KnowledgeFSInitialSourcePreviewJobCreateResponse(job_id=result.job_id), ), HTTPStatus.ACCEPTED @@ -914,20 +915,27 @@ class KnowledgeFSInitialSourcePreviewJobApi(Resource): remote = _console_services().facade.get_namespace_source_preview( tenant_id=tenant_id, account_id=account.id, job_id=job_id ) + status: Literal["pending", "running", "completed", "failed", "canceled"] + if remote.status == "queued": + status = "pending" + elif remote.status == "consumed": + status = "completed" + else: + status = remote.status result = KnowledgeFSInitialSourcePreviewJobResponse( - jobId=remote.job_id, - status={"queued": "pending", "consumed": "completed"}.get(remote.status, remote.status), + job_id=remote.job_id, + status=status, result=( KnowledgeFSInitialSourcePreviewResponse( kind="website_crawl", - configurationFingerprint=remote.configuration_fingerprint, + configuration_fingerprint=remote.configuration_fingerprint, pages=[ - { - "pageId": page.page_id, - "sourceUrl": page.source_url, - "title": page.title, - "description": page.description, - } + KnowledgeFSInitialSourcePreviewPageResponse( + page_id=page.page_id, + source_url=page.source_url, + title=page.title, + description=page.description, + ) for page in remote.pages ], ) @@ -954,10 +962,14 @@ class KnowledgeFSInitialSourcePreviewJobApi(Resource): remote = _console_services().facade.cancel_namespace_source_preview( tenant_id=tenant_id, account_id=account.id, job_id=job_id ) - result = KnowledgeFSInitialSourcePreviewJobResponse( - jobId=remote.job_id, - status={"queued": "pending", "consumed": "completed"}.get(remote.status, remote.status), - ) + status: Literal["pending", "running", "completed", "failed", "canceled"] + if remote.status == "queued": + status = "pending" + elif remote.status == "consumed": + status = "completed" + else: + status = remote.status + result = KnowledgeFSInitialSourcePreviewJobResponse(job_id=remote.job_id, status=status) except KnowledgeFSProductResourceNotFoundError as exc: raise NotFound() from exc return dump_response(KnowledgeFSInitialSourcePreviewJobResponse, result) diff --git a/api/services/knowledge_fs/data_facade.py b/api/services/knowledge_fs/data_facade.py index bbcbba00f84..a043287e6da 100644 --- a/api/services/knowledge_fs/data_facade.py +++ b/api/services/knowledge_fs/data_facade.py @@ -2559,7 +2559,11 @@ class KnowledgeFSDataFacade: capability_token=issued.token, trace_id=issued.trace_id, payload=payload, + query=(), + bind_space_in_body=False, + resource_id=None, path_parameters=path_parameters, + headers=(), ) def _interactive_child( diff --git a/api/tasks/knowledge_fs_initial_source_tasks.py b/api/tasks/knowledge_fs_initial_source_tasks.py index 71873ed1e85..70f51592669 100644 --- a/api/tasks/knowledge_fs_initial_source_tasks.py +++ b/api/tasks/knowledge_fs_initial_source_tasks.py @@ -286,6 +286,7 @@ def _start_workflow( not selection.page_id for selection in payload.selection ): raise ValueError("Website namespace preview selection requires page IDs and fingerprint") + page_ids = [selection.page_id for selection in payload.selection if selection.page_id is not None] imported = facade.consume_namespace_source_preview( tenant_id=tenant_id, account_id=account_id, @@ -293,7 +294,7 @@ def _start_workflow( source_id=source_id, payload=KnowledgeFSNamespacePreviewConsumePayload( previewJobId=payload.preview_job_id, - pageIds=[selection.page_id for selection in payload.selection], + pageIds=page_ids, configurationFingerprint=payload.preview_configuration_fingerprint, ), idempotency_key=f"{request_id}:crawl-import", diff --git a/api/tests/unit_tests/controllers/test_knowledge_fs_product_controllers.py b/api/tests/unit_tests/controllers/test_knowledge_fs_product_controllers.py index 6656373d2b1..780a907f0f8 100644 --- a/api/tests/unit_tests/controllers/test_knowledge_fs_product_controllers.py +++ b/api/tests/unit_tests/controllers/test_knowledge_fs_product_controllers.py @@ -33,6 +33,8 @@ from services.knowledge_fs.product_dto import ( KnowledgeFSDocumentStagedUploadAcceptedResponse, KnowledgeFSDocumentUploadAcceptedResponse, KnowledgeFSDurableDeletionAcceptedResponse, + KnowledgeFSNamespacePreviewJobResponse, + KnowledgeFSNamespacePreviewPageResponse, KnowledgeFSSmallFileUploadResponse, KnowledgeFSSpaceCreatePayload, KnowledgeFSStagedUploadResponse, @@ -157,6 +159,95 @@ def test_console_and_service_api_routes_are_registered() -> None: } +def test_initial_website_preview_job_routes_delegate_to_namespace_capabilities( + monkeypatch: pytest.MonkeyPatch, +) -> None: + account = SimpleNamespace(id="account-1") + preview_service = SimpleNamespace(require_visible_credential=MagicMock()) + facade = SimpleNamespace( + create_namespace_source_preview=MagicMock(return_value=SimpleNamespace(job_id="job-1")), + get_namespace_source_preview=MagicMock( + return_value=KnowledgeFSNamespacePreviewJobResponse( + job_id="job-1", + status="completed", + configuration_fingerprint="f" * 64, + expires_at="2030-01-01T00:00:00Z", + pages=[ + KnowledgeFSNamespacePreviewPageResponse( + page_id="page-1", + source_url="https://example.com/page", + title="Page", + ) + ], + ) + ), + cancel_namespace_source_preview=MagicMock( + return_value=KnowledgeFSNamespacePreviewJobResponse( + job_id="job-1", + status="consumed", + configuration_fingerprint="f" * 64, + expires_at="2030-01-01T00:00:00Z", + ) + ), + ) + monkeypatch.setattr(console_resources, "current_account_with_tenant", lambda: (account, "tenant-1")) + monkeypatch.setattr(console_resources, "KnowledgeFSInitialSourcePreviewService", lambda _: preview_service) + monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=facade)) + app = Flask(__name__) + + with app.test_request_context( + method="POST", + json={ + "kind": "website_crawl", + "credentialId": "credential-1", + "datasource": "website", + "pluginId": "plugin-1", + "provider": "firecrawl", + "parameters": {"url": "https://example.com"}, + }, + ): + response, status = inspect.unwrap(console_resources.KnowledgeFSInitialSourcePreviewJobsApi.post)( + console_resources.KnowledgeFSInitialSourcePreviewJobsApi() + ) + assert status == 202 + assert response == {"job_id": "job-1", "status": "pending"} + preview_service.require_visible_credential.assert_called_once() + facade.create_namespace_source_preview.assert_called_once() + + with app.test_request_context(): + response = inspect.unwrap(console_resources.KnowledgeFSInitialSourcePreviewJobApi.get)( + console_resources.KnowledgeFSInitialSourcePreviewJobApi(), "job-1" + ) + assert response["status"] == "completed" + assert response["result"]["pages"] == [ + { + "description": None, + "page_id": "page-1", + "source_url": "https://example.com/page", + "title": "Page", + } + ] + + facade.get_namespace_source_preview.return_value = KnowledgeFSNamespacePreviewJobResponse( + job_id="job-1", + status="queued", + configuration_fingerprint="f" * 64, + expires_at="2030-01-01T00:00:00Z", + ) + with app.test_request_context(): + response = inspect.unwrap(console_resources.KnowledgeFSInitialSourcePreviewJobApi.get)( + console_resources.KnowledgeFSInitialSourcePreviewJobApi(), "job-1" + ) + assert response["status"] == "pending" + assert response["result"] is None + + with app.test_request_context(method="DELETE"): + response = inspect.unwrap(console_resources.KnowledgeFSInitialSourcePreviewJobApi.delete)( + console_resources.KnowledgeFSInitialSourcePreviewJobApi(), "job-1" + ) + assert response == {"job_id": "job-1", "result": None, "status": "completed"} + + @pytest.mark.parametrize( ("api_class", "method_name", "path_args"), [ diff --git a/api/tests/unit_tests/services/test_knowledge_fs_product_operations.py b/api/tests/unit_tests/services/test_knowledge_fs_product_operations.py index d8b63bdc315..cdb5067fe8a 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_product_operations.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_product_operations.py @@ -35,6 +35,7 @@ def test_ready_product_operations_exactly_match_capability_method_path_and_actio "bulkDeleteLogicalDocuments", "cancelBackgroundTask", "cancelCompilationJob", + "cancelNamespaceSourcePreview", "cancelResearchTask", "cancelSourceWorkflow", "captureWorkflowFailedRetrieval", @@ -44,6 +45,7 @@ def test_ready_product_operations_exactly_match_capability_method_path_and_actio "createDocument", "createGoldenQuestion", "createMetadataField", + "createNamespaceSourcePreview", "createQuery", "createQualityBadCase", "createQualityReplay", @@ -51,6 +53,7 @@ def test_ready_product_operations_exactly_match_capability_method_path_and_actio "createSource", "createSourceConnection", "createUploadSession", + "consumeNamespaceSourcePreview", "deleteDocument", "deleteGoldenQuestion", "deleteLogicalDocument", @@ -68,6 +71,7 @@ def test_ready_product_operations_exactly_match_capability_method_path_and_actio "getQualityBadCaseTraceReference", "getQualityReplay", "getLogicalDocument", + "getNamespaceSourcePreview", "getOverviewHealth", "getOverviewInventory", "getOverviewQueryOutcomes",