Files
ST-Bionic-Memory-Ecology/retrieval/authority-candidate-provider.js
2026-04-29 14:25:16 +08:00

379 lines
12 KiB
JavaScript

import {
buildContextQueryBlend,
buildVectorQueryPlan,
clampPositiveInt,
} from "./shared-ranking.js";
import {
filterAuthorityTriviumNodes,
isAuthorityVectorConfig,
queryAuthorityTriviumNeighbors,
searchAuthorityTriviumNodes,
} from "../vector/authority-vector-primary-adapter.js";
import { embedText } from "../vector/embedding.js";
import { runLimited } from "../runtime/concurrency.js";
function nowMs() {
if (typeof performance?.now === "function") {
return performance.now();
}
return Date.now();
}
function roundMs(value) {
return Math.round((Number(value) || 0) * 10) / 10;
}
function normalizeRecordId(value) {
return String(value ?? "").trim();
}
function toArray(value) {
return Array.isArray(value) ? value : [];
}
function uniqueIds(values = []) {
const result = [];
const seen = new Set();
for (const value of values) {
const normalized = normalizeRecordId(value);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
result.push(normalized);
}
return result;
}
function isAbortError(error) {
return error?.name === "AbortError";
}
function buildAuthorityCandidateQueryPlan(userMessage, recentMessages = [], options = {}) {
const queryBlend = buildContextQueryBlend(userMessage, recentMessages, {
enabled: options.enableContextQueryBlend !== false,
assistantWeight: Number(options.contextAssistantWeight ?? 0.2),
previousUserWeight: Number(options.contextPreviousUserWeight ?? 0.1),
maxTextLength: Number(options.maxTextLength || 400),
});
const vectorQueryPlan = buildVectorQueryPlan(queryBlend, {
enableMultiIntent: options.enableMultiIntent !== false,
maxSegments: clampPositiveInt(options.multiIntentMaxSegments, 4),
});
const maxQueryTexts = clampPositiveInt(options.maxQueryTexts, 6);
const queries = [];
const seen = new Set();
for (const part of vectorQueryPlan.plan || []) {
for (const queryText of part.queries || []) {
const normalizedText = String(queryText || "").trim();
const key = normalizedText.toLowerCase();
if (!normalizedText || seen.has(key)) continue;
seen.add(key);
queries.push({
text: normalizedText,
weight: Math.max(0.05, Number(part.weight || 0) || 0.05),
});
if (queries.length >= maxQueryTexts) {
return {
queryBlend,
vectorQueryPlan,
queries,
};
}
}
}
return {
queryBlend,
vectorQueryPlan,
queries,
};
}
function resolveSceneOwnerNames(sceneOwnerCandidates = [], ownerKeys = []) {
const ownerKeySet = new Set(uniqueIds(ownerKeys));
return uniqueIds(
toArray(sceneOwnerCandidates)
.filter((candidate) => ownerKeySet.has(normalizeRecordId(candidate?.ownerKey)))
.map((candidate) => candidate?.ownerName)
.filter(Boolean),
);
}
function buildAuthorityCandidateFilters({
ownerKeys = [],
ownerNames = [],
regionKeys = [],
storySegmentIds = [],
} = {}) {
return {
archived: false,
ownerKeys: uniqueIds(ownerKeys),
ownerNames: uniqueIds(ownerNames),
regionKeys: uniqueIds(regionKeys),
storySegmentIds: uniqueIds(storySegmentIds),
};
}
function mapCandidateNodes(candidateIds = [], availableNodes = []) {
const nodeMap = new Map(
toArray(availableNodes)
.map((node) => [normalizeRecordId(node?.id), node])
.filter(([nodeId]) => nodeId),
);
return uniqueIds(candidateIds)
.map((nodeId) => nodeMap.get(nodeId))
.filter(Boolean);
}
export async function resolveAuthorityRecallCandidates({
graph,
userMessage,
recentMessages = [],
embeddingConfig,
availableNodes = [],
activeRegion = "",
activeStoryContext = {},
activeRecallOwnerKeys = [],
sceneOwnerCandidates = [],
signal = undefined,
options = {},
} = {}) {
const startedAt = nowMs();
const diagnostics = {
provider: "authority-trivium",
available: false,
used: false,
candidateCount: 0,
filteredCount: 0,
searchHits: 0,
neighborCount: 0,
queryTexts: [],
fallbackReason: "",
timings: {
total: 0,
embed: 0,
filter: 0,
search: 0,
neighbors: 0,
},
};
const candidateNodes = toArray(availableNodes).filter((node) => node && !node.archived);
if (options.enabled === false) {
diagnostics.fallbackReason = "authority-graph-query-disabled";
diagnostics.timings.total = roundMs(nowMs() - startedAt);
return {
available: false,
used: false,
candidateNodes: [],
diagnostics,
};
}
if (!graph || candidateNodes.length === 0 || !isAuthorityVectorConfig(embeddingConfig)) {
diagnostics.fallbackReason = "authority-vector-unavailable";
diagnostics.timings.total = roundMs(nowMs() - startedAt);
return {
available: false,
used: false,
candidateNodes: [],
diagnostics,
};
}
diagnostics.available = true;
const collectionId = normalizeRecordId(
options.collectionId || graph?.vectorIndexState?.collectionId,
);
const chatId = normalizeRecordId(options.chatId || graph?.historyState?.chatId);
if (!collectionId) {
diagnostics.fallbackReason = "authority-collection-missing";
diagnostics.timings.total = roundMs(nowMs() - startedAt);
return {
available: true,
used: false,
candidateNodes: [],
diagnostics,
};
}
const allowedIds = new Set(candidateNodes.map((node) => normalizeRecordId(node?.id)));
const limit = clampPositiveInt(
options.limit,
Math.min(candidateNodes.length, Math.max(Number(options.topK || 0) * 4, 24)),
);
const neighborLimit = clampPositiveInt(
options.neighborLimit,
Math.min(limit, Math.max(4, Math.ceil(limit / 4))),
);
const minimumUsedCandidateCount = clampPositiveInt(
options.minimumUsedCandidateCount,
Math.min(candidateNodes.length, Math.max(Number(options.maxRecallNodes || 0), 6)),
);
const ownerKeys = uniqueIds(activeRecallOwnerKeys);
const ownerNames = resolveSceneOwnerNames(sceneOwnerCandidates, ownerKeys);
const regionKeys = uniqueIds([activeRegion]);
const storySegmentIds = uniqueIds([activeStoryContext?.activeSegmentId]);
const filterPayload = buildAuthorityCandidateFilters({
ownerKeys,
ownerNames,
regionKeys,
storySegmentIds,
});
const queryPlan = buildAuthorityCandidateQueryPlan(userMessage, recentMessages, options);
diagnostics.queryTexts = queryPlan.queries.map((entry) => entry.text);
let filteredIds = [];
const filterStartedAt = nowMs();
try {
filteredIds = (await filterAuthorityTriviumNodes(embeddingConfig, {
namespace: collectionId,
collectionId,
chatId,
limit,
topK: limit,
filters: filterPayload,
filter: filterPayload,
where: filterPayload,
searchText: queryPlan.queries[0]?.text || String(userMessage || "").trim(),
signal,
}))
.filter((nodeId) => allowedIds.has(normalizeRecordId(nodeId)))
.slice(0, limit);
diagnostics.filteredCount = filteredIds.length;
} catch (error) {
if (isAbortError(error)) throw error;
diagnostics.fallbackReason = "authority-candidate-filter-failed";
if (embeddingConfig?.failOpen === false) {
throw error;
}
}
diagnostics.timings.filter = roundMs(nowMs() - filterStartedAt);
const searchScores = new Map();
let embedMs = 0;
const searchStartedAt = nowMs();
const searchResultsByQuery = await runLimited(
queryPlan.queries,
async (queryEntry) => {
const embedStartedAt = nowMs();
const queryVec = await embedText(queryEntry.text, embeddingConfig, {
signal,
isQuery: true,
});
embedMs += nowMs() - embedStartedAt;
if (!queryVec) {
diagnostics.fallbackReason ||= "authority-candidate-query-embed-empty";
return [];
}
const searchResults = await searchAuthorityTriviumNodes(
graph,
queryEntry.text,
embeddingConfig,
{
namespace: collectionId,
collectionId,
chatId,
topK: limit,
candidateIds: filteredIds.length > 0 ? filteredIds : undefined,
queryVector: Array.from(queryVec),
signal,
},
);
return searchResults.map((result) => ({ ...result, queryWeight: queryEntry.weight }));
},
{
concurrency: Math.max(1, Math.floor(Number(options.queryConcurrency || 1)) || 1),
signal,
failFast: false,
},
);
diagnostics.timings.embed = roundMs(embedMs);
for (const searchResults of searchResultsByQuery) {
if (searchResults?.error) {
diagnostics.fallbackReason ||= "authority-candidate-search-failed";
if (embeddingConfig?.failOpen === false) {
throw searchResults.error;
}
continue;
}
for (const result of searchResults || []) {
const nodeId = normalizeRecordId(result?.nodeId);
if (!nodeId || !allowedIds.has(nodeId)) continue;
const weightedScore =
Math.max(0.001, Number(result?.score || 0) || 0) *
Math.max(0.05, Number(result?.queryWeight || 0) || 0.05);
const previous = Number(searchScores.get(nodeId) || 0) || 0;
if (weightedScore > previous) {
searchScores.set(nodeId, weightedScore);
}
}
}
for (const item of searchResultsByQuery) {
if (item?.error) {
const error = item.error;
if (isAbortError(error)) throw error;
diagnostics.fallbackReason ||= "authority-candidate-search-failed";
if (embeddingConfig?.failOpen === false) {
throw error;
}
}
}
diagnostics.timings.search = roundMs(nowMs() - searchStartedAt);
diagnostics.searchHits = searchScores.size;
const seedIds = [...searchScores.entries()]
.sort((left, right) => right[1] - left[1])
.map(([nodeId]) => nodeId)
.slice(0, Math.min(limit, Math.max(4, neighborLimit)));
let neighborIds = [];
const neighborsStartedAt = nowMs();
if (seedIds.length > 0) {
try {
neighborIds = (await queryAuthorityTriviumNeighbors(embeddingConfig, seedIds, {
namespace: collectionId,
collectionId,
chatId,
limit: neighborLimit,
topK: neighborLimit,
candidateIds: filteredIds.length > 0 ? filteredIds : undefined,
signal,
}))
.filter((nodeId) => allowedIds.has(normalizeRecordId(nodeId)))
.slice(0, neighborLimit);
diagnostics.neighborCount = neighborIds.length;
} catch (error) {
if (isAbortError(error)) throw error;
diagnostics.fallbackReason ||= "authority-candidate-neighbors-failed";
if (embeddingConfig?.failOpen === false) {
throw error;
}
}
}
diagnostics.timings.neighbors = roundMs(nowMs() - neighborsStartedAt);
const prioritizedNodeIds = uniqueIds([
...seedIds,
...filteredIds,
...neighborIds,
]).slice(0, limit);
const resolvedCandidateNodes = mapCandidateNodes(prioritizedNodeIds, candidateNodes);
diagnostics.candidateCount = resolvedCandidateNodes.length;
diagnostics.used =
resolvedCandidateNodes.length >= minimumUsedCandidateCount &&
resolvedCandidateNodes.length < candidateNodes.length;
if (!diagnostics.used && !diagnostics.fallbackReason) {
diagnostics.fallbackReason =
resolvedCandidateNodes.length === 0
? "authority-candidate-empty"
: resolvedCandidateNodes.length >= candidateNodes.length
? "authority-candidate-not-reduced"
: "authority-candidate-too-small";
}
diagnostics.timings.total = roundMs(nowMs() - startedAt);
return {
available: true,
used: diagnostics.used,
candidateNodes: diagnostics.used ? resolvedCandidateNodes : [],
diagnostics,
};
}