@game_ryo/lsji 0.1.0 → 0.3.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 +15 -7
- package/src/cli.js +395 -62
- package/src/execution/budget/circuit-breaker.js +245 -0
- package/src/execution/budget/cost-tracker.js +387 -0
- package/src/execution/budget/index.js +63 -0
- package/src/execution/budget/token-counter.js +159 -0
- package/src/execution/engine.js +428 -0
- package/src/execution/hitl/approval-gate.js +210 -0
- package/src/execution/hitl/index.js +12 -0
- package/src/execution/hitl/notifier.js +151 -0
- package/src/execution/hitl/store.js +311 -0
- package/src/execution/idempotency.js +312 -0
- package/src/execution/index.js +14 -0
- package/src/index.js +80 -4
- package/src/llm/index.js +21 -0
- package/src/llm/llm-agent.js +357 -0
- package/src/llm/memory/conversation.js +271 -0
- package/src/llm/memory/episodic.js +312 -0
- package/src/llm/memory/index.js +12 -0
- package/src/llm/memory/semantic.js +324 -0
- package/src/llm/plugins/index.js +202 -0
- package/src/llm/prompt-manager.js +332 -0
- package/src/llm/providers/anthropic.js +250 -0
- package/src/llm/providers/base.js +116 -0
- package/src/llm/providers/local.js +163 -0
- package/src/llm/providers/openai.js +212 -0
- package/src/llm/tools/registry.js +342 -0
- package/src/server/index.js +416 -0
- package/src/server/ui/index.html +16 -0
- package/src/server/ui/package.json +19 -0
- package/src/server/ui/src/main.jsx +10 -0
- package/src/server/ui/src/styles.css +260 -0
- package/src/server/ui/vite.config.js +27 -0
- package/docs/README.md +0 -43
- package/docs/blog/2019-05-28-first-blog-post.mdx +0 -12
- package/docs/blog/2019-05-29-long-blog-post.mdx +0 -44
- package/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
- package/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
- package/docs/blog/2021-08-26-welcome/index.mdx +0 -29
- package/docs/blog/authors.yml +0 -25
- package/docs/blog/tags.yml +0 -19
- package/docs/docs/api/agent.md +0 -151
- package/docs/docs/api/env.md +0 -133
- package/docs/docs/api/environments.md +0 -102
- package/docs/docs/api/qlearning.md +0 -138
- package/docs/docs/api/storage.md +0 -168
- package/docs/docs/architecture.md +0 -155
- package/docs/docs/cli.md +0 -210
- package/docs/docs/contributing.md +0 -162
- package/docs/docs/core-concepts.md +0 -152
- package/docs/docs/examples/advanced-training.md +0 -244
- package/docs/docs/examples/custom-environment.md +0 -198
- package/docs/docs/examples/custom-storage.md +0 -251
- package/docs/docs/getting-started.md +0 -91
- package/docs/docusaurus.config.ts +0 -149
- package/docs/package-lock.json +0 -19522
- package/docs/package.json +0 -49
- package/docs/sidebars.ts +0 -33
- package/docs/src/components/HomepageFeatures/index.tsx +0 -71
- package/docs/src/components/HomepageFeatures/styles.module.css +0 -11
- package/docs/src/css/custom.css +0 -79
- package/docs/src/pages/index.module.css +0 -23
- package/docs/src/pages/index.tsx +0 -44
- package/docs/src/pages/markdown-page.mdx +0 -7
- package/docs/static/.nojekyll +0 -0
- package/docs/static/img/docusaurus-social-card.jpg +0 -0
- package/docs/static/img/docusaurus.png +0 -0
- package/docs/static/img/favicon.ico +0 -0
- package/docs/static/img/logo.png +0 -0
- package/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
- package/docs/static/img/undraw_docusaurus_react.svg +0 -170
- package/docs/static/img/undraw_docusaurus_tree.svg +0 -40
- package/docs/tsconfig.json +0 -12
- package/legacy/worker.js +0 -166
- package/legacy/wrangler.toml +0 -11
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Episodic Memory
|
|
3
|
+
*
|
|
4
|
+
* Event-based memory for tracking agent actions and outcomes.
|
|
5
|
+
* Useful for learning from past experiences.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createStorage } from '../../index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Episode record
|
|
12
|
+
* @typedef {Object} Episode
|
|
13
|
+
* @property {string} id - Unique ID
|
|
14
|
+
* @property {string} task - Task description
|
|
15
|
+
* @property {Array} steps - Steps taken
|
|
16
|
+
* @property {string} outcome - 'success' | 'failure' | 'partial'
|
|
17
|
+
* @property {Object} context - Initial context
|
|
18
|
+
* @property {Object} result - Final result
|
|
19
|
+
* @property {number} duration - Duration in ms
|
|
20
|
+
* @property {number} tokensUsed - Total tokens used
|
|
21
|
+
* @property {number} cost - Total cost
|
|
22
|
+
* @property {Date} startedAt
|
|
23
|
+
* @property {Date} completedAt
|
|
24
|
+
* @property {Array} [tags] - Tags for categorization
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Episodic Memory - Event-based experience storage
|
|
29
|
+
*/
|
|
30
|
+
export class EpisodicMemory {
|
|
31
|
+
constructor({ storage } = {}) {
|
|
32
|
+
this.storage = storage;
|
|
33
|
+
this.currentEpisode = null;
|
|
34
|
+
this.initialized = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize episodes table
|
|
39
|
+
*/
|
|
40
|
+
async initialize() {
|
|
41
|
+
if (this.initialized) return;
|
|
42
|
+
|
|
43
|
+
if (this.storage.db) {
|
|
44
|
+
await this.storage.db.exec(`
|
|
45
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
task TEXT NOT NULL,
|
|
48
|
+
steps TEXT NOT NULL,
|
|
49
|
+
outcome TEXT NOT NULL,
|
|
50
|
+
context TEXT NOT NULL,
|
|
51
|
+
result TEXT,
|
|
52
|
+
duration INTEGER,
|
|
53
|
+
tokens_used INTEGER,
|
|
54
|
+
cost REAL,
|
|
55
|
+
started_at TEXT NOT NULL,
|
|
56
|
+
completed_at TEXT,
|
|
57
|
+
tags TEXT
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
await this.storage.db.exec(`
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_outcome ON episodes(outcome)
|
|
63
|
+
`);
|
|
64
|
+
|
|
65
|
+
await this.storage.db.exec(`
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_started ON episodes(started_at)
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
await this.storage.db.exec(`
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_tags ON episodes(tags)
|
|
71
|
+
`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.initialized = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Start a new episode
|
|
79
|
+
*/
|
|
80
|
+
async startEpisode(task, context = {}, tags = []) {
|
|
81
|
+
await this.initialize();
|
|
82
|
+
|
|
83
|
+
this.currentEpisode = {
|
|
84
|
+
id: `episode_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
85
|
+
task,
|
|
86
|
+
steps: [],
|
|
87
|
+
outcome: 'in_progress',
|
|
88
|
+
context,
|
|
89
|
+
result: null,
|
|
90
|
+
duration: 0,
|
|
91
|
+
tokensUsed: 0,
|
|
92
|
+
cost: 0,
|
|
93
|
+
startedAt: new Date().toISOString(),
|
|
94
|
+
completedAt: null,
|
|
95
|
+
tags,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return this.currentEpisode.id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add a step to current episode
|
|
103
|
+
*/
|
|
104
|
+
async addStep(step) {
|
|
105
|
+
if (!this.currentEpisode) {
|
|
106
|
+
throw new Error('No active episode. Call startEpisode() first.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.currentEpisode.steps.push({
|
|
110
|
+
...step,
|
|
111
|
+
timestamp: new Date().toISOString(),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* End current episode
|
|
117
|
+
*/
|
|
118
|
+
async endEpisode(outcome, result = null) {
|
|
119
|
+
if (!this.currentEpisode) {
|
|
120
|
+
throw new Error('No active episode. Call startEpisode() first.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const completedAt = new Date().toISOString();
|
|
124
|
+
const startedAt = new Date(this.currentEpisode.startedAt);
|
|
125
|
+
|
|
126
|
+
this.currentEpisode.outcome = outcome;
|
|
127
|
+
this.currentEpisode.result = result;
|
|
128
|
+
this.currentEpisode.completedAt = completedAt;
|
|
129
|
+
this.currentEpisode.duration = new Date(completedAt).getTime() - startedAt.getTime();
|
|
130
|
+
|
|
131
|
+
// Persist
|
|
132
|
+
if (this.storage.db) {
|
|
133
|
+
await this.storage.db.run(
|
|
134
|
+
`INSERT INTO episodes (id, task, steps, outcome, context, result, duration, tokens_used, cost, started_at, completed_at, tags)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
136
|
+
[
|
|
137
|
+
this.currentEpisode.id,
|
|
138
|
+
this.currentEpisode.task,
|
|
139
|
+
JSON.stringify(this.currentEpisode.steps),
|
|
140
|
+
this.currentEpisode.outcome,
|
|
141
|
+
JSON.stringify(this.currentEpisode.context),
|
|
142
|
+
JSON.stringify(this.currentEpisode.result),
|
|
143
|
+
this.currentEpisode.duration,
|
|
144
|
+
this.currentEpisode.tokensUsed,
|
|
145
|
+
this.currentEpisode.cost,
|
|
146
|
+
this.currentEpisode.startedAt,
|
|
147
|
+
this.currentEpisode.completedAt,
|
|
148
|
+
JSON.stringify(this.currentEpisode.tags),
|
|
149
|
+
]
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
if (!this.memoryEpisodes) this.memoryEpisodes = new Map();
|
|
153
|
+
this.memoryEpisodes.set(this.currentEpisode.id, this.currentEpisode);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const episode = this.currentEpisode;
|
|
157
|
+
this.currentEpisode = null;
|
|
158
|
+
return episode;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Record token usage for current episode
|
|
163
|
+
*/
|
|
164
|
+
recordTokens(tokens) {
|
|
165
|
+
if (this.currentEpisode) {
|
|
166
|
+
this.currentEpisode.tokensUsed += tokens;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Record cost for current episode
|
|
172
|
+
*/
|
|
173
|
+
recordCost(cost) {
|
|
174
|
+
if (this.currentEpisode) {
|
|
175
|
+
this.currentEpisode.cost += cost;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get episode by ID
|
|
181
|
+
*/
|
|
182
|
+
async getEpisode(id) {
|
|
183
|
+
await this.initialize();
|
|
184
|
+
|
|
185
|
+
if (this.storage.db) {
|
|
186
|
+
const row = await this.storage.db.get('SELECT * FROM episodes WHERE id = ?', [id]);
|
|
187
|
+
if (!row) return null;
|
|
188
|
+
return this.rowToEpisode(row);
|
|
189
|
+
} else {
|
|
190
|
+
return this.memoryEpisodes?.get(id) || null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Search episodes
|
|
196
|
+
*/
|
|
197
|
+
async search({ outcome, tags = [], limit = 50, since = null } = {}) {
|
|
198
|
+
await this.initialize();
|
|
199
|
+
|
|
200
|
+
let results = [];
|
|
201
|
+
|
|
202
|
+
if (this.storage.db) {
|
|
203
|
+
let sql = 'SELECT * FROM episodes WHERE 1=1';
|
|
204
|
+
const params = [];
|
|
205
|
+
|
|
206
|
+
if (outcome) {
|
|
207
|
+
sql += ' AND outcome = ?';
|
|
208
|
+
params.push(outcome);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (tags.length > 0) {
|
|
212
|
+
const tagConditions = tags.map(() => 'tags LIKE ?').join(' OR ');
|
|
213
|
+
sql += ` AND (${tagConditions})`;
|
|
214
|
+
for (const tag of tags) {
|
|
215
|
+
params.push(`%"${tag}"%`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (since) {
|
|
220
|
+
sql += ' AND started_at >= ?';
|
|
221
|
+
params.push(since.toISOString());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
sql += ' ORDER BY started_at DESC LIMIT ?';
|
|
225
|
+
params.push(limit);
|
|
226
|
+
|
|
227
|
+
const rows = await this.storage.db.all(sql, params);
|
|
228
|
+
results = rows.map(r => this.rowToEpisode(r));
|
|
229
|
+
} else {
|
|
230
|
+
for (const ep of this.memoryEpisodes?.values() || []) {
|
|
231
|
+
if (outcome && ep.outcome !== outcome) continue;
|
|
232
|
+
if (tags.length > 0 && !tags.some(t => ep.tags.includes(t))) continue;
|
|
233
|
+
if (since && new Date(ep.startedAt) < since) continue;
|
|
234
|
+
results.push(ep);
|
|
235
|
+
}
|
|
236
|
+
results.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
|
|
237
|
+
results = results.slice(0, limit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get successful episodes for a task type
|
|
245
|
+
*/
|
|
246
|
+
async getSuccessfulEpisodes(taskPattern, limit = 10) {
|
|
247
|
+
const episodes = await this.search({ outcome: 'success', limit: 100 });
|
|
248
|
+
return episodes
|
|
249
|
+
.filter(e => e.task.includes(taskPattern))
|
|
250
|
+
.slice(0, limit);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get failure episodes for analysis
|
|
255
|
+
*/
|
|
256
|
+
async getFailures({ limit = 50, since = null } = {}) {
|
|
257
|
+
return this.search({ outcome: 'failure', limit, since });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get episode statistics
|
|
262
|
+
*/
|
|
263
|
+
async getStats() {
|
|
264
|
+
await this.initialize();
|
|
265
|
+
|
|
266
|
+
if (this.storage.db) {
|
|
267
|
+
const row = await this.storage.db.get(`
|
|
268
|
+
SELECT
|
|
269
|
+
COUNT(*) as total,
|
|
270
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
|
|
271
|
+
SUM(CASE WHEN outcome = 'failure' THEN 1 ELSE 0 END) as failures,
|
|
272
|
+
SUM(CASE WHEN outcome = 'partial' THEN 1 ELSE 0 END) as partials,
|
|
273
|
+
AVG(duration) as avg_duration,
|
|
274
|
+
AVG(tokens_used) as avg_tokens,
|
|
275
|
+
AVG(cost) as avg_cost
|
|
276
|
+
FROM episodes
|
|
277
|
+
`);
|
|
278
|
+
return row;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { total: 0, successes: 0, failures: 0, partials: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
rowToEpisode(row) {
|
|
285
|
+
return {
|
|
286
|
+
id: row.id,
|
|
287
|
+
task: row.task,
|
|
288
|
+
steps: JSON.parse(row.steps || '[]'),
|
|
289
|
+
outcome: row.outcome,
|
|
290
|
+
context: JSON.parse(row.context || '{}'),
|
|
291
|
+
result: row.result ? JSON.parse(row.result) : null,
|
|
292
|
+
duration: row.duration,
|
|
293
|
+
tokensUsed: row.tokens_used,
|
|
294
|
+
cost: row.cost,
|
|
295
|
+
startedAt: row.started_at,
|
|
296
|
+
completedAt: row.completed_at,
|
|
297
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Create episodic memory from config
|
|
304
|
+
*/
|
|
305
|
+
export async function createEpisodicMemory(config = {}) {
|
|
306
|
+
const storage = await createStorage(
|
|
307
|
+
config.storage?.type || 'sqlite',
|
|
308
|
+
config.storage?.options || {}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return new EpisodicMemory({ storage });
|
|
312
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Systems
|
|
3
|
+
*
|
|
4
|
+
* Exports all memory components:
|
|
5
|
+
* - ConversationMemory: Short-term conversation history
|
|
6
|
+
* - SemanticMemory: Long-term knowledge with retrieval
|
|
7
|
+
* - EpisodicMemory: Event-based experience tracking
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { ConversationMemory, createConversationMemory } from './conversation.js';
|
|
11
|
+
export { SemanticMemory, createSemanticMemory } from './semantic.js';
|
|
12
|
+
export { EpisodicMemory, createEpisodicMemory } from './episodic.js';
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Memory
|
|
3
|
+
*
|
|
4
|
+
* Long-term knowledge storage with vector embeddings for retrieval.
|
|
5
|
+
* Uses simple keyword-based retrieval (can be extended with vector DB).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createStorage } from '../../index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Knowledge entry
|
|
12
|
+
* @typedef {Object} KnowledgeEntry
|
|
13
|
+
* @property {string} id - Unique ID
|
|
14
|
+
* @property {string} content - Knowledge content
|
|
15
|
+
* @property {Array<string>} tags - Tags for categorization
|
|
16
|
+
* @property {Object} metadata - Additional metadata
|
|
17
|
+
* @property {number} [embedding] - Vector embedding (placeholder)
|
|
18
|
+
* @property {Date} createdAt
|
|
19
|
+
* @property {Date} updatedAt
|
|
20
|
+
* @property {number} accessCount - Number of times accessed
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Semantic Memory - Long-term knowledge storage
|
|
25
|
+
*/
|
|
26
|
+
export class SemanticMemory {
|
|
27
|
+
constructor({ storage, embedder } = {}) {
|
|
28
|
+
this.storage = storage;
|
|
29
|
+
this.embedder = embedder; // Function to generate embeddings
|
|
30
|
+
this.initialized = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Initialize knowledge table
|
|
35
|
+
*/
|
|
36
|
+
async initialize() {
|
|
37
|
+
if (this.initialized) return;
|
|
38
|
+
|
|
39
|
+
if (this.storage.db) {
|
|
40
|
+
await this.storage.db.exec(`
|
|
41
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
42
|
+
id TEXT PRIMARY KEY,
|
|
43
|
+
content TEXT NOT NULL,
|
|
44
|
+
tags TEXT NOT NULL,
|
|
45
|
+
metadata TEXT,
|
|
46
|
+
embedding TEXT,
|
|
47
|
+
created_at TEXT NOT NULL,
|
|
48
|
+
updated_at TEXT NOT NULL,
|
|
49
|
+
access_count INTEGER DEFAULT 0
|
|
50
|
+
)
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
await this.storage.db.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_tags ON knowledge(tags)
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
await this.storage.db.exec(`
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_knowledge_created ON knowledge(created_at)
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.initialized = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Store knowledge
|
|
67
|
+
*/
|
|
68
|
+
async store(content, { tags = [], metadata = {}, id = null } = {}) {
|
|
69
|
+
await this.initialize();
|
|
70
|
+
|
|
71
|
+
const entryId = id || `knowledge_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
72
|
+
const now = new Date().toISOString();
|
|
73
|
+
|
|
74
|
+
let embedding = null;
|
|
75
|
+
if (this.embedder) {
|
|
76
|
+
embedding = await this.embedder(content);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entry = {
|
|
80
|
+
id: entryId,
|
|
81
|
+
content,
|
|
82
|
+
tags: JSON.stringify(tags),
|
|
83
|
+
metadata: JSON.stringify(metadata),
|
|
84
|
+
embedding: embedding ? JSON.stringify(embedding) : null,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
accessCount: 0,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (this.storage.db) {
|
|
91
|
+
await this.storage.db.run(
|
|
92
|
+
`INSERT INTO knowledge (id, content, tags, metadata, embedding, created_at, updated_at, access_count)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
94
|
+
[entry.id, entry.content, entry.tags, entry.metadata, entry.embedding, entry.createdAt, entry.updatedAt, entry.accessCount]
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
if (!this.memoryKnowledge) this.memoryKnowledge = new Map();
|
|
98
|
+
this.memoryKnowledge.set(entryId, entry);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return entry;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieve knowledge by query
|
|
106
|
+
*/
|
|
107
|
+
async retrieve(query, { limit = 10, tags = [] } = {}) {
|
|
108
|
+
await this.initialize();
|
|
109
|
+
|
|
110
|
+
let results = [];
|
|
111
|
+
|
|
112
|
+
if (this.storage.db) {
|
|
113
|
+
let sql = 'SELECT * FROM knowledge WHERE 1=1';
|
|
114
|
+
const params = [];
|
|
115
|
+
|
|
116
|
+
// Add tag filter
|
|
117
|
+
if (tags.length > 0) {
|
|
118
|
+
const tagConditions = tags.map(() => 'tags LIKE ?').join(' OR ');
|
|
119
|
+
sql += ` AND (${tagConditions})`;
|
|
120
|
+
for (const tag of tags) {
|
|
121
|
+
params.push(`%"${tag}"%`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sql += ' ORDER BY access_count DESC, created_at DESC LIMIT ?';
|
|
126
|
+
params.push(limit);
|
|
127
|
+
|
|
128
|
+
const rows = await this.storage.db.all(sql, params);
|
|
129
|
+
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
results.push(this.rowToEntry(row));
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
for (const entry of this.memoryKnowledge?.values() || []) {
|
|
135
|
+
if (tags.length === 0 || tags.some(t => entry.tags.includes(t))) {
|
|
136
|
+
results.push(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
results.sort((a, b) => b.accessCount - a.accessCount);
|
|
140
|
+
results = results.slice(0, limit);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Simple keyword relevance scoring
|
|
144
|
+
const queryWords = query.toLowerCase().split(/\s+/);
|
|
145
|
+
for (const entry of results) {
|
|
146
|
+
const contentWords = entry.content.toLowerCase().split(/\s+/);
|
|
147
|
+
let score = 0;
|
|
148
|
+
for (const qw of queryWords) {
|
|
149
|
+
for (const cw of contentWords) {
|
|
150
|
+
if (cw.includes(qw) || qw.includes(cw)) score++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
entry.relevanceScore = score;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Sort by relevance
|
|
157
|
+
results.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0));
|
|
158
|
+
|
|
159
|
+
// Update access count
|
|
160
|
+
for (const entry of results) {
|
|
161
|
+
await this.incrementAccess(entry.id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get knowledge by ID
|
|
169
|
+
*/
|
|
170
|
+
async get(id) {
|
|
171
|
+
await this.initialize();
|
|
172
|
+
|
|
173
|
+
if (this.storage.db) {
|
|
174
|
+
const row = await this.storage.db.get('SELECT * FROM knowledge WHERE id = ?', [id]);
|
|
175
|
+
if (!row) return null;
|
|
176
|
+
return this.rowToEntry(row);
|
|
177
|
+
} else {
|
|
178
|
+
return this.memoryKnowledge?.get(id) || null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update knowledge
|
|
184
|
+
*/
|
|
185
|
+
async update(id, { content, tags, metadata } = {}) {
|
|
186
|
+
await this.initialize();
|
|
187
|
+
|
|
188
|
+
const entry = await this.get(id);
|
|
189
|
+
if (!entry) throw new Error(`Knowledge not found: ${id}`);
|
|
190
|
+
|
|
191
|
+
const updates = [];
|
|
192
|
+
const params = [];
|
|
193
|
+
|
|
194
|
+
if (content !== undefined) {
|
|
195
|
+
entry.content = content;
|
|
196
|
+
updates.push('content = ?');
|
|
197
|
+
params.push(content);
|
|
198
|
+
|
|
199
|
+
if (this.embedder) {
|
|
200
|
+
entry.embedding = JSON.stringify(await this.embedder(content));
|
|
201
|
+
updates.push('embedding = ?');
|
|
202
|
+
params.push(entry.embedding);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (tags !== undefined) {
|
|
207
|
+
entry.tags = JSON.stringify(tags);
|
|
208
|
+
updates.push('tags = ?');
|
|
209
|
+
params.push(entry.tags);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (metadata !== undefined) {
|
|
213
|
+
entry.metadata = JSON.stringify({ ...JSON.parse(entry.metadata), ...metadata });
|
|
214
|
+
updates.push('metadata = ?');
|
|
215
|
+
params.push(entry.metadata);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
entry.updatedAt = new Date().toISOString();
|
|
219
|
+
updates.push('updated_at = ?');
|
|
220
|
+
params.push(entry.updatedAt);
|
|
221
|
+
|
|
222
|
+
params.push(id);
|
|
223
|
+
|
|
224
|
+
if (this.storage.db) {
|
|
225
|
+
await this.storage.db.run(
|
|
226
|
+
`UPDATE knowledge SET ${updates.join(', ')} WHERE id = ?`,
|
|
227
|
+
params
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
this.memoryKnowledge.set(id, entry);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return entry;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Delete knowledge
|
|
238
|
+
*/
|
|
239
|
+
async delete(id) {
|
|
240
|
+
await this.initialize();
|
|
241
|
+
|
|
242
|
+
if (this.storage.db) {
|
|
243
|
+
await this.storage.db.run('DELETE FROM knowledge WHERE id = ?', [id]);
|
|
244
|
+
} else {
|
|
245
|
+
this.memoryKnowledge?.delete(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Increment access count
|
|
251
|
+
*/
|
|
252
|
+
async incrementAccess(id) {
|
|
253
|
+
await this.initialize();
|
|
254
|
+
|
|
255
|
+
if (this.storage.db) {
|
|
256
|
+
await this.storage.db.run(
|
|
257
|
+
'UPDATE knowledge SET access_count = access_count + 1 WHERE id = ?',
|
|
258
|
+
[id]
|
|
259
|
+
);
|
|
260
|
+
} else {
|
|
261
|
+
const entry = this.memoryKnowledge?.get(id);
|
|
262
|
+
if (entry) {
|
|
263
|
+
entry.accessCount++;
|
|
264
|
+
this.memoryKnowledge.set(id, entry);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* List all knowledge
|
|
271
|
+
*/
|
|
272
|
+
async list({ limit = 100, tags = [] } = {}) {
|
|
273
|
+
await this.initialize();
|
|
274
|
+
|
|
275
|
+
if (this.storage.db) {
|
|
276
|
+
let sql = 'SELECT * FROM knowledge WHERE 1=1';
|
|
277
|
+
const params = [];
|
|
278
|
+
|
|
279
|
+
if (tags.length > 0) {
|
|
280
|
+
const tagConditions = tags.map(() => 'tags LIKE ?').join(' OR ');
|
|
281
|
+
sql += ` AND (${tagConditions})`;
|
|
282
|
+
for (const tag of tags) {
|
|
283
|
+
params.push(`%"${tag}"%`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
288
|
+
params.push(limit);
|
|
289
|
+
|
|
290
|
+
const rows = await this.storage.db.all(sql, params);
|
|
291
|
+
return rows.map(r => this.rowToEntry(r));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
rowToEntry(row) {
|
|
298
|
+
return {
|
|
299
|
+
id: row.id,
|
|
300
|
+
content: row.content,
|
|
301
|
+
tags: JSON.parse(row.tags || '[]'),
|
|
302
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
303
|
+
embedding: row.embedding ? JSON.parse(row.embedding) : null,
|
|
304
|
+
createdAt: row.created_at,
|
|
305
|
+
updatedAt: row.updated_at,
|
|
306
|
+
accessCount: row.access_count || 0,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Create semantic memory from config
|
|
313
|
+
*/
|
|
314
|
+
export async function createSemanticMemory(config = {}) {
|
|
315
|
+
const storage = await createStorage(
|
|
316
|
+
config.storage?.type || 'sqlite',
|
|
317
|
+
config.storage?.options || {}
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return new SemanticMemory({
|
|
321
|
+
storage,
|
|
322
|
+
embedder: config.embedder,
|
|
323
|
+
});
|
|
324
|
+
}
|