@lupaflow/memory 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 +31 -0
- package/src/index.ts +9 -0
- package/src/interface.ts +11 -0
- package/src/long-term.ts +70 -0
- package/src/project.ts +67 -0
- package/src/semantic.ts +70 -0
- package/src/session.ts +41 -0
- package/src/short-term.ts +54 -0
- package/src/store.ts +91 -0
- package/src/user.ts +46 -0
- package/src/vector.ts +73 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lupaflow/memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"require": "./dist/index.js",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@lupaflow/core": "workspace:*",
|
|
25
|
+
"@lupaflow/types": "workspace:*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.3.0",
|
|
29
|
+
"typescript": "^5.6.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { MemoryStore } from "./store"
|
|
2
|
+
export { ShortTermMemory } from "./short-term"
|
|
3
|
+
export { LongTermMemory } from "./long-term"
|
|
4
|
+
export { SemanticMemory } from "./semantic"
|
|
5
|
+
export { SessionMemory } from "./session"
|
|
6
|
+
export { UserMemory } from "./user"
|
|
7
|
+
export { ProjectMemory } from "./project"
|
|
8
|
+
export { VectorMemory } from "./vector"
|
|
9
|
+
export type { MemoryBackend } from "./interface"
|
package/src/interface.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult, MemoryType } from "@lupaflow/types"
|
|
2
|
+
|
|
3
|
+
export interface MemoryBackend {
|
|
4
|
+
readonly type: MemoryType
|
|
5
|
+
|
|
6
|
+
store(entry: MemoryEntry): Promise<void>
|
|
7
|
+
retrieve(query: MemoryQuery): Promise<MemoryEntry[]>
|
|
8
|
+
search(query: string, limit?: number): Promise<MemorySearchResult[]>
|
|
9
|
+
delete(key: string): Promise<void>
|
|
10
|
+
clear(): Promise<void>
|
|
11
|
+
}
|
package/src/long-term.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile, writeFile, access } from "fs/promises"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
4
|
+
import type { MemoryBackend } from "./interface"
|
|
5
|
+
import { generateId } from "@lupaflow/core"
|
|
6
|
+
|
|
7
|
+
export class LongTermMemory implements MemoryBackend {
|
|
8
|
+
readonly type = "long-term" as const
|
|
9
|
+
private entries: MemoryEntry[] = []
|
|
10
|
+
private filePath: string
|
|
11
|
+
|
|
12
|
+
constructor(name = "default") {
|
|
13
|
+
this.filePath = join(process.cwd(), ".lupaflow", `memory-${name}.json`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async init(): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
await access(this.filePath)
|
|
19
|
+
const data = await readFile(this.filePath, "utf-8")
|
|
20
|
+
this.entries = JSON.parse(data)
|
|
21
|
+
} catch {
|
|
22
|
+
this.entries = []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async persist(): Promise<void> {
|
|
27
|
+
const dir = join(process.cwd(), ".lupaflow")
|
|
28
|
+
try {
|
|
29
|
+
await access(dir)
|
|
30
|
+
} catch {
|
|
31
|
+
await writeFile(dir, "", { flag: "a" })
|
|
32
|
+
}
|
|
33
|
+
await writeFile(this.filePath, JSON.stringify(this.entries, null, 2))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
37
|
+
this.entries.push({
|
|
38
|
+
...entry,
|
|
39
|
+
id: entry.id || generateId(),
|
|
40
|
+
timestamp: entry.timestamp || Date.now(),
|
|
41
|
+
})
|
|
42
|
+
await this.persist()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
46
|
+
let results = this.entries
|
|
47
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
48
|
+
if (query.type) results = results.filter((e) => e.type === query.type)
|
|
49
|
+
if (query.scope) results = results.filter((e) => e.scope === query.scope)
|
|
50
|
+
return results.slice(-(query.limit || 50))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async search(_query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
54
|
+
const q = _query.toLowerCase()
|
|
55
|
+
return this.entries
|
|
56
|
+
.filter((e) => e.content.toLowerCase().includes(q))
|
|
57
|
+
.slice(-limit)
|
|
58
|
+
.map((entry) => ({ entry, score: 1 }))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async delete(key: string): Promise<void> {
|
|
62
|
+
this.entries = this.entries.filter((e) => e.key !== key)
|
|
63
|
+
await this.persist()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async clear(): Promise<void> {
|
|
67
|
+
this.entries = []
|
|
68
|
+
await this.persist()
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
import { readFile, writeFile, access } from "fs/promises"
|
|
5
|
+
|
|
6
|
+
const PROJECT_MEMORY_FILE = ".lupaflow/project-memory.json"
|
|
7
|
+
|
|
8
|
+
export class ProjectMemory implements MemoryBackend {
|
|
9
|
+
readonly type = "project" as const
|
|
10
|
+
private entries: MemoryEntry[] = []
|
|
11
|
+
private loaded = false
|
|
12
|
+
|
|
13
|
+
async ensureLoaded(): Promise<void> {
|
|
14
|
+
if (this.loaded) return
|
|
15
|
+
try {
|
|
16
|
+
await access(PROJECT_MEMORY_FILE)
|
|
17
|
+
const data = await readFile(PROJECT_MEMORY_FILE, "utf-8")
|
|
18
|
+
this.entries = JSON.parse(data)
|
|
19
|
+
} catch {
|
|
20
|
+
this.entries = []
|
|
21
|
+
}
|
|
22
|
+
this.loaded = true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async persist(): Promise<void> {
|
|
26
|
+
await writeFile(PROJECT_MEMORY_FILE, JSON.stringify(this.entries, null, 2))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
30
|
+
await this.ensureLoaded()
|
|
31
|
+
this.entries.push({
|
|
32
|
+
...entry,
|
|
33
|
+
id: entry.id || generateId(),
|
|
34
|
+
scope: "project",
|
|
35
|
+
timestamp: entry.timestamp || Date.now(),
|
|
36
|
+
})
|
|
37
|
+
await this.persist()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
41
|
+
await this.ensureLoaded()
|
|
42
|
+
let results = this.entries.filter((e) => e.scope === "project")
|
|
43
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
44
|
+
return results.slice(-(query.limit || 50))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async search(_query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
48
|
+
await this.ensureLoaded()
|
|
49
|
+
const q = _query.toLowerCase()
|
|
50
|
+
return this.entries
|
|
51
|
+
.filter((e) => e.scope === "project" && e.content.toLowerCase().includes(q))
|
|
52
|
+
.slice(-limit)
|
|
53
|
+
.map((entry) => ({ entry, score: 1 }))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async delete(key: string): Promise<void> {
|
|
57
|
+
await this.ensureLoaded()
|
|
58
|
+
this.entries = this.entries.filter((e) => !(e.key === key && e.scope === "project"))
|
|
59
|
+
await this.persist()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async clear(): Promise<void> {
|
|
63
|
+
await this.ensureLoaded()
|
|
64
|
+
this.entries = this.entries.filter((e) => e.scope !== "project")
|
|
65
|
+
await this.persist()
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/semantic.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
|
|
5
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
6
|
+
let dot = 0, magA = 0, magB = 0
|
|
7
|
+
for (let i = 0; i < a.length; i++) {
|
|
8
|
+
dot += a[i] * b[i]
|
|
9
|
+
magA += a[i] * a[i]
|
|
10
|
+
magB += b[i] * b[i]
|
|
11
|
+
}
|
|
12
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB)
|
|
13
|
+
return denom === 0 ? 0 : dot / denom
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function simpleEmbed(text: string): number[] {
|
|
17
|
+
const dim = 128
|
|
18
|
+
const vec = new Array(dim).fill(0)
|
|
19
|
+
for (let i = 0; i < text.length; i++) {
|
|
20
|
+
const idx = (text.charCodeAt(i) * 7 + i * 13) % dim
|
|
21
|
+
vec[idx] += 1 + (text.charCodeAt(i) % 5) * 0.1
|
|
22
|
+
}
|
|
23
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
|
|
24
|
+
return mag > 0 ? vec.map((v) => v / mag) : vec
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SemanticEntry extends MemoryEntry {
|
|
28
|
+
embedding: number[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SemanticMemory implements MemoryBackend {
|
|
32
|
+
readonly type = "semantic" as const
|
|
33
|
+
private entries: SemanticEntry[] = []
|
|
34
|
+
|
|
35
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
36
|
+
this.entries.push({
|
|
37
|
+
...entry,
|
|
38
|
+
id: entry.id || generateId(),
|
|
39
|
+
timestamp: entry.timestamp || Date.now(),
|
|
40
|
+
embedding: simpleEmbed(entry.content),
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
45
|
+
let results = this.entries
|
|
46
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
47
|
+
if (query.scope) results = results.filter((e) => e.scope === query.scope)
|
|
48
|
+
return results.slice(-(query.limit || 20))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async search(_query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
52
|
+
const queryEmbedding = simpleEmbed(_query)
|
|
53
|
+
const scored = this.entries
|
|
54
|
+
.map((entry) => ({
|
|
55
|
+
entry,
|
|
56
|
+
score: cosineSimilarity(queryEmbedding, entry.embedding),
|
|
57
|
+
}))
|
|
58
|
+
.sort((a, b) => b.score - a.score)
|
|
59
|
+
.slice(0, limit)
|
|
60
|
+
return scored
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async delete(key: string): Promise<void> {
|
|
64
|
+
this.entries = this.entries.filter((e) => e.key !== key)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async clear(): Promise<void> {
|
|
68
|
+
this.entries = []
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
|
|
5
|
+
const SESSION_KEY = "__session__"
|
|
6
|
+
|
|
7
|
+
export class SessionMemory implements MemoryBackend {
|
|
8
|
+
readonly type = "session" as const
|
|
9
|
+
private entries: MemoryEntry[] = []
|
|
10
|
+
|
|
11
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
12
|
+
this.entries.push({
|
|
13
|
+
...entry,
|
|
14
|
+
id: entry.id || generateId(),
|
|
15
|
+
scope: SESSION_KEY,
|
|
16
|
+
timestamp: entry.timestamp || Date.now(),
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
21
|
+
let results = this.entries.filter((e) => e.scope === SESSION_KEY)
|
|
22
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
23
|
+
return results.slice(-(query.limit || 50))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async search(_query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
27
|
+
const q = _query.toLowerCase()
|
|
28
|
+
return this.entries
|
|
29
|
+
.filter((e) => e.scope === SESSION_KEY && e.content.toLowerCase().includes(q))
|
|
30
|
+
.slice(-limit)
|
|
31
|
+
.map((entry) => ({ entry, score: 1 }))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async delete(key: string): Promise<void> {
|
|
35
|
+
this.entries = this.entries.filter((e) => e.key !== key && e.scope === SESSION_KEY)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async clear(): Promise<void> {
|
|
39
|
+
this.entries = this.entries.filter((e) => e.scope !== SESSION_KEY)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
|
|
5
|
+
export class ShortTermMemory implements MemoryBackend {
|
|
6
|
+
readonly type = "short-term" as const
|
|
7
|
+
private entries: MemoryEntry[] = []
|
|
8
|
+
private maxEntries: number
|
|
9
|
+
|
|
10
|
+
constructor(maxEntries = 50) {
|
|
11
|
+
this.maxEntries = maxEntries
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
15
|
+
this.entries.push({
|
|
16
|
+
...entry,
|
|
17
|
+
id: entry.id || generateId(),
|
|
18
|
+
timestamp: entry.timestamp || Date.now(),
|
|
19
|
+
})
|
|
20
|
+
if (this.entries.length > this.maxEntries) {
|
|
21
|
+
this.entries.shift()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
26
|
+
let results = this.entries
|
|
27
|
+
if (query.key) {
|
|
28
|
+
results = results.filter((e) => e.key === query.key)
|
|
29
|
+
}
|
|
30
|
+
if (query.scope) {
|
|
31
|
+
results = results.filter((e) => e.scope === query.scope)
|
|
32
|
+
}
|
|
33
|
+
return results.slice(-(query.limit || 20))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async search(_query: string, limit = 5): Promise<MemorySearchResult[]> {
|
|
37
|
+
const q = _query.toLowerCase()
|
|
38
|
+
const results = this.entries
|
|
39
|
+
.filter((e) => e.content.toLowerCase().includes(q))
|
|
40
|
+
.slice(-limit)
|
|
41
|
+
return results.map((entry) => ({
|
|
42
|
+
entry,
|
|
43
|
+
score: 1,
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async delete(key: string): Promise<void> {
|
|
48
|
+
this.entries = this.entries.filter((e) => e.key !== key)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async clear(): Promise<void> {
|
|
52
|
+
this.entries = []
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { MemoryBackend } from "./interface"
|
|
2
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult, MemoryType } from "@lupaflow/types"
|
|
3
|
+
import { ShortTermMemory } from "./short-term"
|
|
4
|
+
import { LongTermMemory } from "./long-term"
|
|
5
|
+
import { SemanticMemory } from "./semantic"
|
|
6
|
+
import { SessionMemory } from "./session"
|
|
7
|
+
import { UserMemory } from "./user"
|
|
8
|
+
import { ProjectMemory } from "./project"
|
|
9
|
+
import { MemoryError } from "@lupaflow/core"
|
|
10
|
+
import { globalEventBus } from "@lupaflow/core"
|
|
11
|
+
|
|
12
|
+
export class MemoryStore {
|
|
13
|
+
private backends: Map<MemoryType, MemoryBackend> = new Map()
|
|
14
|
+
|
|
15
|
+
constructor(config?: { shortTerm?: boolean; longTerm?: boolean; semantic?: boolean; session?: boolean; user?: string; project?: boolean }) {
|
|
16
|
+
if (config?.shortTerm !== false) this.backends.set("short-term", new ShortTermMemory())
|
|
17
|
+
if (config?.longTerm) this.backends.set("long-term", new LongTermMemory())
|
|
18
|
+
if (config?.semantic) this.backends.set("semantic", new SemanticMemory())
|
|
19
|
+
if (config?.session !== false) this.backends.set("session", new SessionMemory())
|
|
20
|
+
if (config?.user) this.backends.set("user", new UserMemory(config.user))
|
|
21
|
+
if (config?.project) this.backends.set("project", new ProjectMemory())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
register(type: MemoryType, backend: MemoryBackend): void {
|
|
25
|
+
this.backends.set(type, backend)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get(type: MemoryType): MemoryBackend {
|
|
29
|
+
const backend = this.backends.get(type)
|
|
30
|
+
if (!backend) throw new MemoryError(`No memory backend registered: ${type}`)
|
|
31
|
+
return backend
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
35
|
+
if (entry.type === "short-term" || entry.type === "session") {
|
|
36
|
+
const backend = this.backends.get(entry.type)
|
|
37
|
+
if (backend) {
|
|
38
|
+
await backend.store(entry)
|
|
39
|
+
await globalEventBus.emit("memory:update", {
|
|
40
|
+
type: entry.type,
|
|
41
|
+
key: entry.key,
|
|
42
|
+
} as any)
|
|
43
|
+
}
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const backend of this.backends.values()) {
|
|
48
|
+
await backend.store({ ...entry })
|
|
49
|
+
}
|
|
50
|
+
await globalEventBus.emit("memory:update", { type: entry.type, key: entry.key } as any)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
54
|
+
if (query.type) {
|
|
55
|
+
const backend = this.get(query.type)
|
|
56
|
+
const results = await backend.retrieve(query)
|
|
57
|
+
await globalEventBus.emit("memory:retrieve", {
|
|
58
|
+
type: query.type,
|
|
59
|
+
key: query.key || "all",
|
|
60
|
+
results: results.length,
|
|
61
|
+
} as any)
|
|
62
|
+
return results
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const all: MemoryEntry[] = []
|
|
66
|
+
for (const backend of this.backends.values()) {
|
|
67
|
+
all.push(...(await backend.retrieve(query)))
|
|
68
|
+
}
|
|
69
|
+
return all
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async search(query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
73
|
+
const all: MemorySearchResult[] = []
|
|
74
|
+
for (const backend of this.backends.values()) {
|
|
75
|
+
all.push(...(await backend.search(query, limit)))
|
|
76
|
+
}
|
|
77
|
+
return all.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async delete(key: string): Promise<void> {
|
|
81
|
+
for (const backend of this.backends.values()) {
|
|
82
|
+
await backend.delete(key)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async clear(): Promise<void> {
|
|
87
|
+
for (const backend of this.backends.values()) {
|
|
88
|
+
await backend.clear()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/user.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
|
|
5
|
+
export class UserMemory implements MemoryBackend {
|
|
6
|
+
readonly type = "user" as const
|
|
7
|
+
private entries: MemoryEntry[] = []
|
|
8
|
+
private userId: string
|
|
9
|
+
|
|
10
|
+
constructor(userId: string) {
|
|
11
|
+
this.userId = userId
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
15
|
+
this.entries.push({
|
|
16
|
+
...entry,
|
|
17
|
+
id: entry.id || generateId(),
|
|
18
|
+
scope: `user:${this.userId}`,
|
|
19
|
+
timestamp: entry.timestamp || Date.now(),
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
24
|
+
let results = this.entries.filter((e) => e.scope === `user:${this.userId}`)
|
|
25
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
26
|
+
return results.slice(-(query.limit || 50))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async search(_query: string, limit = 10): Promise<MemorySearchResult[]> {
|
|
30
|
+
const q = _query.toLowerCase()
|
|
31
|
+
return this.entries
|
|
32
|
+
.filter((e) => e.scope === `user:${this.userId}` && e.content.toLowerCase().includes(q))
|
|
33
|
+
.slice(-limit)
|
|
34
|
+
.map((entry) => ({ entry, score: 1 }))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async delete(key: string): Promise<void> {
|
|
38
|
+
this.entries = this.entries.filter(
|
|
39
|
+
(e) => !(e.key === key && e.scope === `user:${this.userId}`)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async clear(): Promise<void> {
|
|
44
|
+
this.entries = this.entries.filter((e) => e.scope !== `user:${this.userId}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/vector.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MemoryEntry, MemoryQuery, MemorySearchResult } from "@lupaflow/types"
|
|
2
|
+
import type { MemoryBackend } from "./interface"
|
|
3
|
+
import { generateId } from "@lupaflow/core"
|
|
4
|
+
|
|
5
|
+
function cosineSimilarity(a: number[], b: number[]): number {
|
|
6
|
+
let dot = 0, magA = 0, magB = 0
|
|
7
|
+
for (let i = 0; i < a.length; i++) {
|
|
8
|
+
dot += a[i] * b[i]
|
|
9
|
+
magA += a[i] * a[i]
|
|
10
|
+
magB += b[i] * b[i]
|
|
11
|
+
}
|
|
12
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB)
|
|
13
|
+
return denom === 0 ? 0 : dot / denom
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateEmbedding(text: string): number[] {
|
|
17
|
+
const dim = 384
|
|
18
|
+
const vec = new Array(dim).fill(0)
|
|
19
|
+
for (let i = 0; i < text.length; i++) {
|
|
20
|
+
const code = text.charCodeAt(i)
|
|
21
|
+
for (let j = 0; j < 5; j++) {
|
|
22
|
+
const idx = (code * (j + 1) + i * 17 + j * 31) % dim
|
|
23
|
+
vec[idx] += 1.0 / (j + 1)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
|
|
27
|
+
return mag > 0 ? vec.map((v) => v / mag) : vec
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface VectorEntry extends MemoryEntry {
|
|
31
|
+
embedding: number[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class VectorMemory implements MemoryBackend {
|
|
35
|
+
readonly type = "vector" as const
|
|
36
|
+
private entries: VectorEntry[] = []
|
|
37
|
+
|
|
38
|
+
async store(entry: MemoryEntry): Promise<void> {
|
|
39
|
+
this.entries.push({
|
|
40
|
+
...entry,
|
|
41
|
+
id: entry.id || generateId(),
|
|
42
|
+
timestamp: entry.timestamp || Date.now(),
|
|
43
|
+
embedding: generateEmbedding(entry.content),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async retrieve(query: MemoryQuery): Promise<MemoryEntry[]> {
|
|
48
|
+
let results: MemoryEntry[] = this.entries
|
|
49
|
+
if (query.key) results = results.filter((e) => e.key === query.key)
|
|
50
|
+
if (query.scope) results = results.filter((e) => e.scope === query.scope)
|
|
51
|
+
return results.slice(-(query.limit || 20))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async search(_query: string, limit = 10, threshold = 0.3): Promise<MemorySearchResult[]> {
|
|
55
|
+
const queryEmbedding = generateEmbedding(_query)
|
|
56
|
+
return this.entries
|
|
57
|
+
.map((entry) => ({
|
|
58
|
+
entry,
|
|
59
|
+
score: cosineSimilarity(queryEmbedding, entry.embedding),
|
|
60
|
+
}))
|
|
61
|
+
.filter((r) => r.score >= threshold)
|
|
62
|
+
.sort((a, b) => b.score - a.score)
|
|
63
|
+
.slice(0, limit)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delete(key: string): Promise<void> {
|
|
67
|
+
this.entries = this.entries.filter((e) => e.key !== key)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async clear(): Promise<void> {
|
|
71
|
+
this.entries = []
|
|
72
|
+
}
|
|
73
|
+
}
|
package/tsconfig.json
ADDED