File size: 7,144 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | import { getSettings } from "@/lib/db/settings";
import type { MemoryConfig } from "./types";
export interface MemorySettings {
enabled: boolean;
maxTokens: number;
retentionDays: number;
strategy: "recent" | "semantic" | "hybrid";
skillsEnabled: boolean;
// Plan 21 β D9: new embedding / vector store fields
embeddingSource: "remote" | "static" | "transformers" | "auto";
embeddingProviderModel: string | null;
transformersEnabled: boolean;
staticEnabled: boolean;
rerankEnabled: boolean;
rerankProviderModel: string | null;
vectorStore: "sqlite-vec" | "qdrant" | "auto";
}
export const DEFAULT_MEMORY_SETTINGS: MemorySettings = {
enabled: true,
maxTokens: 2000,
retentionDays: 30,
strategy: "hybrid",
skillsEnabled: true,
// Plan 21 β D9 defaults
embeddingSource: "auto",
embeddingProviderModel: null,
transformersEnabled: false,
staticEnabled: false,
rerankEnabled: false,
rerankProviderModel: null,
vectorStore: "auto",
};
let cachedMemorySettings: MemorySettings | null = null;
function toBoolean(value: unknown, fallback: boolean): boolean {
return typeof value === "boolean" ? value : fallback;
}
function clampInteger(value: unknown, fallback: number, min: number, max: number): number {
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
return Math.min(Math.max(Math.round(value), min), max);
}
function normalizeStrategy(value: unknown): MemorySettings["strategy"] {
return value === "recent" || value === "semantic" || value === "hybrid"
? value
: DEFAULT_MEMORY_SETTINGS.strategy;
}
function normalizeEmbeddingSource(value: unknown): MemorySettings["embeddingSource"] {
return value === "remote" || value === "static" || value === "transformers" || value === "auto"
? value
: DEFAULT_MEMORY_SETTINGS.embeddingSource;
}
function normalizeVectorStore(value: unknown): MemorySettings["vectorStore"] {
return value === "sqlite-vec" || value === "qdrant" || value === "auto"
? value
: DEFAULT_MEMORY_SETTINGS.vectorStore;
}
function normalizeNullableString(value: unknown, fallback: string | null): string | null {
if (value === null || value === undefined) return fallback;
return typeof value === "string" && value.length > 0 ? value : fallback;
}
export function normalizeMemorySettings(rawSettings: Record<string, unknown> = {}): MemorySettings {
return {
enabled: toBoolean(rawSettings.memoryEnabled, DEFAULT_MEMORY_SETTINGS.enabled),
maxTokens: clampInteger(
rawSettings.memoryMaxTokens,
DEFAULT_MEMORY_SETTINGS.maxTokens,
0,
16000
),
retentionDays: clampInteger(
rawSettings.memoryRetentionDays,
DEFAULT_MEMORY_SETTINGS.retentionDays,
1,
365
),
strategy: normalizeStrategy(rawSettings.memoryStrategy),
skillsEnabled: toBoolean(rawSettings.skillsEnabled, DEFAULT_MEMORY_SETTINGS.skillsEnabled),
// Plan 21 β D9 new fields
embeddingSource: normalizeEmbeddingSource(rawSettings.memoryEmbeddingSource),
embeddingProviderModel: normalizeNullableString(
rawSettings.memoryEmbeddingProviderModel,
DEFAULT_MEMORY_SETTINGS.embeddingProviderModel
),
transformersEnabled: toBoolean(
rawSettings.memoryTransformersEnabled,
DEFAULT_MEMORY_SETTINGS.transformersEnabled
),
staticEnabled: toBoolean(rawSettings.memoryStaticEnabled, DEFAULT_MEMORY_SETTINGS.staticEnabled),
rerankEnabled: toBoolean(rawSettings.memoryRerankEnabled, DEFAULT_MEMORY_SETTINGS.rerankEnabled),
rerankProviderModel: normalizeNullableString(
rawSettings.memoryRerankProviderModel,
DEFAULT_MEMORY_SETTINGS.rerankProviderModel
),
vectorStore: normalizeVectorStore(rawSettings.memoryVectorStore),
};
}
export function toMemorySettingsUpdates(
settings: Partial<MemorySettings>
): Record<string, unknown> {
const updates: Record<string, unknown> = {};
if (settings.enabled !== undefined) updates.memoryEnabled = settings.enabled;
if (settings.maxTokens !== undefined) updates.memoryMaxTokens = settings.maxTokens;
if (settings.retentionDays !== undefined) updates.memoryRetentionDays = settings.retentionDays;
if (settings.strategy !== undefined) updates.memoryStrategy = settings.strategy;
if (settings.skillsEnabled !== undefined) updates.skillsEnabled = settings.skillsEnabled;
// Plan 21 β D9 new fields
if (settings.embeddingSource !== undefined)
updates.memoryEmbeddingSource = settings.embeddingSource;
if (settings.embeddingProviderModel !== undefined)
updates.memoryEmbeddingProviderModel = settings.embeddingProviderModel;
if (settings.transformersEnabled !== undefined)
updates.memoryTransformersEnabled = settings.transformersEnabled;
if (settings.staticEnabled !== undefined) updates.memoryStaticEnabled = settings.staticEnabled;
if (settings.rerankEnabled !== undefined) updates.memoryRerankEnabled = settings.rerankEnabled;
if (settings.rerankProviderModel !== undefined)
updates.memoryRerankProviderModel = settings.rerankProviderModel;
if (settings.vectorStore !== undefined) updates.memoryVectorStore = settings.vectorStore;
return updates;
}
export function toMemoryRetrievalConfig(
settings: MemorySettings,
extra: { query?: string } = {}
): Partial<MemoryConfig> & { query?: string } {
const enabled = settings.enabled && settings.maxTokens > 0;
const config: Partial<MemoryConfig> & { query?: string } = {
enabled,
maxTokens: enabled ? settings.maxTokens : 0,
retrievalStrategy: settings.strategy === "recent" ? "exact" : settings.strategy,
autoSummarize: false,
persistAcrossModels: false,
retentionDays: settings.retentionDays,
scope: "apiKey",
};
// Plan 21 FAIL #1 fix: forward the last user message as `query` so that
// semantic / hybrid strategies actually exercise the vector store in the
// chat hot path (chatCore.ts), not only in the Playground.
//
// BUT only for query-driven strategies. The "recent" strategy (mapped to the
// internal "exact" path) is recency-based: it must return the most recent
// memories regardless of the current prompt. Forwarding a query there makes
// retrieveMemories apply relevance filtering (score > 0), which silently
// drops recent memories whose text doesn't overlap the prompt β breaking the
// "recent" contract. So skip query forwarding when strategy === "recent".
if (extra.query && extra.query.trim().length > 0 && settings.strategy !== "recent") {
config.query = extra.query.trim();
}
return config;
}
export async function getMemorySettings(): Promise<MemorySettings> {
if (cachedMemorySettings !== null) {
return cachedMemorySettings;
}
const settings = (await getSettings()) as Record<string, unknown>;
cachedMemorySettings = normalizeMemorySettings(settings);
return cachedMemorySettings;
}
export function invalidateMemorySettingsCache(): void {
cachedMemorySettings = null;
}
|