@world-engines/project-host 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { AuthoringModelConnectionError } from "./authoring-model-connection.js";
|
|
4
|
+
import { classifyDocumentRelationRole } from "./document-relation-role.js";
|
|
5
|
+
import { enumerateDiverseGraphPaths } from "./graph-traversal.js";
|
|
6
|
+
import { checkSelectedPathQuotes } from "./document-query-path-proof.js";
|
|
7
|
+
import { canonicalizeSelectedProofPaths } from "./document-query-proof-paths.js";
|
|
8
|
+
import { renderDocumentQueryPathOptions } from "./document-query-path-renderer.js";
|
|
9
|
+
const REQUIRED_MODEL = "deepseek-v4-flash";
|
|
10
|
+
const DEFAULT_DEPTH = 2;
|
|
11
|
+
const MAX_DEPTH = 4;
|
|
12
|
+
const DEFAULT_SEARCH_LIMIT = 12;
|
|
13
|
+
const MAX_SEARCH_LIMIT = 32;
|
|
14
|
+
const DEFAULT_PATH_LIMIT = 48;
|
|
15
|
+
const MAX_PATH_LIMIT = 128;
|
|
16
|
+
const DEFAULT_EVIDENCE_CHARACTERS = 16_000;
|
|
17
|
+
const MAX_EVIDENCE_CHARACTERS = 60_000;
|
|
18
|
+
const MAX_QUERY_CHARACTERS = 1_000;
|
|
19
|
+
const MAX_PLANNED_QUERIES = 4;
|
|
20
|
+
const MAX_PLANNED_RELATIONS = 16;
|
|
21
|
+
const MAX_READBACK_IDS = 512;
|
|
22
|
+
const MAX_QUOTE_CHARACTERS = 500;
|
|
23
|
+
const GROUNDED_MAX_OUTPUT_TOKENS = 4_096;
|
|
24
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
25
|
+
const IDENTIFIER = /^[^\0\r\n]{1,512}$/u;
|
|
26
|
+
function pathIdentifier(path) {
|
|
27
|
+
return `path:${createHash("sha256").update(JSON.stringify([path.nodeIds, path.edges.map((edge) => JSON.stringify([edge.src, edge.dst, edge.kind]))])).digest("hex").slice(0, 24)}`;
|
|
28
|
+
}
|
|
29
|
+
function renderedPaths(paths, entities, relations) {
|
|
30
|
+
return renderDocumentQueryPathOptions({ paths: paths.map((path) => ({ ...path, pathId: pathIdentifier(path) })),
|
|
31
|
+
entities: new Map(entities.map((entity) => [entity.id, { id: entity.id, kind: entity.kind, props: record(entity.props, "readback_entity_props_invalid") }])),
|
|
32
|
+
relations: relations.map((edge) => ({ src: edge.src, dst: edge.dst, kind: edge.kind, props: record(edge.props, "readback_relation_props_invalid") })),
|
|
33
|
+
}).path_options;
|
|
34
|
+
}
|
|
35
|
+
// factory 的消息要求 NFC;仅转义含非 NFC 字节序列的 JSON 字符串,解码后原文不变。
|
|
36
|
+
function modelJson(value) {
|
|
37
|
+
return JSON.stringify(value).replace(/"(?:\\.|[^"\\])*"/gu, (token) => token.normalize("NFC") === token ? token
|
|
38
|
+
: token.replace(/[^\x20-\x7e]/g, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`));
|
|
39
|
+
}
|
|
40
|
+
function quoteOptions(evidence) {
|
|
41
|
+
return Object.freeze(evidence.flatMap((source) => {
|
|
42
|
+
const options = [];
|
|
43
|
+
let start = 0;
|
|
44
|
+
while (start < source.text.length) {
|
|
45
|
+
let end = Math.min(start + MAX_QUOTE_CHARACTERS, source.text.length);
|
|
46
|
+
if (end < source.text.length) {
|
|
47
|
+
// 保留完整句边界;无句界的长句才在硬上限切分。
|
|
48
|
+
const prefix = source.text.slice(start, end);
|
|
49
|
+
const boundaries = [...prefix.matchAll(/[。!?!?;;][”’」』】))"']*[ \t\r\n]*/gu)];
|
|
50
|
+
const last = boundaries.at(-1);
|
|
51
|
+
if (last !== undefined)
|
|
52
|
+
end = start + last.index + last[0].length;
|
|
53
|
+
const previous = source.text.charCodeAt(end - 1);
|
|
54
|
+
const next = source.text.charCodeAt(end);
|
|
55
|
+
if ((previous >= 0xd800 && previous <= 0xdbff && next >= 0xdc00 && next <= 0xdfff)
|
|
56
|
+
|| (source.text[end - 1] === "\r" && source.text[end] === "\n"))
|
|
57
|
+
end -= 1;
|
|
58
|
+
}
|
|
59
|
+
if (end <= start)
|
|
60
|
+
return fail("quote_option_boundary_invalid");
|
|
61
|
+
const text = source.text.slice(start, end);
|
|
62
|
+
const quoteId = `q:${createHash("sha256").update(JSON.stringify([source.evidenceId, source.contentSha256, start, end])).digest("hex")}`;
|
|
63
|
+
options.push(Object.freeze({ quoteId, evidenceId: source.evidenceId, start, end, text }));
|
|
64
|
+
start = end;
|
|
65
|
+
}
|
|
66
|
+
return options;
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
function fail(code) {
|
|
70
|
+
throw new Error(`document_query:${code}`);
|
|
71
|
+
}
|
|
72
|
+
function record(value, code) {
|
|
73
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
74
|
+
return fail(code);
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
function identifier(value, code) {
|
|
78
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value) || value.trim() !== value || value.normalize("NFC") !== value)
|
|
79
|
+
return fail(code);
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function boundedInteger(value, fallback, maximum, code) {
|
|
83
|
+
const resolved = value ?? fallback;
|
|
84
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > maximum)
|
|
85
|
+
return fail(code);
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
function stringArray(value, code, maximum) {
|
|
89
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > maximum)
|
|
90
|
+
return fail(code);
|
|
91
|
+
return Object.freeze(value.map((item) => identifier(item, code)));
|
|
92
|
+
}
|
|
93
|
+
function usageTotal(usages, providerId, modelId) {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
inputTokens: usages.reduce((sum, usage) => sum + usage.inputTokens, 0),
|
|
96
|
+
outputTokens: usages.reduce((sum, usage) => sum + usage.outputTokens, 0),
|
|
97
|
+
totalTokens: usages.reduce((sum, usage) => sum + usage.totalTokens, 0),
|
|
98
|
+
cachedInputTokens: usages.reduce((sum, usage) => sum + (usage.cachedInputTokens ?? 0), 0),
|
|
99
|
+
requests: usages.reduce((sum, usage) => sum + usage.requests, 0),
|
|
100
|
+
providerId,
|
|
101
|
+
modelId,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async function generate(connection, request, signal, usages) {
|
|
105
|
+
const result = await connection.structuredJson.generate({
|
|
106
|
+
...request,
|
|
107
|
+
...(signal === undefined ? {} : { signal }),
|
|
108
|
+
});
|
|
109
|
+
usages.push(result.usage);
|
|
110
|
+
return result.value;
|
|
111
|
+
}
|
|
112
|
+
function validatePlan(value, request, searchOnly = false) {
|
|
113
|
+
const candidate = record(value, "planner_output_invalid");
|
|
114
|
+
const searchQueries = stringArray(candidate.search_queries, "planner_queries_invalid", MAX_PLANNED_QUERIES);
|
|
115
|
+
if (searchQueries.some((query) => query.length > 256))
|
|
116
|
+
return fail("planner_query_too_long");
|
|
117
|
+
if (searchOnly)
|
|
118
|
+
return { search_queries: searchQueries, direction: "out", depth: request.maximumDepth, relation_kinds: [] };
|
|
119
|
+
if (candidate.direction !== "in" && candidate.direction !== "out" && candidate.direction !== "both")
|
|
120
|
+
return fail("planner_direction_invalid");
|
|
121
|
+
if (!Number.isSafeInteger(candidate.depth) || candidate.depth < 1 || candidate.depth > request.maximumDepth)
|
|
122
|
+
return fail("planner_depth_out_of_bounds");
|
|
123
|
+
const relationKinds = Array.isArray(candidate.relation_kinds) && candidate.relation_kinds.length === 0
|
|
124
|
+
? Object.freeze([]) : stringArray(candidate.relation_kinds, "planner_relation_kinds_invalid", MAX_PLANNED_RELATIONS);
|
|
125
|
+
if (relationKinds.some((kind) => !request.allowedRelationKinds.has(kind)))
|
|
126
|
+
return fail("planner_relation_kind_forbidden");
|
|
127
|
+
return Object.freeze({ search_queries: searchQueries, direction: candidate.direction, depth: candidate.depth, relation_kinds: relationKinds });
|
|
128
|
+
}
|
|
129
|
+
function searchHits(value, scopeId) {
|
|
130
|
+
const root = record(value, "search_result_invalid");
|
|
131
|
+
if (!Array.isArray(root.results))
|
|
132
|
+
return fail("search_result_invalid");
|
|
133
|
+
return Object.freeze(root.results.map((entry) => {
|
|
134
|
+
const hit = record(entry, "search_hit_invalid");
|
|
135
|
+
const hitScope = hit.scopeId ?? hit.scope_id ?? null;
|
|
136
|
+
if (hitScope !== null && hitScope !== scopeId)
|
|
137
|
+
return fail("search_scope_mismatch");
|
|
138
|
+
const channels = Array.isArray(hit.channels) ? hit.channels.map((channel) => identifier(channel, "search_channel_invalid")) : [];
|
|
139
|
+
return Object.freeze({ id: identifier(hit.id, "search_id_invalid"), kind: identifier(hit.kind, "search_kind_invalid"), channels: Object.freeze(channels) });
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
function graphPaths(value, seedIds, plan, pathLimit) {
|
|
143
|
+
const root = record(value, "traverse_result_invalid");
|
|
144
|
+
if (!Array.isArray(root.paths) || root.paths.length > pathLimit)
|
|
145
|
+
return fail("traverse_path_limit_exceeded");
|
|
146
|
+
return Object.freeze(root.paths.map((entry) => {
|
|
147
|
+
const path = record(entry, "traverse_path_invalid");
|
|
148
|
+
const nodeIds = stringArray(path.nodeIds ?? path.node_ids, "traverse_node_ids_invalid", plan.depth + 1);
|
|
149
|
+
const firstNodeId = nodeIds[0];
|
|
150
|
+
if (firstNodeId === undefined || !seedIds.has(firstNodeId) || nodeIds.length < 2 || new Set(nodeIds).size !== nodeIds.length)
|
|
151
|
+
return fail("traverse_path_out_of_bounds");
|
|
152
|
+
if (!Array.isArray(path.edges) || path.edges.length !== nodeIds.length - 1)
|
|
153
|
+
return fail("traverse_edges_invalid");
|
|
154
|
+
const edges = path.edges.map((entryValue, index) => {
|
|
155
|
+
const edge = record(entryValue, "traverse_edge_invalid");
|
|
156
|
+
const src = identifier(edge.src, "traverse_edge_src_invalid");
|
|
157
|
+
const dst = identifier(edge.dst, "traverse_edge_dst_invalid");
|
|
158
|
+
const kind = identifier(edge.kind, "traverse_edge_kind_invalid");
|
|
159
|
+
if (!plan.relation_kinds.includes(kind))
|
|
160
|
+
return fail("traverse_relation_kind_forbidden");
|
|
161
|
+
const left = nodeIds[index];
|
|
162
|
+
const right = nodeIds[index + 1];
|
|
163
|
+
if (left === undefined || right === undefined)
|
|
164
|
+
return fail("traverse_edge_out_of_bounds");
|
|
165
|
+
const forward = src === left && dst === right;
|
|
166
|
+
const backward = src === right && dst === left;
|
|
167
|
+
if ((plan.direction === "out" && !forward) || (plan.direction === "in" && !backward) || (plan.direction === "both" && !forward && !backward))
|
|
168
|
+
return fail("traverse_edge_direction_mismatch");
|
|
169
|
+
const props = edge.props === undefined ? undefined : record(edge.props, "traverse_edge_props_invalid");
|
|
170
|
+
const qualifiers = edge.qualifiers ?? props?.qualifiers;
|
|
171
|
+
if (qualifiers !== undefined && (typeof qualifiers !== "object" || qualifiers === null || Array.isArray(qualifiers)))
|
|
172
|
+
return fail("traverse_edge_qualifiers_invalid");
|
|
173
|
+
return Object.freeze({ src, dst, kind, ...(qualifiers === undefined ? {} : { qualifiers: qualifiers }) });
|
|
174
|
+
});
|
|
175
|
+
return Object.freeze({ nodeIds, edges: Object.freeze(edges) });
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
function sourceBinding(schema) {
|
|
179
|
+
if (schema.project_id === undefined && schema.project_revision === undefined && schema.source_sha256 === undefined)
|
|
180
|
+
return null;
|
|
181
|
+
const projectId = identifier(schema.project_id, "schema_project_id_invalid");
|
|
182
|
+
if (!Number.isSafeInteger(schema.project_revision) || schema.project_revision < 0)
|
|
183
|
+
return fail("schema_project_revision_invalid");
|
|
184
|
+
if (typeof schema.source_sha256 !== "string" || !SHA256.test(schema.source_sha256))
|
|
185
|
+
return fail("schema_source_sha256_invalid");
|
|
186
|
+
return Object.freeze({ projectId, projectRevision: schema.project_revision, sourceSha256: schema.source_sha256 });
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 外部 harness 的检索 seam:Host 只做 hybrid recall、物理图遍历、原文回读与完整性校验,
|
|
190
|
+
* 不调用 structured model,也不生成答案。结构理解、否定/时间推理由调用方基于 bundle 完成。
|
|
191
|
+
*/
|
|
192
|
+
export async function retrieveDocumentEvidenceBundle(host, request) {
|
|
193
|
+
if (typeof request.query !== "string" || request.query.trim().length === 0 || request.query.includes("\0") || request.query.normalize("NFC") !== request.query)
|
|
194
|
+
return fail("query_invalid");
|
|
195
|
+
if (request.query.length > MAX_QUERY_CHARACTERS)
|
|
196
|
+
return fail("query_too_long");
|
|
197
|
+
const query = request.query;
|
|
198
|
+
const searchMode = request.searchMode ?? "hybrid";
|
|
199
|
+
if (searchMode !== "keyword" && searchMode !== "hybrid")
|
|
200
|
+
return fail("search_mode_invalid");
|
|
201
|
+
const scopeId = identifier(request.scopeId, "scope_invalid");
|
|
202
|
+
const maximumDepth = boundedInteger(request.maxDepth, DEFAULT_DEPTH, MAX_DEPTH, "depth_out_of_bounds");
|
|
203
|
+
const searchLimit = boundedInteger(request.searchLimit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, "search_limit_out_of_bounds");
|
|
204
|
+
const pathLimit = boundedInteger(request.pathLimit, DEFAULT_PATH_LIMIT, MAX_PATH_LIMIT, "path_limit_out_of_bounds");
|
|
205
|
+
const evidenceBudget = boundedInteger(request.evidenceCharacterBudget, DEFAULT_EVIDENCE_CHARACTERS, MAX_EVIDENCE_CHARACTERS, "evidence_budget_out_of_bounds");
|
|
206
|
+
const queries = request.searchQueries === undefined ? Object.freeze([query])
|
|
207
|
+
: stringArray(request.searchQueries, "search_queries_invalid", MAX_PLANNED_QUERIES);
|
|
208
|
+
if (queries.some((value) => value.length > 256))
|
|
209
|
+
return fail("search_query_too_long");
|
|
210
|
+
const firstSchema = record(await host.dispatchTool({ name: "world_schema", input: {} }), "schema_invalid");
|
|
211
|
+
if (!Array.isArray(firstSchema.relation_kinds))
|
|
212
|
+
return fail("schema_relation_kinds_invalid");
|
|
213
|
+
const actualKinds = new Set(firstSchema.relation_kinds.map((kind) => identifier(kind, "schema_relation_kind_invalid")));
|
|
214
|
+
const allowedKinds = new Set(request.allowedRelationKinds ?? actualKinds);
|
|
215
|
+
if (request.allowedRelationKinds !== undefined && (allowedKinds.size < 1 || allowedKinds.size > 128))
|
|
216
|
+
return fail("allowed_relation_kinds_invalid");
|
|
217
|
+
for (const kind of allowedKinds)
|
|
218
|
+
if (!actualKinds.has(identifier(kind, "allowed_relation_kind_invalid")))
|
|
219
|
+
return fail("allowed_relation_kind_unknown");
|
|
220
|
+
const binding = sourceBinding(firstSchema);
|
|
221
|
+
const trace = [];
|
|
222
|
+
const candidates = [];
|
|
223
|
+
const seedIds = new Set();
|
|
224
|
+
const hitKinds = new Map();
|
|
225
|
+
const seedScores = new Map();
|
|
226
|
+
for (const searchQuery of queries) {
|
|
227
|
+
const hits = searchHits(await host.dispatchTool({ name: "world_search", input: {
|
|
228
|
+
query: searchQuery, mode: searchMode, limit: searchLimit, filters: { scope_id: scopeId },
|
|
229
|
+
} }), scopeId).slice(0, searchLimit);
|
|
230
|
+
for (const [rank, hit] of hits.entries()) {
|
|
231
|
+
candidates.push({ ...hit, query: searchQuery, rank });
|
|
232
|
+
if (hit.kind === "world_source_document" || hit.kind === "source_document")
|
|
233
|
+
continue;
|
|
234
|
+
seedIds.add(hit.id);
|
|
235
|
+
hitKinds.set(hit.id, hit.kind);
|
|
236
|
+
seedScores.set(hit.id, (seedScores.get(hit.id) ?? 0) + 1 / (rank + 1));
|
|
237
|
+
}
|
|
238
|
+
trace.push({ stage: "search", detail: { query: searchQuery, seedIds: hits.map((hit) => hit.id), channels: hits.map((hit) => hit.channels) } });
|
|
239
|
+
}
|
|
240
|
+
if (seedIds.size === 0) {
|
|
241
|
+
if (binding !== null) {
|
|
242
|
+
const after = sourceBinding(record(await host.dispatchTool({ name: "world_schema", input: {} }), "schema_invalid"));
|
|
243
|
+
if (after === null || !isDeepStrictEqual(after, binding))
|
|
244
|
+
return fail("source_changed_during_retrieval");
|
|
245
|
+
}
|
|
246
|
+
return Object.freeze({ query, scopeId, searchMode, candidates: Object.freeze(candidates), graphPaths: [], pathOptions: [], evidence: [],
|
|
247
|
+
integrity: Object.freeze({ sourceBinding: binding === null ? "unavailable" : "verified", evidenceCoverage: "empty", traversalTruncated: false }),
|
|
248
|
+
retrievalTrace: Object.freeze(trace) });
|
|
249
|
+
}
|
|
250
|
+
const seedReadback = await readGraph(host, seedIds, scopeId);
|
|
251
|
+
const sourceSeedIds = new Set([...hitKinds].filter(([, kind]) => kind === "world_source_chunk" || kind === "source_chunk").map(([id]) => id));
|
|
252
|
+
const seedAssertions = await readGraph(host, assertionIds([seedReadback], new Set(seedIds)), scopeId);
|
|
253
|
+
const semantic = semanticSeeds([seedReadback, seedAssertions], hitKinds);
|
|
254
|
+
const traversalValue = semantic.size === 0 ? { paths: [], truncated: false } : record(await host.dispatchTool({ name: "world_traverse", input: {
|
|
255
|
+
start_ids: [...semantic], scope_id: scopeId, direction: "both", depth: maximumDepth,
|
|
256
|
+
relation_kinds: [...allowedKinds], semantic_only: true, limit: pathLimit,
|
|
257
|
+
} }), "traverse_result_invalid");
|
|
258
|
+
const nativeTraversalTruncated = traversalValue.truncated === true;
|
|
259
|
+
const nativePaths = semantic.size === 0 ? [] : graphPaths(traversalValue, semantic, {
|
|
260
|
+
search_queries: queries, direction: "both", depth: maximumDepth, relation_kinds: [...allowedKinds],
|
|
261
|
+
}, pathLimit);
|
|
262
|
+
const nativeEdges = [...new Map(nativePaths.flatMap((path) => path.edges).map((edge) => [JSON.stringify([edge.src, edge.dst, edge.kind]), edge])).values()];
|
|
263
|
+
const enumerated = enumerateDiverseGraphPaths({ edges: nativeEdges, seeds: semantic, maximumDepth, limit: pathLimit });
|
|
264
|
+
const traversalTruncated = nativeTraversalTruncated || enumerated.truncated;
|
|
265
|
+
const candidateIds = new Set(enumerated.paths.flatMap((path) => path.nodeIds));
|
|
266
|
+
const rawPaths = graphPaths({ paths: enumerated.paths }, candidateIds, {
|
|
267
|
+
search_queries: queries, direction: "both", depth: maximumDepth, relation_kinds: [...allowedKinds],
|
|
268
|
+
}, pathLimit).filter((path) => path.nodeIds.some((id) => semantic.has(id)));
|
|
269
|
+
trace.push({ stage: "traverse", detail: { authority: "world_traverse_physical_relations", pathCount: rawPaths.length, truncated: traversalTruncated } });
|
|
270
|
+
const pathIds = new Set(rawPaths.flatMap((path) => path.nodeIds));
|
|
271
|
+
const pathReadback = await readGraph(host, pathIds, scopeId);
|
|
272
|
+
const pathAssertions = await readGraph(host, assertionIds([pathReadback], pathIds), scopeId);
|
|
273
|
+
const readbacks = [seedReadback, seedAssertions, pathReadback, pathAssertions];
|
|
274
|
+
const rankedChunkIds = [...rankedSourceChunkIds(readbacks, seedScores, rawPaths, rawPaths)];
|
|
275
|
+
for (const id of sourceSeedIds)
|
|
276
|
+
if (!rankedChunkIds.includes(id))
|
|
277
|
+
rankedChunkIds.push(id);
|
|
278
|
+
const chunksReadback = await readGraph(host, new Set(rankedChunkIds), scopeId);
|
|
279
|
+
readbacks.push(chunksReadback);
|
|
280
|
+
const allIds = new Set([...seedIds, ...pathIds, ...rankedChunkIds]);
|
|
281
|
+
const uniqueEntities = [...new Map(readbacks.flatMap((item) => item.entities).map((entity) => [entity.id, entity])).values()];
|
|
282
|
+
const evidenceById = new Map(evidenceFromReadback({ entities: uniqueEntities }, allIds, scopeId).map((item) => [item.evidenceId, item]));
|
|
283
|
+
const rankedEvidence = rankedChunkIds.flatMap((id) => evidenceById.get(id) ?? []);
|
|
284
|
+
const evidence = budgetEvidence(rankedEvidence, evidenceBudget);
|
|
285
|
+
const paths = supportedPaths(rawPaths, readbacks, evidence);
|
|
286
|
+
const relations = [...relationIndex(readbacks.flatMap((item) => item.relations)).values()];
|
|
287
|
+
const pathOptions = renderedPaths(paths, uniqueEntities, relations);
|
|
288
|
+
trace.push({ stage: "readback", detail: { evidenceIds: evidence.map((item) => item.evidenceId), usedCharacters: evidence.reduce((sum, item) => sum + item.text.length, 0),
|
|
289
|
+
supportedPathCount: paths.length, candidatePathCount: rawPaths.length } });
|
|
290
|
+
if (binding !== null) {
|
|
291
|
+
const after = sourceBinding(record(await host.dispatchTool({ name: "world_schema", input: {} }), "schema_invalid"));
|
|
292
|
+
if (after === null || !isDeepStrictEqual(after, binding))
|
|
293
|
+
return fail("source_changed_during_retrieval");
|
|
294
|
+
}
|
|
295
|
+
const fullySupportedPathCount = paths.filter((path) => path.edges.every((edge) => (edge.evidenceIds?.length ?? 0) > 0)).length;
|
|
296
|
+
const coverage = rankedEvidence.length === 0 ? "empty"
|
|
297
|
+
: (!traversalTruncated && evidence.length === rankedEvidence.length && fullySupportedPathCount === rawPaths.length ? "complete" : "partial");
|
|
298
|
+
return Object.freeze({ query, scopeId, searchMode, candidates: Object.freeze(candidates), graphPaths: Object.freeze(paths), pathOptions: Object.freeze(pathOptions),
|
|
299
|
+
evidence: Object.freeze(evidence), integrity: Object.freeze({ sourceBinding: binding === null ? "unavailable" : "verified", evidenceCoverage: coverage, traversalTruncated }),
|
|
300
|
+
retrievalTrace: Object.freeze(trace) });
|
|
301
|
+
}
|
|
302
|
+
function evidenceFromReadback(value, allowedIds, scopeId) {
|
|
303
|
+
const root = record(value, "readback_invalid");
|
|
304
|
+
if (!Array.isArray(root.entities))
|
|
305
|
+
return fail("readback_entities_invalid");
|
|
306
|
+
return Object.freeze(root.entities.flatMap((entry) => {
|
|
307
|
+
const entity = record(entry, "readback_entity_invalid");
|
|
308
|
+
const id = identifier(entity.id, "readback_entity_id_invalid");
|
|
309
|
+
if (!allowedIds.has(id))
|
|
310
|
+
return fail("readback_unknown_entity");
|
|
311
|
+
if (entity.kind !== "world_source_chunk" && entity.kind !== "source_chunk")
|
|
312
|
+
return [];
|
|
313
|
+
const props = record(entity.props, "readback_chunk_props_invalid");
|
|
314
|
+
const nodeScope = props.scopeId ?? props.scope_id ?? entity.scopeId ?? entity.scope_id;
|
|
315
|
+
if (nodeScope !== undefined && nodeScope !== scopeId)
|
|
316
|
+
return fail("readback_scope_mismatch");
|
|
317
|
+
if (props.generated_context === true || props.evidence_authority === "generated_context")
|
|
318
|
+
return [];
|
|
319
|
+
const text = props.index_text;
|
|
320
|
+
const documentId = props.documentId ?? props.document_id;
|
|
321
|
+
const startOffset = props.startOffset ?? props.start_offset;
|
|
322
|
+
const endOffset = props.endOffset ?? props.end_offset;
|
|
323
|
+
const contentSha256 = props.contentSha256 ?? props.content_sha256 ?? props.chunk_sha256;
|
|
324
|
+
if (typeof text !== "string" || text.length === 0)
|
|
325
|
+
return fail("readback_chunk_text_invalid");
|
|
326
|
+
if (!Number.isSafeInteger(startOffset) || !Number.isSafeInteger(endOffset) || startOffset < 0 || endOffset <= startOffset)
|
|
327
|
+
return fail("readback_chunk_offsets_invalid");
|
|
328
|
+
if (typeof contentSha256 !== "string" || !SHA256.test(contentSha256)
|
|
329
|
+
|| createHash("sha256").update(text, "utf8").digest("hex") !== contentSha256)
|
|
330
|
+
return fail("readback_chunk_hash_invalid");
|
|
331
|
+
if (endOffset - startOffset !== text.length)
|
|
332
|
+
return fail("readback_chunk_offsets_invalid");
|
|
333
|
+
return [Object.freeze({
|
|
334
|
+
evidenceId: id,
|
|
335
|
+
documentId: identifier(documentId, "readback_document_id_invalid"),
|
|
336
|
+
startOffset: startOffset,
|
|
337
|
+
endOffset: endOffset,
|
|
338
|
+
contentSha256,
|
|
339
|
+
text,
|
|
340
|
+
})];
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
function budgetEvidence(values, maximumCharacters) {
|
|
344
|
+
const result = [];
|
|
345
|
+
let used = 0;
|
|
346
|
+
for (const evidence of values) {
|
|
347
|
+
if (used + evidence.text.length > maximumCharacters)
|
|
348
|
+
continue;
|
|
349
|
+
used += evidence.text.length;
|
|
350
|
+
result.push(evidence);
|
|
351
|
+
}
|
|
352
|
+
return Object.freeze(result);
|
|
353
|
+
}
|
|
354
|
+
function relationIndex(relations) {
|
|
355
|
+
const result = new Map();
|
|
356
|
+
for (const edge of relations) {
|
|
357
|
+
const key = JSON.stringify([edge.src, edge.dst, edge.kind]);
|
|
358
|
+
const previous = result.get(key);
|
|
359
|
+
// 当前 physical identity 不含断言版本;不同限定/证据不能用 last-write-wins 猜一个事实。
|
|
360
|
+
if (previous !== undefined && !isDeepStrictEqual(previous.props, edge.props))
|
|
361
|
+
return fail("physical_relation_conflict");
|
|
362
|
+
result.set(key, edge);
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
async function readGraph(host, ids, scopeId) {
|
|
367
|
+
if (ids.size === 0)
|
|
368
|
+
return { entities: [], relations: [] };
|
|
369
|
+
if (ids.size > MAX_READBACK_IDS)
|
|
370
|
+
return fail("readback_limit_exceeded");
|
|
371
|
+
const root = record(await host.dispatchTool({ name: "world_get", input: { ids: [...ids], include_relations: true } }), "readback_invalid");
|
|
372
|
+
if (!Array.isArray(root.entities) || !Array.isArray(root.relations))
|
|
373
|
+
return fail("readback_invalid");
|
|
374
|
+
const entities = root.entities.map((value) => {
|
|
375
|
+
const entity = record(value, "readback_entity_invalid");
|
|
376
|
+
if (!ids.has(identifier(entity.id, "readback_entity_id_invalid")))
|
|
377
|
+
return fail("readback_unknown_entity");
|
|
378
|
+
const props = record(entity.props, "readback_entity_props_invalid");
|
|
379
|
+
const actualScope = props.scope_id ?? props.scopeId ?? entity.scope_id ?? entity.scopeId;
|
|
380
|
+
if (actualScope !== undefined && actualScope !== scopeId)
|
|
381
|
+
return fail("readback_scope_mismatch");
|
|
382
|
+
return entity;
|
|
383
|
+
});
|
|
384
|
+
const relations = root.relations.map((value) => {
|
|
385
|
+
const edge = record(value, "readback_relation_invalid");
|
|
386
|
+
const src = identifier(edge.src, "readback_relation_src_invalid");
|
|
387
|
+
const dst = identifier(edge.dst, "readback_relation_dst_invalid");
|
|
388
|
+
identifier(edge.kind, "readback_relation_kind_invalid");
|
|
389
|
+
if (!ids.has(src) && !ids.has(dst))
|
|
390
|
+
return fail("readback_relation_not_incident");
|
|
391
|
+
return edge;
|
|
392
|
+
});
|
|
393
|
+
return { entities, relations: [...relationIndex(relations).values()] };
|
|
394
|
+
}
|
|
395
|
+
/** 搜索之后才基于实际候选选择关系及物理遍历方向;候选本身不构成事实证据。 */
|
|
396
|
+
async function refineRoute(host, input, dependencies, usages, trace) {
|
|
397
|
+
const readbacks = [];
|
|
398
|
+
const nodes = new Map();
|
|
399
|
+
const endpointNodes = new Map();
|
|
400
|
+
const loadedIds = new Set();
|
|
401
|
+
const edges = new Map();
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
const nodeLimit = Math.min(MAX_READBACK_IDS, Math.max(input.seedIds.size, input.pathLimit * (input.maximumDepth + 1)));
|
|
404
|
+
const edgeLimit = Math.min(MAX_READBACK_IDS, input.pathLimit * input.maximumDepth);
|
|
405
|
+
let truncated = false;
|
|
406
|
+
let frontier = new Set([...input.seedIds].slice(0, nodeLimit));
|
|
407
|
+
const safeNode = (entity) => !String(entity.kind).includes("source_")
|
|
408
|
+
&& entity.kind !== "world_ingestion_source_control" && entity.kind !== "world_document_statement" && entity.kind !== "world_document_edge_assertion";
|
|
409
|
+
const remember = (readback, requested) => {
|
|
410
|
+
for (const id of requested)
|
|
411
|
+
loadedIds.add(id);
|
|
412
|
+
for (const entity of readback.entities) {
|
|
413
|
+
const props = entity.props;
|
|
414
|
+
const scope = props.scope_id ?? props.scopeId ?? entity.scope_id ?? entity.scopeId;
|
|
415
|
+
if (scope !== input.scopeId)
|
|
416
|
+
return fail("route_scope_missing");
|
|
417
|
+
endpointNodes.set(entity.id, entity);
|
|
418
|
+
if (safeNode(entity)) {
|
|
419
|
+
if (nodes.has(entity.id) || nodes.size < nodeLimit)
|
|
420
|
+
nodes.set(entity.id, entity);
|
|
421
|
+
else
|
|
422
|
+
truncated = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
readbacks.push({ entities: readback.entities.filter(safeNode), relations: readback.relations });
|
|
426
|
+
};
|
|
427
|
+
for (let round = 0; round <= input.maximumDepth && frontier.size > 0; round += 1) {
|
|
428
|
+
const readback = await readGraph(host, frontier, input.scopeId);
|
|
429
|
+
for (const id of frontier)
|
|
430
|
+
seen.add(id);
|
|
431
|
+
remember(readback, frontier);
|
|
432
|
+
const incident = readback.relations.filter((edge) => input.allowedKinds.has(edge.kind));
|
|
433
|
+
// 先回读端点再交给唯一角色分类器;unknown 不得当成 semantic,也不能沿 source hub 扩散。
|
|
434
|
+
const unresolved = new Set();
|
|
435
|
+
const byAssertion = [...incident].sort((left, right) => Number(typeof right.props?.assertion === "string") - Number(typeof left.props?.assertion === "string"));
|
|
436
|
+
for (const edge of byAssertion)
|
|
437
|
+
for (const value of [edge.src, edge.dst]) {
|
|
438
|
+
const id = identifier(value, "route_candidate_id_invalid");
|
|
439
|
+
if (loadedIds.has(id) || unresolved.has(id))
|
|
440
|
+
continue;
|
|
441
|
+
if (loadedIds.size + unresolved.size >= MAX_READBACK_IDS) {
|
|
442
|
+
truncated = true;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
unresolved.add(id);
|
|
446
|
+
}
|
|
447
|
+
if (unresolved.size > 0)
|
|
448
|
+
remember(await readGraph(host, unresolved, input.scopeId), unresolved);
|
|
449
|
+
const next = new Set();
|
|
450
|
+
for (const edge of incident) {
|
|
451
|
+
if (!nodes.has(edge.src) || !nodes.has(edge.dst))
|
|
452
|
+
continue;
|
|
453
|
+
if (classifyDocumentRelationRole({ relation: { kind: edge.kind, props: record(edge.props, "route_edge_props_invalid") },
|
|
454
|
+
endpoints: { srcKind: endpointNodes.get(edge.src)?.kind, dstKind: endpointNodes.get(edge.dst)?.kind } }) !== "semantic")
|
|
455
|
+
continue;
|
|
456
|
+
const key = JSON.stringify([edge.src, edge.dst, edge.kind]);
|
|
457
|
+
if (!edges.has(key) && edges.size >= edgeLimit) {
|
|
458
|
+
truncated = true;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const previous = edges.get(key);
|
|
462
|
+
if (previous !== undefined)
|
|
463
|
+
relationIndex([previous, edge]);
|
|
464
|
+
edges.set(key, edge);
|
|
465
|
+
if (round === input.maximumDepth)
|
|
466
|
+
continue;
|
|
467
|
+
for (const value of [edge.src, edge.dst]) {
|
|
468
|
+
const id = identifier(value, "route_candidate_id_invalid");
|
|
469
|
+
if (seen.has(id) || next.has(id))
|
|
470
|
+
continue;
|
|
471
|
+
next.add(id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
frontier = next;
|
|
475
|
+
}
|
|
476
|
+
const candidates = [...edges.values()].filter((edge) => nodes.has(edge.src) && nodes.has(edge.dst));
|
|
477
|
+
const candidateIds = new Set(candidates.flatMap((edge) => [edge.src, edge.dst]));
|
|
478
|
+
const candidateSeeds = new Set([...input.seedIds].filter((id) => candidateIds.has(id)).slice(0, input.startLimit));
|
|
479
|
+
if (candidateSeeds.size < [...input.seedIds].filter((id) => candidateIds.has(id)).length)
|
|
480
|
+
truncated = true;
|
|
481
|
+
const enumerated = enumerateDiverseGraphPaths({
|
|
482
|
+
edges: candidates.map((edge) => ({ src: edge.src, dst: edge.dst, kind: edge.kind })),
|
|
483
|
+
seeds: candidateSeeds,
|
|
484
|
+
maximumDepth: input.maximumDepth,
|
|
485
|
+
limit: input.pathLimit,
|
|
486
|
+
});
|
|
487
|
+
truncated ||= enumerated.truncated;
|
|
488
|
+
const candidatePaths = graphPaths({ paths: enumerated.paths }, candidateIds, { search_queries: input.searchQueries, direction: "both", depth: input.maximumDepth, relation_kinds: [...input.allowedKinds] }, input.pathLimit);
|
|
489
|
+
if (candidatePaths.some((path) => !path.nodeIds.some((id) => candidateSeeds.has(id))))
|
|
490
|
+
return fail("route_path_anchor_missing");
|
|
491
|
+
const diagnostic = { phase: "route_refinement", candidateNodeCount: candidateIds.size, candidateEdgeCount: candidates.length, truncated, nodeLimit, edgeLimit,
|
|
492
|
+
endpointReadCount: loadedIds.size, endpointReadLimit: MAX_READBACK_IDS, candidatePathCount: candidatePaths.length,
|
|
493
|
+
candidatePathLengths: candidatePaths.map((path) => path.edges.length), pathAuthority: "world_get_physical_relations",
|
|
494
|
+
candidatePaths: candidatePaths.map((path) => ({ pathId: pathIdentifier(path), ...path })) };
|
|
495
|
+
if (candidatePaths.length === 0) {
|
|
496
|
+
trace.push({ stage: "plan", detail: { ...diagnostic, route: null } });
|
|
497
|
+
return { paths: [], candidates: candidatePaths, readbacks, truncated };
|
|
498
|
+
}
|
|
499
|
+
const optionById = new Map(candidatePaths.map((path) => [pathIdentifier(path), path]));
|
|
500
|
+
const selectionLimit = Math.min(input.startLimit, MAX_PLANNED_RELATIONS);
|
|
501
|
+
const routeSchema = { type: "object", additionalProperties: false, required: ["path_ids"], properties: {
|
|
502
|
+
path_ids: { type: "array", minItems: 0, maxItems: selectionLimit, items: { type: "string", enum: [...optionById.keys()] } },
|
|
503
|
+
} };
|
|
504
|
+
const payload = { query: input.query, scope_id: input.scopeId,
|
|
505
|
+
maximum_depth: input.maximumDepth, path_limit: input.pathLimit, candidate_truncated: truncated,
|
|
506
|
+
path_options: renderedPaths(candidatePaths, [...nodes.values()], candidates),
|
|
507
|
+
};
|
|
508
|
+
const payloadCharacters = modelJson(payload).length;
|
|
509
|
+
let feedback;
|
|
510
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
511
|
+
const before = dependencies.modelConnection.usage().byProviderModel?.find((usage) => usage.providerId === dependencies.modelConnection.structuredJson.providerId && usage.modelId === REQUIRED_MODEL);
|
|
512
|
+
try {
|
|
513
|
+
const value = record(await generate(dependencies.modelConnection, {
|
|
514
|
+
schemaName: "document_query_route_v1", jsonSchema: routeSchema, taskMode: "simulation_retrieval", maxOutputTokens: 1_024,
|
|
515
|
+
messages: [
|
|
516
|
+
{ role: "system", content: "只根据用户问题选择优先回读原文的导航路径,不回答问题。每项hops自包含实际关系、端点标签及物理方向;相同label不同ref不能拼接。只返回少量相关path_ids,不生成节点、关系、方向或深度。优先选择覆盖问题锚点与待求事实的路径,无关旁支不要选;无法找到相关路线时path_ids=[]。导航只用于找证据和排序,不是事实,也不是最终答案必须引用的路径;作答阶段将在实际原文证据可用后另选proof_path_ids。候选截断不代表全图穷尽。" },
|
|
517
|
+
{ role: "user", content: modelJson({ ...payload, ...(feedback === undefined ? {} : { validation_error: feedback, instruction: "仅有这一次修复机会,严格选择已有候选并保留 scope、物理方向和请求上限。" }) }) },
|
|
518
|
+
],
|
|
519
|
+
}, dependencies.signal, usages), "route_output_invalid");
|
|
520
|
+
if (Object.keys(value).some((key) => key !== "path_ids"))
|
|
521
|
+
return fail("route_fields_invalid");
|
|
522
|
+
const ids = Array.isArray(value.path_ids) && value.path_ids.length === 0 ? [] : stringArray(value.path_ids, "route_ids_invalid", selectionLimit);
|
|
523
|
+
const selected = [...new Set(ids)].map((id) => { const option = optionById.get(id); if (option === undefined)
|
|
524
|
+
return fail("route_id_forbidden"); return option; });
|
|
525
|
+
trace.push({ stage: "plan", detail: { ...diagnostic, payloadCharacters, attempt: attempt + 1, route: { selectedPathIds: [...new Set(ids)], selectedPathLengths: selected.map((path) => path.edges.length) } } });
|
|
526
|
+
return { paths: selected, candidates: candidatePaths, readbacks, truncated };
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
const providerInvalid = error instanceof AuthoringModelConnectionError && error.code === "provider_response_invalid"
|
|
530
|
+
&& (error.validationStage === "closed_schema" || error.validationStage === "response_parse" || error.validationStage === "response_shape");
|
|
531
|
+
if (!providerInvalid && !(error instanceof Error && /^document_query:(route_|planner_)/u.test(error.message)))
|
|
532
|
+
throw error;
|
|
533
|
+
if (providerInvalid) {
|
|
534
|
+
const after = dependencies.modelConnection.usage().byProviderModel?.find((usage) => usage.providerId === dependencies.modelConnection.structuredJson.providerId && usage.modelId === REQUIRED_MODEL);
|
|
535
|
+
if (after !== undefined && after.requests > (before?.requests ?? 0))
|
|
536
|
+
usages.push({ inputTokens: after.inputTokens - (before?.inputTokens ?? 0), outputTokens: after.outputTokens - (before?.outputTokens ?? 0), totalTokens: after.totalTokens - (before?.totalTokens ?? 0), requests: after.requests - (before?.requests ?? 0), cachedInputTokens: (after.cachedInputTokens ?? 0) - (before?.cachedInputTokens ?? 0) });
|
|
537
|
+
feedback = { code: error.code, stage: error.validationStage, issues: error.validationIssues };
|
|
538
|
+
}
|
|
539
|
+
else
|
|
540
|
+
feedback = { code: error.message };
|
|
541
|
+
trace.push({ stage: "plan", detail: { ...diagnostic, attempt: attempt + 1, validationError: feedback } });
|
|
542
|
+
if (attempt === 1)
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return fail("route_output_invalid");
|
|
547
|
+
}
|
|
548
|
+
function addBounded(ids, value) {
|
|
549
|
+
if (ids.size < MAX_READBACK_IDS && typeof value === "string")
|
|
550
|
+
ids.add(identifier(value, "readback_reference_invalid"));
|
|
551
|
+
}
|
|
552
|
+
// 检索命中可能是原文或独立 assertion;先投影到语义端点,不能经 document hub 扩散。
|
|
553
|
+
function semanticSeeds(readbacks, hits) {
|
|
554
|
+
const result = new Set();
|
|
555
|
+
for (const [id, kind] of hits) {
|
|
556
|
+
if (!kind.includes("source_") && kind !== "world_document_statement" && kind !== "world_document_edge_assertion")
|
|
557
|
+
addBounded(result, id);
|
|
558
|
+
}
|
|
559
|
+
for (const readback of readbacks) {
|
|
560
|
+
for (const entity of readback.entities) {
|
|
561
|
+
const props = entity.props;
|
|
562
|
+
if (entity.kind === "world_document_statement")
|
|
563
|
+
addBounded(result, props.subject_entity_id);
|
|
564
|
+
if (entity.kind === "world_document_edge_assertion") {
|
|
565
|
+
addBounded(result, props.source_entity_id);
|
|
566
|
+
addBounded(result, props.target_entity_id);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const relation of readback.relations) {
|
|
570
|
+
if (relation.kind === "mentions" && hits.has(relation.src))
|
|
571
|
+
addBounded(result, relation.dst);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
function assertionIds(readbacks, nodeIds) {
|
|
577
|
+
const ids = new Set();
|
|
578
|
+
for (const readback of readbacks)
|
|
579
|
+
for (const relation of readback.relations) {
|
|
580
|
+
if ((relation.kind === "describes_source" || relation.kind === "describes_target" || relation.kind === "describes")
|
|
581
|
+
&& nodeIds.has(relation.dst))
|
|
582
|
+
addBounded(ids, relation.src);
|
|
583
|
+
if (relation.kind === "supports" && nodeIds.has(relation.src))
|
|
584
|
+
addBounded(ids, relation.dst);
|
|
585
|
+
}
|
|
586
|
+
return ids;
|
|
587
|
+
}
|
|
588
|
+
function rankedSourceChunkIds(readbacks, seedScores, paths, candidates = []) {
|
|
589
|
+
const entities = new Map(readbacks.flatMap((readback) => readback.entities).map((entity) => [entity.id, entity]));
|
|
590
|
+
const edgeKey = (edge) => JSON.stringify([edge.src, edge.dst, edge.kind]);
|
|
591
|
+
const relations = relationIndex(readbacks.flatMap((readback) => readback.relations));
|
|
592
|
+
const pathEdges = new Set(paths.flatMap((path) => path.edges.map(edgeKey)));
|
|
593
|
+
const pathAssertions = new Set([...entities.values()].filter((entity) => {
|
|
594
|
+
const props = entity.props;
|
|
595
|
+
return entity.kind === "world_document_edge_assertion" && pathEdges.has(edgeKey({ src: props.source_entity_id, dst: props.target_entity_id, kind: props.predicate }));
|
|
596
|
+
}).map((entity) => entity.id));
|
|
597
|
+
const priorities = new Map();
|
|
598
|
+
const include = (value, tier, score) => {
|
|
599
|
+
const id = identifier(value, "readback_reference_invalid");
|
|
600
|
+
const previous = priorities.get(id);
|
|
601
|
+
if (previous === undefined)
|
|
602
|
+
priorities.set(id, { tier, score, order: priorities.size });
|
|
603
|
+
else if (tier < previous.tier)
|
|
604
|
+
priorities.set(id, { tier, score, order: previous.order });
|
|
605
|
+
else if (tier === previous.tier)
|
|
606
|
+
previous.score += score;
|
|
607
|
+
};
|
|
608
|
+
// 直接命中及其精确 supports 优先;图路径次之,普通 incident mentions 最后。
|
|
609
|
+
// world_get 返回顺序是 source 存储顺序,不能成为检索相关性或预算取舍依据。
|
|
610
|
+
for (const [id, score] of seedScores) {
|
|
611
|
+
const kind = entities.get(id)?.kind;
|
|
612
|
+
if (kind === "world_source_chunk" || kind === "source_chunk")
|
|
613
|
+
include(id, 0, score);
|
|
614
|
+
}
|
|
615
|
+
for (const relation of relations.values()) {
|
|
616
|
+
const score = seedScores.get(relation.dst);
|
|
617
|
+
const kind = entities.get(relation.dst)?.kind;
|
|
618
|
+
if (relation.kind === "supports" && score !== undefined
|
|
619
|
+
&& (kind === "world_document_statement" || kind === "world_document_edge_assertion"))
|
|
620
|
+
include(relation.src, 0, score);
|
|
621
|
+
}
|
|
622
|
+
for (const path of paths)
|
|
623
|
+
for (const edge of path.edges) {
|
|
624
|
+
const relation = relations.get(edgeKey(edge));
|
|
625
|
+
const props = relation?.props;
|
|
626
|
+
if (Array.isArray(props?.source_chunk_ids))
|
|
627
|
+
for (const id of props.source_chunk_ids)
|
|
628
|
+
include(id, 1, 0);
|
|
629
|
+
}
|
|
630
|
+
for (const path of paths)
|
|
631
|
+
for (const id of path.nodeIds) {
|
|
632
|
+
const kind = entities.get(id)?.kind;
|
|
633
|
+
if (kind === "world_source_chunk" || kind === "source_chunk")
|
|
634
|
+
include(id, 1, 0);
|
|
635
|
+
}
|
|
636
|
+
// 未优先导航的真实候选仍可由已有预算接纳;不能因导航为空而隐藏其原文或阻止后续重选。
|
|
637
|
+
for (const path of candidates)
|
|
638
|
+
for (const edge of path.edges) {
|
|
639
|
+
const props = relations.get(edgeKey(edge))?.props;
|
|
640
|
+
if (Array.isArray(props?.source_chunk_ids))
|
|
641
|
+
for (const id of props.source_chunk_ids)
|
|
642
|
+
include(id, 2, 0);
|
|
643
|
+
}
|
|
644
|
+
for (const relation of relations.values()) {
|
|
645
|
+
if (relation.kind === "supports" && pathAssertions.has(relation.dst))
|
|
646
|
+
include(relation.src, 1, 0);
|
|
647
|
+
if (relation.kind === "supports" || relation.kind === "mentions")
|
|
648
|
+
include(relation.src, 2, seedScores.get(relation.dst) ?? 0);
|
|
649
|
+
}
|
|
650
|
+
return [...priorities].sort(([, left], [, right]) => left.tier - right.tier || right.score - left.score || left.order - right.order)
|
|
651
|
+
.slice(0, MAX_READBACK_IDS).map(([id]) => id);
|
|
652
|
+
}
|
|
653
|
+
function semanticEvidenceSpans(proof) {
|
|
654
|
+
return Array.isArray(proof.spans) ? proof.spans : proof.offset_semantics === "envelope_only" ? [] : [proof];
|
|
655
|
+
}
|
|
656
|
+
function supportedPaths(paths, readbacks, evidence) {
|
|
657
|
+
const entities = readbacks.flatMap((readback) => readback.entities);
|
|
658
|
+
const relations = [...relationIndex(readbacks.flatMap((readback) => readback.relations)).values()];
|
|
659
|
+
const byId = new Map(evidence.map((item) => [item.evidenceId, item]));
|
|
660
|
+
return paths.map((path) => ({ ...path, edges: path.edges.map((edge) => {
|
|
661
|
+
const assertions = entities.filter((entity) => {
|
|
662
|
+
const props = entity.props;
|
|
663
|
+
return entity.kind === "world_document_edge_assertion" && props.source_entity_id === edge.src
|
|
664
|
+
&& props.target_entity_id === edge.dst && props.predicate === edge.kind;
|
|
665
|
+
});
|
|
666
|
+
// 新导入直接把每跳 chunk IDs 放在语义边,旧图仍支持 assertion→supports 回溯。
|
|
667
|
+
for (const relation of relations) {
|
|
668
|
+
if (relation.src !== edge.src || relation.dst !== edge.dst || relation.kind !== edge.kind)
|
|
669
|
+
continue;
|
|
670
|
+
const props = record(relation.props, "readback_relation_props_invalid");
|
|
671
|
+
if (!Array.isArray(props.source_chunk_ids))
|
|
672
|
+
continue;
|
|
673
|
+
assertions.push({ id: `edge:${edge.src}:${edge.kind}:${edge.dst}`, props });
|
|
674
|
+
}
|
|
675
|
+
const evidenceIds = new Set();
|
|
676
|
+
for (const assertion of assertions) {
|
|
677
|
+
const props = assertion.props;
|
|
678
|
+
const proof = record(props.evidence, "assertion_evidence_invalid");
|
|
679
|
+
const chunkIds = Array.isArray(props.source_chunk_ids) ? props.source_chunk_ids
|
|
680
|
+
: relations.filter((relation) => relation.kind === "supports" && relation.dst === assertion.id).map((relation) => relation.src);
|
|
681
|
+
const chunks = chunkIds.map((chunkId) => byId.get(chunkId)).filter((item) => item !== undefined)
|
|
682
|
+
.sort((left, right) => left.startOffset - right.startOffset);
|
|
683
|
+
const unique = [...new Map(chunks.map((item) => [item.evidenceId, item])).values()];
|
|
684
|
+
if (unique.length === 0 || unique.some((item) => item.documentId !== unique[0].documentId))
|
|
685
|
+
continue;
|
|
686
|
+
// 新 semantic evidence 的 offsets 只是 envelope,聚合 quote 不能按 envelope 切原文。
|
|
687
|
+
// 仅旧式连续证据可直接使用顶层 span;新格式缺失 spans 不得降级误用 envelope。
|
|
688
|
+
const spans = semanticEvidenceSpans(proof);
|
|
689
|
+
if (spans.length === 0)
|
|
690
|
+
continue;
|
|
691
|
+
const supportedIds = new Set();
|
|
692
|
+
const allSpansSupported = spans.every((value) => {
|
|
693
|
+
const span = record(value, "assertion_evidence_span_invalid");
|
|
694
|
+
if (typeof span.quote !== "string" || !Number.isSafeInteger(span.start_offset) || !Number.isSafeInteger(span.end_offset))
|
|
695
|
+
return false;
|
|
696
|
+
const start = span.start_offset;
|
|
697
|
+
const end = span.end_offset;
|
|
698
|
+
if (start < 0 || end <= start || end - start !== span.quote.length)
|
|
699
|
+
return false;
|
|
700
|
+
const overlapping = unique.filter((chunk) => chunk.startOffset < end && chunk.endOffset > start);
|
|
701
|
+
if (overlapping.length === 0 || start < overlapping[0].startOffset || end > overlapping.at(-1).endOffset)
|
|
702
|
+
return false;
|
|
703
|
+
if (overlapping.some((chunk, index) => index > 0 && overlapping[index - 1].endOffset !== chunk.startOffset))
|
|
704
|
+
return false;
|
|
705
|
+
const sourceText = overlapping.map((chunk) => chunk.text).join("");
|
|
706
|
+
if (sourceText.slice(start - overlapping[0].startOffset, end - overlapping[0].startOffset) !== span.quote)
|
|
707
|
+
return false;
|
|
708
|
+
for (const chunk of overlapping)
|
|
709
|
+
supportedIds.add(chunk.evidenceId);
|
|
710
|
+
return true;
|
|
711
|
+
});
|
|
712
|
+
if (allSpansSupported)
|
|
713
|
+
for (const id of supportedIds)
|
|
714
|
+
evidenceIds.add(id);
|
|
715
|
+
}
|
|
716
|
+
const physical = relations.find((relation) => relation.src === edge.src && relation.dst === edge.dst && relation.kind === edge.kind);
|
|
717
|
+
const qualifiers = physical?.props?.qualifiers;
|
|
718
|
+
return { ...edge, ...(qualifiers === undefined ? {} : { qualifiers: record(qualifiers, "relation_qualifiers_invalid") }), evidenceIds: [...evidenceIds] };
|
|
719
|
+
}) }));
|
|
720
|
+
}
|
|
721
|
+
function quoteRequirementsForPaths(paths, readbacks, evidence, options) {
|
|
722
|
+
const edgeKey = (edge) => JSON.stringify([edge.src, edge.dst, edge.kind]);
|
|
723
|
+
const relations = relationIndex(readbacks.flatMap((item) => item.relations));
|
|
724
|
+
const byId = new Map(evidence.map((item) => [item.evidenceId, item]));
|
|
725
|
+
return paths.map((path) => {
|
|
726
|
+
const hops = path.edges.map((edge) => {
|
|
727
|
+
const missing = (reason) => ({ src: edge.src, dst: edge.dst, kind: edge.kind, status: "missing",
|
|
728
|
+
required_quotes: [], missing_reason: reason });
|
|
729
|
+
const relation = relations.get(edgeKey(edge));
|
|
730
|
+
if (relation === undefined)
|
|
731
|
+
return missing("physical_edge_unavailable");
|
|
732
|
+
const props = record(relation.props, "readback_relation_props_invalid");
|
|
733
|
+
if (props.evidence === undefined)
|
|
734
|
+
return missing("canonical_proof_unavailable");
|
|
735
|
+
const proof = record(props.evidence, "assertion_evidence_invalid");
|
|
736
|
+
const sourceIds = Array.isArray(props.source_chunk_ids) ? props.source_chunk_ids : edge.evidenceIds ?? [];
|
|
737
|
+
// 与 graphPaths 共用同一精确 span 校验,不能凭命中 chunk 或 envelope 宣称 proof 可用。
|
|
738
|
+
const validated = supportedPaths([{ nodeIds: [edge.src, edge.dst], edges: [edge] }], [{ entities: [], relations: [{ ...relation,
|
|
739
|
+
props: { ...props, source_chunk_ids: sourceIds } }] }], evidence)[0].edges[0].evidenceIds ?? [];
|
|
740
|
+
if (validated.length === 0)
|
|
741
|
+
return missing("proof_not_fully_in_evidence");
|
|
742
|
+
const required = new Map();
|
|
743
|
+
for (const value of semanticEvidenceSpans(proof)) {
|
|
744
|
+
const span = record(value, "assertion_evidence_span_invalid");
|
|
745
|
+
const start = span.start_offset, end = span.end_offset;
|
|
746
|
+
const covering = options.filter((option) => {
|
|
747
|
+
const source = byId.get(option.evidenceId);
|
|
748
|
+
return source !== undefined && validated.includes(option.evidenceId)
|
|
749
|
+
&& source.startOffset + option.start < end && source.startOffset + option.end > start;
|
|
750
|
+
}).sort((left, right) => byId.get(left.evidenceId).startOffset + left.start - byId.get(right.evidenceId).startOffset - right.start);
|
|
751
|
+
let cursor = start;
|
|
752
|
+
for (const option of covering) {
|
|
753
|
+
const source = byId.get(option.evidenceId);
|
|
754
|
+
if (source.startOffset + option.start > cursor)
|
|
755
|
+
return missing("quote_option_coverage_gap");
|
|
756
|
+
cursor = Math.max(cursor, source.startOffset + option.end);
|
|
757
|
+
required.set(option.quoteId, { evidence_id: option.evidenceId, quote_id: option.quoteId });
|
|
758
|
+
}
|
|
759
|
+
if (cursor < end)
|
|
760
|
+
return missing("quote_option_coverage_gap");
|
|
761
|
+
}
|
|
762
|
+
return { src: edge.src, dst: edge.dst, kind: edge.kind, status: "available", required_quotes: [...required.values()], missing_reason: null };
|
|
763
|
+
});
|
|
764
|
+
return { path_id: pathIdentifier(path),
|
|
765
|
+
usable: hops.every((hop) => hop.status === "available"), hops };
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function answerCitations(value, evidence, options) {
|
|
769
|
+
if (typeof value.answerable !== "boolean" || (value.answer !== null && typeof value.answer !== "string") || !Array.isArray(value.citations))
|
|
770
|
+
return fail("answer_output_invalid");
|
|
771
|
+
if (!value.answerable)
|
|
772
|
+
return [];
|
|
773
|
+
if (typeof value.answer !== "string" || value.answer.trim().length === 0)
|
|
774
|
+
return fail("answer_text_invalid");
|
|
775
|
+
if (value.citations.length === 0 || value.citations.length > 32)
|
|
776
|
+
return fail("answer_citation_required");
|
|
777
|
+
const byId = new Map(evidence.map((item) => [item.evidenceId, item]));
|
|
778
|
+
const optionById = new Map(options.map((item) => [item.quoteId, item]));
|
|
779
|
+
return Object.freeze(value.citations.map((entry) => {
|
|
780
|
+
const citation = record(entry, "citation_invalid");
|
|
781
|
+
const evidenceId = identifier(citation.evidence_id, "citation_id_invalid");
|
|
782
|
+
const source = byId.get(evidenceId);
|
|
783
|
+
if (source === undefined)
|
|
784
|
+
return fail("citation_unknown");
|
|
785
|
+
const quoteId = identifier(citation.quote_id, "citation_quote_id_invalid");
|
|
786
|
+
if (Object.keys(citation).some((key) => key !== "evidence_id" && key !== "quote_id"))
|
|
787
|
+
return fail("citation_fields_invalid");
|
|
788
|
+
const option = optionById.get(quoteId);
|
|
789
|
+
if (option === undefined)
|
|
790
|
+
return fail("citation_quote_id_unknown");
|
|
791
|
+
if (option.evidenceId !== evidenceId)
|
|
792
|
+
return fail("citation_quote_evidence_mismatch");
|
|
793
|
+
const quote = source.text.slice(option.start, option.end);
|
|
794
|
+
if (quote.length === 0 || quote.length > MAX_QUOTE_CHARACTERS || quote !== option.text)
|
|
795
|
+
return fail("citation_quote_not_in_evidence");
|
|
796
|
+
return Object.freeze({ evidenceId, documentId: source.documentId, startOffset: source.startOffset, endOffset: source.endOffset, contentSha256: source.contentSha256, quote });
|
|
797
|
+
}));
|
|
798
|
+
}
|
|
799
|
+
const PLANNER_SCHEMA = Object.freeze({
|
|
800
|
+
type: "object",
|
|
801
|
+
additionalProperties: false,
|
|
802
|
+
required: ["search_queries"],
|
|
803
|
+
properties: {
|
|
804
|
+
search_queries: { type: "array", minItems: 1, maxItems: MAX_PLANNED_QUERIES, items: { type: "string" } },
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
const ANSWER_SCHEMA = Object.freeze({
|
|
808
|
+
type: "object",
|
|
809
|
+
additionalProperties: false,
|
|
810
|
+
required: ["answerable", "answer", "citations"],
|
|
811
|
+
properties: {
|
|
812
|
+
answerable: { type: "boolean" },
|
|
813
|
+
// 生产 closed-schema 子集不接受联合 type;wire 用空串表示不可回答,公开结果仍为 null。
|
|
814
|
+
answer: { type: "string" },
|
|
815
|
+
citations: { type: "array", maxItems: 32, items: { type: "object", additionalProperties: false, required: ["evidence_id", "quote_id"], properties: { evidence_id: { type: "string" }, quote_id: { type: "string" } } } },
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
const VERIFIER_SCHEMA = Object.freeze({
|
|
819
|
+
type: "object",
|
|
820
|
+
additionalProperties: false,
|
|
821
|
+
required: ["supported", "supported_quote_ids", "missing_premises", "unsupported_quote_ids", "answers_question", "proof_path_ids"],
|
|
822
|
+
properties: {
|
|
823
|
+
supported: { type: "boolean" },
|
|
824
|
+
answers_question: { type: "boolean" },
|
|
825
|
+
supported_quote_ids: { type: "array", maxItems: 32, items: { type: "string" } },
|
|
826
|
+
missing_premises: { type: "array", maxItems: 8, items: { type: "string", minLength: 1, maxLength: 256 } },
|
|
827
|
+
unsupported_quote_ids: { type: "array", maxItems: 32, items: { type: "string" } },
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
/**
|
|
831
|
+
* D4F 只负责制定有界检索计划和基于回读证据回答;图路径始终来自 exact-21 Host。
|
|
832
|
+
* 原始 source_document、rawBase64 与 generated context 永不进入模型 evidence。
|
|
833
|
+
*/
|
|
834
|
+
export async function answerDocumentQuestion(host, request, dependencies) {
|
|
835
|
+
if (typeof request.query !== "string" || request.query.trim().length === 0 || request.query.includes("\0") || request.query.normalize("NFC") !== request.query)
|
|
836
|
+
return fail("query_invalid");
|
|
837
|
+
const query = request.query;
|
|
838
|
+
if (query.length > MAX_QUERY_CHARACTERS)
|
|
839
|
+
return fail("query_too_long");
|
|
840
|
+
const scopeId = identifier(request.scopeId, "scope_invalid");
|
|
841
|
+
const maximumDepth = boundedInteger(request.maxDepth, DEFAULT_DEPTH, MAX_DEPTH, "depth_out_of_bounds");
|
|
842
|
+
const searchLimit = boundedInteger(request.searchLimit, DEFAULT_SEARCH_LIMIT, MAX_SEARCH_LIMIT, "search_limit_out_of_bounds");
|
|
843
|
+
const pathLimit = boundedInteger(request.pathLimit, DEFAULT_PATH_LIMIT, MAX_PATH_LIMIT, "path_limit_out_of_bounds");
|
|
844
|
+
const evidenceBudget = boundedInteger(request.evidenceCharacterBudget, DEFAULT_EVIDENCE_CHARACTERS, MAX_EVIDENCE_CHARACTERS, "evidence_budget_out_of_bounds");
|
|
845
|
+
if (dependencies.modelConnection.structuredJson.modelId !== REQUIRED_MODEL)
|
|
846
|
+
return fail("structured_model_forbidden");
|
|
847
|
+
const schema = record(await host.dispatchTool({ name: "world_schema", input: {} }), "schema_invalid");
|
|
848
|
+
if (!Array.isArray(schema.relation_kinds))
|
|
849
|
+
return fail("schema_relation_kinds_invalid");
|
|
850
|
+
const actualKinds = new Set(schema.relation_kinds.map((kind) => identifier(kind, "schema_relation_kind_invalid")));
|
|
851
|
+
const allowedRelationKinds = new Set(request.allowedRelationKinds ?? actualKinds);
|
|
852
|
+
if (request.allowedRelationKinds !== undefined && (allowedRelationKinds.size < 1 || allowedRelationKinds.size > 128))
|
|
853
|
+
return fail("allowed_relation_kinds_invalid");
|
|
854
|
+
for (const kind of allowedRelationKinds) {
|
|
855
|
+
identifier(kind, "allowed_relation_kind_invalid");
|
|
856
|
+
if (!actualKinds.has(kind))
|
|
857
|
+
return fail("allowed_relation_kind_unknown");
|
|
858
|
+
}
|
|
859
|
+
const trace = [];
|
|
860
|
+
const usages = [];
|
|
861
|
+
let plan;
|
|
862
|
+
let plannerFeedback;
|
|
863
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
864
|
+
const previousUsage = dependencies.modelConnection.usage().byProviderModel?.find((usage) => usage.providerId === dependencies.modelConnection.structuredJson.providerId && usage.modelId === REQUIRED_MODEL);
|
|
865
|
+
try {
|
|
866
|
+
const plannerValue = await generate(dependencies.modelConnection, {
|
|
867
|
+
schemaName: "document_query_plan_v1",
|
|
868
|
+
jsonSchema: PLANNER_SCHEMA,
|
|
869
|
+
taskMode: "simulation_retrieval",
|
|
870
|
+
maxOutputTokens: 1_024,
|
|
871
|
+
messages: [
|
|
872
|
+
{ role: "system", content: "你是搜索计划器。只分解用户问题并返回少量 search_queries,不回答问题,不编造节点 ID,不选择图关系、方向或目标答案。保留用户措辞,可补充适合原文的别名或繁体关键词。关系与方向将在读回真实候选图后另行确定,当前不需要猜测。" },
|
|
873
|
+
{ role: "user", content: modelJson({ query, scope_id: scopeId, maximum_depth: maximumDepth,
|
|
874
|
+
constraints: { search_queries: { min_items: 1, max_items: MAX_PLANNED_QUERIES, max_characters: 256 } },
|
|
875
|
+
...(plannerFeedback === undefined ? {} : { validation_error: plannerFeedback, instruction: "上次计划未通过校验。这是唯一一次修复机会;只修正具体约束错误,保留原始 query、scope 和来源,不复制候选列表、不降级或改写用户问题。" }),
|
|
876
|
+
}) },
|
|
877
|
+
],
|
|
878
|
+
}, dependencies.signal, usages);
|
|
879
|
+
plan = validatePlan(plannerValue, { maximumDepth, allowedRelationKinds }, true);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
const providerOutputInvalid = error instanceof AuthoringModelConnectionError && error.code === "provider_response_invalid"
|
|
884
|
+
&& (error.validationStage === "closed_schema" || error.validationStage === "response_parse" || error.validationStage === "response_shape");
|
|
885
|
+
const planInvalid = error instanceof Error && error.message.startsWith("document_query:planner_");
|
|
886
|
+
if (!providerOutputInvalid && !planInvalid)
|
|
887
|
+
throw error;
|
|
888
|
+
if (providerOutputInvalid) {
|
|
889
|
+
// factory 在输出校验前已累计 provider usage;不能漏算这次已付费的失败响应。
|
|
890
|
+
const currentUsage = dependencies.modelConnection.usage().byProviderModel?.find((usage) => usage.providerId === dependencies.modelConnection.structuredJson.providerId && usage.modelId === REQUIRED_MODEL);
|
|
891
|
+
if (currentUsage !== undefined && currentUsage.requests > (previousUsage?.requests ?? 0))
|
|
892
|
+
usages.push({
|
|
893
|
+
inputTokens: currentUsage.inputTokens - (previousUsage?.inputTokens ?? 0), outputTokens: currentUsage.outputTokens - (previousUsage?.outputTokens ?? 0),
|
|
894
|
+
totalTokens: currentUsage.totalTokens - (previousUsage?.totalTokens ?? 0), requests: currentUsage.requests - (previousUsage?.requests ?? 0),
|
|
895
|
+
cachedInputTokens: (currentUsage.cachedInputTokens ?? 0) - (previousUsage?.cachedInputTokens ?? 0),
|
|
896
|
+
});
|
|
897
|
+
plannerFeedback = { code: error.code, stage: error.validationStage, issues: error.validationIssues };
|
|
898
|
+
}
|
|
899
|
+
else
|
|
900
|
+
plannerFeedback = { code: error.message };
|
|
901
|
+
trace.push(Object.freeze({ stage: "plan", detail: { attempt: attempt + 1, validationError: plannerFeedback } }));
|
|
902
|
+
if (attempt === 1)
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (plan === undefined)
|
|
907
|
+
return fail("planner_output_invalid");
|
|
908
|
+
trace.push(Object.freeze({ stage: "plan", detail: { phase: "search", queries: plan.search_queries } }));
|
|
909
|
+
const seedIds = new Set();
|
|
910
|
+
const hitKinds = new Map();
|
|
911
|
+
const seedScores = new Map();
|
|
912
|
+
for (const plannedQuery of plan.search_queries) {
|
|
913
|
+
const hits = searchHits(await host.dispatchTool({
|
|
914
|
+
name: "world_search",
|
|
915
|
+
input: { query: plannedQuery, mode: "hybrid", limit: searchLimit, filters: { scope_id: scopeId } },
|
|
916
|
+
}), scopeId);
|
|
917
|
+
for (const [rank, hit] of hits.slice(0, searchLimit).entries()) {
|
|
918
|
+
if (hit.kind !== "world_source_document" && hit.kind !== "source_document") {
|
|
919
|
+
seedIds.add(hit.id);
|
|
920
|
+
hitKinds.set(hit.id, hit.kind);
|
|
921
|
+
seedScores.set(hit.id, (seedScores.get(hit.id) ?? 0) + 1 / (rank + 1));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
trace.push(Object.freeze({ stage: "search", detail: { query: plannedQuery, seedIds: hits.map((hit) => hit.id), channels: hits.map((hit) => hit.channels) } }));
|
|
925
|
+
}
|
|
926
|
+
if (seedIds.size === 0) {
|
|
927
|
+
return Object.freeze({ answerable: false, answer: null, citations: Object.freeze([]), graphPaths: Object.freeze([]), retrievalTrace: Object.freeze(trace), usage: usageTotal(usages, dependencies.modelConnection.structuredJson.providerId, REQUIRED_MODEL) });
|
|
928
|
+
}
|
|
929
|
+
if (seedIds.size > searchLimit * plan.search_queries.length)
|
|
930
|
+
return fail("seed_limit_exceeded");
|
|
931
|
+
const seedReadback = await readGraph(host, seedIds, scopeId);
|
|
932
|
+
const sourceSeedIds = new Set([...hitKinds].filter(([, kind]) => kind === "world_source_chunk" || kind === "source_chunk").map(([id]) => id));
|
|
933
|
+
const seedAssertions = await readGraph(host, assertionIds([seedReadback], sourceSeedIds), scopeId);
|
|
934
|
+
const semanticIds = semanticSeeds([seedReadback, seedAssertions], hitKinds);
|
|
935
|
+
const refined = await refineRoute(host, { query, scopeId, seedIds: semanticIds, allowedKinds: allowedRelationKinds, maximumDepth,
|
|
936
|
+
pathLimit, startLimit: searchLimit, searchQueries: plan.search_queries }, dependencies, usages, trace);
|
|
937
|
+
const navigationPaths = refined.paths;
|
|
938
|
+
const rawPaths = refined.candidates;
|
|
939
|
+
const navigationPathIds = navigationPaths.map(pathIdentifier);
|
|
940
|
+
trace.push(Object.freeze({ stage: "traverse", detail: { authority: "world_get_physical_relations", nativeTraverseCalled: false,
|
|
941
|
+
phase: "navigation_paths", selectedPathIds: navigationPathIds, pathCount: navigationPaths.length, paths: navigationPaths,
|
|
942
|
+
truncated: refined.truncated, candidateTruncated: refined.truncated, exhaustive: !refined.truncated } }));
|
|
943
|
+
const readbackIds = new Set([...seedIds, ...rawPaths.flatMap((path) => path.nodeIds)]);
|
|
944
|
+
const pathReadback = await readGraph(host, readbackIds, scopeId);
|
|
945
|
+
const legacyPathIds = new Set(rawPaths.flatMap((path) => path.edges.flatMap((edge) => {
|
|
946
|
+
const relation = pathReadback.relations.find((item) => item.src === edge.src && item.dst === edge.dst && item.kind === edge.kind);
|
|
947
|
+
return relation !== undefined && Array.isArray(relation.props?.source_chunk_ids) ? [] : [edge.src, edge.dst];
|
|
948
|
+
})));
|
|
949
|
+
const pathAssertions = await readGraph(host, assertionIds([pathReadback], legacyPathIds), scopeId);
|
|
950
|
+
const readbacks = [seedReadback, seedAssertions, ...refined.readbacks, pathReadback, pathAssertions];
|
|
951
|
+
const rankedChunkIds = rankedSourceChunkIds(readbacks, seedScores, navigationPaths, rawPaths);
|
|
952
|
+
const chunkIds = new Set(rankedChunkIds);
|
|
953
|
+
const chunksReadback = await readGraph(host, chunkIds, scopeId);
|
|
954
|
+
readbacks.push(chunksReadback);
|
|
955
|
+
const allIds = new Set([...readbackIds, ...chunkIds, ...readbacks.flatMap((item) => item.entities.map((entity) => entity.id))]);
|
|
956
|
+
const uniqueEntities = [...new Map([...chunksReadback.entities, ...readbacks.flatMap((item) => item.entities)].map((entity) => [entity.id, entity])).values()];
|
|
957
|
+
const evidenceById = new Map(evidenceFromReadback({ entities: uniqueEntities }, allIds, scopeId).map((item) => [item.evidenceId, item]));
|
|
958
|
+
const rankedEvidence = rankedChunkIds.flatMap((id) => {
|
|
959
|
+
const item = evidenceById.get(id);
|
|
960
|
+
return item === undefined ? [] : [item];
|
|
961
|
+
});
|
|
962
|
+
const evidence = budgetEvidence(rankedEvidence, evidenceBudget);
|
|
963
|
+
const paths = supportedPaths(rawPaths, readbacks, evidence);
|
|
964
|
+
trace.push(Object.freeze({ stage: "readback", detail: { requestedIds: [...allIds], sourceChunkIds: [...chunkIds], evidenceIds: evidence.map((item) => item.evidenceId),
|
|
965
|
+
droppedEvidenceIds: rankedEvidence.filter((item) => !evidence.includes(item)).map((item) => item.evidenceId),
|
|
966
|
+
supportedHopCount: paths.reduce((sum, path) => sum + path.edges.filter((edge) => (edge.evidenceIds?.length ?? 0) > 0).length, 0),
|
|
967
|
+
usedCharacters: evidence.reduce((sum, item) => sum + item.text.length, 0) } }));
|
|
968
|
+
if (evidence.length === 0) {
|
|
969
|
+
return Object.freeze({ answerable: false, answer: null, citations: Object.freeze([]), graphPaths: [], retrievalTrace: Object.freeze(trace), usage: usageTotal(usages, dependencies.modelConnection.structuredJson.providerId, REQUIRED_MODEL) });
|
|
970
|
+
}
|
|
971
|
+
const options = quoteOptions(evidence);
|
|
972
|
+
const evidencePayload = evidence.map(({ evidenceId, documentId, startOffset, endOffset, contentSha256 }) => ({ evidence_id: evidenceId, document_id: documentId,
|
|
973
|
+
start_offset: startOffset, end_offset: endOffset, content_sha256: contentSha256,
|
|
974
|
+
quote_options: options.filter((option) => option.evidenceId === evidenceId).map(({ quoteId, text }) => ({ quote_id: quoteId, text })) }));
|
|
975
|
+
const pathQuoteRequirements = quoteRequirementsForPaths(paths, readbacks, evidence, options);
|
|
976
|
+
const availablePathIds = new Set(pathQuoteRequirements.filter((path) => path.usable).map((path) => path.path_id));
|
|
977
|
+
const availablePaths = paths.filter((path) => availablePathIds.has(pathIdentifier(path)));
|
|
978
|
+
const availableById = new Map(availablePaths.map((path) => [pathIdentifier(path), path]));
|
|
979
|
+
const allRelations = [...relationIndex(readbacks.flatMap((item) => item.relations)).values()];
|
|
980
|
+
const proofSelectionLimit = Math.min(searchLimit, MAX_PLANNED_RELATIONS);
|
|
981
|
+
trace.push({ stage: "readback", detail: { phase: "path_quote_requirements", usablePathIds: pathQuoteRequirements.filter((path) => path.usable).map((path) => path.path_id),
|
|
982
|
+
missingPathIds: pathQuoteRequirements.filter((path) => !path.usable).map((path) => path.path_id) } });
|
|
983
|
+
let answerValue = { answerable: false, answer: null, citations: [] };
|
|
984
|
+
let citations = [];
|
|
985
|
+
let proofPaths = [];
|
|
986
|
+
let proofPathIds = [];
|
|
987
|
+
let repairFeedback;
|
|
988
|
+
const providerUsage = () => dependencies.modelConnection.usage().byProviderModel?.find((usage) => usage.providerId === dependencies.modelConnection.structuredJson.providerId && usage.modelId === REQUIRED_MODEL);
|
|
989
|
+
const structuredAttempt = async (modelRequest) => {
|
|
990
|
+
const before = providerUsage();
|
|
991
|
+
try {
|
|
992
|
+
return { value: await generate(dependencies.modelConnection, modelRequest, dependencies.signal, usages) };
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
const invalid = error instanceof AuthoringModelConnectionError && error.code === "provider_response_invalid"
|
|
996
|
+
&& (error.validationStage === "closed_schema" || error.validationStage === "response_parse" || error.validationStage === "response_shape");
|
|
997
|
+
if (!invalid)
|
|
998
|
+
throw error;
|
|
999
|
+
// factory 已消费但闭 schema 拒绝的响应也计入真实 usage,不能只计算被接受的 value。
|
|
1000
|
+
const after = providerUsage();
|
|
1001
|
+
if (after !== undefined && after.requests > (before?.requests ?? 0))
|
|
1002
|
+
usages.push({
|
|
1003
|
+
inputTokens: after.inputTokens - (before?.inputTokens ?? 0), outputTokens: after.outputTokens - (before?.outputTokens ?? 0),
|
|
1004
|
+
totalTokens: after.totalTokens - (before?.totalTokens ?? 0), requests: after.requests - (before?.requests ?? 0),
|
|
1005
|
+
cachedInputTokens: (after.cachedInputTokens ?? 0) - (before?.cachedInputTokens ?? 0),
|
|
1006
|
+
});
|
|
1007
|
+
return { validationError: { code: error.code, stage: error.validationStage, issues: error.validationIssues } };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
// 结构错误、漏前提及不受支持引用共享这两个 answer 尝试;没有第三次隐藏修复。
|
|
1011
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1012
|
+
const generatedAnswer = await structuredAttempt({
|
|
1013
|
+
schemaName: "document_grounded_answer_v1", jsonSchema: ANSWER_SCHEMA, taskMode: "simulation_retrieval", reasoningEffort: "low", maxOutputTokens: GROUNDED_MAX_OUTPUT_TOKENS,
|
|
1014
|
+
messages: [
|
|
1015
|
+
{ role: "system", content: "输出根对象必须且只能包含answerable:boolean、answer:string、citations:array,成功与拒答都必须包含全部三字段。只能依据evidence中原文回答;quote_options按原文顺序完整覆盖已读chunk,不是摘要。先识别问题所求结果、限定与必要身份/别名/指代等中间前提,再选择最小联合充分的引文。500字符边界可能切开前提,确实需要才加相邻选项;每条引文须支持答案事实或必要前提,不能硬加无关背景。citations只选evidence_id和已有quote_id,不生成quote/offset。answerable=true须实际回答问题所求,而非仅说明未知、证据不足或拒答;若问题本身问是否可确定,则按该问题语义回答。证据不足以给出所求结果时answerable=false、answer=空字符串、citations=[]。" },
|
|
1016
|
+
{ role: "user", content: modelJson({ query, evidence: evidencePayload,
|
|
1017
|
+
...(repairFeedback === undefined ? {} : { repair_feedback: repairFeedback, instruction: "这是第二次且最后一次answer尝试。依据原始原文与反馈重新选择必要引文或修正答案;不能把反馈当原文,新引用仍须完整核验,程序不会自动删除不受支持引用来算通过;无法回答问题所求则拒答。" }) }) },
|
|
1018
|
+
],
|
|
1019
|
+
});
|
|
1020
|
+
if (generatedAnswer.validationError !== undefined) {
|
|
1021
|
+
repairFeedback = { stage: "answer", validation_error: generatedAnswer.validationError };
|
|
1022
|
+
trace.push({ stage: "answer", detail: { attempt: attempt + 1, validationError: generatedAnswer.validationError } });
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
answerValue = record(generatedAnswer.value, "answer_output_invalid");
|
|
1027
|
+
citations = answerCitations(answerValue, evidence, options);
|
|
1028
|
+
}
|
|
1029
|
+
catch (error) {
|
|
1030
|
+
if (!(error instanceof Error) || !error.message.startsWith("document_query:"))
|
|
1031
|
+
throw error;
|
|
1032
|
+
repairFeedback = { stage: "answer", validation_error: error.message };
|
|
1033
|
+
trace.push({ stage: "answer", detail: { attempt: attempt + 1, validationError: error.message } });
|
|
1034
|
+
continue;
|
|
1035
|
+
}
|
|
1036
|
+
trace.push({ stage: "answer", detail: { attempt: attempt + 1, answerable: answerValue.answerable, citationIds: citations.map((item) => item.evidenceId) } });
|
|
1037
|
+
if (!answerValue.answerable)
|
|
1038
|
+
break;
|
|
1039
|
+
// verifier 的事实输入只有本轮真正选中的原文;不发送完整未选 evidence/options。
|
|
1040
|
+
const selectedCitations = citations.map((citation, index) => ({
|
|
1041
|
+
quote_id: answerValue.citations[index].quote_id,
|
|
1042
|
+
evidence_id: citation.evidenceId, quote: citation.quote, document_id: citation.documentId,
|
|
1043
|
+
start_offset: citation.startOffset, end_offset: citation.endOffset, content_sha256: citation.contentSha256,
|
|
1044
|
+
}));
|
|
1045
|
+
const closedPaths = availablePaths.filter((path) => checkSelectedPathQuotes(pathQuoteRequirements, [pathIdentifier(path)], selectedCitations).complete);
|
|
1046
|
+
const closedCandidates = closedPaths.map((path) => ({ ...path, pathId: pathIdentifier(path) }));
|
|
1047
|
+
const closedPathIds = closedCandidates.map((path) => path.pathId);
|
|
1048
|
+
const verificationPaths = renderedPaths(closedPaths, uniqueEntities, allRelations);
|
|
1049
|
+
const verifierSchema = { ...VERIFIER_SCHEMA, properties: { ...VERIFIER_SCHEMA.properties, proof_path_ids: {
|
|
1050
|
+
type: "array", minItems: 0, maxItems: closedPathIds.length === 0 ? 0 : proofSelectionLimit,
|
|
1051
|
+
items: { type: "string", ...(closedPathIds.length === 0 ? {} : { enum: closedPathIds }) },
|
|
1052
|
+
} } };
|
|
1053
|
+
trace.push({ stage: "verify", detail: { phase: "closed_proof_candidates", attempt: attempt + 1, closedPathIds,
|
|
1054
|
+
availablePathCount: availablePaths.length, closedPathCount: closedPaths.length, candidateTruncated: refined.truncated } });
|
|
1055
|
+
const generatedVerifier = await structuredAttempt({
|
|
1056
|
+
schemaName: "document_grounded_verifier_v1", jsonSchema: verifierSchema, taskMode: "simulation_retrieval", reasoningEffort: "low", maxOutputTokens: GROUNDED_MAX_OUTPUT_TOKENS,
|
|
1057
|
+
messages: [
|
|
1058
|
+
{ role: "system", content: "按问题要求和答案实际断言的粒度核验,不得追加更细粒度的必要条件;答案自行增加的名称、数值或身份细节仍须逐项有原文支持。分别判断supported(原文是否支持答案断言)与answers_question(答案是否实际给出了query所求结果);有据的未知/拒答陈述不等于回答了事实问题,但query本身若问是否可确定则按其语义判断,不能按词表判定。只能联合使用当前citations逐字quote,不得凭外部知识、图标签或上轮结果补前提。每条引文须支持答案事实或必要中间前提,不要求每条引文独立包含最终答案;核对身份、别名、指代、方向、否定、时间、归属。proof_path_options仅列当前引文已逐跳机械覆盖的真实路径,不代表与问题相关;选择其中实际连接问题锚点、必要前提与答案的完整路径ID到proof_path_ids,不要自动选全部、不要选无关背景。多关系问题有对应完整链时应选完整证明而非尾段;若只有原文直接证明且无相关路径,proof_path_ids=[]。相同标签不同ref不能拼接。supported_quote_ids与unsupported_quote_ids只能来自当前quote_id且不重复交叉;missing_premises仅简述真正缺失的必要前提,不编造事实或引文。所有选中引文均有支持时才能supported=true,不能仅删掉不支持的ID就当全部通过。" },
|
|
1059
|
+
{ role: "user", content: modelJson({ query, answer: answerValue.answer, citations: selectedCitations,
|
|
1060
|
+
proof_path_options: verificationPaths, graph_authority: "navigation_only_not_fact_evidence" }) },
|
|
1061
|
+
],
|
|
1062
|
+
});
|
|
1063
|
+
if (generatedVerifier.validationError !== undefined) {
|
|
1064
|
+
repairFeedback = { stage: "verify", validation_error: generatedVerifier.validationError, previous_answer: answerValue.answer, previous_citations: answerValue.citations };
|
|
1065
|
+
trace.push({ stage: "verify", detail: { attempt: attempt + 1, supported: false, validationError: generatedVerifier.validationError } });
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
const verifierValue = record(generatedVerifier.value, "verifier_output_invalid");
|
|
1069
|
+
if (typeof verifierValue.supported !== "boolean" || typeof verifierValue.answers_question !== "boolean" || !Array.isArray(verifierValue.supported_quote_ids)
|
|
1070
|
+
|| !Array.isArray(verifierValue.unsupported_quote_ids) || !Array.isArray(verifierValue.missing_premises))
|
|
1071
|
+
return fail("verifier_output_invalid");
|
|
1072
|
+
if (verifierValue.supported_quote_ids.length > selectedCitations.length || verifierValue.unsupported_quote_ids.length > selectedCitations.length)
|
|
1073
|
+
return fail("verifier_quotes_invalid");
|
|
1074
|
+
const supportedQuoteIds = verifierValue.supported_quote_ids.map((id) => identifier(id, "verifier_quotes_invalid"));
|
|
1075
|
+
const unsupportedQuoteIds = verifierValue.unsupported_quote_ids.map((id) => identifier(id, "verifier_quotes_invalid"));
|
|
1076
|
+
const selectedQuoteIds = new Set(selectedCitations.map((citation) => citation.quote_id));
|
|
1077
|
+
if ([...supportedQuoteIds, ...unsupportedQuoteIds].some((id) => !selectedQuoteIds.has(id)))
|
|
1078
|
+
return fail("verifier_quote_unknown");
|
|
1079
|
+
if (new Set([...supportedQuoteIds, ...unsupportedQuoteIds]).size !== supportedQuoteIds.length + unsupportedQuoteIds.length)
|
|
1080
|
+
return fail("verifier_quotes_invalid");
|
|
1081
|
+
const missingPremises = verifierValue.missing_premises;
|
|
1082
|
+
if (missingPremises.length > 8 || missingPremises.some((value) => typeof value !== "string" || value.trim().length === 0 || value.length > 256))
|
|
1083
|
+
return fail("verifier_feedback_invalid");
|
|
1084
|
+
const requestedProofIds = Array.isArray(verifierValue.proof_path_ids) && verifierValue.proof_path_ids.length === 0 ? []
|
|
1085
|
+
: stringArray(verifierValue.proof_path_ids, "verifier_proof_path_ids_invalid", proofSelectionLimit);
|
|
1086
|
+
const canonical = canonicalizeSelectedProofPaths(closedCandidates, requestedProofIds);
|
|
1087
|
+
proofPathIds = canonical.canonicalPathIds;
|
|
1088
|
+
proofPaths = proofPathIds.map((id) => availableById.get(id));
|
|
1089
|
+
const pathProof = checkSelectedPathQuotes(pathQuoteRequirements, proofPathIds, selectedCitations);
|
|
1090
|
+
trace.push({ stage: "verify", detail: { phase: "proof_path_selection", attempt: attempt + 1, requestedProofPathIds: requestedProofIds,
|
|
1091
|
+
canonicalProofPathIds: proofPathIds, mapping: canonical.mapping, ...pathProof } });
|
|
1092
|
+
const supported = verifierValue.supported && verifierValue.answers_question && pathProof.complete && missingPremises.length === 0 && unsupportedQuoteIds.length === 0
|
|
1093
|
+
&& selectedCitations.every((citation) => supportedQuoteIds.includes(citation.quote_id));
|
|
1094
|
+
const supportedIds = [...new Set(selectedCitations.filter((citation) => supportedQuoteIds.includes(citation.quote_id)).map((citation) => citation.evidence_id))];
|
|
1095
|
+
trace.push({ stage: "verify", detail: { attempt: attempt + 1, supported, answersQuestion: verifierValue.answers_question,
|
|
1096
|
+
supportedCitationIds: supportedIds, supportedQuoteIds, missingPremises, unsupportedQuoteIds } });
|
|
1097
|
+
if (supported)
|
|
1098
|
+
return Object.freeze({ answerable: true, answer: answerValue.answer, citations, graphPaths: proofPaths,
|
|
1099
|
+
retrievalTrace: Object.freeze(trace), usage: usageTotal(usages, dependencies.modelConnection.structuredJson.providerId, REQUIRED_MODEL) });
|
|
1100
|
+
repairFeedback = { stage: "verify", supported: false, reason: verifierValue.answers_question ? "unsupported" : "not_answered", answers_question: verifierValue.answers_question, missing_premises: missingPremises,
|
|
1101
|
+
unsupported_quote_ids: [...new Set([...unsupportedQuoteIds, ...selectedCitations.filter((citation) => !supportedQuoteIds.includes(citation.quote_id)).map((citation) => citation.quote_id)])],
|
|
1102
|
+
supported_quote_ids: supportedQuoteIds, previous_answer: answerValue.answer, previous_citations: answerValue.citations };
|
|
1103
|
+
}
|
|
1104
|
+
return Object.freeze({ answerable: false, answer: null, citations: Object.freeze([]), graphPaths: [],
|
|
1105
|
+
retrievalTrace: Object.freeze(trace), usage: usageTotal(usages, dependencies.modelConnection.structuredJson.providerId, REQUIRED_MODEL) });
|
|
1106
|
+
}
|