From b79b8a879b51987bb8a44e18af056d099a7fed50 Mon Sep 17 00:00:00 2001 From: Jyong Date: Fri, 4 Sep 2026 05:42:43 -0400 Subject: [PATCH] feat(knowledge-fs): record and count every knowledge space query Workflow KnowledgeFS retrieval nodes ran through `retrieval-tests`, which never wrote an AnswerTrace or an overview event, so their queries were missing from the retrieval history and from the space overview; the history also only ever showed each member their own traces. - AnswerTrace gains a `source` (retrieval_test | workflow | service_api | agent | mcp; migration 0051_answer_trace_source) derived from the Capability v2 caller kind. The retrieval-tests route records one trace per run (stages, evidence bundle, profile metadata), returns its id as `answerTraceId`, and the workflow node's failed-retrieval capture attaches to that trace instead of creating a second record. - The quality trace list exposes and filters by `source`; traces from other caller kinds are visible to any current reader of the space, and counts and scores fall back to the evidence embedded in the trace when no bundle row exists. - Overview accounting: retrieval-tests and Research tasks now emit `query.requested`, and the Research job state machine emits `query.completed` / `query.failed` on terminal stages (wired for both the in-process gateway and the durable runtime), so query volume, answer rate and outcomes include every caller. Activity details keep `source` and `taskKind`. - Console and service trace routes accept a `source` filter; the retrieval test page shows a source badge and an all / retrieval tests / workflow filter, with translations for every locale. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015zw5G5SX3HmVfnZof6YWAc --- .../console/knowledge_fs/resources.py | 6 +- .../service_api/knowledge_fs/resources.py | 7 +- .../knowledge_retrieval_v2_node.py | 8 +- api/knowledge-fs-contract.lock.json | 4 +- api/openapi/markdown/console-openapi.md | 32 +++- api/openapi/markdown/service-openapi.md | 9 ++ api/services/knowledge_fs/data_facade.py | 19 ++- api/services/knowledge_fs/product_dto.py | 14 ++ .../test_knowledge_retrieval_v2_node.py | 19 +++ .../services/test_knowledge_fs_product_dto.py | 36 +++++ knowledge-fs/apps/api/src/index.ts | 3 + knowledge-fs/apps/api/src/migrate.test.ts | 3 + .../api/src/research-task-runtime-options.ts | 5 + .../adapters/src/migration-runner.test.ts | 2 + .../packages/api/src/answer-trace-recorder.ts | 4 +- .../api/src/answer-trace-repository.test.ts | 56 +++++++ .../api/src/answer-trace-repository.ts | 4 + .../api/src/answer-trace-source.test.ts | 29 ++++ .../packages/api/src/answer-trace-source.ts | 45 ++++++ .../packages/api/src/gateway-sse-responses.ts | 4 + knowledge-fs/packages/api/src/gateway.test.ts | 2 +- knowledge-fs/packages/api/src/index.ts | 2 + .../api/src/knowledge-space-overview.ts | 4 + ...uality-control-database-repository.test.ts | 141 ++++++++++++++++++ .../quality-control-database-repository.ts | 67 +++++++-- .../api/src/quality-control-handlers.ts | 1 + .../api/src/quality-control-routes.ts | 4 +- .../packages/api/src/quality-control.ts | 4 + .../packages/api/src/query-handlers.ts | 5 + .../api/src/research-task-handlers.test.ts | 27 ++++ .../api/src/research-task-handlers.ts | 74 +++++++++ .../api/src/research-task-job.test.ts | 71 +++++++++ .../packages/api/src/research-task-job.ts | 45 ++++++ .../api/src/retrieval-test-handlers.test.ts | 9 +- .../api/src/retrieval-test-handlers.ts | 29 +++- .../packages/api/src/retrieval-test-routes.ts | 2 + .../packages/api/src/retrieval-test.test.ts | 78 ++++++++++ .../packages/api/src/retrieval-test.ts | 46 ++++++ .../api/src/workflow-failed-retrieval.test.ts | 47 ++++++ .../api/src/workflow-failed-retrieval.ts | 40 +++-- knowledge-fs/packages/core/src/models.ts | 21 +++ .../0053_answer_trace_source.postgres.sql | 11 ++ .../0053_answer_trace_source.tidb.sql | 11 ++ .../src/migration-artifacts.generated.ts | 2 + .../database/src/migration-file.test.ts | 4 + .../api/console/knowledge-fs/types.gen.ts | 5 + .../api/console/knowledge-fs/zod.gen.ts | 11 ++ .../generated/api/service/types.gen.ts | 5 + .../generated/api/service/zod.gen.ts | 39 +++-- .../new-rag/retrieval/__tests__/model.spec.ts | 32 ++++ .../new-rag/retrieval/history-panel.tsx | 26 +++- web/features/new-rag/retrieval/history.tsx | 8 + web/features/new-rag/retrieval/model.ts | 11 ++ web/features/new-rag/retrieval/state/graph.ts | 26 +++- .../new-rag/retrieval/state/scoped.ts | 4 + web/i18n/ar-TN/knowledge-space.json | 9 ++ web/i18n/de-DE/knowledge-space.json | 9 ++ web/i18n/en-US/knowledge-space.json | 9 ++ web/i18n/es-ES/knowledge-space.json | 9 ++ web/i18n/fa-IR/knowledge-space.json | 9 ++ web/i18n/fr-FR/knowledge-space.json | 9 ++ web/i18n/hi-IN/knowledge-space.json | 9 ++ web/i18n/id-ID/knowledge-space.json | 9 ++ web/i18n/it-IT/knowledge-space.json | 9 ++ web/i18n/ja-JP/knowledge-space.json | 9 ++ web/i18n/ko-KR/knowledge-space.json | 9 ++ web/i18n/lo-LA/knowledge-space.json | 9 ++ web/i18n/nl-NL/knowledge-space.json | 9 ++ web/i18n/pl-PL/knowledge-space.json | 9 ++ web/i18n/pt-BR/knowledge-space.json | 9 ++ web/i18n/ro-RO/knowledge-space.json | 9 ++ web/i18n/ru-RU/knowledge-space.json | 9 ++ web/i18n/sl-SI/knowledge-space.json | 9 ++ web/i18n/th-TH/knowledge-space.json | 9 ++ web/i18n/tr-TR/knowledge-space.json | 9 ++ web/i18n/uk-UA/knowledge-space.json | 9 ++ web/i18n/vi-VN/knowledge-space.json | 9 ++ web/i18n/zh-Hans/knowledge-space.json | 9 ++ web/i18n/zh-Hant/knowledge-space.json | 9 ++ 79 files changed, 1381 insertions(+), 58 deletions(-) create mode 100644 knowledge-fs/packages/api/src/answer-trace-source.test.ts create mode 100644 knowledge-fs/packages/api/src/answer-trace-source.ts create mode 100644 knowledge-fs/packages/database/migrations/0053_answer_trace_source.postgres.sql create mode 100644 knowledge-fs/packages/database/migrations/0053_answer_trace_source.tidb.sql diff --git a/api/controllers/console/knowledge_fs/resources.py b/api/controllers/console/knowledge_fs/resources.py index 83362e3a39f..67a3b022dc3 100644 --- a/api/controllers/console/knowledge_fs/resources.py +++ b/api/controllers/console/knowledge_fs/resources.py @@ -240,6 +240,7 @@ from services.knowledge_fs.product_dto import ( KnowledgeFSStreamCapabilityResponse, KnowledgeFSTraceEntriesQuery, KnowledgeFSTraceEntryListResponse, + KnowledgeFSTraceListQuery, KnowledgeFSTraceListResponse, KnowledgeFSUploadPartPresignPayload, KnowledgeFSUploadSessionAbortPayload, @@ -3351,7 +3352,7 @@ class KnowledgeFSSpaceResearchTaskPartialsApi(Resource): @console_ns.route("/knowledge-fs/spaces//traces") class KnowledgeFSSpaceTracesApi(Resource): - @console_ns.doc(params=query_params_from_model(KnowledgeFSCursorQuery)) + @console_ns.doc(params=query_params_from_model(KnowledgeFSTraceListQuery)) @console_ns.response( HTTPStatus.OK, "KnowledgeFS traces", @@ -3363,12 +3364,13 @@ class KnowledgeFSSpaceTracesApi(Resource): @_knowledge_fs_errors def get(self, control_space_id: str): actor_id, tenant_id = _actor() - query = KnowledgeFSCursorQuery.model_validate(request.args.to_dict()) + query = KnowledgeFSTraceListQuery.model_validate(request.args.to_dict()) result = _console_services().facade.list_traces( tenant_id=tenant_id, account_id=actor_id, control_space_id=control_space_id, cursor=query.cursor, + source=query.source, ) return dump_response(KnowledgeFSTraceListResponse, result) diff --git a/api/controllers/service_api/knowledge_fs/resources.py b/api/controllers/service_api/knowledge_fs/resources.py index a7ea7bd76c4..b4351ab8d89 100644 --- a/api/controllers/service_api/knowledge_fs/resources.py +++ b/api/controllers/service_api/knowledge_fs/resources.py @@ -84,6 +84,7 @@ from services.knowledge_fs.product_dto import ( KnowledgeFSSourceUpdatePayload, KnowledgeFSTraceEntriesQuery, KnowledgeFSTraceEntryListResponse, + KnowledgeFSTraceListQuery, KnowledgeFSTraceListResponse, ) from services.knowledge_fs.product_operations import product_operation_action @@ -955,7 +956,7 @@ class KnowledgeFSServiceResearchTaskPartialsApi(Resource): @service_api_ns.route("/knowledge-fs/spaces//traces") class KnowledgeFSServiceTracesApi(Resource): - @service_api_ns.doc(params=query_params_from_model(KnowledgeFSCursorQuery)) + @service_api_ns.doc(params=query_params_from_model(KnowledgeFSTraceListQuery)) @service_api_ns.response( HTTPStatus.OK, "KnowledgeFS traces", @@ -965,11 +966,11 @@ class KnowledgeFSServiceTracesApi(Resource): def get(self, control_space_id: str): runtime = _runtime() profile = _profile(runtime, operation_id="listTraces", control_space_id=control_space_id) - query = KnowledgeFSCursorQuery.model_validate(request.args.to_dict()) + query = KnowledgeFSTraceListQuery.model_validate(request.args.to_dict()) raw = runtime.facade.execute_service( profile=profile, operation_id="listTraces", - query=(("cursor", query.cursor),) if query.cursor else (), + query=tuple((name, value) for name, value in (("cursor", query.cursor), ("source", query.source)) if value), ) return dump_response(KnowledgeFSTraceListResponse, raw) diff --git a/api/core/workflow/nodes/knowledge_retrieval_v2/knowledge_retrieval_v2_node.py b/api/core/workflow/nodes/knowledge_retrieval_v2/knowledge_retrieval_v2_node.py index a81c4dd65ea..6cd5b9e8aa1 100644 --- a/api/core/workflow/nodes/knowledge_retrieval_v2/knowledge_retrieval_v2_node.py +++ b/api/core/workflow/nodes/knowledge_retrieval_v2/knowledge_retrieval_v2_node.py @@ -6,7 +6,7 @@ import logging import math import time import uuid -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import Generator, Mapping, Sequence from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from dataclasses import dataclass @@ -409,7 +409,7 @@ class KnowledgeRetrievalV2Node(Node[KnowledgeRetrievalV2NodeData]): @staticmethod @contextmanager - def _map_space_errors(control_space_id: str, *, action: str) -> Iterator[None]: + def _map_space_errors(control_space_id: str, *, action: str) -> Generator[None]: try: yield except KnowledgeFSAppChannelDisabledError as exc: @@ -807,7 +807,9 @@ class KnowledgeRetrievalV2Node(Node[KnowledgeRetrievalV2NodeData]): control_space_id=control_space_id, query=query, mode=response.mode, - retrieval_trace_id=response.trace_id, + # Prefer the recorded history trace so the capture attaches to it instead of + # creating a second record; older gateways only return the transport trace. + retrieval_trace_id=response.answer_trace_id or response.trace_id, ) except Exception: # The helper owns broker failures in production. Keep a second boundary here so a diff --git a/api/knowledge-fs-contract.lock.json b/api/knowledge-fs-contract.lock.json index 86e8b139c14..6b1aa89559e 100644 --- a/api/knowledge-fs-contract.lock.json +++ b/api/knowledge-fs-contract.lock.json @@ -1,7 +1,7 @@ { "schemaVersion": 5, - "subtreeTree": "219d157af4b240de61ce77c7c3473bdca699addd", - "openapiSha256": "edeae0b8fd3bd6dc9512b396da508bb99862b12ad0faeebc130996c108496d85", + "subtreeTree": "dd565881a8b5a5c113292abeaef493579eccf97c", + "openapiSha256": "20e7f698679f6c9b51aba2cc386e49eee1fbc76d05b1c0c35609963c5fcc105a", "capabilityV2AuthManifestSha256": "e322a2fa779d1f40b95c54c1021cffecaec77abbd7b34899573dcdf4ff353109", "capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3", "productOperationManifestSha256": "e4611c61c69478f20c1e1e482c44d8c48e48adfc67f1e8941ce36225e10b32c0", diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index a08380d1e26..60315c22924 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -9053,6 +9053,7 @@ Claim a workspace-staged upload. Multipart file bodies remain accepted as a lega | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | cursor | query | | No | string | +| source | query | | No | string,
**Available values:** "agent", "mcp", "retrieval_test", "service_api", "workflow" | | control_space_id | path | | Yes | string | #### Responses @@ -21056,6 +21057,7 @@ Input field definition for snippet parameters. | mode | string,
**Available values:** "auto", "deep", "fast", "research" | *Enum:* `"auto"`, `"deep"`, `"fast"`, `"research"` | Yes | | query | string | | Yes | | query_images | [ [KnowledgeFSQueryImageResponse](#knowledgefsqueryimageresponse) ] | | No | +| source | [KnowledgeFSTraceSource](#knowledgefstracesource) | | No | | steps | [ [KnowledgeFSAnswerTraceStepResponse](#knowledgefsanswertracestepresponse) ] | | Yes | #### KnowledgeFSAnswerTraceStepResponse @@ -21344,10 +21346,20 @@ Input field definition for snippet parameters. | ---- | ---- | ----------- | -------- | | KnowledgeFSControlSpaceVisibility | string | | | +#### KnowledgeFSCrawlImportPagePayload + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| content | string | | Yes | +| description | string | | No | +| sourceUrl | string | | Yes | +| title | string | | No | + #### KnowledgeFSCrawlImportPayload | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | +| pages | [ [KnowledgeFSCrawlImportPagePayload](#knowledgefscrawlimportpagepayload) ] | | No | | sourceUrls | [ string ] | | Yes | #### KnowledgeFSCrawlPreviewPageListQuery @@ -21918,6 +21930,7 @@ Input field definition for snippet parameters. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | description | string | | No | +| page_id | string | | No | | source_url | string | | Yes | | title | string | | No | @@ -21956,6 +21969,7 @@ Input field definition for snippet parameters. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | canonical_url | string | | No | +| pageId | string | | No | | source_url | string | | Yes | | title | string | | No | @@ -21972,6 +21986,7 @@ Input field definition for snippet parameters. | parameters | object | | No | | pluginId | string | | No | | previewConfigurationFingerprint | string | | No | +| previewJobId | string | | No | | provider | string | | Yes | | providerDisplayName | string | | No | | root_url | string | | Yes | @@ -22390,9 +22405,17 @@ Input field definition for snippet parameters. #### KnowledgeFSProductPermission +Product-level capability of a caller on one KnowledgeFS space. + +These values are what the console returns as ``permission_keys`` so the web can gate +its UI. Enterprise RBAC is not evaluated in this vocabulary: KnowledgeFS spaces reuse +the legacy knowledge base (``dataset_*``) permission points, and each capability is +granted when the caller holds the dataset permission it maps to (see +:data:`RBAC_PERMISSION_BY_PRODUCT_PERMISSION`). + | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| KnowledgeFSProductPermission | string | | | +| KnowledgeFSProductPermission | string | Product-level capability of a caller on one KnowledgeFS space. These values are what the console returns as ``permission_keys`` so the web can gate its UI. Enterprise RBAC is not evaluated in this vocabulary: KnowledgeFS spaces reuse the legacy knowledge base (``dataset_*``) permission points, and each capability is granted when the caller holds the dataset permission it maps to (see :data:`RBAC_PERMISSION_BY_PRODUCT_PERMISSION`). | | #### KnowledgeFSProductRerankProfile @@ -23476,6 +23499,7 @@ the file name and a short-lived signed preview URL for files the caller still ow | query_images | [ [KnowledgeFSQueryImageResponse](#knowledgefsqueryimageresponse) ] | | No | | result_count | integer | | Yes | | scores | [KnowledgeFSTraceScoresResponse](#knowledgefstracescoresresponse) | | Yes | +| source | [KnowledgeFSTraceSource](#knowledgefstracesource) | | No | | stages | [ [KnowledgeFSTraceStageResponse](#knowledgefstracestageresponse) ] | | Yes | #### KnowledgeFSTraceScoresResponse @@ -23486,6 +23510,12 @@ the file name and a short-lived signed preview URL for files the caller still ow | rerank | number | | No | | retrieval | number | | No | +#### KnowledgeFSTraceSource + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| KnowledgeFSTraceSource | string | | | + #### KnowledgeFSTraceStageResponse | Name | Type | Description | Required | diff --git a/api/openapi/markdown/service-openapi.md b/api/openapi/markdown/service-openapi.md index 56703a961bc..1697ff31d91 100644 --- a/api/openapi/markdown/service-openapi.md +++ b/api/openapi/markdown/service-openapi.md @@ -663,6 +663,7 @@ Deprecated legacy alias for updating an existing document by providing text cont | Name | Located in | Description | Required | Schema | | ---- | ---------- | ----------- | -------- | ------ | | cursor | query | | No | string | +| source | query | | No | string,
**Available values:** "agent", "mcp", "retrieval_test", "service_api", "workflow" | | control_space_id | path | | Yes | string | #### Responses @@ -4274,6 +4275,7 @@ Model class for i18n object. | mode | string,
**Available values:** "auto", "deep", "fast", "research" | *Enum:* `"auto"`, `"deep"`, `"fast"`, `"research"` | Yes | | query | string | | Yes | | query_images | [ [KnowledgeFSQueryImageResponse](#knowledgefsqueryimageresponse) ] | | No | +| source | [KnowledgeFSTraceSource](#knowledgefstracesource) | | No | | steps | [ [KnowledgeFSAnswerTraceStepResponse](#knowledgefsanswertracestepresponse) ] | | Yes | #### KnowledgeFSAnswerTraceStepResponse @@ -5223,6 +5225,7 @@ the file name and a short-lived signed preview URL for files the caller still ow | query_images | [ [KnowledgeFSQueryImageResponse](#knowledgefsqueryimageresponse) ] | | No | | result_count | integer | | Yes | | scores | [KnowledgeFSTraceScoresResponse](#knowledgefstracescoresresponse) | | Yes | +| source | [KnowledgeFSTraceSource](#knowledgefstracesource) | | No | | stages | [ [KnowledgeFSTraceStageResponse](#knowledgefstracestageresponse) ] | | Yes | #### KnowledgeFSTraceScoresResponse @@ -5233,6 +5236,12 @@ the file name and a short-lived signed preview URL for files the caller still ow | rerank | number | | No | | retrieval | number | | No | +#### KnowledgeFSTraceSource + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| KnowledgeFSTraceSource | string | | | + #### KnowledgeFSTraceStageResponse | Name | Type | Description | Required | diff --git a/api/services/knowledge_fs/data_facade.py b/api/services/knowledge_fs/data_facade.py index a043287e6da..e0ab9adb217 100644 --- a/api/services/knowledge_fs/data_facade.py +++ b/api/services/knowledge_fs/data_facade.py @@ -2079,14 +2079,20 @@ class KnowledgeFSDataFacade: return tasks def list_traces( - self, *, tenant_id: str, account_id: str, control_space_id: str, cursor: str | None = None + self, + *, + tenant_id: str, + account_id: str, + control_space_id: str, + cursor: str | None = None, + source: str | None = None, ) -> KnowledgeFSTraceListResponse: raw = self._interactive( tenant_id=tenant_id, account_id=account_id, control_space_id=control_space_id, operation_id="listTraces", - query=(("cursor", cursor),) if cursor else (), + query=_trace_list_query(cursor=cursor, source=source), ) traces = KnowledgeFSTraceListResponse.model_validate(raw) self._attach_query_image_previews( @@ -2738,3 +2744,12 @@ def _assert_sse_bff_ready(operation_id: str) -> None: __all__ = ["KnowledgeFSDataFacade"] + + +def _trace_list_query(*, cursor: str | None, source: str | None) -> tuple[tuple[str, str], ...]: + query: list[tuple[str, str]] = [] + if cursor: + query.append(("cursor", cursor)) + if source: + query.append(("source", source)) + return tuple(query) diff --git a/api/services/knowledge_fs/product_dto.py b/api/services/knowledge_fs/product_dto.py index a2fc86dc74d..b18e6912799 100644 --- a/api/services/knowledge_fs/product_dto.py +++ b/api/services/knowledge_fs/product_dto.py @@ -498,6 +498,15 @@ class KnowledgeFSCursorQuery(BaseModel): model_config = ConfigDict(extra="forbid") +# Who produced a retrieval trace: the console retrieval test, a KnowledgeFS workflow node, the +# service API, an agent, or an MCP client. Mirrors KnowledgeFS `AnswerTraceSourceSchema`. +type KnowledgeFSTraceSource = Literal["retrieval_test", "workflow", "service_api", "agent", "mcp"] + + +class KnowledgeFSTraceListQuery(KnowledgeFSCursorQuery): + source: KnowledgeFSTraceSource | None = Field(default=None) + + type KnowledgeFSConsistencyClass = Literal[ "path-consistent", "snapshot-consistent", @@ -3133,6 +3142,9 @@ class KnowledgeFSRetrievalTestMetricsResponse(ResponseModel): class KnowledgeFSRetrievalTestResponse(ResponseModel): + # AnswerTrace recorded for this run in the space's retrieval history; a failed-retrieval capture + # attaches to it so one record stays per retrieval. + answer_trace_id: str | None = Field(default=None, validation_alias=AliasChoices("answer_trace_id", "answerTraceId")) items: list[KnowledgeFSRetrievalTestItemResponse] = Field(max_length=100) metrics: KnowledgeFSRetrievalTestMetricsResponse mode: Literal["deep", "fast", "research"] @@ -3334,6 +3346,7 @@ class KnowledgeFSAnswerTraceResponse(ResponseModel): validation_alias=AliasChoices("query_images", "queryImages"), exclude_if=lambda value: not value, ) + source: KnowledgeFSTraceSource = "retrieval_test" steps: list[KnowledgeFSAnswerTraceStepResponse] @@ -3423,6 +3436,7 @@ class KnowledgeFSTraceResponse(ResponseModel): ) result_count: int = Field(ge=0, validation_alias=AliasChoices("result_count", "resultCount")) scores: KnowledgeFSTraceScoresResponse + source: KnowledgeFSTraceSource = "retrieval_test" stages: list[KnowledgeFSTraceStageResponse] diff --git a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval_v2/test_knowledge_retrieval_v2_node.py b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval_v2/test_knowledge_retrieval_v2_node.py index a4d8e5f9d70..1c9b15feec0 100644 --- a/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval_v2/test_knowledge_retrieval_v2_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/knowledge_retrieval_v2/test_knowledge_retrieval_v2_node.py @@ -1328,3 +1328,22 @@ def test_multi_space_calls_are_concurrent_but_never_exceed_the_bound() -> None: assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED assert service.max_active == 4 + + +def test_quality_capture_prefers_the_recorded_history_trace_over_the_transport_trace( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Newer gateways return the AnswerTrace id so the capture attaches to the same history record.""" + dispatched: list[dict[str, object]] = [] + monkeypatch.setattr( + node_module, + "enqueue_workflow_failed_retrieval_capture", + lambda **kwargs: dispatched.append(kwargs), + ) + recorded = _empty_response(mode="fast", space="a").model_copy( + update={"answer_trace_id": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60"} + ) + result = _node(service=RecordingCapabilityService({"space-a": recorded}), spaces=["space-a"])._run() + + assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED + assert [call["retrieval_trace_id"] for call in dispatched] == ["018f0d60-7a49-7cc2-9c1b-5b36f18f2c60"] diff --git a/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py b/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py index d870aa9628a..cd56d6e31a3 100644 --- a/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py +++ b/api/tests/unit_tests/services/test_knowledge_fs_product_dto.py @@ -62,6 +62,7 @@ from services.knowledge_fs.product_dto import ( KnowledgeFSSpaceCreatePayload, KnowledgeFSSpaceListItemResponse, KnowledgeFSStatResponse, + KnowledgeFSTraceListQuery, KnowledgeFSTraceResponse, KnowledgeFSTreeResponse, KnowledgeFSUploadPartPresignPayload, @@ -1641,3 +1642,38 @@ def test_overview_dtos_accept_the_knowledge_fs_wire_shape() -> None: assert KnowledgeFSOverviewWindowQuery().window == "24h" with pytest.raises(ValidationError): KnowledgeFSOverviewWindowQuery.model_validate({"window": "1h"}) + + +def test_trace_responses_carry_the_caller_source_and_default_to_retrieval_tests() -> None: + """Workflow-node retrievals share the history with console retrieval tests, distinguished by source.""" + base = { + "completed": True, + "created_at": "2026-09-04T10:00:00.000Z", + "id": "trace-1", + "mode": "fast", + "profile": {}, + "query": "why", + "result_count": 2, + "scores": {}, + "stages": [], + } + + assert KnowledgeFSTraceResponse.model_validate(base).source == "retrieval_test" + assert KnowledgeFSTraceResponse.model_validate({**base, "source": "workflow"}).model_dump(mode="json")[ + "source" + ] == ("workflow") + assert KnowledgeFSTraceListQuery.model_validate({"source": "workflow"}).source == "workflow" + assert KnowledgeFSTraceListQuery.model_validate({}).source is None + with pytest.raises(ValidationError): + KnowledgeFSTraceListQuery.model_validate({"source": "console"}) + + +def test_retrieval_test_response_exposes_the_recorded_history_trace_id() -> None: + base = {"items": [], "metrics": {"degradationFlags": [], "totalMs": 4}, "mode": "fast", "traceId": "trace-a"} + assert KnowledgeFSRetrievalTestResponse.model_validate(base).answer_trace_id is None + assert ( + KnowledgeFSRetrievalTestResponse.model_validate( + {**base, "answerTraceId": "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60"} + ).answer_trace_id + == "018f0d60-7a49-7cc2-9c1b-5b36f18f2c60" + ) diff --git a/knowledge-fs/apps/api/src/index.ts b/knowledge-fs/apps/api/src/index.ts index 05e910953fd..9e05906e7d8 100644 --- a/knowledge-fs/apps/api/src/index.ts +++ b/knowledge-fs/apps/api/src/index.ts @@ -943,6 +943,9 @@ const researchTaskRuntime = })}\n`, ); }, + ...(databaseRepositories.gatewayOptions.knowledgeSpaceOverview + ? { overview: databaseRepositories.gatewayOptions.knowledgeSpaceOverview } + : {}), partials: databaseRepositories.researchTaskPartialResults, progress: databaseRepositories.researchTaskProgressEvents, ...(researchProjectionSnapshotResolver diff --git a/knowledge-fs/apps/api/src/migrate.test.ts b/knowledge-fs/apps/api/src/migrate.test.ts index 6ddc9610f0a..c36b6cbc5be 100644 --- a/knowledge-fs/apps/api/src/migrate.test.ts +++ b/knowledge-fs/apps/api/src/migrate.test.ts @@ -153,6 +153,8 @@ describe("runApiDatabaseMigrations", () => { "insert", "schema", "insert", + "schema", + "insert", ]); expect(migrationSql).toHaveLength(expectedPostgresMigrationIds.length); expect(migrationSql[2]).toContain("-- Migration id: 0003_projection_set_publications\n"); @@ -236,6 +238,7 @@ describe("runApiDatabaseMigrations", () => { ); expect(migrationSql[50]).toContain("-- Migration id: 0051_failed_query_trace_outcome_index\n"); expect(migrationSql[51]).toContain("-- Migration id: 0052_namespace_source_previews\n"); + expect(migrationSql[52]).toContain("-- Migration id: 0053_answer_trace_source\n"); expect(closed).toBe(true); }); diff --git a/knowledge-fs/apps/api/src/research-task-runtime-options.ts b/knowledge-fs/apps/api/src/research-task-runtime-options.ts index ddb92e439d8..6b1cbb05d4b 100644 --- a/knowledge-fs/apps/api/src/research-task-runtime-options.ts +++ b/knowledge-fs/apps/api/src/research-task-runtime-options.ts @@ -7,6 +7,7 @@ import { type KnowledgeGatewayOptions, type KnowledgeSpaceAccessService, type KnowledgeSpaceManifestRepository, + type KnowledgeSpaceOverviewRepository, type ModelInputModalityResolver, type PublishedProjectionReadSnapshotResolver, type QueryGenerator, @@ -46,6 +47,8 @@ export interface CreateApiResearchTaskRuntimeOptions { readonly researchTaskJob?: { readonly id: string; readonly stage: string } | undefined; }) => void) | undefined; + /** Overview activity sink so durable Research tasks count as answered/failed queries. */ + readonly overview?: Pick | undefined; readonly partials: ResearchTaskPartialResultRepository; readonly progress: ResearchTaskProgressRepository; readonly projectionSnapshotResolver?: PublishedProjectionReadSnapshotResolver | undefined; @@ -89,6 +92,7 @@ export function createApiResearchTaskRuntime({ metrics, modelInputModalityResolver, onError, + overview, partials, progress, projectionSnapshotResolver, @@ -120,6 +124,7 @@ export function createApiResearchTaskRuntime({ ...(metrics ? { metrics } : {}), ...(modelInputModalityResolver ? { modelInputModalityResolver } : {}), ...(onError ? { onError } : {}), + ...(overview ? { overview } : {}), repository, }); const runtime = createResearchTaskRuntime({ diff --git a/knowledge-fs/packages/adapters/src/migration-runner.test.ts b/knowledge-fs/packages/adapters/src/migration-runner.test.ts index f389123db29..e5ff6c2db1a 100644 --- a/knowledge-fs/packages/adapters/src/migration-runner.test.ts +++ b/knowledge-fs/packages/adapters/src/migration-runner.test.ts @@ -60,6 +60,7 @@ const answerTraceQueryImagesMigrationId = "0049_answer_trace_query_images"; const deletionJobCapabilityProvenanceMigrationId = "0050_deletion_job_capability_provenance"; const failedQueryTraceOutcomeIndexMigrationId = "0051_failed_query_trace_outcome_index"; const namespaceSourcePreviewsMigrationId = "0052_namespace_source_previews"; +const answerTraceSourceMigrationId = "0053_answer_trace_source"; const migrationsAfterDurableDeletion = [ versionedSpaceProfilesMigrationId, profilePublicationBindingsMigrationId, @@ -96,6 +97,7 @@ const migrationsAfterDurableDeletion = [ deletionJobCapabilityProvenanceMigrationId, failedQueryTraceOutcomeIndexMigrationId, namespaceSourcePreviewsMigrationId, + answerTraceSourceMigrationId, ] as const; const migrationsAfterTidbBaselineRepair = [ spaceAccessControlMigrationId, diff --git a/knowledge-fs/packages/api/src/answer-trace-recorder.ts b/knowledge-fs/packages/api/src/answer-trace-recorder.ts index 3bcc4a7af27..a34b5b84682 100644 --- a/knowledge-fs/packages/api/src/answer-trace-recorder.ts +++ b/knowledge-fs/packages/api/src/answer-trace-recorder.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; -import { type AnswerTrace, AnswerTraceSchema } from "@knowledge/core"; +import { type AnswerTrace, AnswerTraceSchema, type AnswerTraceSource } from "@knowledge/core"; import type { QueryImageMetadata } from "./query-images"; import { @@ -23,6 +23,8 @@ interface RecordAnswerTraceBaseInput { readonly mode: AnswerTrace["mode"]; readonly query: string; readonly queryImages?: readonly QueryImageMetadata[] | undefined; + /** Caller that produced the trace; omitted means a console retrieval test. */ + readonly source?: AnswerTraceSource | undefined; readonly steps: readonly RecordAnswerTraceStepInput[]; readonly traceId?: string | undefined; } diff --git a/knowledge-fs/packages/api/src/answer-trace-repository.test.ts b/knowledge-fs/packages/api/src/answer-trace-repository.test.ts index f7fda991c51..0dd83950012 100644 --- a/knowledge-fs/packages/api/src/answer-trace-repository.test.ts +++ b/knowledge-fs/packages/api/src/answer-trace-repository.test.ts @@ -293,6 +293,7 @@ describe("AnswerTrace repositories", () => { true, trace.createdAt, null, + null, ], tableName: "answer_traces", }), @@ -1219,3 +1220,58 @@ function hasWholeSpaceDeletionFence( ) ); } + +describe("AnswerTrace source column", () => { + it.each(["postgres", "tidb"] as const)("round-trips the trace source on %s", async (kind) => { + let storedTrace: DatabaseRow | undefined; + const inserts: DatabaseExecuteInput[] = []; + const executor = async (input: DatabaseExecuteInput): Promise => { + if (input.tableName === "knowledge_spaces") { + return { rows: [{ id: input.params[0], tenant_id: "tenant-1" }], rowsAffected: 1 }; + } + if (input.tableName === "answer_traces" && input.operation === "select") { + return { rows: storedTrace ? [storedTrace] : [], rowsAffected: storedTrace ? 1 : 0 }; + } + if (input.tableName === "answer_traces" && input.operation === "insert") { + inserts.push(input); + storedTrace = { + access_channel: input.params[10], + capability_grant_id: input.params[3], + completed: input.params[11], + created_at: input.params[12], + evidence_bundle_id: input.params[4], + id: input.params[0], + knowledge_space_id: input.params[2], + mode: input.params[6], + permission_snapshot_id: input.params[8], + permission_snapshot_revision: input.params[9], + query: input.params[5], + query_images: input.params[13], + source: input.params[14], + subject_id: input.params[7], + tenant_id: input.params[1], + }; + return { rows: [], rowsAffected: 1 }; + } + return { rows: [], rowsAffected: input.operation === "insert" ? 1 : 0 }; + }; + const repository = createDatabaseAnswerTraceRepository({ + database: createSchemaDatabaseAdapter({ + executor, + kind, + transaction: async (callback) => callback({ execute: executor }), + }), + }); + const trace = AnswerTraceSchema.parse({ ...databaseTrace(), source: "workflow" }); + + await repository.create(trace); + + expect(inserts[0]?.sql).toContain(kind === "postgres" ? '"source"' : "`source`"); + expect(inserts[0]?.params[14]).toBe("workflow"); + expect((await repository.getById(trace.id))?.source).toBe("workflow"); + + // Rows written before the column existed read back without a source (= retrieval test). + storedTrace = { ...storedTrace, source: null }; + expect((await repository.getById(trace.id))?.source).toBeUndefined(); + }); +}); diff --git a/knowledge-fs/packages/api/src/answer-trace-repository.ts b/knowledge-fs/packages/api/src/answer-trace-repository.ts index 0e31fb26c37..976319ab457 100644 --- a/knowledge-fs/packages/api/src/answer-trace-repository.ts +++ b/knowledge-fs/packages/api/src/answer-trace-repository.ts @@ -240,6 +240,7 @@ export function createDatabaseAnswerTraceRepository({ "completed", "created_at", "query_images", + "source", ]; const traceParams = [ persistedTrace.id, @@ -256,6 +257,7 @@ export function createDatabaseAnswerTraceRepository({ answerTraceCompleted(persistedTrace), persistedTrace.createdAt, persistedTrace.queryImages?.length ? JSON.stringify(persistedTrace.queryImages) : null, + persistedTrace.source ?? null, ] satisfies readonly DatabaseQueryValue[]; const admissionSpaceParameter = database.dialect === "postgres" @@ -605,6 +607,7 @@ function mapAnswerTraceRows(traceRow: DatabaseRow, stepRows: readonly DatabaseRo ); const accessChannel = optionalStringColumn(traceRow, "access_channel"); const tenantId = optionalStringColumn(traceRow, "tenant_id"); + const source = optionalStringColumn(traceRow, "source"); return parseAnswerTraceProvenance({ ...(capabilityGrantId === undefined ? {} : { capabilityGrantId }), @@ -626,6 +629,7 @@ function mapAnswerTraceRows(traceRow: DatabaseRow, stepRows: readonly DatabaseRo ...(traceRow.query_images == null ? {} : { queryImages: jsonArrayColumn(traceRow, "query_images") }), + ...(source === undefined ? {} : { source }), ...(subjectId === undefined ? {} : { subjectId }), steps: stepRows.map((row) => ({ endedAt: stringColumn(row, "ended_at"), diff --git a/knowledge-fs/packages/api/src/answer-trace-source.test.ts b/knowledge-fs/packages/api/src/answer-trace-source.test.ts new file mode 100644 index 00000000000..62c7b3e2abf --- /dev/null +++ b/knowledge-fs/packages/api/src/answer-trace-source.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { answerTraceSourceForCaller } from "./answer-trace-source"; + +describe("answerTraceSourceForCaller", () => { + it("prefers the precise Capability v2 caller kind over the collapsed gateway kind", () => { + // The gateway maps workflow grants to the "agent" access channel; history must keep them apart. + expect( + answerTraceSourceForCaller({ callerKind: "agent", capabilityCallerKind: "workflow" }), + ).toBe("workflow"); + expect(answerTraceSourceForCaller({ capabilityCallerKind: "agent" })).toBe("agent"); + expect(answerTraceSourceForCaller({ capabilityCallerKind: "mcp" })).toBe("mcp"); + expect(answerTraceSourceForCaller({ capabilityCallerKind: "service" })).toBe("service_api"); + expect(answerTraceSourceForCaller({ capabilityCallerKind: "internal_worker" })).toBe( + "service_api", + ); + expect(answerTraceSourceForCaller({ capabilityCallerKind: "interactive" })).toBe( + "retrieval_test", + ); + }); + + it("falls back to the gateway caller kind for legacy sessions and API keys", () => { + expect(answerTraceSourceForCaller({ callerKind: "api_key" })).toBe("service_api"); + expect(answerTraceSourceForCaller({ callerKind: "service_api" })).toBe("service_api"); + expect(answerTraceSourceForCaller({ callerKind: "agent" })).toBe("agent"); + expect(answerTraceSourceForCaller({ callerKind: "mcp" })).toBe("mcp"); + expect(answerTraceSourceForCaller({ callerKind: "interactive" })).toBe("retrieval_test"); + expect(answerTraceSourceForCaller({})).toBe("retrieval_test"); + }); +}); diff --git a/knowledge-fs/packages/api/src/answer-trace-source.ts b/knowledge-fs/packages/api/src/answer-trace-source.ts new file mode 100644 index 00000000000..c29d43f668d --- /dev/null +++ b/knowledge-fs/packages/api/src/answer-trace-source.ts @@ -0,0 +1,45 @@ +import type { AnswerTraceSource } from "@knowledge/core"; +import type { DifyCapabilityV2SanitizedGrant } from "./dify-capability-v2-grant"; +import type { KnowledgeSpaceCallerKind } from "./knowledge-space-authorization"; + +/** + * Derive the AnswerTrace source from the authenticated caller. + * + * Capability v2 grants carry the precise caller kind (the gateway collapses `workflow` into + * `agent` for access-channel purposes, which is too coarse for history); legacy sessions and + * API keys only expose the gateway caller kind. Anything unknown is a console retrieval test. + */ +export function answerTraceSourceForCaller({ + callerKind, + capabilityCallerKind, +}: { + readonly callerKind?: KnowledgeSpaceCallerKind | undefined; + readonly capabilityCallerKind?: DifyCapabilityV2SanitizedGrant["callerKind"] | undefined; +}): AnswerTraceSource { + switch (capabilityCallerKind) { + case "workflow": + return "workflow"; + case "agent": + return "agent"; + case "mcp": + return "mcp"; + case "service": + case "internal_worker": + return "service_api"; + case "interactive": + return "retrieval_test"; + default: + break; + } + switch (callerKind) { + case "agent": + return "agent"; + case "mcp": + return "mcp"; + case "api_key": + case "service_api": + return "service_api"; + default: + return "retrieval_test"; + } +} diff --git a/knowledge-fs/packages/api/src/gateway-sse-responses.ts b/knowledge-fs/packages/api/src/gateway-sse-responses.ts index 35d1ec15c04..e90ae87cdbf 100644 --- a/knowledge-fs/packages/api/src/gateway-sse-responses.ts +++ b/knowledge-fs/packages/api/src/gateway-sse-responses.ts @@ -1,4 +1,5 @@ import type { + AnswerTraceSource, AuthSubject, EvidenceBundle, KnowledgeSpaceEmbeddingProfile, @@ -36,6 +37,8 @@ import { export type QueryGenerationMode = "deep" | "fast" | "research"; export interface QueryGenerationInput { + /** Caller that produced this query; recorded on the AnswerTrace for history filtering. */ + readonly source?: AnswerTraceSource | undefined; readonly capabilityGrantId?: string | undefined; readonly embeddingProfile?: KnowledgeSpaceEmbeddingProfile | undefined; readonly embeddingInputModalities?: readonly ModelInputModality[] | undefined; @@ -472,6 +475,7 @@ async function recordAnswerTrace({ mode: input.mode, query: input.query, ...(input.queryImageMetadata?.length ? { queryImages: input.queryImageMetadata } : {}), + ...(input.source ? { source: input.source } : {}), steps: [ ...stageSteps, { diff --git a/knowledge-fs/packages/api/src/gateway.test.ts b/knowledge-fs/packages/api/src/gateway.test.ts index 9c15e4f9206..6da18a0337d 100644 --- a/knowledge-fs/packages/api/src/gateway.test.ts +++ b/knowledge-fs/packages/api/src/gateway.test.ts @@ -12872,7 +12872,7 @@ describe("createKnowledgeGateway", () => { trace.permissionSnapshot?.accessChannel, true, trace.createdAt, - null, + ...[null, trace.source ?? null], ], tableName: "answer_traces", }), diff --git a/knowledge-fs/packages/api/src/index.ts b/knowledge-fs/packages/api/src/index.ts index 10c4b70b4de..6530b2e37c4 100644 --- a/knowledge-fs/packages/api/src/index.ts +++ b/knowledge-fs/packages/api/src/index.ts @@ -1221,6 +1221,7 @@ export function createKnowledgeGateway({ createResearchTaskJobStateMachine({ generateId: generateResearchTaskJobId, jobs: adapter.jobs, + overview: overviewRepository, progress: createResearchTaskProgressPublisher({ repository: researchTaskProgressEvents, }), @@ -1984,6 +1985,7 @@ export function createKnowledgeGateway({ } : {}), now: () => Date.parse(now()), + overview: overviewRepository, researchTaskJobs, researchTaskPartialResults, researchTaskProgressEvents, diff --git a/knowledge-fs/packages/api/src/knowledge-space-overview.ts b/knowledge-fs/packages/api/src/knowledge-space-overview.ts index 1a945044d18..6c3cccf8c3a 100644 --- a/knowledge-fs/packages/api/src/knowledge-space-overview.ts +++ b/knowledge-fs/packages/api/src/knowledge-space-overview.ts @@ -503,7 +503,11 @@ const SAFE_ACTIVITY_DETAIL_KEYS = new Set([ "question", "questionTruncated", "reasonCode", + // Who issued a query (console retrieval test, workflow node, service API, agent, MCP). + "source", "statusCode", + // Distinguishes Research tasks from streamed queries in the query events. + "taskKind", ]); export function sanitizeKnowledgeSpaceActivityDetails( diff --git a/knowledge-fs/packages/api/src/quality-control-database-repository.test.ts b/knowledge-fs/packages/api/src/quality-control-database-repository.test.ts index 68fd648dedc..61bd00c184d 100644 --- a/knowledge-fs/packages/api/src/quality-control-database-repository.test.ts +++ b/knowledge-fs/packages/api/src/quality-control-database-repository.test.ts @@ -3008,3 +3008,144 @@ function permissionFenceRow( visibility: "all_members", }; } + +describe("answer trace history sources", () => { + const WORKFLOW_TRACE_ID = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c70"; + const evidenceBundle = { + createdAt: NOW, + id: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c71", + items: [ + { + citations: [ + { documentAssetId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c72", documentVersion: 1 }, + ], + conflicts: [], + freshness: { status: "unknown" }, + metadata: {}, + nodeId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c73", + score: 0.8, + scores: { final: 0.8, retrieval: 0.7 }, + text: "workflow evidence", + }, + ], + missingEvidence: [], + query: "workflow question", + state: "answerable", + }; + + it.each(["postgres", "tidb"] as const)( + "lists workflow-sourced traces for any current reader and reads their embedded evidence on %s", + async (dialect) => { + const calls: DatabaseExecuteInput[] = []; + const database = testDatabase(dialect, async (input) => { + calls.push(input); + if (input.tableName === "answer_traces") { + return { + rows: [ + { + completed: dialect === "tidb" ? "1" : true, + created_at: NOW, + evidence_bundle_id: null, + evidence_items: null, + evidence_state: null, + id: WORKFLOW_TRACE_ID, + mode: "fast", + query: "workflow question", + source: "workflow", + }, + ], + rowsAffected: 1, + }; + } + if (input.tableName === "answer_trace_steps") { + const metadata = { evidenceBundle, resultCount: 1 }; + return { + rows: [ + { + ended_at: NOW, + id: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c74", + metadata: dialect === "tidb" ? JSON.stringify(metadata) : metadata, + name: "retrieval.test", + started_at: NOW, + status: "ok", + trace_id: WORKFLOW_TRACE_ID, + }, + ], + rowsAffected: 1, + }; + } + return { rows: [], rowsAffected: 0 }; + }); + const repository = createDatabaseQualityControlRepository({ database, maxListLimit: 100 }); + + const page = await repository.listTraces({ + candidateGrants: ["tenant:tenant-1"], + capabilityRequester: { callerKind: "interactive", subjectId: "reader-1" }, + knowledgeSpaceId: SPACE_ID, + limit: 10, + source: "workflow", + subjectId: "reader-1", + tenantId: "tenant-1", + }); + + expect(page.items[0]).toMatchObject({ + evidenceState: "answerable", + id: WORKFLOW_TRACE_ID, + resultCount: 1, + scores: { final: 0.8, retrieval: 0.7 }, + source: "workflow", + }); + const call = calls.find((candidate) => candidate.tableName === "answer_traces"); + // Own traces still join through the requester's grant; shared sources need only a current + // reader grant on the space, so the join is no longer an inner join. + expect(call?.sql).toContain("LEFT JOIN"); + expect(call?.sql).toContain("'workflow', 'service_api', 'agent', 'mcp'"); + expect(call?.sql).toContain("reader"); + expect(call?.sql).not.toContain("knowledge_space_permission_snapshots"); + expect(call?.params).toContain("workflow"); + }, + ); + + it.each(["postgres", "tidb"] as const)( + "treats rows without a stored source as retrieval tests when filtering on %s", + async (dialect) => { + const calls: DatabaseExecuteInput[] = []; + const database = testDatabase(dialect, async (input) => { + calls.push(input); + if (input.tableName === "answer_traces") { + return { + rows: [ + { + completed: dialect === "tidb" ? "1" : true, + created_at: NOW, + evidence_bundle_id: null, + evidence_items: [], + evidence_state: null, + id: TRACE_ID, + mode: "fast", + query: "legacy question", + source: null, + }, + ], + rowsAffected: 1, + }; + } + return { rows: [], rowsAffected: 0 }; + }); + const repository = createDatabaseQualityControlRepository({ database, maxListLimit: 100 }); + + const page = await repository.listTraces({ + candidateGrants: ["tenant:tenant-1"], + knowledgeSpaceId: SPACE_ID, + limit: 10, + source: "retrieval_test", + subjectId: "editor-1", + tenantId: "tenant-1", + }); + + expect(page.items[0]?.source).toBe("retrieval_test"); + const call = calls.find((candidate) => candidate.tableName === "answer_traces"); + expect(call?.sql).toMatch(/IS NULL OR trace\.[`"]source[`"] = /u); + }, + ); +}); diff --git a/knowledge-fs/packages/api/src/quality-control-database-repository.ts b/knowledge-fs/packages/api/src/quality-control-database-repository.ts index 834d2c7c4bb..b29805a8193 100644 --- a/knowledge-fs/packages/api/src/quality-control-database-repository.ts +++ b/knowledge-fs/packages/api/src/quality-control-database-repository.ts @@ -6,7 +6,12 @@ import type { DatabaseQueryValue, DatabaseRow, } from "@knowledge/core"; -import { QueryImageMetadataSchema } from "@knowledge/core"; +import { + type AnswerTraceSource, + AnswerTraceSourceSchema, + EvidenceBundleSchema, + QueryImageMetadataSchema, +} from "@knowledge/core"; import { answerTraceKnowledgeSpaceVisibilitySql } from "./answer-trace-repository"; import { resolveCapabilityJobPublicationGrant } from "./capability-job-fence"; @@ -1080,18 +1085,28 @@ async function listTraces( const params: DatabaseQueryValue[] = [input.tenantId, input.knowledgeSpaceId, input.subjectId]; let authorizationFilter: string; let authorizationJoin: string; + // A subject sees the traces it produced itself (joined through the grant or permission snapshot + // that authorized them) plus every trace another caller kind produced in the space (workflow + // nodes, agents, service API, MCP), provided the subject currently holds an authorization for + // the space whose content scope admits the caller's candidate grants. Console retrieval tests of + // other members stay private to their author, exactly as before. + const sharedSourcesSql = SHARED_ANSWER_TRACE_SOURCES.map((source) => `'${source}'`).join(", "); if (input.capabilityRequester) { params.push(input.capabilityRequester.callerKind, JSON.stringify(input.candidateGrants)); - authorizationJoin = `INNER JOIN ${q(database, "capability_grants")} capability ON capability.${q(database, "tenant_id")} = ${p(database, 1)} AND capability.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND capability.${q(database, "grant_id")} = trace.${q(database, "capability_grant_id")} AND capability.${q(database, "subject_id")} = ${p(database, 3)} AND capability.${q(database, "caller_kind")} = ${p(database, 4)}`; - authorizationFilter = permissionScopeSql( + authorizationJoin = `LEFT JOIN ${q(database, "capability_grants")} capability ON capability.${q(database, "tenant_id")} = ${p(database, 1)} AND capability.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND capability.${q(database, "grant_id")} = trace.${q(database, "capability_grant_id")} AND capability.${q(database, "subject_id")} = ${p(database, 3)} AND capability.${q(database, "caller_kind")} = ${p(database, 4)}`; + const ownTrace = `capability.${q(database, "grant_id")} IS NOT NULL AND ${permissionScopeSql( database, `capability.${q(database, "content_scope_ids")}`, p(database, 5), - ); + )}`; + const sharedTrace = `trace.${q(database, "source")} IN (${sharedSourcesSql}) AND EXISTS (SELECT 1 FROM ${q(database, "capability_grants")} reader WHERE reader.${q(database, "tenant_id")} = ${p(database, 1)} AND reader.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND reader.${q(database, "subject_id")} = ${p(database, 3)} AND reader.${q(database, "caller_kind")} = ${p(database, 4)} AND reader.${q(database, "state")} = 'active' AND ${permissionScopeSql(database, `reader.${q(database, "content_scope_ids")}`, p(database, 5))})`; + authorizationFilter = `((${ownTrace}) OR (${sharedTrace}))`; } else { params.push(JSON.stringify(input.candidateGrants)); - authorizationJoin = `INNER JOIN ${q(database, "knowledge_space_permission_snapshots")} permission ON permission.${q(database, "tenant_id")} = ${p(database, 1)} AND permission.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND permission.${q(database, "id")} = trace.${q(database, "permission_snapshot_id")} AND permission.${q(database, "subject_id")} = trace.${q(database, "subject_id")}`; - authorizationFilter = `trace.${q(database, "subject_id")} = ${p(database, 3)} AND ${permissionScopeSql(database, `permission.${q(database, "permission_scopes")}`, p(database, 4))}`; + authorizationJoin = `LEFT JOIN ${q(database, "knowledge_space_permission_snapshots")} permission ON permission.${q(database, "tenant_id")} = ${p(database, 1)} AND permission.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND permission.${q(database, "id")} = trace.${q(database, "permission_snapshot_id")} AND permission.${q(database, "subject_id")} = trace.${q(database, "subject_id")}`; + const ownTrace = `trace.${q(database, "subject_id")} = ${p(database, 3)} AND permission.${q(database, "id")} IS NOT NULL AND ${permissionScopeSql(database, `permission.${q(database, "permission_scopes")}`, p(database, 4))}`; + const sharedTrace = `trace.${q(database, "source")} IN (${sharedSourcesSql}) AND EXISTS (SELECT 1 FROM ${q(database, "knowledge_space_permission_snapshots")} reader WHERE reader.${q(database, "tenant_id")} = ${p(database, 1)} AND reader.${q(database, "knowledge_space_id")} = trace.${q(database, "knowledge_space_id")} AND reader.${q(database, "subject_id")} = ${p(database, 3)} AND ${permissionScopeSql(database, `reader.${q(database, "permission_scopes")}`, p(database, 4))})`; + authorizationFilter = `((${ownTrace}) OR (${sharedTrace}))`; } const traceTenantFilter = input.capabilityRequester ? `trace.${q(database, "tenant_id")} = ${p(database, 1)} AND ` @@ -1115,6 +1130,14 @@ async function listTraces( params.push(input.mode); filters.push(`trace.${q(database, "mode")} = ${p(database, params.length)}`); } + if (input.source) { + params.push(input.source); + filters.push( + input.source === "retrieval_test" + ? `(trace.${q(database, "source")} IS NULL OR trace.${q(database, "source")} = ${p(database, params.length)})` + : `trace.${q(database, "source")} = ${p(database, params.length)}`, + ); + } if (input.status) { filters.push( input.status === "completed" @@ -1176,7 +1199,13 @@ function mapTraceSummary( row: DatabaseRow, steps: readonly DatabaseRow[], ): QualityAnswerTraceSummary { - const evidenceItems = row.evidence_items == null ? [] : jsonArrayColumn(row, "evidence_items"); + // Retrieval tests recorded by workflow nodes carry their evidence inside the summary step instead + // of a persisted bundle row; read counts and scores from there when no bundle is joined. + const embeddedBundle = row.evidence_items == null ? embeddedEvidenceBundle(steps) : undefined; + const evidenceItems = + row.evidence_items == null + ? (embeddedBundle?.items ?? []) + : jsonArrayColumn(row, "evidence_items"); const queryImages = row.query_images == null ? [] @@ -1218,12 +1247,13 @@ function mapTraceSummary( ...(optionalStringColumn(row, "evidence_bundle_id") ? { evidenceBundleId: optionalStringColumn(row, "evidence_bundle_id") } : {}), - ...(optionalStringColumn(row, "evidence_state") - ? { evidenceState: optionalStringColumn(row, "evidence_state") } + ...((optionalStringColumn(row, "evidence_state") ?? embeddedBundle?.state) + ? { evidenceState: optionalStringColumn(row, "evidence_state") ?? embeddedBundle?.state } : {}), ...(score("final") !== undefined ? { finalScore: score("final") } : {}), id: stringColumn(row, "id"), mode: stringColumn(row, "mode") as QualityAnswerTraceSummary["mode"], + source: traceSourceColumn(row), ...(optionalStringColumn(row, "open_bad_case_id") ? { openBadCaseId: optionalStringColumn(row, "open_bad_case_id") } : {}), @@ -2226,3 +2256,22 @@ function p(database: DatabaseAdapter, position: number) { function jsonP(database: DatabaseAdapter, position: number) { return jsonInsertPlaceholder(database, position, undefined); } + +/** Every source except console retrieval tests; visible to any current reader of the space. */ +const SHARED_ANSWER_TRACE_SOURCES: readonly AnswerTraceSource[] = + AnswerTraceSourceSchema.options.filter((source) => source !== "retrieval_test"); + +function traceSourceColumn(row: DatabaseRow): AnswerTraceSource { + const parsed = AnswerTraceSourceSchema.safeParse(optionalStringColumn(row, "source")); + return parsed.success ? parsed.data : "retrieval_test"; +} + +function embeddedEvidenceBundle(steps: readonly DatabaseRow[]) { + for (const step of steps) { + const candidate = EvidenceBundleSchema.safeParse( + jsonObjectColumn(step, "metadata").evidenceBundle, + ); + if (candidate.success) return candidate.data; + } + return undefined; +} diff --git a/knowledge-fs/packages/api/src/quality-control-handlers.ts b/knowledge-fs/packages/api/src/quality-control-handlers.ts index 22a54745684..4cfb97c32d0 100644 --- a/knowledge-fs/packages/api/src/quality-control-handlers.ts +++ b/knowledge-fs/packages/api/src/quality-control-handlers.ts @@ -108,6 +108,7 @@ export function registerQualityControlHandlers({ limit: query.limit, ...(query.mode ? { mode: query.mode } : {}), ...(query.query ? { query: query.query } : {}), + ...(query.source ? { source: query.source } : {}), ...(query.status ? { status: query.status } : {}), subjectId: scope.subject.subjectId, tenantId: scope.subject.tenantId, diff --git a/knowledge-fs/packages/api/src/quality-control-routes.ts b/knowledge-fs/packages/api/src/quality-control-routes.ts index 425d4617759..3b1b16f689a 100644 --- a/knowledge-fs/packages/api/src/quality-control-routes.ts +++ b/knowledge-fs/packages/api/src/quality-control-routes.ts @@ -1,5 +1,5 @@ import { createRoute, z } from "@hono/zod-openapi"; -import { QueryImageMetadataSchema } from "@knowledge/core"; +import { AnswerTraceSourceSchema, QueryImageMetadataSchema } from "@knowledge/core"; import { ForbiddenResponse, UnauthorizedResponse } from "./gateway-openapi-contracts"; import { ErrorResponseSchema } from "./gateway-route-schemas"; @@ -28,6 +28,7 @@ export const QualityTraceHistoryQuerySchema = z limit: z.coerce.number().int().min(1).max(100).default(50), mode: z.enum(["auto", "deep", "fast", "research"]).optional(), query: z.string().trim().min(1).max(500).optional(), + source: AnswerTraceSourceSchema.optional(), status: z.enum(["completed", "failed"]).optional(), to: DateTime.optional(), }) @@ -58,6 +59,7 @@ export const QualityTraceSummarySchema = z queryImages: z.array(QueryImageMetadataSchema).max(4).optional(), resultCount: z.number().int().nonnegative(), scores: z.object({ final: ScoreSchema, rerank: ScoreSchema, retrieval: ScoreSchema }), + source: AnswerTraceSourceSchema, stages: z.array( z.object({ candidateCount: z.number().int().nonnegative().optional(), diff --git a/knowledge-fs/packages/api/src/quality-control.ts b/knowledge-fs/packages/api/src/quality-control.ts index 8914fe3355c..c4322b84dcc 100644 --- a/knowledge-fs/packages/api/src/quality-control.ts +++ b/knowledge-fs/packages/api/src/quality-control.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import type { + AnswerTraceSource, AuthSubject, KnowledgeSpaceEmbeddingProfile, KnowledgeSpaceRetrievalProfile, @@ -55,6 +56,8 @@ export interface QualityAnswerTraceSummary { readonly finalScore?: number | undefined; readonly id: string; readonly mode: "auto" | "deep" | "fast" | "research"; + /** Caller that produced the trace; legacy rows without a stored value are retrieval tests. */ + readonly source: AnswerTraceSource; /** The unresolved (open or replaying) bad case already filed for this trace, if any. */ readonly openBadCaseId?: string | undefined; readonly profile: { @@ -95,6 +98,7 @@ export interface QualityAnswerTraceHistoryInput { readonly knowledgeSpaceId: string; readonly limit: number; readonly mode?: "auto" | "deep" | "fast" | "research" | undefined; + readonly source?: AnswerTraceSource | undefined; readonly query?: string | undefined; readonly status?: "completed" | "failed" | undefined; readonly subjectId: string; diff --git a/knowledge-fs/packages/api/src/query-handlers.ts b/knowledge-fs/packages/api/src/query-handlers.ts index f3a8a13336e..87b1a7f3a5c 100644 --- a/knowledge-fs/packages/api/src/query-handlers.ts +++ b/knowledge-fs/packages/api/src/query-handlers.ts @@ -2,6 +2,7 @@ import type { OpenAPIHono } from "@hono/zod-openapi"; import { validateKnowledgeSpaceRetrievalProfileForMode } from "@knowledge/core"; import type { AnswerTraceRecorder } from "./answer-trace-recorder"; +import { answerTraceSourceForCaller } from "./answer-trace-source"; import { type AutoRetrievalModeResolver, resolveRetrievalModeRequest, @@ -500,6 +501,10 @@ export function registerQueryHandlers({ input: { ...(capabilityGrant ? { capabilityGrantId: capabilityGrant.grantId } : {}), knowledgeSpaceId: space.id, + source: answerTraceSourceForCaller({ + callerKind: context.get("callerKind"), + capabilityCallerKind: capabilityGrant?.callerKind, + }), ...(runtimeSnapshot?.embeddingProfile ? { embeddingProfile: runtimeSnapshot.embeddingProfile } : {}), diff --git a/knowledge-fs/packages/api/src/research-task-handlers.test.ts b/knowledge-fs/packages/api/src/research-task-handlers.test.ts index 5db0c7725f1..5273fa839c1 100644 --- a/knowledge-fs/packages/api/src/research-task-handlers.test.ts +++ b/knowledge-fs/packages/api/src/research-task-handlers.test.ts @@ -15,6 +15,7 @@ import { encodeResearchTaskListCursor, researchTaskRuntimeSnapshotFromMetadata, } from "./index"; +import { createInMemoryKnowledgeSpaceOverviewRepository } from "./knowledge-space-overview"; describe("research task handlers", () => { it("round-trips list cursors and rejects every malformed cursor boundary", () => { @@ -84,9 +85,14 @@ describe("research task handlers", () => { repository: createInMemoryResearchTaskJobRepository({ maxJobs: 10 }), }); let deletionActive = false; + const overview = createInMemoryKnowledgeSpaceOverviewRepository({ + maxEvents: 100, + maxListLimit: 50, + }); const app = createKnowledgeGateway({ adapter, allowLegacyResearchTaskProfileFallback: true, + knowledgeSpaceOverview: overview, auth: createStaticAuthVerifier({ subjectsByToken: { "editor-token": { @@ -129,6 +135,27 @@ describe("research task handlers", () => { }); expect(response.status).toBe(201); + // Research tasks count as knowledge-space queries in the overview, keyed by the task id. + expect( + ( + await overview.listActivity({ + candidateGrants: ["tenant:tenant-1"], + knowledgeSpaceId, + limit: 10, + tenantId: "tenant-1", + }) + ).items, + ).toEqual([ + expect.objectContaining({ + action: "query.requested", + details: expect.objectContaining({ + question: "Research semantic retrieval regressions", + source: "retrieval_test", + taskKind: "research", + }), + resource: { id: "research-task-job-1", type: "query" }, + }), + ]); const responseBody = (await response.json()) as Record; expect(responseBody).toMatchObject({ id: "research-task-job-1", diff --git a/knowledge-fs/packages/api/src/research-task-handlers.ts b/knowledge-fs/packages/api/src/research-task-handlers.ts index dd729ce6f5f..e52eab5dd83 100644 --- a/knowledge-fs/packages/api/src/research-task-handlers.ts +++ b/knowledge-fs/packages/api/src/research-task-handlers.ts @@ -4,6 +4,7 @@ import { validateKnowledgeSpaceRetrievalProfileForMode, } from "@knowledge/core"; +import { answerTraceSourceForCaller } from "./answer-trace-source"; import { isAuthenticatedApiKeyBoundToKnowledgeSpace } from "./auth"; import { AUTO_RETRIEVAL_MODE_DECISION_METADATA_KEY, @@ -31,6 +32,10 @@ import { type KnowledgeSpaceAuthorizationGuard, type KnowledgeSpaceCallerKind, } from "./knowledge-space-authorization"; +import { + type KnowledgeSpaceOverviewRepository, + deterministicKnowledgeSpaceActivityId, +} from "./knowledge-space-overview"; import type { KnowledgeSpaceRepository } from "./knowledge-space-repository"; import { type LooseOpenApiContext, openApiHandler } from "./openapi-handler-utils"; import type { PublishedKnowledgeSpaceRuntimeSnapshotResolver } from "./published-knowledge-space-runtime-snapshot"; @@ -88,6 +93,8 @@ export interface RegisterResearchTaskHandlersOptions { readonly assets: Pick; readonly dryRunResearchPlanner: ResearchTaskDryRunPlanner; readonly deletionVisibility?: ResearchTaskDeletionVisibility | undefined; + /** Counts each task as a knowledge-space query in the overview, like the query stream does. */ + readonly overview?: Pick | undefined; readonly researchTaskJobs: ResearchTaskJobStateMachine; readonly researchTaskPartialResults: ResearchTaskPartialResultRepository; readonly researchTaskProgressEvents: ResearchTaskProgressRepository; @@ -109,6 +116,7 @@ export function registerResearchTaskHandlers({ dryRunResearchPlanner, deletionVisibility, directStream, + overview, researchTaskJobs, researchTaskPartialResults, researchTaskProgressEvents, @@ -390,6 +398,17 @@ export function registerResearchTaskHandlers({ tenantId: subject.tenantId, topK: plan.retrievalPlan.topK, }); + await appendResearchTaskQueryRequested({ + job, + occurredAt: new Date(now()).toISOString(), + overview, + query: body.query, + source: answerTraceSourceForCaller({ + callerKind, + capabilityCallerKind: capabilityGrant?.callerKind, + }), + subject, + }); return context.json(toPublicResearchTaskJob(job), 201); } catch (error) { @@ -927,3 +946,58 @@ async function authorizeJobAccess(input: { throw error; } } + +/** + * Count the Research task as a knowledge-space query. The overview derives query volume from + * `query.requested` events and marks the task answered when the job state machine emits + * `query.completed` for the same id. Best effort: task creation must not fail because the + * activity feed is unavailable. + */ +async function appendResearchTaskQueryRequested({ + job, + occurredAt, + overview, + query, + source, + subject, +}: { + readonly job: { + readonly id: string; + readonly knowledgeSpaceId: string; + readonly mode?: string | undefined; + readonly tenantId: string; + }; + readonly occurredAt: string; + readonly overview: Pick | undefined; + readonly query: string | undefined; + readonly source: string; + readonly subject: KnowledgeGatewayEnv["Variables"]["subject"]; +}): Promise { + if (!overview) return; + try { + await overview.appendActivity({ + action: "query.requested", + actor: { id: subject.subjectId, type: "member" }, + details: { + mode: job.mode ?? "research", + ...(query ? { question: query } : {}), + source, + taskKind: "research", + }, + id: deterministicKnowledgeSpaceActivityId( + "query.requested", + job.tenantId, + job.knowledgeSpaceId, + job.id, + ), + knowledgeSpaceId: job.knowledgeSpaceId, + occurredAt, + requiredPermissionScope: [], + resource: { id: job.id, type: "query" }, + result: "success", + tenantId: job.tenantId, + }); + } catch { + // Best effort: see the function comment. + } +} diff --git a/knowledge-fs/packages/api/src/research-task-job.test.ts b/knowledge-fs/packages/api/src/research-task-job.test.ts index 6d15395702d..f55b6066d0f 100644 --- a/knowledge-fs/packages/api/src/research-task-job.test.ts +++ b/knowledge-fs/packages/api/src/research-task-job.test.ts @@ -1009,3 +1009,74 @@ function evidenceBundle(id: string, text: string) { state: "partial" as const, }; } + +describe("research task overview outcomes", () => { + it("reports completion and failure to the space overview as query outcomes", async () => { + const appendActivity = vi.fn(async (input: unknown) => input as never); + const machine = createResearchTaskJobStateMachine({ + generateId: (() => { + let next = 0; + return () => `research-task-job-${++next}`; + })(), + jobs: new FakeJobQueue(), + now: () => 5_000, + overview: { appendActivity }, + repository: createInMemoryResearchTaskJobRepository({ maxJobs: 10 }), + }); + + const completed = await machine.start(baseStartInput()); + for (const stage of [ + "planning", + "retrieving", + "analyzing", + "generating", + "completed", + ] as const) { + await machine.advance(completed.id, stage); + } + const failed = await machine.start(baseStartInput()); + await machine.fail(failed.id, "model unavailable"); + const canceled = await machine.start(baseStartInput()); + await machine.cancel(canceled.id, "operator"); + + expect(appendActivity.mock.calls.map((call) => call[0])).toEqual([ + expect.objectContaining({ + action: "query.completed", + actor: { type: "system" }, + knowledgeSpaceId: "space-1", + resource: { id: completed.id, type: "query" }, + result: "success", + tenantId: "tenant-1", + }), + expect.objectContaining({ + action: "query.failed", + details: expect.objectContaining({ reasonCode: "failed", taskKind: "research" }), + resource: { id: failed.id, type: "query" }, + result: "failure", + }), + expect.objectContaining({ + action: "query.failed", + details: expect.objectContaining({ reasonCode: "canceled", taskKind: "research" }), + resource: { id: canceled.id, type: "query" }, + result: "canceled", + }), + ]); + }); + + it("never lets an overview write failure break a terminal transition", async () => { + const machine = createResearchTaskJobStateMachine({ + generateId: () => "research-task-job-1", + jobs: new FakeJobQueue(), + now: () => 5_000, + overview: { + appendActivity: async () => { + throw new Error("activity feed down"); + }, + }, + repository: createInMemoryResearchTaskJobRepository({ maxJobs: 10 }), + }); + const job = await machine.start(baseStartInput()); + + await expect(machine.fail(job.id, "boom")).resolves.toMatchObject({ stage: "failed" }); + }); +}); diff --git a/knowledge-fs/packages/api/src/research-task-job.ts b/knowledge-fs/packages/api/src/research-task-job.ts index 3390384c9c4..6d8453f419e 100644 --- a/knowledge-fs/packages/api/src/research-task-job.ts +++ b/knowledge-fs/packages/api/src/research-task-job.ts @@ -4,6 +4,10 @@ import { type JobPayload, type JobQueueAdapter, } from "@knowledge/core"; +import { + type KnowledgeSpaceOverviewRepository, + deterministicKnowledgeSpaceActivityId, +} from "./knowledge-space-overview"; import type { CapabilityCallerKind, @@ -196,6 +200,8 @@ export interface ResearchTaskJobStateMachineOptions { readonly maxQueryBytes?: number; readonly metrics?: DurableTaskOperationalMetrics | undefined; readonly now?: () => number; + /** Reports terminal outcomes to the space overview so Research tasks count as answered/failed queries. */ + readonly overview?: Pick | undefined; readonly progress?: ResearchTaskProgressPublisher | undefined; readonly repository: ResearchTaskJobRepository; } @@ -291,6 +297,7 @@ export function createResearchTaskJobStateMachine({ maxQueryBytes = defaultMaxQueryBytes, metrics, now = Date.now, + overview, progress, repository, }: ResearchTaskJobStateMachineOptions): ResearchTaskJobStateMachine { @@ -316,6 +323,7 @@ export function createResearchTaskJobStateMachine({ outcome: "completed", taskKind: "research", }); + await appendResearchTaskOutcome(overview, updated, "query.completed", "success", {}); } await progress?.publish(updated, "research_task.stage_changed", { previousStage: job.stage, @@ -341,6 +349,9 @@ export function createResearchTaskJobStateMachine({ outcome: "canceled", taskKind: "research", }); + await appendResearchTaskOutcome(overview, updated, "query.failed", "canceled", { + reasonCode: "canceled", + }); await progress?.publish(updated, "research_task.canceled", reason ? { reason } : {}); return cloneResearchTaskJob(updated); }, @@ -363,6 +374,9 @@ export function createResearchTaskJobStateMachine({ outcome: "failed", taskKind: "research", }); + await appendResearchTaskOutcome(overview, updated, "query.failed", "failure", { + reasonCode: "failed", + }); await progress?.publish(updated, "research_task.failed", { error }); return cloneResearchTaskJob(updated); }, @@ -1135,3 +1149,34 @@ function clonePartialResult(result: ResearchTaskPartialResult): ResearchTaskPart function cloneEvidenceBundle(bundle: EvidenceBundle): EvidenceBundle { return JSON.parse(JSON.stringify(bundle)) as EvidenceBundle; } + +/** + * Mirror a terminal Research outcome into the space overview. The task was counted as a query + * when it was created; `query.completed` marks it answered and `query.failed` keeps it in the + * unanswered bucket. Best effort: the state transition has already been persisted. + */ +async function appendResearchTaskOutcome( + overview: Pick | undefined, + job: ResearchTaskJob, + action: "query.completed" | "query.failed", + result: "canceled" | "failure" | "success", + details: Readonly>, +): Promise { + if (!overview) return; + try { + await overview.appendActivity({ + action, + actor: { type: "system" }, + details: { ...details, taskKind: "research" }, + id: deterministicKnowledgeSpaceActivityId(action, job.tenantId, job.knowledgeSpaceId, job.id), + knowledgeSpaceId: job.knowledgeSpaceId, + occurredAt: new Date(job.completedAt ?? job.updatedAt).toISOString(), + requiredPermissionScope: [], + resource: { id: job.id, type: "query" }, + result, + tenantId: job.tenantId, + }); + } catch { + // Best effort: see the function comment. + } +} diff --git a/knowledge-fs/packages/api/src/retrieval-test-handlers.test.ts b/knowledge-fs/packages/api/src/retrieval-test-handlers.test.ts index bcc9b421fc9..b47eea4c07d 100644 --- a/knowledge-fs/packages/api/src/retrieval-test-handlers.test.ts +++ b/knowledge-fs/packages/api/src/retrieval-test-handlers.test.ts @@ -90,21 +90,28 @@ describe("retrieval test route", () => { }); expect(response.status).toBe(200); + // The node forwards this id when it captures an empty retrieval, so one record stays per run. + expect((await response.json()) as Record).toMatchObject({ + answerTraceId: expectedTraceId, + }); expect(overview.appendActivity).toHaveBeenCalledWith( expect.objectContaining({ action: "query.requested", - details: expect.objectContaining({ mode: "fast" }), + details: expect.objectContaining({ mode: "fast", source: "workflow" }), resource: { id: expectedTraceId, type: "query" }, }), ); expect(answerTraceRecorder.record).toHaveBeenCalledWith( expect.objectContaining({ capabilityGrantId: "10000000-0000-4000-8000-000000000011", + source: "workflow", traceId: expectedTraceId, steps: [ expect.objectContaining({ metadata: expect.objectContaining({ queryOutcome: expectedOutcome, + resultCount: result.items.length, + stages: result.stages, workflowQueryId: WORKFLOW_QUERY_ID, }), name: "query.generate", diff --git a/knowledge-fs/packages/api/src/retrieval-test-handlers.ts b/knowledge-fs/packages/api/src/retrieval-test-handlers.ts index 91d2808e244..0a261708ff4 100644 --- a/knowledge-fs/packages/api/src/retrieval-test-handlers.ts +++ b/knowledge-fs/packages/api/src/retrieval-test-handlers.ts @@ -2,10 +2,14 @@ import type { OpenAPIHono } from "@hono/zod-openapi"; import { validateKnowledgeSpaceRetrievalProfileForMode } from "@knowledge/core"; import type { AnswerTraceRecorder } from "./answer-trace-recorder"; +import { answerTraceSourceForCaller } from "./answer-trace-source"; import { currentCandidateGrants } from "./candidate-content-authorization"; import { classifyQueryOutcome } from "./failed-query-recorder"; import type { KnowledgeGatewayEnv } from "./gateway-openapi-contracts"; -import { queryRetrievalProfileMetadata } from "./gateway-sse-responses"; +import { + queryProjectionSnapshotMetadata, + queryRetrievalProfileMetadata, +} from "./gateway-sse-responses"; import { type KnowledgeSpaceOverviewRepository, deterministicKnowledgeSpaceActivityId, @@ -70,6 +74,10 @@ export function registerRetrievalTestHandlers({ const body = context.req.valid("json"); const capabilityGrant = context.get("capabilityV2Grant"); const workflowQueryId = capabilityGrant?.callerKind === "workflow" ? body.queryId : undefined; + const source = answerTraceSourceForCaller({ + callerKind: context.get("callerKind"), + capabilityCallerKind: capabilityGrant?.callerKind, + }); const space = await spaces.get({ id: knowledgeSpaceId, tenantId: subject.tenantId }); if (!space) { return context.json({ error: "Knowledge space not found" }, 404); @@ -233,6 +241,7 @@ export function registerRetrievalTestHandlers({ details: { mode, ...(body.query ? { question: body.query } : {}), + source, }, id: deterministicKnowledgeSpaceActivityId( "query.requested", @@ -274,6 +283,9 @@ export function registerRetrievalTestHandlers({ traceId, }); await executionLease.assertActive(); + const recordsTrace = Boolean( + workflowAnswerTraceId && capabilityGrant && answerTraceRecorder && overview, + ); if (workflowAnswerTraceId && capabilityGrant && answerTraceRecorder && overview) { const finishReason = result.items.length > 0 ? "retrieval-evidence" : "no-retrieval-evidence"; @@ -297,6 +309,9 @@ export function registerRetrievalTestHandlers({ } : {}), }); + // One `query.generate` step carries the outcome classification the overview reads plus + // everything the retrieval history needs to show the run like a console retrieval test: + // the evidence bundle (counts, scores, evidence view), stages, plan, and publication. await answerTraceRecorder.record({ capabilityGrantId: capabilityGrant.grantId, knowledgeSpaceId, @@ -305,12 +320,20 @@ export function registerRetrievalTestHandlers({ ...(resolvedQueryImages.length > 0 ? { queryImages: resolvedQueryImages.map(queryImageMetadata) } : {}), + source, steps: [ { metadata: { ...outcomeMetadata, + ...(result.evidenceBundle ? { evidenceBundle: result.evidenceBundle } : {}), + plan: result.plan, + projectionSnapshot: queryProjectionSnapshotMetadata( + runtimeSnapshot.projectionSnapshot, + ), queryOutcome: classification.outcome, ...(classification.trigger ? { failedQueryTrigger: classification.trigger } : {}), + resultCount: result.items.length, + stages: result.stages, }, name: "query.generate", status: "ok", @@ -327,6 +350,10 @@ export function registerRetrievalTestHandlers({ : "disabled"; const response = RetrievalTestResponseSchema.parse({ + // Lets the caller attach a later failed-retrieval capture to this history record. + ...(recordsTrace && workflowAnswerTraceId + ? { answerTraceId: workflowAnswerTraceId } + : {}), capabilityStatus: { embedding: embeddingCapabilityStatus, reasoning: "verified" as const, diff --git a/knowledge-fs/packages/api/src/retrieval-test-routes.ts b/knowledge-fs/packages/api/src/retrieval-test-routes.ts index 76c4e3fabd7..87a7b23a809 100644 --- a/knowledge-fs/packages/api/src/retrieval-test-routes.ts +++ b/knowledge-fs/packages/api/src/retrieval-test-routes.ts @@ -204,6 +204,8 @@ export const RetrievalTestMetricsSchema = z export const RetrievalTestResponseSchema = z .object({ + /** AnswerTrace recorded for this run in the space's retrieval history, when one was written. */ + answerTraceId: z.string().uuid().optional(), capabilityStatus: z .object({ embedding: z.enum(["not-required", "verified"]), diff --git a/knowledge-fs/packages/api/src/retrieval-test.test.ts b/knowledge-fs/packages/api/src/retrieval-test.test.ts index 33350d8e942..0a79c545973 100644 --- a/knowledge-fs/packages/api/src/retrieval-test.test.ts +++ b/knowledge-fs/packages/api/src/retrieval-test.test.ts @@ -231,6 +231,9 @@ describe("createRetrievalTestExecutor", () => { sources: ["dense", "fts"], }, ]); + // This fixture's node/document ids are not UUIDs, so no history bundle can be assembled; the + // diagnostics are unaffected. See the dedicated evidence bundle test for the real shape. + expect(result.evidenceBundle).toBeUndefined(); expect(JSON.stringify(result)).not.toContain("secret candidate text"); expect(stageStatuses(result)).toMatchObject({ dense: "executed", @@ -734,3 +737,78 @@ function stageStatuses( ) { return Object.fromEntries(result.stages.map((stage) => [stage.name, stage.status])); } + +describe("retrieval test history evidence", () => { + it("assembles the same EvidenceBundle a query would produce from the raw retrieval", async () => { + const nodeId = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c61"; + const documentAssetId = "018f0d60-7a49-7cc2-9c1b-5b36f18f2c62"; + const embeddings: EmbeddingProvider = { + embed: async () => ({ + dense: [[0.1, 0.2, 0.3]], + metadata: { dimension: 3, model: embeddingSelection.model, provider: "dify-model-runtime" }, + model: embeddingSelection.model, + }), + kind: "dify-model-runtime", + models: async () => [], + }; + const planner = createRetrievalPlanner({ maxTopK: 100 }); + const retriever: BasicHybridRetriever = { + retrieve: async (input) => ({ + items: [ + { + citation: { + artifactHash: "a".repeat(64), + documentAssetId, + documentVersion: 1, + sectionPath: ["Sensor"], + }, + metadata: { text: "camera sensor evidence text" }, + nodeId, + permissionScope: ["tenant:tenant-1"], + projectionIds: ["projection-1"], + score: 0.8, + sources: ["dense", "fts"], + }, + ], + metrics: ordinaryMetrics({ rerank: true }), + plan: planner.plan({ + hasQueryImages: false, + mode: "fast", + query: input.query, + topK: input.topK, + }), + }), + }; + const executor = createRetrievalTestExecutor({ + embeddingModel: embeddingSelection.model, + embeddings, + retriever, + }); + + const result = await executor.execute({ + embeddingProfile, + // Text is withheld from the diagnostics, but the history bundle still carries evidence. + includeText: false, + knowledgeSpaceId: SPACE_ID, + mode: "fast", + permissionScope: ["tenant:tenant-1", "subject:owner-1"], + projectionSnapshot, + query: "camera sensor evidence", + retrievalProfile, + subject, + traceId: "trace-history", + }); + + expect(result.items[0]?.text).toBeUndefined(); + expect(result.evidenceBundle).toMatchObject({ + query: "camera sensor evidence", + state: expect.any(String), + }); + expect(result.evidenceBundle?.items).toHaveLength(1); + expect(result.evidenceBundle?.items[0]).toMatchObject({ + citations: [expect.objectContaining({ documentAssetId })], + nodeId, + text: "camera sensor evidence text", + }); + }); +}); diff --git a/knowledge-fs/packages/api/src/retrieval-test.ts b/knowledge-fs/packages/api/src/retrieval-test.ts index 2fb6c662823..d9dc10391a8 100644 --- a/knowledge-fs/packages/api/src/retrieval-test.ts +++ b/knowledge-fs/packages/api/src/retrieval-test.ts @@ -1,5 +1,6 @@ import { type AuthSubject, + type EvidenceBundle, type KnowledgeSpaceEmbeddingProfile, type KnowledgeSpaceModelSelection, type KnowledgeSpaceRetrievalProfile, @@ -9,6 +10,7 @@ import type { EmbeddingProvider } from "@knowledge/embeddings"; import { runWithAbortSignal } from "./bounded-concurrency"; import { candidatePermissionScopeAllows } from "./candidate-content-authorization"; +import { createEvidenceBundleAssembler } from "./evidence-bundle-assembler"; import { type KnowledgeSpaceEmbeddingResolver, assertEmbeddingModelMatchesProfile, @@ -30,6 +32,7 @@ import { QUERY_IMAGE_MAX_COUNT, QueryImageDegradationReasonSchema, type ResolvedQueryImage, + queryImageMetadata, } from "./query-images"; import type { RetrievalMetadataFilters } from "./retrieval-candidates"; import type { RetrievalSource } from "./retrieval-candidates"; @@ -69,6 +72,12 @@ export interface RetrievalTestStage { } export interface RetrievalTestResult { + /** + * The same EvidenceBundle a query would have produced from this retrieval, so the run can be + * recorded as an AnswerTrace with evidence for the retrieval history. Absent when the bundle + * could not be assembled; the diagnostics above are unaffected. + */ + readonly evidenceBundle?: EvidenceBundle | undefined; readonly items: readonly { readonly citation: { readonly artifactHash: string; @@ -294,7 +303,13 @@ export function createRetrievalTestExecutor({ permissionScope: input.permissionScope, profile: input.retrievalProfile, }); + const evidenceBundle = assembleRetrievalTestEvidenceBundle({ + query: preparedQuery.query || input.query, + queryImages: preparedQuery.queryImages, + retrieval, + }); return { + ...(evidenceBundle ? { evidenceBundle } : {}), items: retrieval.items.map((item) => safeRetrievalTestItem(item, input.includeText === true), ), @@ -391,6 +406,37 @@ async function resolveRetrievalTestEmbedding({ return [...vector]; } +const retrievalTestEvidenceBundleAssembler = createEvidenceBundleAssembler(); + +/** + * Build the history EvidenceBundle for a retrieval test. Assembly validates every evidence item, + * so a retrieval whose items cannot be projected (for example text-less legacy nodes) yields no + * bundle rather than failing the diagnostics the caller asked for. + */ +function assembleRetrievalTestEvidenceBundle({ + query, + queryImages, + retrieval, +}: { + readonly query: string; + readonly queryImages: readonly ResolvedQueryImage[]; + readonly retrieval: Parameters[0]["retrieval"]; +}): EvidenceBundle | undefined { + const images = queryImages.map(queryImageMetadata); + if (!query.trim() && images.length === 0) return undefined; + try { + return retrievalTestEvidenceBundleAssembler.assemble({ + query, + ...(images.length > 0 ? { queryImages: images } : {}), + retrieval, + }); + } catch { + return undefined; + } +} + +type EvidenceBundleAssemblerAssemble = ReturnType["assemble"]; + async function prepareRetrievalTestQuery({ input, provider, diff --git a/knowledge-fs/packages/api/src/workflow-failed-retrieval.test.ts b/knowledge-fs/packages/api/src/workflow-failed-retrieval.test.ts index 0992e87ce32..a2c5c790379 100644 --- a/knowledge-fs/packages/api/src/workflow-failed-retrieval.test.ts +++ b/knowledge-fs/packages/api/src/workflow-failed-retrieval.test.ts @@ -9,6 +9,7 @@ const SPACE_ID = "10000000-0000-4000-8000-000000000001"; const EVENT_ID = "10000000-0000-4000-8000-000000000002"; const FIRST_GRANT_ID = "10000000-0000-4000-8000-000000000003"; const RETRY_GRANT_ID = "10000000-0000-4000-8000-000000000004"; +const RETRIEVAL_TRACE_ID = "10000000-0000-4000-8000-000000000005"; function baseInput(capabilityGrantId = FIRST_GRANT_ID) { return { @@ -101,3 +102,49 @@ describe("workflow failed-retrieval capture", () => { }, ); }); + +describe("workflow failed-retrieval history linkage", () => { + it("attaches the capture to the retrieval-test trace the node reports instead of a second record", async () => { + const { answerTraces, createBadCase, failedQueries, service } = setup("retrieval-miss"); + const retrievalTrace = await createAnswerTraceRecorder({ + now: () => "2026-08-12T00:00:00.000Z", + repository: answerTraces, + }).record({ + capabilityGrantId: FIRST_GRANT_ID, + knowledgeSpaceId: SPACE_ID, + mode: "deep", + query: "发票号码在哪里?", + source: "workflow", + steps: [{ metadata: { resultCount: 0 }, name: "retrieval.test", status: "ok" }], + tenantId: "tenant-1", + traceId: RETRIEVAL_TRACE_ID, + }); + + const result = await service.capture({ ...baseInput(), retrievalTraceId: retrievalTrace.id }); + + expect(result.verdict).toBe("retrieval-miss"); + expect(await answerTraces.getById(EVENT_ID)).toBeNull(); + const failedQuery = await failedQueries.get({ + candidateGrants: ["tenant:tenant-1"], + id: EVENT_ID, + knowledgeSpaceId: SPACE_ID, + subjectId: "dify-app:workflow-app", + tenantId: "tenant-1", + }); + expect(failedQuery?.answerTraceId).toBe(RETRIEVAL_TRACE_ID); + expect(createBadCase).toHaveBeenCalledWith( + expect.objectContaining({ id: EVENT_ID, traceId: RETRIEVAL_TRACE_ID }), + ); + }); + + it("falls back to a workflow-sourced trace keyed by the event when only a transport id is known", async () => { + const { answerTraces, service } = setup("coverage-gap"); + + await service.capture(baseInput()); + + expect(await answerTraces.getById(EVENT_ID)).toMatchObject({ + id: EVENT_ID, + source: "workflow", + }); + }); +}); diff --git a/knowledge-fs/packages/api/src/workflow-failed-retrieval.ts b/knowledge-fs/packages/api/src/workflow-failed-retrieval.ts index e8e6d29c229..75eda5cd0ee 100644 --- a/knowledge-fs/packages/api/src/workflow-failed-retrieval.ts +++ b/knowledge-fs/packages/api/src/workflow-failed-retrieval.ts @@ -77,17 +77,16 @@ export function createWorkflowFailedRetrievalCaptureService({ }; let failedQuery = await failedQueries.get(lookup); let traceCapabilityGrantId = input.capabilityGrantId; + let answerTraceId = failedQuery?.answerTraceId ?? input.eventId; if (!failedQuery) { - traceCapabilityGrantId = await ensureWorkflowAnswerTrace( - answerTraces, - answerTraceRecorder, - input, - ); + const trace = await ensureWorkflowAnswerTrace(answerTraces, answerTraceRecorder, input); + traceCapabilityGrantId = trace.capabilityGrantId; + answerTraceId = trace.id; } failedQuery = await failedQueries.captureWorkflowFailedRetrieval({ actorSubjectId: input.actorSubjectId, - answerTraceId: input.eventId, + answerTraceId, candidateGrants: input.candidateGrants, capabilityGrantId: input.capabilityGrantId, id: input.eventId, @@ -141,18 +140,36 @@ export function createWorkflowFailedRetrievalCaptureService({ "Workflow retrieval returned no evidence even though the knowledge base appears to contain relevant answer material.", tags: ["workflow", "auto-captured", "retrieval-miss"], tenantId: input.tenantId, - traceId: input.eventId, + traceId: answerTraceId, }); return { badCaseId: badCase.id, failedQueryId: failedQuery.id, verdict }; }, }; } +/** + * Resolve the AnswerTrace a failed workflow retrieval belongs to. + * + * The retrieval-tests route already records one trace per workflow run and returns its id as + * `answerTraceId`, which the node forwards as `retrievalTraceId`; attaching the failed query to + * that trace keeps one history record per retrieval and lets the overview classify the query as + * "no evidence". Older nodes send an opaque transport trace id instead, in which case a minimal + * trace keyed by the capture event id is created (or replayed) as before. + */ async function ensureWorkflowAnswerTrace( answerTraces: Pick, recorder: AnswerTraceRecorder, input: CaptureWorkflowFailedRetrievalInput, -): Promise { +): Promise<{ readonly capabilityGrantId: string; readonly id: string }> { + if (UUID_PATTERN.test(input.retrievalTraceId) && input.retrievalTraceId !== input.eventId) { + const retrievalTrace = await answerTraces.get({ + id: input.retrievalTraceId, + knowledgeSpaceId: input.knowledgeSpaceId, + }); + if (retrievalTrace?.capabilityGrantId && retrievalTrace.query === input.query) { + return { capabilityGrantId: retrievalTrace.capabilityGrantId, id: retrievalTrace.id }; + } + } const existing = await answerTraces.get({ id: input.eventId, knowledgeSpaceId: input.knowledgeSpaceId, @@ -162,13 +179,14 @@ async function ensureWorkflowAnswerTrace( if (!existing.capabilityGrantId) { throw new WorkflowFailedRetrievalReplayConflictError(input.eventId); } - return existing.capabilityGrantId; + return { capabilityGrantId: existing.capabilityGrantId, id: existing.id }; } const recorded = await recorder.record({ capabilityGrantId: input.capabilityGrantId, knowledgeSpaceId: input.knowledgeSpaceId, mode: input.mode, query: input.query, + source: "workflow", steps: [ { metadata: { @@ -188,9 +206,11 @@ async function ensureWorkflowAnswerTrace( if (!recorded.capabilityGrantId) { throw new Error("Workflow failed-retrieval AnswerTrace lost Capability provenance"); } - return recorded.capabilityGrantId; + return { capabilityGrantId: recorded.capabilityGrantId, id: recorded.id }; } +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu; + function assertWorkflowAnswerTraceReplay( existing: AnswerTrace, input: CaptureWorkflowFailedRetrievalInput, diff --git a/knowledge-fs/packages/core/src/models.ts b/knowledge-fs/packages/core/src/models.ts index 692adfadc31..dca0dfff470 100644 --- a/knowledge-fs/packages/core/src/models.ts +++ b/knowledge-fs/packages/core/src/models.ts @@ -1533,6 +1533,25 @@ export const AnswerTraceStepSchema = z.object({ }); export type AnswerTraceStep = z.infer; +/** + * Which caller produced an AnswerTrace. Console retrieval tests, KnowledgeFS workflow nodes, + * service API clients, agents, and MCP clients all record traces into the same history, and the + * console distinguishes them by this value. Rows written before the column existed are + * `retrieval_test`. + */ +export const AnswerTraceSourceSchema = z.enum([ + "retrieval_test", + "workflow", + "service_api", + "agent", + "mcp", +]); +export type AnswerTraceSource = z.infer; + +export function answerTraceSourceOf(trace: { readonly source?: AnswerTraceSource | undefined }) { + return trace.source ?? "retrieval_test"; +} + export const AnswerTraceSchema = z.object({ /** Durable Capability v2 provenance; mutually exclusive with legacy member snapshots. */ capabilityGrantId: UuidSchema.optional(), @@ -1550,6 +1569,8 @@ export const AnswerTraceSchema = z.object({ .optional(), query: z.string(), queryImages: z.array(QueryImageMetadataSchema).max(4).optional(), + /** Absent on legacy rows; read it through `answerTraceSourceOf`. */ + source: AnswerTraceSourceSchema.optional(), /** Authenticated creator. Legacy traces without it are intentionally unreadable via the API. */ subjectId: z.string().min(1).max(255).optional(), steps: z.array(AnswerTraceStepSchema), diff --git a/knowledge-fs/packages/database/migrations/0053_answer_trace_source.postgres.sql b/knowledge-fs/packages/database/migrations/0053_answer_trace_source.postgres.sql new file mode 100644 index 00000000000..2dbe9c035ef --- /dev/null +++ b/knowledge-fs/packages/database/migrations/0053_answer_trace_source.postgres.sql @@ -0,0 +1,11 @@ +-- Knowledge Platform schema migration +-- Migration id: 0053_answer_trace_source +-- Dialect: postgres +-- Answer traces record which caller produced them (console retrieval test, workflow node, service +-- API, agent, MCP) so retrieval history can distinguish and filter them. NULL means retrieval_test. + +ALTER TABLE "answer_traces" + ADD COLUMN IF NOT EXISTS "source" TEXT; + +CREATE INDEX IF NOT EXISTS "answer_traces_space_source_created_idx" + ON "answer_traces" ("knowledge_space_id", "source", "created_at", "id"); diff --git a/knowledge-fs/packages/database/migrations/0053_answer_trace_source.tidb.sql b/knowledge-fs/packages/database/migrations/0053_answer_trace_source.tidb.sql new file mode 100644 index 00000000000..62c7dda6889 --- /dev/null +++ b/knowledge-fs/packages/database/migrations/0053_answer_trace_source.tidb.sql @@ -0,0 +1,11 @@ +-- Knowledge Platform schema migration +-- Migration id: 0053_answer_trace_source +-- Dialect: tidb +-- Answer traces record which caller produced them (console retrieval test, workflow node, service +-- API, agent, MCP) so retrieval history can distinguish and filter them. NULL means retrieval_test. + +ALTER TABLE `answer_traces` + ADD COLUMN IF NOT EXISTS `source` VARCHAR(32) NULL; + +CREATE INDEX IF NOT EXISTS `answer_traces_space_source_created_idx` + ON `answer_traces` (`knowledge_space_id`, `source`, `created_at`, `id`); diff --git a/knowledge-fs/packages/database/src/migration-artifacts.generated.ts b/knowledge-fs/packages/database/src/migration-artifacts.generated.ts index 1a22941538f..98171cc0bf8 100644 --- a/knowledge-fs/packages/database/src/migration-artifacts.generated.ts +++ b/knowledge-fs/packages/database/src/migration-artifacts.generated.ts @@ -107,4 +107,6 @@ export const migrationArtifacts = [ { content: "-- Knowledge Platform schema migration\n-- Migration id: 0051_failed_query_trace_outcome_index\n-- Dialect: tidb\n-- Supports Overview outcome joins by exact trace and trigger without scanning space history.\n\nCREATE INDEX IF NOT EXISTS `failed_queries_trace_trigger_idx`\n ON `failed_queries` (\n `tenant_id`, `knowledge_space_id`, `answer_trace_id`\n );\n", path: "packages/database/migrations/0051_failed_query_trace_outcome_index.tidb.sql" }, { content: "-- Knowledge Platform schema migration\n-- Migration id: 0052_namespace_source_previews\n-- Dialect: postgres\n\nCREATE TABLE IF NOT EXISTS \"namespace_source_preview_jobs\" (\n \"id\" uuid PRIMARY KEY,\n \"tenant_id\" varchar(255) NOT NULL,\n \"account_id\" varchar(255) NOT NULL,\n \"status\" varchar(32) NOT NULL,\n \"provider_config\" jsonb NOT NULL,\n \"configuration_fingerprint\" varchar(128) NOT NULL,\n \"expires_at\" timestamptz NOT NULL,\n \"consumed_at\" timestamptz NULL,\n \"content_cleaned_at\" timestamptz NULL,\n \"import_workflow_id\" uuid NULL,\n \"error_code\" varchar(128) NULL,\n \"created_at\" timestamptz NOT NULL,\n \"updated_at\" timestamptz NOT NULL\n);\nCREATE INDEX IF NOT EXISTS \"namespace_source_preview_jobs_claim_idx\"\n ON \"namespace_source_preview_jobs\" (\"status\", \"expires_at\", \"created_at\");\nCREATE INDEX IF NOT EXISTS \"namespace_source_preview_jobs_owner_idx\"\n ON \"namespace_source_preview_jobs\" (\"tenant_id\", \"account_id\", \"created_at\");\nCREATE INDEX IF NOT EXISTS \"namespace_source_preview_jobs_cleanup_idx\"\n ON \"namespace_source_preview_jobs\" (\"status\", \"content_cleaned_at\", \"updated_at\");\n\nCREATE TABLE IF NOT EXISTS \"namespace_source_preview_pages\" (\n \"job_id\" uuid NOT NULL REFERENCES \"namespace_source_preview_jobs\" (\"id\") ON DELETE CASCADE,\n \"page_id\" varchar(128) NOT NULL,\n \"source_url\" varchar(4096) NOT NULL,\n \"title\" varchar(500) NULL,\n \"description\" text NULL,\n \"content_hash\" varchar(64) NOT NULL,\n \"content_object_key\" varchar(2048) NOT NULL,\n \"created_at\" timestamptz NOT NULL,\n PRIMARY KEY (\"job_id\", \"page_id\")\n);\n", path: "packages/database/migrations/0052_namespace_source_previews.postgres.sql" }, { content: "-- Knowledge Platform schema migration\n-- Migration id: 0052_namespace_source_previews\n-- Dialect: tidb\n\nCREATE TABLE IF NOT EXISTS `namespace_source_preview_jobs` (\n `id` char(36) PRIMARY KEY,\n `tenant_id` varchar(255) NOT NULL,\n `account_id` varchar(255) NOT NULL,\n `status` varchar(32) NOT NULL,\n `provider_config` json NOT NULL,\n `configuration_fingerprint` varchar(128) NOT NULL,\n `expires_at` datetime(6) NOT NULL,\n `consumed_at` datetime(6) NULL,\n `content_cleaned_at` datetime(6) NULL,\n `import_workflow_id` char(36) NULL,\n `error_code` varchar(128) NULL,\n `created_at` datetime(6) NOT NULL,\n `updated_at` datetime(6) NOT NULL,\n INDEX `namespace_source_preview_jobs_claim_idx` (`status`, `expires_at`, `created_at`),\n INDEX `namespace_source_preview_jobs_owner_idx` (`tenant_id`, `account_id`, `created_at`),\n INDEX `namespace_source_preview_jobs_cleanup_idx` (`status`, `content_cleaned_at`, `updated_at`)\n);\n\nCREATE TABLE IF NOT EXISTS `namespace_source_preview_pages` (\n `job_id` char(36) NOT NULL,\n `page_id` varchar(128) NOT NULL,\n `source_url` varchar(4096) NOT NULL,\n `title` varchar(500) NULL,\n `description` text NULL,\n `content_hash` varchar(64) NOT NULL,\n `content_object_key` varchar(2048) NOT NULL,\n `created_at` datetime(6) NOT NULL,\n PRIMARY KEY (`job_id`, `page_id`),\n CONSTRAINT `namespace_source_preview_pages_job_fk` FOREIGN KEY (`job_id`) REFERENCES `namespace_source_preview_jobs` (`id`) ON DELETE CASCADE\n);\n", path: "packages/database/migrations/0052_namespace_source_previews.tidb.sql" }, + { content: "-- Knowledge Platform schema migration\n-- Migration id: 0053_answer_trace_source\n-- Dialect: postgres\n-- Answer traces record which caller produced them (console retrieval test, workflow node, service\n-- API, agent, MCP) so retrieval history can distinguish and filter them. NULL means retrieval_test.\n\nALTER TABLE \"answer_traces\"\n ADD COLUMN IF NOT EXISTS \"source\" TEXT;\n\nCREATE INDEX IF NOT EXISTS \"answer_traces_space_source_created_idx\"\n ON \"answer_traces\" (\"knowledge_space_id\", \"source\", \"created_at\", \"id\");\n", path: "packages/database/migrations/0053_answer_trace_source.postgres.sql" }, + { content: "-- Knowledge Platform schema migration\n-- Migration id: 0053_answer_trace_source\n-- Dialect: tidb\n-- Answer traces record which caller produced them (console retrieval test, workflow node, service\n-- API, agent, MCP) so retrieval history can distinguish and filter them. NULL means retrieval_test.\n\nALTER TABLE `answer_traces`\n ADD COLUMN IF NOT EXISTS `source` VARCHAR(32) NULL;\n\nCREATE INDEX IF NOT EXISTS `answer_traces_space_source_created_idx`\n ON `answer_traces` (`knowledge_space_id`, `source`, `created_at`, `id`);\n", path: "packages/database/migrations/0053_answer_trace_source.tidb.sql" }, ] as const satisfies readonly MigrationArtifact[]; diff --git a/knowledge-fs/packages/database/src/migration-file.test.ts b/knowledge-fs/packages/database/src/migration-file.test.ts index 787d153e2b2..fe81966824f 100644 --- a/knowledge-fs/packages/database/src/migration-file.test.ts +++ b/knowledge-fs/packages/database/src/migration-file.test.ts @@ -164,6 +164,8 @@ describe("migration file rendering", () => { "packages/database/migrations/0051_failed_query_trace_outcome_index.tidb.sql", "packages/database/migrations/0052_namespace_source_previews.postgres.sql", "packages/database/migrations/0052_namespace_source_previews.tidb.sql", + "packages/database/migrations/0053_answer_trace_source.postgres.sql", + "packages/database/migrations/0053_answer_trace_source.tidb.sql", ]); const workflowCapturePostgres = artifacts.find( (artifact) => @@ -946,6 +948,7 @@ describe("migration file rendering", () => { "packages/database/migrations/0050_deletion_job_capability_provenance.postgres.sql", "packages/database/migrations/0051_failed_query_trace_outcome_index.postgres.sql", "packages/database/migrations/0052_namespace_source_previews.postgres.sql", + "packages/database/migrations/0053_answer_trace_source.postgres.sql", ]); expect( getPendingMigrationArtifacts({ @@ -1002,6 +1005,7 @@ describe("migration file rendering", () => { "0050_deletion_job_capability_provenance", "0051_failed_query_trace_outcome_index", "0052_namespace_source_previews", + "0053_answer_trace_source", ], dialect: "postgres", }), diff --git a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts index bf56808be57..eb304054823 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/types.gen.ts @@ -1197,6 +1197,7 @@ export type KnowledgeFsAnswerTraceResponse = { mode: 'auto' | 'deep' | 'fast' | 'research' query: string query_images?: Array + source?: KnowledgeFsTraceSource steps: Array } @@ -2164,9 +2165,12 @@ export type KnowledgeFsTraceResponse = { query_images?: Array result_count: number scores: KnowledgeFsTraceScoresResponse + source?: KnowledgeFsTraceSource stages: Array } +export type KnowledgeFsTraceSource = 'agent' | 'mcp' | 'retrieval_test' | 'service_api' | 'workflow' + export type KnowledgeFsAnswerTraceStepResponse = { ended_at?: string | null metadata: { @@ -4404,6 +4408,7 @@ export type GetKnowledgeFsSpacesByControlSpaceIdTracesData = { } query?: { cursor?: string + source?: 'agent' | 'mcp' | 'retrieval_test' | 'service_api' | 'workflow' } url: '/knowledge-fs/spaces/{control_space_id}/traces' } diff --git a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts index 4c233ce3215..abf0eb381e1 100644 --- a/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts +++ b/packages/contracts/generated/api/console/knowledge-fs/zod.gen.ts @@ -2159,6 +2159,14 @@ export const zKnowledgeFsSpaceListResponse = z.object({ page: z.int(), }) +export const zKnowledgeFsTraceSource = z.enum([ + 'agent', + 'mcp', + 'retrieval_test', + 'service_api', + 'workflow', +]) + /** * KnowledgeFSAnswerTraceStepResponse */ @@ -2181,6 +2189,7 @@ export const zKnowledgeFsAnswerTraceResponse = z.object({ mode: z.enum(['auto', 'deep', 'fast', 'research']), query: z.string(), query_images: z.array(zKnowledgeFsQueryImageResponse).optional(), + source: zKnowledgeFsTraceSource.optional().default('retrieval_test'), steps: z.array(zKnowledgeFsAnswerTraceStepResponse), }) @@ -3060,6 +3069,7 @@ export const zKnowledgeFsTraceResponse = z.object({ query_images: z.array(zKnowledgeFsQueryImageResponse).optional(), result_count: z.int().gte(0), scores: zKnowledgeFsTraceScoresResponse, + source: zKnowledgeFsTraceSource.optional().default('retrieval_test'), stages: z.array(zKnowledgeFsTraceStageResponse), }) @@ -4651,6 +4661,7 @@ export const zGetKnowledgeFsSpacesByControlSpaceIdTracesPath = z.object({ export const zGetKnowledgeFsSpacesByControlSpaceIdTracesQuery = z.object({ cursor: z.string().min(1).max(1000).optional(), + source: z.enum(['agent', 'mcp', 'retrieval_test', 'service_api', 'workflow']).optional(), }) /** diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index 336a9fd1839..0b9cf1a129e 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -1269,6 +1269,7 @@ export type KnowledgeFsAnswerTraceResponse = { mode: 'auto' | 'deep' | 'fast' | 'research' query: string query_images?: Array + source?: KnowledgeFsTraceSource steps: Array } @@ -2487,6 +2488,7 @@ export type KnowledgeFsTraceResponse = { query_images?: Array result_count: number scores: KnowledgeFsTraceScoresResponse + source?: KnowledgeFsTraceSource stages: Array } @@ -2496,6 +2498,8 @@ export type KnowledgeFsTraceScoresResponse = { retrieval?: number | null } +export type KnowledgeFsTraceSource = 'agent' | 'mcp' | 'retrieval_test' | 'service_api' | 'workflow' + export type KnowledgeFsTraceStageResponse = { candidate_count?: number | null name: string @@ -6048,6 +6052,7 @@ export type GetKnowledgeFsSpacesByControlSpaceIdTracesData = { } query?: { cursor?: string + source?: 'agent' | 'mcp' | 'retrieval_test' | 'service_api' | 'workflow' } url: '/knowledge-fs/spaces/{control_space_id}/traces' } diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 3a22750bd40..36a74ca8560 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -2047,20 +2047,6 @@ export const zKnowledgeFsQueryImageResponse = z.object({ upload_file_id: z.string(), }) -/** - * KnowledgeFSAnswerTraceResponse - */ -export const zKnowledgeFsAnswerTraceResponse = z.object({ - created_at: z.iso.datetime(), - evidence_bundle_id: z.string().nullish(), - id: z.string(), - knowledge_space_id: z.string(), - mode: z.enum(['auto', 'deep', 'fast', 'research']), - query: z.string(), - query_images: z.array(zKnowledgeFsQueryImageResponse).optional(), - steps: z.array(zKnowledgeFsAnswerTraceStepResponse), -}) - /** * KnowledgeFSReadinessCapabilities */ @@ -2905,6 +2891,29 @@ export const zKnowledgeFsTraceScoresResponse = z.object({ retrieval: z.number().gte(0).lte(1).nullish(), }) +export const zKnowledgeFsTraceSource = z.enum([ + 'agent', + 'mcp', + 'retrieval_test', + 'service_api', + 'workflow', +]) + +/** + * KnowledgeFSAnswerTraceResponse + */ +export const zKnowledgeFsAnswerTraceResponse = z.object({ + created_at: z.iso.datetime(), + evidence_bundle_id: z.string().nullish(), + id: z.string(), + knowledge_space_id: z.string(), + mode: z.enum(['auto', 'deep', 'fast', 'research']), + query: z.string(), + query_images: z.array(zKnowledgeFsQueryImageResponse).optional(), + source: zKnowledgeFsTraceSource.optional().default('retrieval_test'), + steps: z.array(zKnowledgeFsAnswerTraceStepResponse), +}) + /** * KnowledgeFSTraceStageResponse */ @@ -2932,6 +2941,7 @@ export const zKnowledgeFsTraceResponse = z.object({ query_images: z.array(zKnowledgeFsQueryImageResponse).optional(), result_count: z.int().gte(0), scores: zKnowledgeFsTraceScoresResponse, + source: zKnowledgeFsTraceSource.optional().default('retrieval_test'), stages: z.array(zKnowledgeFsTraceStageResponse), }) @@ -5958,6 +5968,7 @@ export const zGetKnowledgeFsSpacesByControlSpaceIdTracesPath = z.object({ export const zGetKnowledgeFsSpacesByControlSpaceIdTracesQuery = z.object({ cursor: z.string().min(1).max(1000).optional(), + source: z.enum(['agent', 'mcp', 'retrieval_test', 'service_api', 'workflow']).optional(), }) /** diff --git a/web/features/new-rag/retrieval/__tests__/model.spec.ts b/web/features/new-rag/retrieval/__tests__/model.spec.ts index 2732d808901..e8d2b117661 100644 --- a/web/features/new-rag/retrieval/__tests__/model.spec.ts +++ b/web/features/new-rag/retrieval/__tests__/model.spec.ts @@ -264,3 +264,35 @@ describe('retrieval test model', () => { ).toBe(false) }) }) + +describe('retrieval record sources', () => { + it('keeps the trace source and defaults legacy traces to retrieval tests', () => { + const [workflow, legacy] = retrievalTestRecords( + [ + { + completed: true, + created_at: '2026-09-04T10:00:00.000Z', + id: 'trace-workflow', + mode: 'fast', + query: 'From a workflow node', + result_count: 3, + source: 'workflow', + } as KnowledgeFsTraceResponse, + { + completed: true, + created_at: '2026-09-04T09:00:00.000Z', + id: 'trace-legacy', + mode: 'fast', + query: 'From the console', + result_count: 1, + } as KnowledgeFsTraceResponse, + ], + [], + ) + + expect(workflow).toEqual(expect.objectContaining({ id: 'trace-workflow', source: 'workflow' })) + expect(legacy).toEqual( + expect.objectContaining({ id: 'trace-legacy', source: 'retrieval_test' }), + ) + }) +}) diff --git a/web/features/new-rag/retrieval/history-panel.tsx b/web/features/new-rag/retrieval/history-panel.tsx index b0f983833c0..b02cca63efe 100644 --- a/web/features/new-rag/retrieval/history-panel.tsx +++ b/web/features/new-rag/retrieval/history-panel.tsx @@ -1,6 +1,8 @@ 'use client' +import type { RetrievalHistorySourceFilter } from './state/scoped' import { Button } from '@langgenius/dify-ui/button' +import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' import { useAtomValue, useSetAtom } from 'jotai' import { useTranslation } from 'react-i18next' import { RecordButton } from './history' @@ -9,20 +11,40 @@ import { retrievalHistoryFactsAtom, selectRetrievalRecordAtom, } from './state/graph' +import { retrievalHistorySourceFilterAtom } from './state/scoped' + +const sourceFilters = ['all', 'retrieval_test', 'workflow'] as const export function RetrievalHistoryPanel() { const { t } = useTranslation('knowledgeSpace') - const { activeRecordKey, displayRecords, hasNextPage, isFetchingNextPage } = + const { activeRecordKey, displayRecords, hasNextPage, isFetchingNextPage, sourceFilter } = useAtomValue(retrievalHistoryFactsAtom) const selectRecord = useSetAtom(selectRetrievalRecordAtom) const loadMore = useSetAtom(loadMoreRetrievalHistoryAtom) + const setSourceFilter = useSetAtom(retrievalHistorySourceFilterAtom) return (
-
+

{t(($) => $['retrievalTest.records'])}

+ + aria-label={t(($) => $['retrievalTest.sourceFilter.label'])} + className="shrink-0" + value={sourceFilter} + onValueChange={setSourceFilter} + > + {sourceFilters.map((filter) => ( + + key={filter} + value={filter} + className="px-1.5 py-0.5 system-2xs-medium" + > + {t(($) => $[`retrievalTest.sourceFilter.${filter}`])} + + ))} +
{displayRecords.length > 0 ? ( diff --git a/web/features/new-rag/retrieval/history.tsx b/web/features/new-rag/retrieval/history.tsx index cc06160ae10..e8accf5fa55 100644 --- a/web/features/new-rag/retrieval/history.tsx +++ b/web/features/new-rag/retrieval/history.tsx @@ -575,6 +575,14 @@ export function RecordButton({ {queryImageCount} )} + {record.kind === 'trace' && record.source !== 'retrieval_test' && ( + + {t(($) => $[`retrievalTest.source.${record.source}`])} + + )} diff --git a/web/features/new-rag/retrieval/model.ts b/web/features/new-rag/retrieval/model.ts index 26146f2deed..33d920e33b9 100644 --- a/web/features/new-rag/retrieval/model.ts +++ b/web/features/new-rag/retrieval/model.ts @@ -2,8 +2,16 @@ import type { KnowledgeFsQueryImageResponse, KnowledgeFsResearchTaskResponse, KnowledgeFsTraceResponse, + KnowledgeFsTraceSource, } from '@dify/contracts/api/console/knowledge-fs/types.gen' +/** Who produced a persisted trace: the console retrieval test or another caller. */ +export type RetrievalTestSource = KnowledgeFsTraceSource + +export function retrievalTestSource(source?: KnowledgeFsTraceSource | null): RetrievalTestSource { + return source ?? 'retrieval_test' +} + export type RetrievalTestMode = 'deep' | 'fast' | 'research' export function normalizedRetrievalTestMode(mode?: string): RetrievalTestMode { @@ -74,6 +82,8 @@ export type RetrievalTestRecord = query: string queryImages?: RetrievalQueryImage[] resultCount?: number + /** Console retrieval test, workflow node, service API, agent, or MCP. */ + source: RetrievalTestSource status: 'completed' | 'failed' } | { @@ -398,6 +408,7 @@ export function retrievalTestRecords( ...(trace.result_count !== null && trace.result_count !== undefined ? { resultCount: trace.result_count } : {}), + source: retrievalTestSource(trace.source), status: trace.completed ? 'completed' : 'failed', })), ...researchTasks.map((task): RetrievalTestRecord => ({ diff --git a/web/features/new-rag/retrieval/state/graph.ts b/web/features/new-rag/retrieval/state/graph.ts index 733f2761738..36a4fadc47c 100644 --- a/web/features/new-rag/retrieval/state/graph.ts +++ b/web/features/new-rag/retrieval/state/graph.ts @@ -24,6 +24,7 @@ import { retrievalAdmittedResearchTasksAtom, retrievalComposerDraftAtom, retrievalComposerImagesAtom, + retrievalHistorySourceFilterAtom, retrievalLocalRunAtom, retrievalLocalSelectedAtom, retrievalRecordImagesAtom, @@ -38,10 +39,17 @@ const tracesQueryAtom = atomWithInfiniteQuery((get) => consoleQuery.knowledgeFs.spaces.byControlSpaceId.traces.get.infiniteOptions({ getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined, initialPageParam: null as string | null, - input: (pageParam) => ({ - params: { control_space_id: get(retrievalKnowledgeSpaceIdAtom) }, - ...(typeof pageParam === 'string' ? { query: { cursor: pageParam } } : {}), - }), + input: (pageParam) => { + const sourceFilter = get(retrievalHistorySourceFilterAtom) + const query = { + ...(typeof pageParam === 'string' ? { cursor: pageParam } : {}), + ...(sourceFilter === 'all' ? {} : { source: sourceFilter }), + } + return { + params: { control_space_id: get(retrievalKnowledgeSpaceIdAtom) }, + ...(Object.keys(query).length > 0 ? { query } : {}), + } + }, refetchInterval: get(retrievalLocalRunAtom)?.status === 'running' ? 1000 : false, }), ) @@ -119,7 +127,14 @@ const retrievalDisplayRecordsAtom = atom((get) => { localRun?.traceId && records.some((record) => record.kind === 'trace' && record.id === localRun.traceId), ) - return localRecord && !traceAlreadyListed ? [localRecord, ...records] : records + const displayRecords = localRecord && !traceAlreadyListed ? [localRecord, ...records] : records + // The server already filters persisted traces by source; local runs and research tasks are + // console retrieval tests, so they only belong to the "all" and "retrieval_test" views. + const sourceFilter = get(retrievalHistorySourceFilterAtom) + if (sourceFilter === 'all') return displayRecords + return displayRecords.filter((record) => + record.kind === 'trace' ? record.source === sourceFilter : sourceFilter === 'retrieval_test', + ) }) const requestedSelectionAtom = atom((get) => { @@ -440,6 +455,7 @@ export const retrievalHistoryFactsAtom = atom((get) => { displayRecords, hasNextPage: Boolean(tracesQuery.hasNextPage || researchTasksQuery.hasNextPage), isFetchingNextPage: tracesQuery.isFetchingNextPage || researchTasksQuery.isFetchingNextPage, + sourceFilter: get(retrievalHistorySourceFilterAtom), } }) diff --git a/web/features/new-rag/retrieval/state/scoped.ts b/web/features/new-rag/retrieval/state/scoped.ts index 63c07663d91..e8e6b2fd35f 100644 --- a/web/features/new-rag/retrieval/state/scoped.ts +++ b/web/features/new-rag/retrieval/state/scoped.ts @@ -62,6 +62,9 @@ export const retrievalAdmittedResearchTasksAtom = atom< Record >({}) export const retrievalResearchRetryPendingAtom = atom(false) +/** History filter: every record, only console retrieval tests, or only workflow-node queries. */ +export type RetrievalHistorySourceFilter = 'all' | 'retrieval_test' | 'workflow' +export const retrievalHistorySourceFilterAtom = atom('all') export const retrievalScopedAtoms = [ retrievalComposerDraftAtom, @@ -73,5 +76,6 @@ export const retrievalScopedAtoms = [ retrievalResearchEventsAtom, retrievalAdmittedResearchTasksAtom, retrievalResearchRetryPendingAtom, + retrievalHistorySourceFilterAtom, retrievalRuntimeBridgeAtom, ] as const diff --git a/web/i18n/ar-TN/knowledge-space.json b/web/i18n/ar-TN/knowledge-space.json index 4958c5733b0..2d7a3ee8aa3 100644 --- a/web/i18n/ar-TN/knowledge-space.json +++ b/web/i18n/ar-TN/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "تم الحفظ كسؤال مرجعي", "retrievalTest.score": "الدرجة", "retrievalTest.showAllChunks": "عرض جميع المقاطع وعددها {{count}}", + "retrievalTest.source.agent": "الوكيل", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "اختبار الاسترجاع", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "سير العمل", + "retrievalTest.sourceFilter.all": "الكل", + "retrievalTest.sourceFilter.label": "مصدر السجل", + "retrievalTest.sourceFilter.retrieval_test": "اختبارات الاسترجاع", + "retrievalTest.sourceFilter.workflow": "سير العمل", "retrievalTest.startResearch": "بدء البحث", "retrievalTest.title": "اختبار الاسترجاع", "retrievalTest.viewInQuality": "عرض في الجودة", diff --git a/web/i18n/de-DE/knowledge-space.json b/web/i18n/de-DE/knowledge-space.json index 8dee3cd5547..c4201f3cd31 100644 --- a/web/i18n/de-DE/knowledge-space.json +++ b/web/i18n/de-DE/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Als Referenzfrage gespeichert", "retrievalTest.score": "Punktzahl", "retrievalTest.showAllChunks": "Alle {{count}} Abschnitte anzeigen", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Abruftest", + "retrievalTest.source.service_api": "Service-API", + "retrievalTest.source.workflow": "Workflow", + "retrievalTest.sourceFilter.all": "Alle", + "retrievalTest.sourceFilter.label": "Quelle des Eintrags", + "retrievalTest.sourceFilter.retrieval_test": "Abruftests", + "retrievalTest.sourceFilter.workflow": "Workflow", "retrievalTest.startResearch": "Recherche starten", "retrievalTest.title": "Abruf testen", "retrievalTest.viewInQuality": "In „Qualität“ anzeigen", diff --git a/web/i18n/en-US/knowledge-space.json b/web/i18n/en-US/knowledge-space.json index c1b5036d812..09c12568478 100644 --- a/web/i18n/en-US/knowledge-space.json +++ b/web/i18n/en-US/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Saved as golden question", "retrievalTest.score": "Score", "retrievalTest.showAllChunks": "Show all {{count}} chunks", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Retrieval test", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "Workflow", + "retrievalTest.sourceFilter.all": "All", + "retrievalTest.sourceFilter.label": "Record source", + "retrievalTest.sourceFilter.retrieval_test": "Retrieval tests", + "retrievalTest.sourceFilter.workflow": "Workflow", "retrievalTest.startResearch": "Start research", "retrievalTest.title": "Retrieval Test", "retrievalTest.viewInQuality": "View in Quality", diff --git a/web/i18n/es-ES/knowledge-space.json b/web/i18n/es-ES/knowledge-space.json index 91ac0f5187e..4ab1d24eae8 100644 --- a/web/i18n/es-ES/knowledge-space.json +++ b/web/i18n/es-ES/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Guardado como pregunta de referencia", "retrievalTest.score": "Puntuación", "retrievalTest.showAllChunks": "Mostrar los {{count}} fragmentos", + "retrievalTest.source.agent": "Agente", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Prueba de recuperación", + "retrievalTest.source.service_api": "API de servicio", + "retrievalTest.source.workflow": "Flujo de trabajo", + "retrievalTest.sourceFilter.all": "Todos", + "retrievalTest.sourceFilter.label": "Origen del registro", + "retrievalTest.sourceFilter.retrieval_test": "Pruebas de recuperación", + "retrievalTest.sourceFilter.workflow": "Flujo de trabajo", "retrievalTest.startResearch": "Iniciar investigación", "retrievalTest.title": "Prueba de recuperación", "retrievalTest.viewInQuality": "Ver en Calidad", diff --git a/web/i18n/fa-IR/knowledge-space.json b/web/i18n/fa-IR/knowledge-space.json index 2850da8b262..eae4302f6a3 100644 --- a/web/i18n/fa-IR/knowledge-space.json +++ b/web/i18n/fa-IR/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "به‌عنوان پرسش مرجع ذخیره شد", "retrievalTest.score": "امتیاز", "retrievalTest.showAllChunks": "نمایش همه {{count}} قطعه", + "retrievalTest.source.agent": "عامل", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "آزمون بازیابی", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "گردش کار", + "retrievalTest.sourceFilter.all": "همه", + "retrievalTest.sourceFilter.label": "منبع رکورد", + "retrievalTest.sourceFilter.retrieval_test": "آزمون‌های بازیابی", + "retrievalTest.sourceFilter.workflow": "گردش کار", "retrievalTest.startResearch": "شروع پژوهش", "retrievalTest.title": "آزمون بازیابی", "retrievalTest.viewInQuality": "نمایش در بخش کیفیت", diff --git a/web/i18n/fr-FR/knowledge-space.json b/web/i18n/fr-FR/knowledge-space.json index 03d9536e304..5152ffc461a 100644 --- a/web/i18n/fr-FR/knowledge-space.json +++ b/web/i18n/fr-FR/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Enregistré comme question de référence", "retrievalTest.score": "Score", "retrievalTest.showAllChunks": "Afficher les {{count}} segments", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Test de récupération", + "retrievalTest.source.service_api": "API de service", + "retrievalTest.source.workflow": "Workflow", + "retrievalTest.sourceFilter.all": "Tout", + "retrievalTest.sourceFilter.label": "Origine de l'enregistrement", + "retrievalTest.sourceFilter.retrieval_test": "Tests de récupération", + "retrievalTest.sourceFilter.workflow": "Workflow", "retrievalTest.startResearch": "Démarrer la recherche", "retrievalTest.title": "Test de récupération", "retrievalTest.viewInQuality": "Afficher dans Qualité", diff --git a/web/i18n/hi-IN/knowledge-space.json b/web/i18n/hi-IN/knowledge-space.json index 2735870c699..14eb99c9e32 100644 --- a/web/i18n/hi-IN/knowledge-space.json +++ b/web/i18n/hi-IN/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "संदर्भ प्रश्न के रूप में सहेजा गया", "retrievalTest.score": "स्कोर", "retrievalTest.showAllChunks": "सभी {{count}} खंड दिखाएँ", + "retrievalTest.source.agent": "एजेंट", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "पुनर्प्राप्ति परीक्षण", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "वर्कफ़्लो", + "retrievalTest.sourceFilter.all": "सभी", + "retrievalTest.sourceFilter.label": "रिकॉर्ड का स्रोत", + "retrievalTest.sourceFilter.retrieval_test": "पुनर्प्राप्ति परीक्षण", + "retrievalTest.sourceFilter.workflow": "वर्कफ़्लो", "retrievalTest.startResearch": "रिसर्च शुरू करें", "retrievalTest.title": "रिट्रीवल परीक्षण", "retrievalTest.viewInQuality": "गुणवत्ता में देखें", diff --git a/web/i18n/id-ID/knowledge-space.json b/web/i18n/id-ID/knowledge-space.json index 02d9537eddd..117a015d7cf 100644 --- a/web/i18n/id-ID/knowledge-space.json +++ b/web/i18n/id-ID/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Disimpan sebagai pertanyaan referensi", "retrievalTest.score": "Skor", "retrievalTest.showAllChunks": "Tampilkan semua {{count}} potongan", + "retrievalTest.source.agent": "Agen", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Uji pengambilan", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "Alur kerja", + "retrievalTest.sourceFilter.all": "Semua", + "retrievalTest.sourceFilter.label": "Sumber catatan", + "retrievalTest.sourceFilter.retrieval_test": "Uji pengambilan", + "retrievalTest.sourceFilter.workflow": "Alur kerja", "retrievalTest.startResearch": "Mulai riset", "retrievalTest.title": "Uji Pengambilan", "retrievalTest.viewInQuality": "Lihat di Kualitas", diff --git a/web/i18n/it-IT/knowledge-space.json b/web/i18n/it-IT/knowledge-space.json index a3792bfcf3f..614c896c69e 100644 --- a/web/i18n/it-IT/knowledge-space.json +++ b/web/i18n/it-IT/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Salvato come domanda di riferimento", "retrievalTest.score": "Punteggio", "retrievalTest.showAllChunks": "Mostra tutti i {{count}} segmenti", + "retrievalTest.source.agent": "Agente", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Test di recupero", + "retrievalTest.source.service_api": "API di servizio", + "retrievalTest.source.workflow": "Workflow", + "retrievalTest.sourceFilter.all": "Tutti", + "retrievalTest.sourceFilter.label": "Origine del record", + "retrievalTest.sourceFilter.retrieval_test": "Test di recupero", + "retrievalTest.sourceFilter.workflow": "Workflow", "retrievalTest.startResearch": "Avvia ricerca", "retrievalTest.title": "Test di recupero", "retrievalTest.viewInQuality": "Visualizza in Qualità", diff --git a/web/i18n/ja-JP/knowledge-space.json b/web/i18n/ja-JP/knowledge-space.json index 4d1ede010d8..6775833d83b 100644 --- a/web/i18n/ja-JP/knowledge-space.json +++ b/web/i18n/ja-JP/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "参考質問として保存しました", "retrievalTest.score": "スコア", "retrievalTest.showAllChunks": "全 {{count}} チャンクを表示", + "retrievalTest.source.agent": "エージェント", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "検索テスト", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "ワークフロー", + "retrievalTest.sourceFilter.all": "すべて", + "retrievalTest.sourceFilter.label": "記録の発生元", + "retrievalTest.sourceFilter.retrieval_test": "検索テスト", + "retrievalTest.sourceFilter.workflow": "ワークフロー", "retrievalTest.startResearch": "調査を開始", "retrievalTest.title": "検索テスト", "retrievalTest.viewInQuality": "品質で表示", diff --git a/web/i18n/ko-KR/knowledge-space.json b/web/i18n/ko-KR/knowledge-space.json index 8d8e120e7d6..a27c498d48e 100644 --- a/web/i18n/ko-KR/knowledge-space.json +++ b/web/i18n/ko-KR/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "참고 질문으로 저장됨", "retrievalTest.score": "점수", "retrievalTest.showAllChunks": "청크 {{count}}개 모두 표시", + "retrievalTest.source.agent": "에이전트", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "검색 테스트", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "워크플로", + "retrievalTest.sourceFilter.all": "전체", + "retrievalTest.sourceFilter.label": "기록 출처", + "retrievalTest.sourceFilter.retrieval_test": "검색 테스트", + "retrievalTest.sourceFilter.workflow": "워크플로", "retrievalTest.startResearch": "리서치 시작", "retrievalTest.title": "검색 테스트", "retrievalTest.viewInQuality": "품질에서 보기", diff --git a/web/i18n/lo-LA/knowledge-space.json b/web/i18n/lo-LA/knowledge-space.json index 6dd81071f7a..5a1f939df92 100644 --- a/web/i18n/lo-LA/knowledge-space.json +++ b/web/i18n/lo-LA/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "ບັນທຶກເປັນຄຳຖາມມາດຕະຖານແລ້ວ", "retrievalTest.score": "ຄະແນນ", "retrievalTest.showAllChunks": "ສະແດງທັງໝົດ {{count}} ສ່ວນ", + "retrievalTest.source.agent": "ຕົວແທນ", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "ທົດສອບການດຶງຂໍ້ມູນ", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "ຂັ້ນຕອນການເຮັດວຽກ", + "retrievalTest.sourceFilter.all": "ທັງໝົດ", + "retrievalTest.sourceFilter.label": "ແຫຼ່ງທີ່ມາຂອງບັນທຶກ", + "retrievalTest.sourceFilter.retrieval_test": "ທົດສອບການດຶງຂໍ້ມູນ", + "retrievalTest.sourceFilter.workflow": "ຂັ້ນຕອນການເຮັດວຽກ", "retrievalTest.startResearch": "ເລີ່ມການຄົ້ນຄວ້າ", "retrievalTest.title": "ທົດສອບການຄົ້ນຄືນ", "retrievalTest.viewInQuality": "ເບິ່ງໃນໜ້າຄຸນະພາບ", diff --git a/web/i18n/nl-NL/knowledge-space.json b/web/i18n/nl-NL/knowledge-space.json index 3908f9483ab..370a3cbedc0 100644 --- a/web/i18n/nl-NL/knowledge-space.json +++ b/web/i18n/nl-NL/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Opgeslagen als referentievraag", "retrievalTest.score": "Score", "retrievalTest.showAllChunks": "Alle {{count}} fragmenten tonen", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Ophaaltest", + "retrievalTest.source.service_api": "Service-API", + "retrievalTest.source.workflow": "Workflow", + "retrievalTest.sourceFilter.all": "Alle", + "retrievalTest.sourceFilter.label": "Bron van record", + "retrievalTest.sourceFilter.retrieval_test": "Ophaaltests", + "retrievalTest.sourceFilter.workflow": "Workflow", "retrievalTest.startResearch": "Onderzoek starten", "retrievalTest.title": "Ophalen testen", "retrievalTest.viewInQuality": "Bekijken in Kwaliteit", diff --git a/web/i18n/pl-PL/knowledge-space.json b/web/i18n/pl-PL/knowledge-space.json index 57934851ab3..3d0ec58e2f5 100644 --- a/web/i18n/pl-PL/knowledge-space.json +++ b/web/i18n/pl-PL/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Zapisano jako pytanie referencyjne", "retrievalTest.score": "Wynik", "retrievalTest.showAllChunks": "Pokaż wszystkie fragmenty ({{count}})", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Test wyszukiwania", + "retrievalTest.source.service_api": "API usługi", + "retrievalTest.source.workflow": "Przepływ pracy", + "retrievalTest.sourceFilter.all": "Wszystkie", + "retrievalTest.sourceFilter.label": "Źródło rekordu", + "retrievalTest.sourceFilter.retrieval_test": "Testy wyszukiwania", + "retrievalTest.sourceFilter.workflow": "Przepływ pracy", "retrievalTest.startResearch": "Rozpocznij badanie", "retrievalTest.title": "Test wyszukiwania", "retrievalTest.viewInQuality": "Zobacz w sekcji Jakość", diff --git a/web/i18n/pt-BR/knowledge-space.json b/web/i18n/pt-BR/knowledge-space.json index f136464634e..6f6dd0e1b34 100644 --- a/web/i18n/pt-BR/knowledge-space.json +++ b/web/i18n/pt-BR/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Salvo como pergunta de referência", "retrievalTest.score": "Pontuação", "retrievalTest.showAllChunks": "Mostrar todos os {{count}} trechos", + "retrievalTest.source.agent": "Agente", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Teste de recuperação", + "retrievalTest.source.service_api": "API de serviço", + "retrievalTest.source.workflow": "Fluxo de trabalho", + "retrievalTest.sourceFilter.all": "Todos", + "retrievalTest.sourceFilter.label": "Origem do registro", + "retrievalTest.sourceFilter.retrieval_test": "Testes de recuperação", + "retrievalTest.sourceFilter.workflow": "Fluxo de trabalho", "retrievalTest.startResearch": "Iniciar pesquisa", "retrievalTest.title": "Teste de recuperação", "retrievalTest.viewInQuality": "Ver em Qualidade", diff --git a/web/i18n/ro-RO/knowledge-space.json b/web/i18n/ro-RO/knowledge-space.json index 993e96e1f6a..a825a43b452 100644 --- a/web/i18n/ro-RO/knowledge-space.json +++ b/web/i18n/ro-RO/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Salvat ca întrebare de referință", "retrievalTest.score": "Scor", "retrievalTest.showAllChunks": "Afișează toate cele {{count}} fragmente", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Test de regăsire", + "retrievalTest.source.service_api": "API de serviciu", + "retrievalTest.source.workflow": "Flux de lucru", + "retrievalTest.sourceFilter.all": "Toate", + "retrievalTest.sourceFilter.label": "Sursa înregistrării", + "retrievalTest.sourceFilter.retrieval_test": "Teste de regăsire", + "retrievalTest.sourceFilter.workflow": "Flux de lucru", "retrievalTest.startResearch": "Începe cercetarea", "retrievalTest.title": "Test de recuperare", "retrievalTest.viewInQuality": "Vezi în Calitate", diff --git a/web/i18n/ru-RU/knowledge-space.json b/web/i18n/ru-RU/knowledge-space.json index 63354725f37..5c2b4c46539 100644 --- a/web/i18n/ru-RU/knowledge-space.json +++ b/web/i18n/ru-RU/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Сохранено как эталонный вопрос", "retrievalTest.score": "Оценка", "retrievalTest.showAllChunks": "Показать все фрагменты ({{count}})", + "retrievalTest.source.agent": "Агент", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Тест поиска", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "Рабочий процесс", + "retrievalTest.sourceFilter.all": "Все", + "retrievalTest.sourceFilter.label": "Источник записи", + "retrievalTest.sourceFilter.retrieval_test": "Тесты поиска", + "retrievalTest.sourceFilter.workflow": "Рабочий процесс", "retrievalTest.startResearch": "Начать исследование", "retrievalTest.title": "Проверка поиска", "retrievalTest.viewInQuality": "Открыть в разделе «Качество»", diff --git a/web/i18n/sl-SI/knowledge-space.json b/web/i18n/sl-SI/knowledge-space.json index f124a739cd8..028de78b0a4 100644 --- a/web/i18n/sl-SI/knowledge-space.json +++ b/web/i18n/sl-SI/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Shranjeno kot referenčno vprašanje", "retrievalTest.score": "Rezultat", "retrievalTest.showAllChunks": "Prikaži vseh {{count}} odsekov", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Preizkus pridobivanja", + "retrievalTest.source.service_api": "Storitveni API", + "retrievalTest.source.workflow": "Potek dela", + "retrievalTest.sourceFilter.all": "Vse", + "retrievalTest.sourceFilter.label": "Vir zapisa", + "retrievalTest.sourceFilter.retrieval_test": "Preizkusi pridobivanja", + "retrievalTest.sourceFilter.workflow": "Potek dela", "retrievalTest.startResearch": "Začni raziskavo", "retrievalTest.title": "Preskus pridobivanja", "retrievalTest.viewInQuality": "Prikaži v razdelku Kakovost", diff --git a/web/i18n/th-TH/knowledge-space.json b/web/i18n/th-TH/knowledge-space.json index 63ad7b28be0..7866ad2e498 100644 --- a/web/i18n/th-TH/knowledge-space.json +++ b/web/i18n/th-TH/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "บันทึกเป็นคำถามอ้างอิงแล้ว", "retrievalTest.score": "คะแนน", "retrievalTest.showAllChunks": "แสดงส่วนเนื้อหาทั้งหมด {{count}} รายการ", + "retrievalTest.source.agent": "เอเจนต์", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "ทดสอบการค้นคืน", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "เวิร์กโฟลว์", + "retrievalTest.sourceFilter.all": "ทั้งหมด", + "retrievalTest.sourceFilter.label": "ที่มาของบันทึก", + "retrievalTest.sourceFilter.retrieval_test": "ทดสอบการค้นคืน", + "retrievalTest.sourceFilter.workflow": "เวิร์กโฟลว์", "retrievalTest.startResearch": "เริ่มการวิจัย", "retrievalTest.title": "ทดสอบการค้นคืน", "retrievalTest.viewInQuality": "ดูในคุณภาพ", diff --git a/web/i18n/tr-TR/knowledge-space.json b/web/i18n/tr-TR/knowledge-space.json index 6f6f0fd66a0..5336e7da5b5 100644 --- a/web/i18n/tr-TR/knowledge-space.json +++ b/web/i18n/tr-TR/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Referans sorusu olarak kaydedildi", "retrievalTest.score": "Puan", "retrievalTest.showAllChunks": "{{count}} parçanın tümünü göster", + "retrievalTest.source.agent": "Ajan", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Getirme testi", + "retrievalTest.source.service_api": "Servis API'si", + "retrievalTest.source.workflow": "İş akışı", + "retrievalTest.sourceFilter.all": "Tümü", + "retrievalTest.sourceFilter.label": "Kayıt kaynağı", + "retrievalTest.sourceFilter.retrieval_test": "Getirme testleri", + "retrievalTest.sourceFilter.workflow": "İş akışı", "retrievalTest.startResearch": "Araştırmayı başlat", "retrievalTest.title": "Getirme Testi", "retrievalTest.viewInQuality": "Kalite bölümünde görüntüle", diff --git a/web/i18n/uk-UA/knowledge-space.json b/web/i18n/uk-UA/knowledge-space.json index 197632c904c..75dcdcac529 100644 --- a/web/i18n/uk-UA/knowledge-space.json +++ b/web/i18n/uk-UA/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Збережено як еталонне запитання", "retrievalTest.score": "Оцінка", "retrievalTest.showAllChunks": "Показати всі фрагменти ({{count}})", + "retrievalTest.source.agent": "Агент", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Тест пошуку", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "Робочий процес", + "retrievalTest.sourceFilter.all": "Усі", + "retrievalTest.sourceFilter.label": "Джерело запису", + "retrievalTest.sourceFilter.retrieval_test": "Тести пошуку", + "retrievalTest.sourceFilter.workflow": "Робочий процес", "retrievalTest.startResearch": "Почати дослідження", "retrievalTest.title": "Перевірка пошуку", "retrievalTest.viewInQuality": "Відкрити в розділі «Якість»", diff --git a/web/i18n/vi-VN/knowledge-space.json b/web/i18n/vi-VN/knowledge-space.json index cecb29a7fc2..4015f7b5978 100644 --- a/web/i18n/vi-VN/knowledge-space.json +++ b/web/i18n/vi-VN/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "Đã lưu làm câu hỏi tham chiếu", "retrievalTest.score": "Điểm", "retrievalTest.showAllChunks": "Hiển thị tất cả {{count}} đoạn", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "Kiểm tra truy xuất", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "Quy trình", + "retrievalTest.sourceFilter.all": "Tất cả", + "retrievalTest.sourceFilter.label": "Nguồn bản ghi", + "retrievalTest.sourceFilter.retrieval_test": "Kiểm tra truy xuất", + "retrievalTest.sourceFilter.workflow": "Quy trình", "retrievalTest.startResearch": "Bắt đầu nghiên cứu", "retrievalTest.title": "Kiểm thử truy xuất", "retrievalTest.viewInQuality": "Xem trong Chất lượng", diff --git a/web/i18n/zh-Hans/knowledge-space.json b/web/i18n/zh-Hans/knowledge-space.json index 53d9a048587..ab15b9a251f 100644 --- a/web/i18n/zh-Hans/knowledge-space.json +++ b/web/i18n/zh-Hans/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "已保存为参考问题", "retrievalTest.score": "得分", "retrievalTest.showAllChunks": "显示全部 {{count}} 个分段", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "检索测试", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "工作流", + "retrievalTest.sourceFilter.all": "全部", + "retrievalTest.sourceFilter.label": "记录来源", + "retrievalTest.sourceFilter.retrieval_test": "检索测试", + "retrievalTest.sourceFilter.workflow": "工作流", "retrievalTest.startResearch": "开始研究", "retrievalTest.title": "检索测试", "retrievalTest.viewInQuality": "前往“质量”页查看", diff --git a/web/i18n/zh-Hant/knowledge-space.json b/web/i18n/zh-Hant/knowledge-space.json index 55fc18d14e4..4d048fb3571 100644 --- a/web/i18n/zh-Hant/knowledge-space.json +++ b/web/i18n/zh-Hant/knowledge-space.json @@ -506,6 +506,15 @@ "retrievalTest.savedGoldenQuestion": "已儲存為參考問題", "retrievalTest.score": "分數", "retrievalTest.showAllChunks": "顯示全部 {{count}} 個片段", + "retrievalTest.source.agent": "Agent", + "retrievalTest.source.mcp": "MCP", + "retrievalTest.source.retrieval_test": "檢索測試", + "retrievalTest.source.service_api": "Service API", + "retrievalTest.source.workflow": "工作流", + "retrievalTest.sourceFilter.all": "全部", + "retrievalTest.sourceFilter.label": "記錄來源", + "retrievalTest.sourceFilter.retrieval_test": "檢索測試", + "retrievalTest.sourceFilter.workflow": "工作流", "retrievalTest.startResearch": "開始研究", "retrievalTest.title": "檢索測試", "retrievalTest.viewInQuality": "在品質中查看",