Skip to content

Commit 487f19e

Browse files
committed
Enhanced reflection and Decay
- Updated ingestion tests to include API key authorization for requests. - Improved logging for test results and added error handling. - Introduced model configuration loading from a YAML file, with defaults for various sectors. - Implemented reflection logic to cluster memories and create reflective summaries. - Added decay and reflection testing to validate memory salience and consolidation. - Created a new YAML configuration file for embedding models.
1 parent e797010 commit 487f19e

15 files changed

Lines changed: 1086 additions & 409 deletions

File tree

.env.example

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ OM_WEAVIATE_CLASS=OpenMemory
5757
# Embeddings Configuration
5858
# --------------------------------------------
5959
# Available providers: openai, gemini, ollama, local, synthetic
60+
# Embedding models per sector can be configured in models.yaml
6061
OM_EMBEDDINGS=openai
6162
OM_VEC_DIM=1536
6263

@@ -104,39 +105,25 @@ OM_DECAY_LAMBDA=0.02
104105
# Brain Sector Configuration (auto-classified, but you can override)
105106
# Sectors: episodic, semantic, procedural, emotional, reflective
106107

108+
# Auto-Reflection System
109+
# Automatically creates reflective memories by clustering similar memories
110+
OM_AUTO_REFLECT=false
111+
# Reflection interval in minutes (default: 10)
112+
OM_REFLECT_INTERVAL=10
113+
# Minimum memories required before reflection runs (default: 20)
114+
OM_REFLECT_MIN_MEMORIES=20
115+
116+
# Compression
117+
# Enable automatic content compression for large memories
118+
OM_COMPRESSION_ENABLED=false
119+
# Minimum content length (characters) to trigger compression (default: 100)
120+
OM_COMPRESSION_MIN_LENGTH=100
121+
# Compression algorithm: semantic, syntactic, aggressive, auto (default: auto)
122+
OM_COMPRESSION_ALGORITHM=auto
123+
107124
# --------------------------------------------
108125
# LangGraph Integration Mode (LGM)
109126
# --------------------------------------------
110127
OM_LG_NAMESPACE=default
111128
OM_LG_MAX_CONTEXT=50
112-
OM_LG_REFLECTIVE=true
113-
114-
# ============================================
115-
# Frontend Dashboard Settings
116-
# ============================================
117-
118-
# Dashboard API URL (connects to backend)
119-
VITE_DASHBOARD_API=http://localhost:3001/api
120-
VITE_BACKEND_API=http://localhost:8080
121-
122-
# Dashboard Server Port
123-
DASHBOARD_PORT=3001
124-
125-
# --------------------------------------------
126-
# Authentication Settings
127-
# --------------------------------------------
128-
# Set to 'true' to enable authentication, 'false' to disable
129-
VITE_AUTH_ENABLED=false
130-
131-
# Better Auth Configuration (only needed if auth is enabled)
132-
# Generate a secure random string for BETTER_AUTH_SECRET
133-
# You can use: openssl rand -base64 32
134-
BETTER_AUTH_SECRET=your-secret-key-here
135-
BETTER_AUTH_URL=http://localhost:3001
136-
137-
# --------------------------------------------
138-
# Real-time Updates
139-
# --------------------------------------------
140-
# Auto-refresh interval in milliseconds (default: 5000ms = 5 seconds)
141-
VITE_AUTO_REFRESH_INTERVAL=5000
142-
129+
OM_LG_REFLECTIVE=true

backend/src/config/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ export const env = {
3838
metadata_backend: str(process.env.OM_METADATA_BACKEND, 'sqlite').toLowerCase(),
3939
vector_backend: str(process.env.OM_VECTOR_BACKEND, 'sqlite').toLowerCase(),
4040
ide_mode: bool(process.env.OM_IDE_MODE),
41-
ide_allowed_origins: str(process.env.OM_IDE_ALLOWED_ORIGINS, 'http://localhost:5173,http://localhost:3000').split(',')
41+
ide_allowed_origins: str(process.env.OM_IDE_ALLOWED_ORIGINS, 'http://localhost:5173,http://localhost:3000').split(','),
42+
auto_reflect: bool(process.env.OM_AUTO_REFLECT),
43+
reflect_interval: num(process.env.OM_REFLECT_INTERVAL, 10),
44+
reflect_min: num(process.env.OM_REFLECT_MIN_MEMORIES, 20)
4245
}
4346

