@twelvehart/supermemory-runtime 1.0.0-next.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/.env.example +57 -0
- package/README.md +374 -0
- package/dist/index.js +189 -0
- package/dist/mcp/index.js +1132 -0
- package/docker-compose.prod.yml +91 -0
- package/docker-compose.yml +358 -0
- package/drizzle/0000_dapper_the_professor.sql +159 -0
- package/drizzle/0001_api_keys.sql +51 -0
- package/drizzle/meta/0000_snapshot.json +1532 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +20 -0
- package/package.json +114 -0
- package/scripts/add-extraction-job.ts +122 -0
- package/scripts/benchmark-pgvector.ts +122 -0
- package/scripts/bootstrap.sh +209 -0
- package/scripts/check-runtime-pack.ts +111 -0
- package/scripts/claude-mcp-config.ts +336 -0
- package/scripts/docker-entrypoint.sh +183 -0
- package/scripts/doctor.ts +377 -0
- package/scripts/init-db.sql +33 -0
- package/scripts/install.sh +1110 -0
- package/scripts/mcp-setup.ts +271 -0
- package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
- package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
- package/scripts/migrations/003_create_hnsw_index.sql +94 -0
- package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
- package/scripts/migrations/005_create_chunks_table.sql +95 -0
- package/scripts/migrations/006_create_processing_queue.sql +45 -0
- package/scripts/migrations/generate_test_data.sql +42 -0
- package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
- package/scripts/migrations/run_migrations.sh +286 -0
- package/scripts/migrations/test_hnsw_index.sql +255 -0
- package/scripts/pre-commit-secrets +282 -0
- package/scripts/run-extraction-worker.ts +46 -0
- package/scripts/run-phase1-tests.sh +291 -0
- package/scripts/setup.ts +222 -0
- package/scripts/smoke-install.sh +12 -0
- package/scripts/test-health-endpoint.sh +328 -0
- package/src/api/index.ts +2 -0
- package/src/api/middleware/auth.ts +80 -0
- package/src/api/middleware/csrf.ts +308 -0
- package/src/api/middleware/errorHandler.ts +166 -0
- package/src/api/middleware/rateLimit.ts +360 -0
- package/src/api/middleware/validation.ts +514 -0
- package/src/api/routes/documents.ts +286 -0
- package/src/api/routes/profiles.ts +237 -0
- package/src/api/routes/search.ts +71 -0
- package/src/api/stores/index.ts +58 -0
- package/src/config/bootstrap-env.ts +3 -0
- package/src/config/env.ts +71 -0
- package/src/config/feature-flags.ts +25 -0
- package/src/config/index.ts +140 -0
- package/src/config/secrets.config.ts +291 -0
- package/src/db/client.ts +92 -0
- package/src/db/index.ts +73 -0
- package/src/db/postgres.ts +72 -0
- package/src/db/schema/chunks.schema.ts +31 -0
- package/src/db/schema/containers.schema.ts +46 -0
- package/src/db/schema/documents.schema.ts +49 -0
- package/src/db/schema/embeddings.schema.ts +32 -0
- package/src/db/schema/index.ts +11 -0
- package/src/db/schema/memories.schema.ts +72 -0
- package/src/db/schema/profiles.schema.ts +34 -0
- package/src/db/schema/queue.schema.ts +59 -0
- package/src/db/schema/relationships.schema.ts +42 -0
- package/src/db/schema.ts +223 -0
- package/src/db/worker-connection.ts +47 -0
- package/src/index.ts +235 -0
- package/src/mcp/CLAUDE.md +1 -0
- package/src/mcp/index.ts +1380 -0
- package/src/mcp/legacyState.ts +22 -0
- package/src/mcp/rateLimit.ts +358 -0
- package/src/mcp/resources.ts +309 -0
- package/src/mcp/results.ts +104 -0
- package/src/mcp/tools.ts +401 -0
- package/src/queues/config.ts +119 -0
- package/src/queues/index.ts +289 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/errors.ts +266 -0
- package/src/sdk/http.ts +560 -0
- package/src/sdk/index.ts +244 -0
- package/src/sdk/resources/base.ts +65 -0
- package/src/sdk/resources/connections.ts +204 -0
- package/src/sdk/resources/documents.ts +163 -0
- package/src/sdk/resources/index.ts +10 -0
- package/src/sdk/resources/memories.ts +150 -0
- package/src/sdk/resources/search.ts +60 -0
- package/src/sdk/resources/settings.ts +36 -0
- package/src/sdk/types.ts +674 -0
- package/src/services/chunking/index.ts +451 -0
- package/src/services/chunking.service.ts +650 -0
- package/src/services/csrf.service.ts +252 -0
- package/src/services/documents.repository.ts +219 -0
- package/src/services/documents.service.ts +191 -0
- package/src/services/embedding.service.ts +404 -0
- package/src/services/extraction.service.ts +300 -0
- package/src/services/extractors/code.extractor.ts +451 -0
- package/src/services/extractors/index.ts +9 -0
- package/src/services/extractors/markdown.extractor.ts +461 -0
- package/src/services/extractors/pdf.extractor.ts +315 -0
- package/src/services/extractors/text.extractor.ts +118 -0
- package/src/services/extractors/url.extractor.ts +243 -0
- package/src/services/index.ts +235 -0
- package/src/services/ingestion.service.ts +177 -0
- package/src/services/llm/anthropic.ts +400 -0
- package/src/services/llm/base.ts +460 -0
- package/src/services/llm/contradiction-detector.service.ts +526 -0
- package/src/services/llm/heuristics.ts +148 -0
- package/src/services/llm/index.ts +309 -0
- package/src/services/llm/memory-classifier.service.ts +383 -0
- package/src/services/llm/memory-extension-detector.service.ts +523 -0
- package/src/services/llm/mock.ts +470 -0
- package/src/services/llm/openai.ts +398 -0
- package/src/services/llm/prompts.ts +438 -0
- package/src/services/llm/types.ts +373 -0
- package/src/services/memory.repository.ts +1769 -0
- package/src/services/memory.service.ts +1338 -0
- package/src/services/memory.types.ts +234 -0
- package/src/services/persistence/index.ts +295 -0
- package/src/services/pipeline.service.ts +509 -0
- package/src/services/profile.repository.ts +436 -0
- package/src/services/profile.service.ts +560 -0
- package/src/services/profile.types.ts +270 -0
- package/src/services/relationships/detector.ts +1128 -0
- package/src/services/relationships/index.ts +268 -0
- package/src/services/relationships/memory-integration.ts +459 -0
- package/src/services/relationships/strategies.ts +132 -0
- package/src/services/relationships/types.ts +370 -0
- package/src/services/search.service.ts +761 -0
- package/src/services/search.types.ts +220 -0
- package/src/services/secrets.service.ts +384 -0
- package/src/services/vectorstore/base.ts +327 -0
- package/src/services/vectorstore/index.ts +444 -0
- package/src/services/vectorstore/memory.ts +286 -0
- package/src/services/vectorstore/migration.ts +295 -0
- package/src/services/vectorstore/mock.ts +403 -0
- package/src/services/vectorstore/pgvector.ts +695 -0
- package/src/services/vectorstore/types.ts +247 -0
- package/src/startup.ts +389 -0
- package/src/types/api.types.ts +193 -0
- package/src/types/document.types.ts +103 -0
- package/src/types/index.ts +241 -0
- package/src/types/profile.base.ts +133 -0
- package/src/utils/errors.ts +447 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/index.ts +101 -0
- package/src/utils/logger.ts +313 -0
- package/src/utils/sanitization.ts +501 -0
- package/src/utils/secret-validation.ts +273 -0
- package/src/utils/synonyms.ts +188 -0
- package/src/utils/validation.ts +581 -0
- package/src/workers/chunking.worker.ts +242 -0
- package/src/workers/embedding.worker.ts +358 -0
- package/src/workers/extraction.worker.ts +346 -0
- package/src/workers/indexing.worker.ts +505 -0
- package/tsconfig.json +38 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Repository - Database operations for user profiles
|
|
3
|
+
*
|
|
4
|
+
* Handles persistence of user profiles and facts using PostgreSQL.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { UserProfile, ProfileFact, FactType, PROFILE_DEFAULTS } from './profile.types.js'
|
|
8
|
+
import { getPostgresDatabase } from '../db/postgres.js'
|
|
9
|
+
import { getDatabaseUrl, isPostgresUrl } from '../db/client.js'
|
|
10
|
+
import { userProfiles } from '../db/schema/profiles.schema.js'
|
|
11
|
+
import { containerTags } from '../db/schema/containers.schema.js'
|
|
12
|
+
import { eq } from 'drizzle-orm'
|
|
13
|
+
|
|
14
|
+
let _db: ReturnType<typeof getPostgresDatabase> | null = null
|
|
15
|
+
|
|
16
|
+
function getDb(): ReturnType<typeof getPostgresDatabase> {
|
|
17
|
+
if (_db) return _db
|
|
18
|
+
const databaseUrl = getDatabaseUrl()
|
|
19
|
+
if (!isPostgresUrl(databaseUrl)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'ProfileRepository requires a PostgreSQL DATABASE_URL. SQLite is only supported in tests and is not compatible with profile persistence.'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
_db = getPostgresDatabase(databaseUrl)
|
|
25
|
+
return _db
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const db = new Proxy({} as ReturnType<typeof getPostgresDatabase>, {
|
|
29
|
+
get(_target, prop) {
|
|
30
|
+
return getDb()[prop as keyof ReturnType<typeof getPostgresDatabase>]
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const PROFILE_VERSION_KEY = 'profileVersion'
|
|
35
|
+
|
|
36
|
+
function normalizeFacts(facts: unknown): ProfileFact[] {
|
|
37
|
+
if (!Array.isArray(facts)) {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return facts.map((fact) => normalizeFact(fact as ProfileFact))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeFact(fact: ProfileFact): ProfileFact {
|
|
45
|
+
return {
|
|
46
|
+
...fact,
|
|
47
|
+
extractedAt: new Date(fact.extractedAt),
|
|
48
|
+
lastAccessedAt: new Date(fact.lastAccessedAt),
|
|
49
|
+
expiresAt: fact.expiresAt ? new Date(fact.expiresAt) : undefined,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeRecordObject(value: unknown): Record<string, unknown> {
|
|
54
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
55
|
+
return { ...(value as Record<string, unknown>) }
|
|
56
|
+
}
|
|
57
|
+
return {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getProfileVersion(record: { computedTraits: unknown } | null | undefined): number {
|
|
61
|
+
const computedTraits = normalizeRecordObject(record?.computedTraits)
|
|
62
|
+
const storedVersion = computedTraits[PROFILE_VERSION_KEY]
|
|
63
|
+
if (typeof storedVersion === 'number' && Number.isFinite(storedVersion)) {
|
|
64
|
+
return storedVersion
|
|
65
|
+
}
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function withProfileVersion(computedTraits: Record<string, unknown>, version: number): Record<string, unknown> {
|
|
70
|
+
return {
|
|
71
|
+
...computedTraits,
|
|
72
|
+
[PROFILE_VERSION_KEY]: version,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function ensureContainerTag(tag: string): Promise<void> {
|
|
77
|
+
await db.insert(containerTags).values({ tag }).onConflictDoNothing({ target: containerTags.tag })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mapDbProfile(record: typeof userProfiles.$inferSelect): UserProfile {
|
|
81
|
+
const computedTraits = normalizeRecordObject(record.computedTraits)
|
|
82
|
+
const version = getProfileVersion({ computedTraits })
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
containerTag: record.containerTag,
|
|
86
|
+
staticFacts: normalizeFacts(record.staticFacts),
|
|
87
|
+
dynamicFacts: normalizeFacts(record.dynamicFacts),
|
|
88
|
+
createdAt: record.createdAt,
|
|
89
|
+
updatedAt: record.updatedAt,
|
|
90
|
+
version,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Database interface for profile storage
|
|
96
|
+
* Implement this interface for different storage backends
|
|
97
|
+
*/
|
|
98
|
+
export interface ProfileDatabase {
|
|
99
|
+
findByContainerTag(containerTag: string): Promise<UserProfile | null>
|
|
100
|
+
upsert(profile: UserProfile): Promise<UserProfile>
|
|
101
|
+
updateFacts(
|
|
102
|
+
containerTag: string,
|
|
103
|
+
staticFacts: ProfileFact[],
|
|
104
|
+
dynamicFacts: ProfileFact[]
|
|
105
|
+
): Promise<UserProfile | null>
|
|
106
|
+
delete(containerTag: string): Promise<boolean>
|
|
107
|
+
listAll(): Promise<UserProfile[]>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* PostgreSQL implementation of ProfileDatabase
|
|
112
|
+
*/
|
|
113
|
+
class PostgresProfileDatabase implements ProfileDatabase {
|
|
114
|
+
async findByContainerTag(containerTag: string): Promise<UserProfile | null> {
|
|
115
|
+
const [record] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, containerTag))
|
|
116
|
+
|
|
117
|
+
return record ? mapDbProfile(record) : null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async upsert(profile: UserProfile): Promise<UserProfile> {
|
|
121
|
+
await ensureContainerTag(profile.containerTag)
|
|
122
|
+
const [existing] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, profile.containerTag))
|
|
123
|
+
|
|
124
|
+
if (existing) {
|
|
125
|
+
const computedTraits = normalizeRecordObject(existing.computedTraits)
|
|
126
|
+
const nextVersion = getProfileVersion({ computedTraits }) + 1
|
|
127
|
+
|
|
128
|
+
const [updated] = await db
|
|
129
|
+
.update(userProfiles)
|
|
130
|
+
.set({
|
|
131
|
+
staticFacts: profile.staticFacts,
|
|
132
|
+
dynamicFacts: profile.dynamicFacts,
|
|
133
|
+
updatedAt: new Date(),
|
|
134
|
+
computedTraits: withProfileVersion(computedTraits, nextVersion),
|
|
135
|
+
})
|
|
136
|
+
.where(eq(userProfiles.containerTag, profile.containerTag))
|
|
137
|
+
.returning()
|
|
138
|
+
|
|
139
|
+
return updated ? mapDbProfile(updated) : profile
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const computedTraits = withProfileVersion({}, 1)
|
|
143
|
+
const [created] = await db
|
|
144
|
+
.insert(userProfiles)
|
|
145
|
+
.values({
|
|
146
|
+
containerTag: profile.containerTag,
|
|
147
|
+
staticFacts: profile.staticFacts,
|
|
148
|
+
dynamicFacts: profile.dynamicFacts,
|
|
149
|
+
createdAt: profile.createdAt ?? new Date(),
|
|
150
|
+
updatedAt: profile.updatedAt ?? new Date(),
|
|
151
|
+
computedTraits,
|
|
152
|
+
})
|
|
153
|
+
.returning()
|
|
154
|
+
|
|
155
|
+
return created ? mapDbProfile(created) : profile
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async updateFacts(
|
|
159
|
+
containerTag: string,
|
|
160
|
+
staticFacts: ProfileFact[],
|
|
161
|
+
dynamicFacts: ProfileFact[]
|
|
162
|
+
): Promise<UserProfile | null> {
|
|
163
|
+
await ensureContainerTag(containerTag)
|
|
164
|
+
const [existing] = await db.select().from(userProfiles).where(eq(userProfiles.containerTag, containerTag))
|
|
165
|
+
|
|
166
|
+
if (!existing) {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const computedTraits = normalizeRecordObject(existing.computedTraits)
|
|
171
|
+
const nextVersion = getProfileVersion({ computedTraits }) + 1
|
|
172
|
+
|
|
173
|
+
const [updated] = await db
|
|
174
|
+
.update(userProfiles)
|
|
175
|
+
.set({
|
|
176
|
+
staticFacts,
|
|
177
|
+
dynamicFacts,
|
|
178
|
+
updatedAt: new Date(),
|
|
179
|
+
computedTraits: withProfileVersion(computedTraits, nextVersion),
|
|
180
|
+
})
|
|
181
|
+
.where(eq(userProfiles.containerTag, containerTag))
|
|
182
|
+
.returning()
|
|
183
|
+
|
|
184
|
+
return updated ? mapDbProfile(updated) : null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async delete(containerTag: string): Promise<boolean> {
|
|
188
|
+
const deleted = await db
|
|
189
|
+
.delete(userProfiles)
|
|
190
|
+
.where(eq(userProfiles.containerTag, containerTag))
|
|
191
|
+
.returning({ containerTag: userProfiles.containerTag })
|
|
192
|
+
|
|
193
|
+
return deleted.length > 0
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async listAll(): Promise<UserProfile[]> {
|
|
197
|
+
const records = await db.select().from(userProfiles)
|
|
198
|
+
return records.map(mapDbProfile)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Profile Repository - Main interface for profile database operations
|
|
204
|
+
*/
|
|
205
|
+
export class ProfileRepository {
|
|
206
|
+
private db: ProfileDatabase
|
|
207
|
+
|
|
208
|
+
constructor(database?: ProfileDatabase) {
|
|
209
|
+
this.db = database ?? new PostgresProfileDatabase()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Find a profile by container tag
|
|
214
|
+
*/
|
|
215
|
+
async findByContainerTag(containerTag: string): Promise<UserProfile | null> {
|
|
216
|
+
return this.db.findByContainerTag(containerTag)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create or update a profile
|
|
221
|
+
*/
|
|
222
|
+
async upsert(profile: UserProfile): Promise<UserProfile> {
|
|
223
|
+
return this.db.upsert(profile)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Update facts for a profile
|
|
228
|
+
*/
|
|
229
|
+
async updateFacts(
|
|
230
|
+
containerTag: string,
|
|
231
|
+
staticFacts: ProfileFact[],
|
|
232
|
+
dynamicFacts: ProfileFact[]
|
|
233
|
+
): Promise<UserProfile | null> {
|
|
234
|
+
return this.db.updateFacts(containerTag, staticFacts, dynamicFacts)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Delete a profile
|
|
239
|
+
*/
|
|
240
|
+
async delete(containerTag: string): Promise<boolean> {
|
|
241
|
+
return this.db.delete(containerTag)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* List all profiles
|
|
246
|
+
*/
|
|
247
|
+
async listAll(): Promise<UserProfile[]> {
|
|
248
|
+
return this.db.listAll()
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Add a single fact to a profile
|
|
253
|
+
*/
|
|
254
|
+
async addFact(containerTag: string, fact: ProfileFact): Promise<UserProfile | null> {
|
|
255
|
+
const profile = await this.findByContainerTag(containerTag)
|
|
256
|
+
if (!profile) {
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const targetArray = fact.type === 'static' ? 'staticFacts' : 'dynamicFacts'
|
|
261
|
+
const updatedFacts = [...profile[targetArray], fact]
|
|
262
|
+
|
|
263
|
+
// Enforce max dynamic facts limit
|
|
264
|
+
let finalDynamicFacts = profile.dynamicFacts
|
|
265
|
+
let finalStaticFacts = profile.staticFacts
|
|
266
|
+
|
|
267
|
+
if (fact.type === 'dynamic') {
|
|
268
|
+
finalDynamicFacts = updatedFacts
|
|
269
|
+
if (finalDynamicFacts.length > PROFILE_DEFAULTS.maxDynamicFacts) {
|
|
270
|
+
// Remove oldest expired or least recently accessed
|
|
271
|
+
finalDynamicFacts = this.pruneExcessDynamicFacts(finalDynamicFacts)
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
finalStaticFacts = updatedFacts
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return this.updateFacts(containerTag, finalStaticFacts, finalDynamicFacts)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Remove expired dynamic facts from a profile
|
|
282
|
+
*/
|
|
283
|
+
async removeExpiredFacts(containerTag: string): Promise<UserProfile | null> {
|
|
284
|
+
const profile = await this.findByContainerTag(containerTag)
|
|
285
|
+
if (!profile) {
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const now = new Date()
|
|
290
|
+
const validDynamicFacts = profile.dynamicFacts.filter(
|
|
291
|
+
(fact: ProfileFact) => !fact.expiresAt || fact.expiresAt > now
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (validDynamicFacts.length === profile.dynamicFacts.length) {
|
|
295
|
+
return profile // No changes needed
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return this.updateFacts(containerTag, profile.staticFacts, validDynamicFacts)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Find facts by content similarity (simple substring match)
|
|
303
|
+
* Replace with vector similarity for production
|
|
304
|
+
*/
|
|
305
|
+
async findSimilarFacts(containerTag: string, content: string, type?: FactType): Promise<ProfileFact[]> {
|
|
306
|
+
const profile = await this.findByContainerTag(containerTag)
|
|
307
|
+
if (!profile) {
|
|
308
|
+
return []
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const searchTerms = content.toLowerCase().split(/\s+/)
|
|
312
|
+
const allFacts =
|
|
313
|
+
type === 'static'
|
|
314
|
+
? profile.staticFacts
|
|
315
|
+
: type === 'dynamic'
|
|
316
|
+
? profile.dynamicFacts
|
|
317
|
+
: [...profile.staticFacts, ...profile.dynamicFacts]
|
|
318
|
+
|
|
319
|
+
return allFacts.filter((fact: ProfileFact) => {
|
|
320
|
+
const factWords = fact.content.toLowerCase()
|
|
321
|
+
return searchTerms.some((term) => factWords.includes(term))
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Reinforce a fact (increment count and update access time)
|
|
327
|
+
*/
|
|
328
|
+
async reinforceFact(containerTag: string, factId: string): Promise<ProfileFact | null> {
|
|
329
|
+
const profile = await this.findByContainerTag(containerTag)
|
|
330
|
+
if (!profile) {
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const updateFact = (facts: ProfileFact[]): ProfileFact[] => {
|
|
335
|
+
return facts.map((fact) => {
|
|
336
|
+
if (fact.id === factId) {
|
|
337
|
+
return {
|
|
338
|
+
...fact,
|
|
339
|
+
reinforcementCount: fact.reinforcementCount + 1,
|
|
340
|
+
lastAccessedAt: new Date(),
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return fact
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const staticFact = profile.staticFacts.find((f: ProfileFact) => f.id === factId)
|
|
348
|
+
const dynamicFact = profile.dynamicFacts.find((f: ProfileFact) => f.id === factId)
|
|
349
|
+
|
|
350
|
+
if (staticFact) {
|
|
351
|
+
await this.updateFacts(containerTag, updateFact(profile.staticFacts), profile.dynamicFacts)
|
|
352
|
+
return { ...staticFact, reinforcementCount: staticFact.reinforcementCount + 1 }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (dynamicFact) {
|
|
356
|
+
await this.updateFacts(containerTag, profile.staticFacts, updateFact(profile.dynamicFacts))
|
|
357
|
+
return { ...dynamicFact, reinforcementCount: dynamicFact.reinforcementCount + 1 }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Promote a dynamic fact to static
|
|
365
|
+
*/
|
|
366
|
+
async promoteFact(containerTag: string, factId: string): Promise<ProfileFact | null> {
|
|
367
|
+
const profile = await this.findByContainerTag(containerTag)
|
|
368
|
+
if (!profile) {
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const factIndex = profile.dynamicFacts.findIndex((f: ProfileFact) => f.id === factId)
|
|
373
|
+
if (factIndex === -1) {
|
|
374
|
+
return null
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const fact = profile.dynamicFacts[factIndex]
|
|
378
|
+
if (!fact) {
|
|
379
|
+
return null
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const promotedFact: ProfileFact = {
|
|
383
|
+
...fact,
|
|
384
|
+
type: 'static',
|
|
385
|
+
expiresAt: undefined,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const newDynamicFacts = [...profile.dynamicFacts.slice(0, factIndex), ...profile.dynamicFacts.slice(factIndex + 1)]
|
|
389
|
+
const newStaticFacts = [...profile.staticFacts, promotedFact]
|
|
390
|
+
|
|
391
|
+
await this.updateFacts(containerTag, newStaticFacts, newDynamicFacts)
|
|
392
|
+
return promotedFact
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Prune excess dynamic facts, keeping most relevant ones
|
|
397
|
+
*/
|
|
398
|
+
private pruneExcessDynamicFacts(facts: ProfileFact[]): ProfileFact[] {
|
|
399
|
+
const now = new Date()
|
|
400
|
+
|
|
401
|
+
// First remove expired facts
|
|
402
|
+
const validFacts = facts.filter((fact) => !fact.expiresAt || fact.expiresAt > now)
|
|
403
|
+
|
|
404
|
+
if (validFacts.length <= PROFILE_DEFAULTS.maxDynamicFacts) {
|
|
405
|
+
return validFacts
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Sort by relevance score (higher is better)
|
|
409
|
+
validFacts.sort((a, b) => {
|
|
410
|
+
const scoreA = this.calculateRelevanceScore(a)
|
|
411
|
+
const scoreB = this.calculateRelevanceScore(b)
|
|
412
|
+
return scoreB - scoreA
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
return validFacts.slice(0, PROFILE_DEFAULTS.maxDynamicFacts)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Calculate relevance score for a fact
|
|
420
|
+
*/
|
|
421
|
+
private calculateRelevanceScore(fact: ProfileFact): number {
|
|
422
|
+
const now = new Date()
|
|
423
|
+
const ageHours = (now.getTime() - fact.extractedAt.getTime()) / (1000 * 60 * 60)
|
|
424
|
+
const recencyScore = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
|
|
425
|
+
|
|
426
|
+
const accessRecency = (now.getTime() - fact.lastAccessedAt.getTime()) / (1000 * 60 * 60)
|
|
427
|
+
const accessScore = Math.max(0, 1 - accessRecency / 168)
|
|
428
|
+
|
|
429
|
+
const reinforcementScore = Math.min(1, fact.reinforcementCount / 10)
|
|
430
|
+
|
|
431
|
+
return fact.confidence * 0.3 + recencyScore * 0.3 + accessScore * 0.2 + reinforcementScore * 0.2
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Export singleton instance
|
|
436
|
+
export const profileRepository = new ProfileRepository()
|