refactor focus concepts: add focusTerms/focusCharacters and switch character filtering
This commit is contained in:
@@ -19,6 +19,7 @@ import { getVectorConfig, getSummaryPanelConfig, getSettings } from "../data/con
|
||||
import { recallMemory } from "../vector/retrieval/recall.js";
|
||||
import { getMeta } from "../vector/storage/chunk-store.js";
|
||||
import { getEngineFingerprint } from "../vector/utils/embedder.js";
|
||||
import { buildTrustedCharacters } from "../vector/retrieval/entity-lexicon.js";
|
||||
|
||||
// Metrics
|
||||
import { formatMetricsLog, detectIssues } from "../vector/retrieval/metrics.js";
|
||||
@@ -239,23 +240,11 @@ function buildPostscript() {
|
||||
* @returns {Set<string>} 角色名称集合(标准化后)
|
||||
*/
|
||||
function getKnownCharacters(store) {
|
||||
const names = new Set();
|
||||
|
||||
const arcs = store?.json?.arcs || [];
|
||||
for (const a of arcs) {
|
||||
if (a.name) names.add(normalize(a.name));
|
||||
}
|
||||
|
||||
const main = store?.json?.characters?.main || [];
|
||||
for (const m of main) {
|
||||
const name = typeof m === 'string' ? m : m.name;
|
||||
if (name) names.add(normalize(name));
|
||||
}
|
||||
|
||||
const { name1, name2 } = getContext();
|
||||
const names = buildTrustedCharacters(store, { name1, name2 }) || new Set();
|
||||
// Keep name1 in known-character filtering domain to avoid behavior regression
|
||||
// for L3 subject filtering (lexicon exclusion and filtering semantics are different concerns).
|
||||
if (name1) names.add(normalize(name1));
|
||||
if (name2) names.add(normalize(name2));
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -272,14 +261,14 @@ function parseRelationTarget(predicate) {
|
||||
/**
|
||||
* 按相关性过滤 facts
|
||||
* @param {object[]} facts - 所有 facts
|
||||
* @param {string[]} focusEntities - 焦点实体
|
||||
* @param {string[]} focusCharacters - 焦点人物
|
||||
* @param {Set<string>} knownCharacters - 已知角色
|
||||
* @returns {object[]} 过滤后的 facts
|
||||
*/
|
||||
function filterConstraintsByRelevance(facts, focusEntities, knownCharacters) {
|
||||
function filterConstraintsByRelevance(facts, focusCharacters, knownCharacters) {
|
||||
if (!facts?.length) return [];
|
||||
|
||||
const focusSet = new Set((focusEntities || []).map(normalize));
|
||||
const focusSet = new Set((focusCharacters || []).map(normalize));
|
||||
|
||||
return facts.filter(f => {
|
||||
if (f._isState === true) return true;
|
||||
@@ -304,13 +293,13 @@ function filterConstraintsByRelevance(facts, focusEntities, knownCharacters) {
|
||||
|
||||
/**
|
||||
* Build people dictionary for constraints display.
|
||||
* Primary source: selected event participants; fallback: focus entities.
|
||||
* Primary source: selected event participants; fallback: focus characters.
|
||||
*
|
||||
* @param {object|null} recallResult
|
||||
* @param {string[]} focusEntities
|
||||
* @param {string[]} focusCharacters
|
||||
* @returns {Map<string, string>} normalize(name) -> display name
|
||||
*/
|
||||
function buildConstraintPeopleDict(recallResult, focusEntities = []) {
|
||||
function buildConstraintPeopleDict(recallResult, focusCharacters = []) {
|
||||
const dict = new Map();
|
||||
const add = (raw) => {
|
||||
const display = String(raw || '').trim();
|
||||
@@ -326,7 +315,7 @@ function buildConstraintPeopleDict(recallResult, focusEntities = []) {
|
||||
}
|
||||
|
||||
if (dict.size === 0) {
|
||||
for (const f of (focusEntities || [])) add(f);
|
||||
for (const f of (focusCharacters || [])) add(f);
|
||||
}
|
||||
|
||||
return dict;
|
||||
@@ -375,16 +364,19 @@ function formatConstraintLine(f, includeSubject = false) {
|
||||
* @param {{ people: Map<string, object[]>, world: object[] }} grouped
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function formatConstraintsStructured(grouped) {
|
||||
function formatConstraintsStructured(grouped, order = 'desc') {
|
||||
const lines = [];
|
||||
const people = grouped?.people || new Map();
|
||||
const world = grouped?.world || [];
|
||||
const sorter = order === 'asc'
|
||||
? ((a, b) => (a.since || 0) - (b.since || 0))
|
||||
: ((a, b) => (b.since || 0) - (a.since || 0));
|
||||
|
||||
if (people.size > 0) {
|
||||
lines.push('people:');
|
||||
for (const [name, facts] of people.entries()) {
|
||||
lines.push(` ${name}:`);
|
||||
const sorted = [...facts].sort((a, b) => (b.since || 0) - (a.since || 0));
|
||||
const sorted = [...facts].sort(sorter);
|
||||
for (const f of sorted) {
|
||||
lines.push(` ${formatConstraintLine(f, false)}`);
|
||||
}
|
||||
@@ -393,7 +385,7 @@ function formatConstraintsStructured(grouped) {
|
||||
|
||||
if (world.length > 0) {
|
||||
lines.push('world:');
|
||||
const sortedWorld = [...world].sort((a, b) => (b.since || 0) - (a.since || 0));
|
||||
const sortedWorld = [...world].sort(sorter);
|
||||
for (const f of sortedWorld) {
|
||||
lines.push(` ${formatConstraintLine(f, true)}`);
|
||||
}
|
||||
@@ -402,6 +394,58 @@ function formatConstraintsStructured(grouped) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function tryConsumeConstraintLineBudget(line, budgetState) {
|
||||
const cost = estimateTokens(line);
|
||||
if (budgetState.used + cost > budgetState.max) return false;
|
||||
budgetState.used += cost;
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectConstraintsByBudgetDesc(grouped, budgetState) {
|
||||
const selectedPeople = new Map();
|
||||
const selectedWorld = [];
|
||||
const people = grouped?.people || new Map();
|
||||
const world = grouped?.world || [];
|
||||
|
||||
if (people.size > 0) {
|
||||
if (!tryConsumeConstraintLineBudget('people:', budgetState)) {
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
for (const [name, facts] of people.entries()) {
|
||||
const header = ` ${name}:`;
|
||||
if (!tryConsumeConstraintLineBudget(header, budgetState)) {
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
const picked = [];
|
||||
const sorted = [...facts].sort((a, b) => (b.since || 0) - (a.since || 0));
|
||||
for (const f of sorted) {
|
||||
const line = ` ${formatConstraintLine(f, false)}`;
|
||||
if (!tryConsumeConstraintLineBudget(line, budgetState)) {
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
picked.push(f);
|
||||
}
|
||||
selectedPeople.set(name, picked);
|
||||
}
|
||||
}
|
||||
|
||||
if (world.length > 0) {
|
||||
if (!tryConsumeConstraintLineBudget('world:', budgetState)) {
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
const sortedWorld = [...world].sort((a, b) => (b.since || 0) - (a.since || 0));
|
||||
for (const f of sortedWorld) {
|
||||
const line = ` ${formatConstraintLine(f, true)}`;
|
||||
if (!tryConsumeConstraintLineBudget(line, budgetState)) {
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
selectedWorld.push(f);
|
||||
}
|
||||
}
|
||||
|
||||
return { people: selectedPeople, world: selectedWorld };
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 格式化函数
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -703,7 +747,7 @@ function buildNonVectorPrompt(store) {
|
||||
nonVectorKnownCharacters
|
||||
);
|
||||
const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, nonVectorPeopleDict);
|
||||
const constraintLines = formatConstraintsStructured(groupedConstraints);
|
||||
const constraintLines = formatConstraintsStructured(groupedConstraints, 'asc');
|
||||
|
||||
if (constraintLines.length) {
|
||||
sections.push(`[定了的事] 已确立的事实\n${constraintLines.join("\n")}`);
|
||||
@@ -772,12 +816,12 @@ export function buildNonVectorPromptText() {
|
||||
* @param {object} store - 存储对象
|
||||
* @param {object} recallResult - 召回结果
|
||||
* @param {Map<string, object>} causalById - 因果事件索引
|
||||
* @param {string[]} focusEntities - 焦点实体
|
||||
* @param {string[]} focusCharacters - 焦点人物
|
||||
* @param {object} meta - 元数据
|
||||
* @param {object} metrics - 指标对象
|
||||
* @returns {Promise<{promptText: string, injectionStats: object, metrics: object}>}
|
||||
*/
|
||||
async function buildVectorPrompt(store, recallResult, causalById, focusEntities, meta, metrics) {
|
||||
async function buildVectorPrompt(store, recallResult, causalById, focusCharacters, meta, metrics) {
|
||||
const T_Start = performance.now();
|
||||
|
||||
const data = store.json || {};
|
||||
@@ -825,21 +869,21 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
|
||||
const allFacts = getFacts();
|
||||
const knownCharacters = getKnownCharacters(store);
|
||||
const filteredConstraints = filterConstraintsByRelevance(allFacts, focusEntities, knownCharacters);
|
||||
const constraintPeopleDict = buildConstraintPeopleDict(recallResult, focusEntities);
|
||||
const filteredConstraints = filterConstraintsByRelevance(allFacts, focusCharacters, knownCharacters);
|
||||
const constraintPeopleDict = buildConstraintPeopleDict(recallResult, focusCharacters);
|
||||
const groupedConstraints = groupConstraintsForDisplay(filteredConstraints, constraintPeopleDict);
|
||||
const constraintLines = formatConstraintsStructured(groupedConstraints);
|
||||
|
||||
if (metrics) {
|
||||
metrics.constraint.total = allFacts.length;
|
||||
metrics.constraint.filtered = allFacts.length - filteredConstraints.length;
|
||||
}
|
||||
|
||||
const constraintBudget = { used: 0, max: Math.min(CONSTRAINT_MAX, total.max - total.used) };
|
||||
const groupedSelectedConstraints = selectConstraintsByBudgetDesc(groupedConstraints, constraintBudget);
|
||||
const constraintLines = formatConstraintsStructured(groupedSelectedConstraints, 'asc');
|
||||
|
||||
if (constraintLines.length) {
|
||||
const constraintBudget = { used: 0, max: Math.min(CONSTRAINT_MAX, total.max - total.used) };
|
||||
for (const line of constraintLines) {
|
||||
if (!pushWithBudget(assembled.constraints.lines, line, constraintBudget)) break;
|
||||
}
|
||||
assembled.constraints.lines.push(...constraintLines);
|
||||
assembled.constraints.tokens = constraintBudget.used;
|
||||
total.used += constraintBudget.used;
|
||||
injectionStats.constraint.count = assembled.constraints.lines.length;
|
||||
@@ -867,7 +911,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const userName = String(name1 || "").trim();
|
||||
|
||||
const relevant = new Set(
|
||||
[userName, ...(focusEntities || [])]
|
||||
[userName, ...(focusCharacters || [])]
|
||||
.map(s => String(s || "").trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
@@ -1048,7 +1092,7 @@ async function buildVectorPrompt(store, recallResult, causalById, focusEntities,
|
||||
const keepVisible = store.keepVisibleCount ?? 3;
|
||||
|
||||
// 收集未被事件消费的 L0,按 rerankScore 降序
|
||||
const focusSetForEvidence = new Set((focusEntities || []).map(normalize).filter(Boolean));
|
||||
const focusSetForEvidence = new Set((focusCharacters || []).map(normalize).filter(Boolean));
|
||||
|
||||
const remainingL0 = l0Selected
|
||||
.filter(l0 => !usedL0Ids.has(l0.id))
|
||||
@@ -1279,7 +1323,9 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) {
|
||||
l0Selected: recallResult?.l0Selected || [],
|
||||
l1ByFloor: recallResult?.l1ByFloor || new Map(),
|
||||
causalChain: recallResult?.causalChain || [],
|
||||
focusEntities: recallResult?.focusEntities || [],
|
||||
focusTerms: recallResult?.focusTerms || recallResult?.focusEntities || [],
|
||||
focusEntities: recallResult?.focusTerms || recallResult?.focusEntities || [], // compat alias
|
||||
focusCharacters: recallResult?.focusCharacters || [],
|
||||
metrics: recallResult?.metrics || null,
|
||||
};
|
||||
|
||||
@@ -1340,7 +1386,7 @@ export async function buildVectorPromptText(excludeLastAi = false, hooks = {}) {
|
||||
store,
|
||||
recallResult,
|
||||
causalById,
|
||||
recallResult?.focusEntities || [],
|
||||
recallResult?.focusCharacters || [],
|
||||
meta,
|
||||
recallResult?.metrics || null
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user