backend/src/config/models.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { readFileSync, existsSync } from 'fs'
2+
import { join } from 'path'
3+
4+
interface ModelConfig {
5+
[sector: string]: Record<string, string>
6+
}
7+
8+
let cfg: ModelConfig | null = null
9+
10+
export const loadModels = (): ModelConfig => {
11+
if (cfg) return cfg
12+
13+
const p = join(__dirname, '../../../models.yml')
14+
if (!existsSync(p)) {
15+
console.warn('⚠️ models.yml not found, using defaults')
16+
return getDefaults()
17+
}
18+
19+
try {
20+
const yml = readFileSync(p, 'utf-8')
21+
cfg = parseYaml(yml)
22+
console.log(`📋 Loaded models.yml (${Object.keys(cfg).length} sectors)`)
23+
return cfg
24+
} catch (e) {
25+
console.error('❌ Failed to parse models.yml:', e)
26+
return getDefaults()
27+
}
28+
}
29+
30+
const parseYaml = (yml: string): ModelConfig => {
31+
const lines = yml.split('\n')
32+
const obj: ModelConfig = {}
33+
let currentSector: string | null = null
34+
35+
for (const line of lines) {
36+
const trimmed = line.trim()
37+
if (!trimmed || trimmed.startsWith('#')) continue
38+
39+
const indent = line.search(/\S/)
40+
const [key, ...valParts] = trimmed.split(':')
41+
const val = valParts.join(':').trim()
42+
43+
if (indent === 0 && val) {
44+
// Top-level key with value (shouldn't happen in our format)
45+
continue
46+
} else if (indent === 0) {
47+
// Sector name
48+
currentSector = key
49+
obj[currentSector] = {}
50+
} else if (currentSector && val) {
51+
// Provider: model mapping
52+
obj[currentSector][key] = val
53+
}
54+
}
55+
56+
return obj
57+
}
58+
59+
const getDefaults = (): ModelConfig => ({
60+
episodic: { ollama: 'nomic-embed-text', openai: 'text-embedding-3-small', gemini: 'models/embedding-001', local: 'all-MiniLM-L6-v2' },
61+
semantic: { ollama: 'nomic-embed-text', openai: 'text-embedding-3-small', gemini: 'models/embedding-001', local: 'all-MiniLM-L6-v2' },
62+
procedural: { ollama: 'nomic-embed-text', openai: 'text-embedding-3-small', gemini: 'models/embedding-001', local: 'all-MiniLM-L6-v2' },
63+
emotional: { ollama: 'nomic-embed-text', openai: 'text-embedding-3-small', gemini: 'models/embedding-001', local: 'all-MiniLM-L6-v2' },
64+
reflective: { ollama: 'nomic-embed-text', openai: 'text-embedding-3-large', gemini: 'models/embedding-001', local: 'all-mpnet-base-v2' }
65+
})
66+
67+
export const getModel = (sector: string, provider: string): string => {
68+
const cfg = loadModels()
69+
return cfg[sector]?.[provider] || cfg.semantic?.[provider] || 'nomic-embed-text'
70+
}
71+
72+
export const getProviderConfig = (provider: string): any => {
73+
return {}
74+
}

backend/src/decay/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
} from '../memory-dynamics'
1010

