mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
341 lines
10 KiB
JavaScript
341 lines
10 KiB
JavaScript
import {
|
|
buildContextQueryBlend,
|
|
buildVectorQueryPlan,
|
|
clampPositiveInt,
|
|
} from "./shared-ranking.js";
|
|
import {
|
|
filterAuthorityTriviumNodes,
|
|
isAuthorityVectorConfig,
|
|
queryAuthorityTriviumNeighbors,
|
|
searchAuthorityTriviumNodes,
|
|
} from "../vector/authority-vector-primary-adapter.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,
|
|
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();
|
|
const searchStartedAt = nowMs();
|
|
for (const queryEntry of queryPlan.queries) {
|
|
try {
|
|
const searchResults = await searchAuthorityTriviumNodes(
|
|
graph,
|
|
queryEntry.text,
|
|
embeddingConfig,
|
|
{
|
|
namespace: collectionId,
|
|
collectionId,
|
|
chatId,
|
|
topK: limit,
|
|
candidateIds: filteredIds.length > 0 ? filteredIds : undefined,
|
|
signal,
|
|
},
|
|
);
|
|
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) * queryEntry.weight;
|
|
const previous = Number(searchScores.get(nodeId) || 0) || 0;
|
|
if (weightedScore > previous) {
|
|
searchScores.set(nodeId, weightedScore);
|
|
}
|
|
}
|
|
} catch (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,
|
|
};
|
|
}
|