@1mbrain/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +42 -0
- package/src/config.ts +50 -0
- package/src/db/index.ts +38 -0
- package/src/db/postgres-provider.ts +525 -0
- package/src/db/sqlite-provider.ts +548 -0
- package/src/embedding/index.ts +56 -0
- package/src/embedding/keyword-provider.ts +71 -0
- package/src/embedding/ollama-provider.ts +78 -0
- package/src/embedding/openai-provider.ts +99 -0
- package/src/engine.ts +1073 -0
- package/src/events.ts +142 -0
- package/src/index.ts +83 -0
- package/src/logger.ts +31 -0
- package/src/passport.ts +118 -0
- package/src/ranking-policy.ts +563 -0
- package/src/schemas.ts +114 -0
- package/src/types.ts +229 -0
- package/tests/benchmark.ts +125 -0
- package/tests/embedding.test.ts +119 -0
- package/tests/engine.test.ts +1017 -0
- package/tests/passport.test.ts +83 -0
- package/tests/ranking-policy.test.ts +268 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1mbrain/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core memory engine, data models, embedding adapters, and association graph for 1MBrain.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"db:migrate": "tsx src/db/migrate.ts",
|
|
21
|
+
"db:seed": "tsx src/db/seed.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"better-sqlite3": "^11.6.0",
|
|
25
|
+
"drizzle-orm": "^0.38.0",
|
|
26
|
+
"ioredis": "^5.4.0",
|
|
27
|
+
"pg": "^8.13.0",
|
|
28
|
+
"pino": "^9.5.0",
|
|
29
|
+
"uuid": "^11.0.0",
|
|
30
|
+
"zod": "^3.24.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
34
|
+
"@types/pg": "^8.11.0",
|
|
35
|
+
"@types/uuid": "^10.0.0",
|
|
36
|
+
"drizzle-kit": "^0.30.0",
|
|
37
|
+
"pino-pretty": "^13.1.3",
|
|
38
|
+
"tsx": "^4.19.0",
|
|
39
|
+
"typescript": "^5.7.0",
|
|
40
|
+
"vitest": "^2.1.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Reads environment variables and constructs a typed OneMBrainConfig.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { OneMBrainConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
export function loadConfig(): OneMBrainConfig {
|
|
10
|
+
return {
|
|
11
|
+
database: {
|
|
12
|
+
provider: (process.env.DB_PROVIDER as 'sqlite' | 'postgres') || 'sqlite',
|
|
13
|
+
sqlitePath: process.env.SQLITE_PATH || './data/1mbrain.db',
|
|
14
|
+
postgresUrl: process.env.DATABASE_URL,
|
|
15
|
+
},
|
|
16
|
+
embedding: {
|
|
17
|
+
provider: (process.env.EMBEDDING_PROVIDER as 'openai' | 'ollama' | 'claude' | 'local-keyword') ||
|
|
18
|
+
(process.env.OPENAI_API_KEY ? 'openai' : 'local-keyword'),
|
|
19
|
+
openai: process.env.OPENAI_API_KEY
|
|
20
|
+
? {
|
|
21
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
22
|
+
model: process.env.OPENAI_EMBEDDING_MODEL || 'text-embedding-3-small',
|
|
23
|
+
}
|
|
24
|
+
: undefined,
|
|
25
|
+
ollama: {
|
|
26
|
+
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
|
27
|
+
model: process.env.OLLAMA_EMBEDDING_MODEL || 'nomic-embed-text',
|
|
28
|
+
},
|
|
29
|
+
claude: process.env.ANTHROPIC_API_KEY
|
|
30
|
+
? {
|
|
31
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
32
|
+
model: process.env.ANTHROPIC_EMBEDDING_MODEL || '',
|
|
33
|
+
}
|
|
34
|
+
: undefined,
|
|
35
|
+
localKeyword: {
|
|
36
|
+
dimensions: process.env.LOCAL_KEYWORD_EMBEDDING_DIMENSIONS
|
|
37
|
+
? parseInt(process.env.LOCAL_KEYWORD_EMBEDDING_DIMENSIONS, 10)
|
|
38
|
+
: undefined,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
redis: process.env.REDIS_URL
|
|
42
|
+
? { url: process.env.REDIS_URL }
|
|
43
|
+
: undefined,
|
|
44
|
+
decay: {
|
|
45
|
+
rate: parseFloat(process.env.DECAY_RATE || '0.01'),
|
|
46
|
+
intervalMs: parseInt(process.env.DECAY_INTERVAL_MS || '3600000', 10),
|
|
47
|
+
minScore: parseFloat(process.env.DECAY_MIN_SCORE || '0.01'),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates the appropriate database provider based on configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DatabaseProvider, OneMBrainConfig } from '../types.js';
|
|
8
|
+
import { SqliteDatabaseProvider } from './sqlite-provider.js';
|
|
9
|
+
import { PostgresDatabaseProvider } from './postgres-provider.js';
|
|
10
|
+
import { createChildLogger } from '../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = createChildLogger('db-factory');
|
|
13
|
+
|
|
14
|
+
export function createDatabaseProvider(config: OneMBrainConfig['database']): DatabaseProvider {
|
|
15
|
+
switch (config.provider) {
|
|
16
|
+
case 'sqlite': {
|
|
17
|
+
if (!config.sqlitePath) {
|
|
18
|
+
throw new Error('sqlitePath is required for SQLite provider');
|
|
19
|
+
}
|
|
20
|
+
log.info({ provider: 'sqlite', path: config.sqlitePath }, 'Creating SQLite provider');
|
|
21
|
+
return new SqliteDatabaseProvider(config.sqlitePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
case 'postgres': {
|
|
25
|
+
if (!config.postgresUrl) {
|
|
26
|
+
throw new Error('postgresUrl is required for PostgreSQL provider');
|
|
27
|
+
}
|
|
28
|
+
log.info({ provider: 'postgres' }, 'Creating PostgreSQL provider');
|
|
29
|
+
return new PostgresDatabaseProvider(config.postgresUrl);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unknown database provider: ${config.provider}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { SqliteDatabaseProvider } from './sqlite-provider.js';
|
|
38
|
+
export { PostgresDatabaseProvider } from './postgres-provider.js';
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Database Provider
|
|
3
|
+
*
|
|
4
|
+
* Implements the DatabaseProvider interface using pg + pgvector.
|
|
5
|
+
* Uses pgvector's native cosine distance operator for vector search,
|
|
6
|
+
* which is significantly faster than JS-based similarity at scale.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Pool as PgPool, PoolClient as PgPoolClient } from 'pg';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
import type {
|
|
12
|
+
DatabaseProvider,
|
|
13
|
+
Memory,
|
|
14
|
+
MemoryType,
|
|
15
|
+
Association,
|
|
16
|
+
AssociationOrigin,
|
|
17
|
+
AssociationRelationType,
|
|
18
|
+
} from '../types.js';
|
|
19
|
+
import { createChildLogger } from '../logger.js';
|
|
20
|
+
|
|
21
|
+
const log = createChildLogger('postgres-provider');
|
|
22
|
+
|
|
23
|
+
export class PostgresDatabaseProvider implements DatabaseProvider {
|
|
24
|
+
private pool!: PgPool;
|
|
25
|
+
private readonly connectionString: string;
|
|
26
|
+
|
|
27
|
+
constructor(connectionString: string) {
|
|
28
|
+
this.connectionString = connectionString;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async initialize(): Promise<void> {
|
|
32
|
+
log.info('Initializing PostgreSQL database');
|
|
33
|
+
|
|
34
|
+
const pgPkg = await import('pg');
|
|
35
|
+
const Pool = pgPkg.default?.Pool || (pgPkg as any).Pool;
|
|
36
|
+
|
|
37
|
+
this.pool = new Pool({
|
|
38
|
+
connectionString: this.connectionString,
|
|
39
|
+
max: 20,
|
|
40
|
+
idleTimeoutMillis: 30000,
|
|
41
|
+
connectionTimeoutMillis: 5000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Test connection
|
|
45
|
+
const client = await this.pool.connect();
|
|
46
|
+
try {
|
|
47
|
+
await this.createTables(client);
|
|
48
|
+
log.info('PostgreSQL database initialized');
|
|
49
|
+
} finally {
|
|
50
|
+
client.release();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async createTables(client: PgPoolClient): Promise<void> {
|
|
55
|
+
// Enable pgvector extension
|
|
56
|
+
await client.query('CREATE EXTENSION IF NOT EXISTS vector');
|
|
57
|
+
await client.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
|
|
58
|
+
|
|
59
|
+
await client.query(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
61
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
62
|
+
agent_id VARCHAR(128) NOT NULL,
|
|
63
|
+
type VARCHAR(20) NOT NULL CHECK(type IN ('episodic', 'semantic', 'procedural', 'entity', 'warning')),
|
|
64
|
+
content TEXT NOT NULL,
|
|
65
|
+
embedding_model VARCHAR(128),
|
|
66
|
+
embedding vector(1536),
|
|
67
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
68
|
+
decay_score REAL NOT NULL DEFAULT 1.0,
|
|
69
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
70
|
+
last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
71
|
+
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
72
|
+
metadata JSONB
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent_id ON memories(agent_id);
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(agent_id, type);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_memories_decay ON memories(decay_score);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(agent_id, created_at DESC);
|
|
79
|
+
`);
|
|
80
|
+
|
|
81
|
+
// Create HNSW index for vector search (if not exists)
|
|
82
|
+
await client.query(`
|
|
83
|
+
DO $$
|
|
84
|
+
BEGIN
|
|
85
|
+
IF NOT EXISTS (
|
|
86
|
+
SELECT 1 FROM pg_indexes WHERE indexname = 'idx_memories_embedding'
|
|
87
|
+
) THEN
|
|
88
|
+
CREATE INDEX idx_memories_embedding ON memories
|
|
89
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
90
|
+
WITH (m = 16, ef_construction = 64);
|
|
91
|
+
END IF;
|
|
92
|
+
END $$;
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
await client.query(`
|
|
96
|
+
CREATE TABLE IF NOT EXISTS associations (
|
|
97
|
+
source_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
98
|
+
target_id UUID NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
99
|
+
strength REAL NOT NULL DEFAULT 0.5,
|
|
100
|
+
origin VARCHAR(20) NOT NULL CHECK(origin IN ('co-occurrence', 'similarity', 'explicit')),
|
|
101
|
+
relation_type VARCHAR(20) NOT NULL DEFAULT 'relates_to',
|
|
102
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
103
|
+
PRIMARY KEY (source_id, target_id)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_associations_source ON associations(source_id);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_associations_target ON associations(target_id);
|
|
108
|
+
`);
|
|
109
|
+
|
|
110
|
+
await client.query(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
112
|
+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
113
|
+
key_hash VARCHAR(128) NOT NULL UNIQUE,
|
|
114
|
+
agent_id VARCHAR(128) NOT NULL,
|
|
115
|
+
name VARCHAR(256) NOT NULL,
|
|
116
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
117
|
+
last_used_at TIMESTAMPTZ,
|
|
118
|
+
is_active BOOLEAN NOT NULL DEFAULT true
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_agent ON api_keys(agent_id);
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── Memory CRUD ──────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
async createMemory(memory: Omit<Memory, 'createdAt' | 'lastAccessedAt'>): Promise<Memory> {
|
|
129
|
+
const id = memory.id || uuidv4();
|
|
130
|
+
|
|
131
|
+
const embeddingStr = memory.embedding ? `[${memory.embedding.join(',')}]` : null;
|
|
132
|
+
|
|
133
|
+
const result = await this.pool.query(
|
|
134
|
+
`INSERT INTO memories (id, agent_id, type, content, embedding_model, embedding, importance, decay_score, tags, metadata)
|
|
135
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector, $7, $8, $9, $10)
|
|
136
|
+
RETURNING *`,
|
|
137
|
+
[
|
|
138
|
+
id,
|
|
139
|
+
memory.agentId,
|
|
140
|
+
memory.type,
|
|
141
|
+
memory.content,
|
|
142
|
+
memory.embeddingModel,
|
|
143
|
+
embeddingStr,
|
|
144
|
+
memory.importance,
|
|
145
|
+
memory.decayScore,
|
|
146
|
+
memory.tags,
|
|
147
|
+
memory.metadata ? JSON.stringify(memory.metadata) : null,
|
|
148
|
+
],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
log.debug({ id, agentId: memory.agentId, type: memory.type }, 'Memory created');
|
|
152
|
+
return this.rowToMemory(result.rows[0]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async getMemoryById(id: string, agentId: string): Promise<Memory | null> {
|
|
156
|
+
const result = await this.pool.query(
|
|
157
|
+
`UPDATE memories SET last_accessed_at = NOW(), decay_score = LEAST(1.0, decay_score + 0.05)
|
|
158
|
+
WHERE id = $1 AND agent_id = $2
|
|
159
|
+
RETURNING *`,
|
|
160
|
+
[id, agentId],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
if (result.rows.length === 0) return null;
|
|
164
|
+
return this.rowToMemory(result.rows[0]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async updateMemory(
|
|
168
|
+
id: string,
|
|
169
|
+
agentId: string,
|
|
170
|
+
updates: Partial<Memory>,
|
|
171
|
+
): Promise<Memory | null> {
|
|
172
|
+
const setClauses: string[] = ['last_accessed_at = NOW()'];
|
|
173
|
+
const values: unknown[] = [];
|
|
174
|
+
let paramIndex = 1;
|
|
175
|
+
|
|
176
|
+
if (updates.content !== undefined) {
|
|
177
|
+
setClauses.push(`content = $${paramIndex++}`);
|
|
178
|
+
values.push(updates.content);
|
|
179
|
+
}
|
|
180
|
+
if (updates.type !== undefined) {
|
|
181
|
+
setClauses.push(`type = $${paramIndex++}`);
|
|
182
|
+
values.push(updates.type);
|
|
183
|
+
}
|
|
184
|
+
if (updates.importance !== undefined) {
|
|
185
|
+
setClauses.push(`importance = $${paramIndex++}`);
|
|
186
|
+
values.push(updates.importance);
|
|
187
|
+
}
|
|
188
|
+
if (updates.decayScore !== undefined) {
|
|
189
|
+
setClauses.push(`decay_score = $${paramIndex++}`);
|
|
190
|
+
values.push(updates.decayScore);
|
|
191
|
+
}
|
|
192
|
+
if (updates.tags !== undefined) {
|
|
193
|
+
setClauses.push(`tags = $${paramIndex++}`);
|
|
194
|
+
values.push(updates.tags);
|
|
195
|
+
}
|
|
196
|
+
if (updates.metadata !== undefined) {
|
|
197
|
+
setClauses.push(`metadata = $${paramIndex++}`);
|
|
198
|
+
values.push(JSON.stringify(updates.metadata));
|
|
199
|
+
}
|
|
200
|
+
if (updates.embedding !== undefined) {
|
|
201
|
+
const embStr = updates.embedding ? `[${updates.embedding.join(',')}]` : null;
|
|
202
|
+
setClauses.push(`embedding = $${paramIndex++}::vector`);
|
|
203
|
+
values.push(embStr);
|
|
204
|
+
setClauses.push(`embedding_model = $${paramIndex++}`);
|
|
205
|
+
values.push(updates.embeddingModel ?? null);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
values.push(id, agentId);
|
|
209
|
+
|
|
210
|
+
const result = await this.pool.query(
|
|
211
|
+
`UPDATE memories SET ${setClauses.join(', ')}
|
|
212
|
+
WHERE id = $${paramIndex++} AND agent_id = $${paramIndex}
|
|
213
|
+
RETURNING *`,
|
|
214
|
+
values,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (result.rows.length === 0) return null;
|
|
218
|
+
return this.rowToMemory(result.rows[0]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async deleteMemory(id: string, agentId: string): Promise<boolean> {
|
|
222
|
+
const result = await this.pool.query('DELETE FROM memories WHERE id = $1 AND agent_id = $2', [
|
|
223
|
+
id,
|
|
224
|
+
agentId,
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
log.debug({ id, agentId, deleted: (result.rowCount ?? 0) > 0 }, 'Memory deleted');
|
|
228
|
+
return (result.rowCount ?? 0) > 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Vector Search ────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
async searchByVector(
|
|
234
|
+
agentId: string,
|
|
235
|
+
embedding: number[],
|
|
236
|
+
options: {
|
|
237
|
+
limit?: number;
|
|
238
|
+
threshold?: number;
|
|
239
|
+
type?: MemoryType;
|
|
240
|
+
tags?: string[];
|
|
241
|
+
} = {},
|
|
242
|
+
): Promise<Array<{ memory: Memory; similarity: number }>> {
|
|
243
|
+
const { limit = 10, threshold = 0.3, type, tags } = options;
|
|
244
|
+
|
|
245
|
+
const embeddingStr = `[${embedding.join(',')}]`;
|
|
246
|
+
let paramIndex = 3;
|
|
247
|
+
const conditions = ['agent_id = $1', 'embedding IS NOT NULL'];
|
|
248
|
+
const params: unknown[] = [agentId, embeddingStr];
|
|
249
|
+
|
|
250
|
+
if (type) {
|
|
251
|
+
conditions.push(`type = $${paramIndex++}`);
|
|
252
|
+
params.push(type);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (tags?.length) {
|
|
256
|
+
conditions.push(`tags && $${paramIndex++}`);
|
|
257
|
+
params.push(tags);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
params.push(threshold, limit);
|
|
261
|
+
|
|
262
|
+
// Use pgvector's cosine distance operator (<=>)
|
|
263
|
+
// cosine_distance = 1 - cosine_similarity, so we compute similarity as 1 - distance
|
|
264
|
+
const result = await this.pool.query(
|
|
265
|
+
`SELECT *, 1 - (embedding <=> $2::vector) AS similarity
|
|
266
|
+
FROM memories
|
|
267
|
+
WHERE ${conditions.join(' AND ')}
|
|
268
|
+
AND 1 - (embedding <=> $2::vector) >= $${paramIndex++}
|
|
269
|
+
ORDER BY embedding <=> $2::vector ASC
|
|
270
|
+
LIMIT $${paramIndex}`,
|
|
271
|
+
params,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Update last_accessed_at for returned memories
|
|
275
|
+
if (result.rows.length > 0) {
|
|
276
|
+
const ids = result.rows.map((r: MemoryRow) => r.id);
|
|
277
|
+
await this.pool.query(
|
|
278
|
+
`UPDATE memories SET last_accessed_at = NOW(), decay_score = LEAST(1.0, decay_score + 0.02)
|
|
279
|
+
WHERE id = ANY($1)`,
|
|
280
|
+
[ids],
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return result.rows.map((row: MemoryRow & { similarity: number }) => ({
|
|
285
|
+
memory: this.rowToMemory(row),
|
|
286
|
+
similarity: row.similarity,
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Associations ─────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async createAssociation(association: Omit<Association, 'createdAt'>): Promise<Association> {
|
|
293
|
+
const result = await this.pool.query(
|
|
294
|
+
`INSERT INTO associations (source_id, target_id, strength, origin, relation_type)
|
|
295
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
296
|
+
ON CONFLICT (source_id, target_id) DO UPDATE SET
|
|
297
|
+
strength = GREATEST(associations.strength, EXCLUDED.strength),
|
|
298
|
+
origin = EXCLUDED.origin,
|
|
299
|
+
relation_type = EXCLUDED.relation_type
|
|
300
|
+
RETURNING *`,
|
|
301
|
+
[association.sourceId, association.targetId, association.strength, association.origin, association.relationType ?? 'relates_to'],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
log.debug(
|
|
305
|
+
{ sourceId: association.sourceId, targetId: association.targetId },
|
|
306
|
+
'Association created/updated',
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
return this.rowToAssociation(result.rows[0]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getAssociations(memoryId: string): Promise<Association[]> {
|
|
313
|
+
const result = await this.pool.query(
|
|
314
|
+
'SELECT * FROM associations WHERE source_id = $1 OR target_id = $1',
|
|
315
|
+
[memoryId],
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return result.rows.map(this.rowToAssociation);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async deleteAssociations(memoryId: string): Promise<number> {
|
|
322
|
+
const result = await this.pool.query(
|
|
323
|
+
'DELETE FROM associations WHERE source_id = $1 OR target_id = $1',
|
|
324
|
+
[memoryId],
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return result.rowCount ?? 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ─── Bulk Operations ──────────────────────────────────
|
|
331
|
+
|
|
332
|
+
async listAgentIds(): Promise<string[]> {
|
|
333
|
+
const result = await this.pool.query(
|
|
334
|
+
'SELECT DISTINCT agent_id FROM memories ORDER BY agent_id ASC',
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return result.rows.map((row: { agent_id: string }) => row.agent_id);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async getAllMemories(agentId: string): Promise<Memory[]> {
|
|
341
|
+
const result = await this.pool.query(
|
|
342
|
+
'SELECT * FROM memories WHERE agent_id = $1 ORDER BY created_at DESC',
|
|
343
|
+
[agentId],
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return result.rows.map(this.rowToMemory);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async getAllAssociations(agentId: string): Promise<Association[]> {
|
|
350
|
+
const result = await this.pool.query(
|
|
351
|
+
`SELECT a.* FROM associations a
|
|
352
|
+
JOIN memories m ON a.source_id = m.id
|
|
353
|
+
WHERE m.agent_id = $1`,
|
|
354
|
+
[agentId],
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return result.rows.map(this.rowToAssociation);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async bulkCreateMemories(
|
|
361
|
+
memories: Array<Omit<Memory, 'createdAt' | 'lastAccessedAt'>>,
|
|
362
|
+
): Promise<Memory[]> {
|
|
363
|
+
const client = await this.pool.connect();
|
|
364
|
+
try {
|
|
365
|
+
await client.query('BEGIN');
|
|
366
|
+
|
|
367
|
+
const results: Memory[] = [];
|
|
368
|
+
for (const m of memories) {
|
|
369
|
+
const embStr = m.embedding ? `[${m.embedding.join(',')}]` : null;
|
|
370
|
+
|
|
371
|
+
const result = await client.query(
|
|
372
|
+
`INSERT INTO memories (id, agent_id, type, content, embedding_model, embedding, importance, decay_score, tags, metadata)
|
|
373
|
+
VALUES ($1, $2, $3, $4, $5, $6::vector, $7, $8, $9, $10)
|
|
374
|
+
RETURNING *`,
|
|
375
|
+
[
|
|
376
|
+
m.id || uuidv4(),
|
|
377
|
+
m.agentId,
|
|
378
|
+
m.type,
|
|
379
|
+
m.content,
|
|
380
|
+
m.embeddingModel,
|
|
381
|
+
embStr,
|
|
382
|
+
m.importance,
|
|
383
|
+
m.decayScore,
|
|
384
|
+
m.tags,
|
|
385
|
+
m.metadata ? JSON.stringify(m.metadata) : null,
|
|
386
|
+
],
|
|
387
|
+
);
|
|
388
|
+
results.push(this.rowToMemory(result.rows[0]));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await client.query('COMMIT');
|
|
392
|
+
return results;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
await client.query('ROLLBACK');
|
|
395
|
+
throw err;
|
|
396
|
+
} finally {
|
|
397
|
+
client.release();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async bulkCreateAssociations(
|
|
402
|
+
associations: Array<Omit<Association, 'createdAt'>>,
|
|
403
|
+
): Promise<Association[]> {
|
|
404
|
+
const client = await this.pool.connect();
|
|
405
|
+
try {
|
|
406
|
+
await client.query('BEGIN');
|
|
407
|
+
|
|
408
|
+
const results: Association[] = [];
|
|
409
|
+
for (const a of associations) {
|
|
410
|
+
const result = await client.query(
|
|
411
|
+
`INSERT INTO associations (source_id, target_id, strength, origin, relation_type)
|
|
412
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
413
|
+
ON CONFLICT (source_id, target_id) DO UPDATE SET
|
|
414
|
+
strength = GREATEST(associations.strength, EXCLUDED.strength)
|
|
415
|
+
RETURNING *`,
|
|
416
|
+
[a.sourceId, a.targetId, a.strength, a.origin, a.relationType ?? 'relates_to'],
|
|
417
|
+
);
|
|
418
|
+
results.push(this.rowToAssociation(result.rows[0]));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
await client.query('COMMIT');
|
|
422
|
+
return results;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
await client.query('ROLLBACK');
|
|
425
|
+
throw err;
|
|
426
|
+
} finally {
|
|
427
|
+
client.release();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Decay ────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
async applyDecay(decayRate: number, minScore: number): Promise<number> {
|
|
434
|
+
const result = await this.pool.query(
|
|
435
|
+
`UPDATE memories SET decay_score = GREATEST($1, decay_score * $2)
|
|
436
|
+
WHERE decay_score > $1`,
|
|
437
|
+
[minScore, 1 - decayRate],
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
log.debug({ affected: result.rowCount, decayRate }, 'Decay applied');
|
|
441
|
+
return result.rowCount ?? 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async applyAssociationDecay(decayRate: number, minStrength: number): Promise<number> {
|
|
445
|
+
const result = await this.pool.query(
|
|
446
|
+
`UPDATE associations SET strength = GREATEST($1, strength * $2)
|
|
447
|
+
WHERE strength > $1 AND origin != 'explicit'`,
|
|
448
|
+
[minStrength, 1 - decayRate],
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
log.debug({ affected: result.rowCount, decayRate }, 'Association decay applied');
|
|
452
|
+
return result.rowCount ?? 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── Lifecycle ────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
async close(): Promise<void> {
|
|
458
|
+
await this.pool.end();
|
|
459
|
+
log.info('PostgreSQL pool closed');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Private Helpers ──────────────────────────────────
|
|
463
|
+
|
|
464
|
+
private rowToMemory(row: MemoryRow): Memory {
|
|
465
|
+
return {
|
|
466
|
+
id: row.id,
|
|
467
|
+
agentId: row.agent_id,
|
|
468
|
+
type: row.type as MemoryType,
|
|
469
|
+
content: row.content,
|
|
470
|
+
embeddingModel: row.embedding_model,
|
|
471
|
+
embedding: row.embedding ? parseVector(row.embedding) : null,
|
|
472
|
+
importance: row.importance,
|
|
473
|
+
decayScore: row.decay_score,
|
|
474
|
+
createdAt: new Date(row.created_at),
|
|
475
|
+
lastAccessedAt: new Date(row.last_accessed_at),
|
|
476
|
+
tags: row.tags ?? [],
|
|
477
|
+
metadata: row.metadata ?? undefined,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private rowToAssociation(row: AssociationRow): Association {
|
|
482
|
+
return {
|
|
483
|
+
sourceId: row.source_id,
|
|
484
|
+
targetId: row.target_id,
|
|
485
|
+
strength: row.strength,
|
|
486
|
+
origin: row.origin as AssociationOrigin,
|
|
487
|
+
relationType: (row.relation_type ?? 'relates_to') as AssociationRelationType,
|
|
488
|
+
createdAt: new Date(row.created_at),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
function parseVector(value: string | number[]): number[] {
|
|
496
|
+
if (Array.isArray(value)) return value;
|
|
497
|
+
// pgvector returns strings like "[0.1,0.2,0.3]"
|
|
498
|
+
return JSON.parse(value) as number[];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── Row Types ──────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
interface MemoryRow {
|
|
504
|
+
id: string;
|
|
505
|
+
agent_id: string;
|
|
506
|
+
type: string;
|
|
507
|
+
content: string;
|
|
508
|
+
embedding_model: string | null;
|
|
509
|
+
embedding: string | null;
|
|
510
|
+
importance: number;
|
|
511
|
+
decay_score: number;
|
|
512
|
+
created_at: string;
|
|
513
|
+
last_accessed_at: string;
|
|
514
|
+
tags: string[];
|
|
515
|
+
metadata: Record<string, unknown> | null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
interface AssociationRow {
|
|
519
|
+
source_id: string;
|
|
520
|
+
target_id: string;
|
|
521
|
+
strength: number;
|
|
522
|
+
origin: string;
|
|
523
|
+
relation_type: string;
|
|
524
|
+
created_at: string;
|
|
525
|
+
}
|