1111
export const apply_decay = async () => {
12+
console.log('[Decay] Starting decay job...')
1213
const all_memory_records_from_database = await allAsync('select id,salience,decay_lambda,last_seen_at,updated_at from memories')
14+
console.log(`[Decay] Fetched ${all_memory_records_from_database.length} memories`)
15+
1316
const current_timestamp_in_milliseconds = now()
1417
const individual_memory_salience_updates = all_memory_records_from_database.map(async (memory_database_row: any) => {
1518
const time_difference_since_last_seen = Math.max(0, (current_timestamp_in_milliseconds - (memory_database_row.last_seen_at || memory_database_row.updated_at)) / 86400000)
@@ -24,10 +27,15 @@ export const apply_decay = async () => {
2427
const blended_decay_value = (combined_dual_phase_retention * 0.7) + (original_sector_decay * 0.3)
2528

2629
const final_updated_salience = Math.max(0, memory_database_row.salience * blended_decay_value)
27-
return { id: memory_database_row.id, salience: final_updated_salience }
30+
return { id: memory_database_row.id, salience: final_updated_salience, old: memory_database_row.salience }
2831
})
29-
await Promise.all((await Promise.all(individual_memory_salience_updates)).map(update_operation =>
32+
33+
const updates = await Promise.all(individual_memory_salience_updates)
34+
const changed = updates.filter(u => Math.abs(u.salience - u.old) > 0.001).length
35+
36+
await Promise.all(updates.map(update_operation =>
3037
runAsync('update memories set salience=?, updated_at=? where id=?', [update_operation.salience, current_timestamp_in_milliseconds, update_operation.id])
3138
))
32-
console.log(`Applied dual-phase decay to ${all_memory_records_from_database.length} memories`)
39+
40+
console.log(`[Decay] Applied dual-phase decay to ${all_memory_records_from_database.length} memories (${changed} changed)`)
3341
}

backend/src/embedding/index.ts

Lines changed: 23 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { createHash } from 'crypto';
2-
import { env } from '../config';
3-
import { q } from '../database';
4-
import { SECTOR_CONFIGS } from '../hsg';
5-
import { addSynonymTokens, canonicalTokensFromText } from '../utils/text';
1+
import { env } from '../config'
2+
import { getModel } from '../config/models'
3+
import { SECTOR_CONFIGS } from '../hsg'
4+
import { q } from '../database'
5+
import { canonicalTokensFromText, addSynonymTokens } from '../utils/text'
66

77
let geminiQueue: Promise<any> = Promise.resolve()
88

@@ -19,20 +19,13 @@ export async function embedForSector(t: string, s: string): Promise<number[]> {
1919
}
2020
}
2121

22-
const MOD: Record<string, string> = {
23-
episodic: 'text-embedding-3-small',
24-
semantic: 'text-embedding-3-small',
25-
procedural: 'text-embedding-3-small',
26-
emotional: 'text-embedding-3-small',
27-
reflective: 'text-embedding-3-large'
28-
}
29-
3022
async function embedWithOpenAI(t: string, s: string): Promise<number[]> {
3123
if (!env.openai_key) throw new Error('OpenAI key missing')
24+
const model = getModel(s, 'openai')
3225
const r = await fetch(`${env.openai_base_url.replace(/\/$/, '')}/embeddings`, {
3326
method: 'POST',
3427
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${env.openai_key}` },
35-
body: JSON.stringify({ input: t, model: env.openai_model || MOD[s] || MOD.semantic, dimensions: env.vec_dim })
28+
body: JSON.stringify({ input: t, model: env.openai_model || model, dimensions: env.vec_dim })
3629
})
3730
if (!r.ok) throw new Error(`OpenAI: ${r.status}`)
3831
return ((await r.json()) as any).data[0].embedding
@@ -41,10 +34,11 @@ async function embedWithOpenAI(t: string, s: string): Promise<number[]> {
4134
async function embedBatchOpenAI(texts: Record<string, string>): Promise<Record<string, number[]>> {
4235
if (!env.openai_key) throw new Error('OpenAI key missing')
4336
const sectors = Object.keys(texts)
37+
const model = getModel('semantic', 'openai')
4438
const r = await fetch(`${env.openai_base_url.replace(/\/$/, '')}/embeddings`, {
4539
method: 'POST',
4640
headers: { 'content-type': 'application/json', 'authorization': `Bearer ${env.openai_key}` },
47-
body: JSON.stringify({ input: Object.values(texts), model: env.openai_model || MOD.semantic, dimensions: env.vec_dim })
41+
body: JSON.stringify({ input: Object.values(texts), model: env.openai_model || model, dimensions: env.vec_dim })
4842
})
4943
if (!r.ok) throw new Error(`OpenAI batch: ${r.status}`)
5044
const d = (await r.json()) as any
@@ -107,19 +101,12 @@ async function embedWithGemini(texts: Record<string, string>): Promise<Record<st
107101
return promise
108102
}
109103

110-
const OMOD: Record<string, string> = {
111-
episodic: 'nomic-embed-text',
112-
semantic: 'nomic-embed-text',
113-
procedural: 'bge-small',
114-
emotional: 'nomic-embed-text',
115-
reflective: 'bge-large'
116-
}
117-
118104
async function embedWithOllama(t: string, s: string): Promise<number[]> {
105+
const model = getModel(s, 'ollama')
119106
const r = await fetch(`${env.ollama_url}/api/embeddings`, {
120107
method: 'POST',
121108
headers: { 'content-type': 'application/json' },
122-
body: JSON.stringify({ model: OMOD[s] || OMOD.semantic, prompt: t })
109+
body: JSON.stringify({ model, prompt: t })
123110
})
124111
if (!r.ok) throw new Error(`Ollama: ${r.status}`)
125112
return resizeVector(((await r.json()) as any).embedding, env.vec_dim)
@@ -131,38 +118,21 @@ async function embedWithLocal(t: string, s: string): Promise<number[]> {
131118
return generateSyntheticEmbedding(t, s)
132119
}
133120
try {
134-
const hash = createHash('sha256').update(t, 'utf8').update(s, 'utf8').digest();
135-
const dim = env.vec_dim;
136-
const e = new Array<number>(dim);
137-
const HLEN = 32;
138-
139-
let i = 0;
140-
for (let idx = 0; idx < dim; idx++) {
141-
const b1 = hash[i];
142-
i = (i + 1) % HLEN;
143-
const b2 = hash[i];
144-
e[idx] = (b1 * 256 + b2) / 65535 * 2 - 1;
145-
}
146-
147-
let sumSquares = 0;
148-
for (let idx = 0; idx < dim; idx++) {
149-
const v = e[idx];
150-
sumSquares += v * v;
151-
}
152-
const norm = Math.sqrt(sumSquares);
153-
154-
for (let idx = 0; idx < dim; idx++) {
155-
e[idx] /= norm;
121+
const { createHash } = await import('crypto')
122+
const h = createHash('sha256').update(t + s).digest()
123+
const e: number[] = []
124+
for (let i = 0; i < env.vec_dim; i++) {
125+
const b1 = h[i % h.length]
126+
const b2 = h[(i + 1) % h.length]
127+
e.push((b1 * 256 + b2) / 65535 * 2 - 1)
156128
}
157-
158-
return e;
129+
const n = Math.sqrt(e.reduce((sum, v) => sum + v * v, 0))
130+
return e.map(v => v / n)
159131
} catch {
160132
console.warn('Local embedding failed, using synthetic')
161133
return generateSyntheticEmbedding(t, s)
162134
}
163-
}
164-
165-
const hash = (v: string) => {
135+
} const hash = (v: string) => {
166136
let h = 0x811c9dc5 | 0;
167137
const len = v.length | 0;
168138
for (let i = 0; i < len; i++) {
@@ -329,15 +299,15 @@ export const getEmbeddingInfo = () => {
329299
i.base_url = env.openai_base_url
330300
i.model_override = env.openai_model || null
331301
i.batch_api = env.embed_mode === 'simple'
332-
i.models = MOD
302+
i.models = { episodic: getModel('episodic', 'openai'), semantic: getModel('semantic', 'openai'), procedural: getModel('procedural', 'openai'), emotional: getModel('emotional', 'openai'), reflective: getModel('reflective', 'openai') }
333303
} else if (env.emb_kind === 'gemini') {
334304
i.configured = !!env.gemini_key
335305
i.batch_api = env.embed_mode === 'simple'
336306
i.model = 'embedding-001'
337307
} else if (env.emb_kind === 'ollama') {
338308
i.configured = true
339309
i.url = env.ollama_url
340-
i.models = OMOD
310+
i.models = { episodic: getModel('episodic', 'ollama'), semantic: getModel('semantic', 'ollama'), procedural: getModel('procedural', 'ollama'), emotional: getModel('emotional', 'ollama'), reflective: getModel('reflective', 'ollama') }
341311
} else if (env.emb_kind === 'local') {
342312
i.configured = !!env.local_model_path
343313
i.path = env.local_model_path

backend/src/hsg/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,15 @@ export async function createSingleWaypoint(
228228
const existingMean = bufferToVector(mem.mean_vec)
229229
const similarity = cosineSimilarity(newMeanVector, existingMean)
230230

231-
if (similarity >= threshold && (!bestMatch || similarity > bestMatch.similarity)) {
231+
if (!bestMatch || similarity > bestMatch.similarity) {
232232
bestMatch = { id: mem.id, similarity }
233233
}
234234
}
235235

236236
if (bestMatch) {
237237
await q.ins_waypoint.run(newId, bestMatch.id, bestMatch.similarity, timestamp, timestamp)
238+
} else {
239+
await q.ins_waypoint.run(newId, newId, 1.0, timestamp, timestamp)
238240
}
239241
}
240242

0 commit comments

Comments
 (0)