release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence
This commit is contained in:
+22
-14
@@ -98,23 +98,31 @@ export class RetrievalOrchestrator {
|
||||
fusionLog.push(`Expanded tokens: [${expandedTokens.slice(0, 15).join(', ')}]`);
|
||||
|
||||
// ── ① Brain File Search (TF-IDF enhanced, optionally hybrid with embeddings) ──
|
||||
// `brainFileLimit === 0` is meaningful (Knowledge Mix "model knowledge only"
|
||||
// mode), so use `??` rather than `||`. When the caller explicitly passes 0,
|
||||
// we skip retrieval entirely instead of falling back to the default of 8.
|
||||
const scopeFolders = options.scopeFolders ?? [];
|
||||
const brainChunks = this.searchBrainFiles(
|
||||
query,
|
||||
expandedTokens,
|
||||
options.brain,
|
||||
options.brainFileLimit || 8,
|
||||
options.includeRawConversations || false,
|
||||
scopeFolders,
|
||||
options.queryEmbedding,
|
||||
options.embeddingModel,
|
||||
options.embeddingBlendAlpha
|
||||
);
|
||||
const brainFileLimit = options.brainFileLimit ?? 8;
|
||||
const brainChunks = brainFileLimit > 0
|
||||
? this.searchBrainFiles(
|
||||
query,
|
||||
expandedTokens,
|
||||
options.brain,
|
||||
brainFileLimit,
|
||||
options.includeRawConversations || false,
|
||||
scopeFolders,
|
||||
options.queryEmbedding,
|
||||
options.embeddingModel,
|
||||
options.embeddingBlendAlpha
|
||||
)
|
||||
: [];
|
||||
allChunks.push(...brainChunks);
|
||||
fusionLog.push(
|
||||
scopeFolders.length > 0
|
||||
? `Brain search (scoped to ${scopeFolders.length} folder(s)): ${brainChunks.length} chunks`
|
||||
: `Brain search: ${brainChunks.length} chunks found`
|
||||
brainFileLimit === 0
|
||||
? 'Brain search: skipped (Knowledge Mix weight = 0)'
|
||||
: scopeFolders.length > 0
|
||||
? `Brain search (scoped to ${scopeFolders.length} folder(s)): ${brainChunks.length} chunks`
|
||||
: `Brain search: ${brainChunks.length} chunks found`
|
||||
);
|
||||
|
||||
// ── ② Memory Layers ──
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Knowledge Mix — controls how much the assistant leans on Second Brain
|
||||
* evidence vs. the model's own general knowledge for a given query.
|
||||
*
|
||||
* The single integer "secondBrainWeight" (0–100) drives three things:
|
||||
*
|
||||
* 1. RAG chunk budget — how many brain files we feed the model.
|
||||
* 2. Retrieval ratio — what fraction of the context budget RAG can claim.
|
||||
* 3. Prompt policy — natural-language instruction injected into the
|
||||
* system prompt telling the model how to balance
|
||||
* its own knowledge against the evidence shown.
|
||||
*
|
||||
* Per-agent overrides (AgentKnowledgeEntry.secondBrainWeight) win over the
|
||||
* global config (g1nation.knowledgeMix.secondBrainWeight). Both fall back to
|
||||
* the default `DEFAULT_WEIGHT` (balanced) when nothing is set.
|
||||
*
|
||||
* Keeping this module isolated and pure makes it trivial to unit-test the
|
||||
* mapping curve and to extend it later (e.g. add a "creative" axis) without
|
||||
* touching retrieval or prompt assembly.
|
||||
*/
|
||||
import { getConfig } from '../config';
|
||||
import { getOrCreateAgentEntry } from '../skills/agentKnowledgeMap';
|
||||
|
||||
export const DEFAULT_WEIGHT = 50;
|
||||
|
||||
/** Where the resolved weight came from — surfaced in `_lastRetrievalInfo` for UX. */
|
||||
export type KnowledgeMixSource = 'agent' | 'global' | 'default';
|
||||
|
||||
export interface ResolvedKnowledgeMix {
|
||||
/** Integer in [0, 100]. */
|
||||
weight: number;
|
||||
source: KnowledgeMixSource;
|
||||
/** Agent name when `source === 'agent'`, else undefined. */
|
||||
agent?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective weight for the active turn.
|
||||
*
|
||||
* Precedence: per-agent → global config → default. Out-of-range values from
|
||||
* either source are clamped, never silently zeroed (a typo in JSON should not
|
||||
* disable retrieval entirely).
|
||||
*/
|
||||
export function resolveKnowledgeMix(agentFileOrName?: string): ResolvedKnowledgeMix {
|
||||
if (agentFileOrName && agentFileOrName !== 'none') {
|
||||
try {
|
||||
const entry = getOrCreateAgentEntry(agentFileOrName);
|
||||
if (typeof entry.secondBrainWeight === 'number' && Number.isFinite(entry.secondBrainWeight)) {
|
||||
return {
|
||||
weight: _clamp(entry.secondBrainWeight),
|
||||
source: 'agent',
|
||||
agent: entry.name,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Map missing or unreadable — fall through to global.
|
||||
}
|
||||
}
|
||||
try {
|
||||
const cfg = getConfig();
|
||||
if (typeof cfg.knowledgeMixSecondBrainWeight === 'number') {
|
||||
return { weight: _clamp(cfg.knowledgeMixSecondBrainWeight), source: 'global' };
|
||||
}
|
||||
} catch {
|
||||
// getConfig should never throw in practice, but keep this safe in tests.
|
||||
}
|
||||
return { weight: DEFAULT_WEIGHT, source: 'default' };
|
||||
}
|
||||
|
||||
function _clamp(n: number): number {
|
||||
if (!Number.isFinite(n)) return DEFAULT_WEIGHT;
|
||||
return Math.max(0, Math.min(100, Math.round(n)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a weight to the maximum number of brain files (long-term memory) the
|
||||
* retriever is allowed to consider for this turn.
|
||||
*
|
||||
* Curve was chosen so that:
|
||||
* - 0 fully disables brain-file retrieval (model-only mode).
|
||||
* - 50 maps to roughly the existing default (`memoryLongTermFiles`), so
|
||||
* behaviour without any per-agent setting matches the status quo.
|
||||
* - 100 pushes the cap toward `selectWithinBudget`'s hard ceiling (12).
|
||||
*
|
||||
* The configured `memoryLongTermFiles` is treated as the "balanced" baseline:
|
||||
* it's scaled up at high weights and damped at low weights.
|
||||
*/
|
||||
export function mapWeightToBrainFileLimit(weight: number, configuredLimit: number): number {
|
||||
const w = _clamp(weight);
|
||||
if (w === 0) return 0;
|
||||
const baseline = Math.max(1, configuredLimit || 6);
|
||||
// Linear interpolation:
|
||||
// w=0 → 0
|
||||
// w=25 → baseline * 0.5
|
||||
// w=50 → baseline
|
||||
// w=75 → baseline * 1.5
|
||||
// w=100 → baseline * 2 (capped at 12 elsewhere)
|
||||
const scaled = Math.round((w / 50) * baseline);
|
||||
// Honour the orchestrator's hard cap (12) so we never blow the budget.
|
||||
return Math.max(0, Math.min(12, scaled));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a weight to the retrieval ratio (fraction of the context-budget that
|
||||
* RAG can claim). Lower weights mean RAG gets a smaller slice and leaves more
|
||||
* room for conversation history / system prompt.
|
||||
*/
|
||||
export function mapWeightToRetrievalRatio(weight: number): number {
|
||||
const w = _clamp(weight);
|
||||
// 0 → 0.05 (still room for tiny lessons block), 50 → 0.40 (status quo), 100 → 0.60.
|
||||
return Math.max(0.05, Math.min(0.6, 0.05 + (w / 100) * 0.55));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the natural-language policy block injected into the system prompt.
|
||||
* Returns `''` when the weight is exactly the default (50) — at the midpoint
|
||||
* there's nothing useful to tell the model, and quieter prompts behave better.
|
||||
*/
|
||||
export function buildKnowledgeMixPolicy(mix: ResolvedKnowledgeMix): string {
|
||||
const w = _clamp(mix.weight);
|
||||
if (w === DEFAULT_WEIGHT) return '';
|
||||
const header = `[KNOWLEDGE MIX POLICY]\nSecond Brain reliance: ${w}% (model knowledge: ${100 - w}%).`;
|
||||
let body: string;
|
||||
if (w === 0) {
|
||||
body = [
|
||||
'Second Brain retrieval is disabled for this turn.',
|
||||
'Answer from your general knowledge and the conversation history alone.',
|
||||
'Do not invent file citations.',
|
||||
].join('\n');
|
||||
} else if (w < 25) {
|
||||
body = [
|
||||
'Rely primarily on your own general knowledge.',
|
||||
'Treat any Second Brain notes shown below as light reference material only.',
|
||||
'Brainstorming, broad explanations and creative synthesis are encouraged.',
|
||||
].join('\n');
|
||||
} else if (w < 50) {
|
||||
body = [
|
||||
'Lean on your general knowledge; use Second Brain notes as supporting context.',
|
||||
'Cite Brain files only when they materially shape the answer.',
|
||||
].join('\n');
|
||||
} else if (w < 75) {
|
||||
body = [
|
||||
'Prefer Second Brain evidence when it is present.',
|
||||
'Use your general knowledge to connect, explain, and fill harmless background.',
|
||||
'Do not override explicit Second Brain evidence with model assumptions.',
|
||||
].join('\n');
|
||||
} else if (w < 100) {
|
||||
body = [
|
||||
'Treat Second Brain notes as the primary evidence for this answer.',
|
||||
'Cite Brain files for any non-trivial claim and quote relevant lines when needed.',
|
||||
'If the notes do not cover a point, say so explicitly instead of guessing.',
|
||||
].join('\n');
|
||||
} else {
|
||||
body = [
|
||||
'Second Brain notes are the only authoritative source for this answer.',
|
||||
'Cite Brain files for every substantive claim.',
|
||||
'If a point is not in the notes, reply that it is outside the recorded knowledge — do not fall back to general knowledge.',
|
||||
].join('\n');
|
||||
}
|
||||
return `${header}\n${body}`;
|
||||
}
|
||||
Reference in New Issue
Block a user