@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,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Agent
|
|
3
|
+
*
|
|
4
|
+
* Production-grade LLM-based agent with ReAct pattern, tools, memory,
|
|
5
|
+
* HITL approval, budget control, and durability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createProvider } from './providers/base.js';
|
|
9
|
+
import { createToolRegistry } from './tools/registry.js';
|
|
10
|
+
import { createConversationMemory } from './memory/conversation.js';
|
|
11
|
+
import { createSemanticMemory } from './memory/semantic.js';
|
|
12
|
+
import { createEpisodicMemory } from './memory/episodic.js';
|
|
13
|
+
import { createPromptManager } from './prompt-manager.js';
|
|
14
|
+
import { createExecutionEngine } from '../execution/engine.js';
|
|
15
|
+
import { createApprovalGate } from '../execution/hitl/approval-gate.js';
|
|
16
|
+
import { createBudgetController } from '../execution/budget/index.js';
|
|
17
|
+
import { IdempotencyStore } from '../execution/idempotency.js';
|
|
18
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* LLM Agent Configuration
|
|
22
|
+
* @typedef {Object} LLMAgentConfig
|
|
23
|
+
* @property {Object} llm - LLM provider config { provider, model, apiKey, ... }
|
|
24
|
+
* @property {Object} [tools] - Tool registry config
|
|
25
|
+
* @property {Object} [memory] - Memory config { conversation, semantic, episodic }
|
|
26
|
+
* @property {Object} [execution] - Execution engine config
|
|
27
|
+
* @property {Object} [hitl] - HITL approval config
|
|
28
|
+
* @property {Object} [budget] - Budget control config
|
|
29
|
+
* @property {Object} [idempotency] - Idempotency config
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* LLM Agent - Main agent class
|
|
34
|
+
*/
|
|
35
|
+
export class LLMAgent {
|
|
36
|
+
constructor(config = {}) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.llm = null;
|
|
39
|
+
this.tools = null;
|
|
40
|
+
this.memory = {};
|
|
41
|
+
this.execution = null;
|
|
42
|
+
this.hitl = null;
|
|
43
|
+
this.budget = null;
|
|
44
|
+
this.idempotency = null;
|
|
45
|
+
this.promptManager = null;
|
|
46
|
+
this.initialized = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize all components
|
|
51
|
+
*/
|
|
52
|
+
async initialize() {
|
|
53
|
+
if (this.initialized) return;
|
|
54
|
+
|
|
55
|
+
// Initialize LLM provider
|
|
56
|
+
this.llm = await createProvider(this.config.llm || { provider: 'openai', model: 'gpt-4o-mini' });
|
|
57
|
+
|
|
58
|
+
// Validate LLM
|
|
59
|
+
const valid = await this.llm.validate();
|
|
60
|
+
if (!valid.valid) {
|
|
61
|
+
throw new Error(`LLM validation failed: ${valid.error}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Initialize tool registry
|
|
65
|
+
this.tools = createToolRegistry({
|
|
66
|
+
idempotencyStore: this.idempotency,
|
|
67
|
+
approvalGate: this.hitl,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Initialize memory systems
|
|
71
|
+
if (this.config.memory?.conversation !== false) {
|
|
72
|
+
this.memory.conversation = await createConversationMemory(this.config.memory?.conversation);
|
|
73
|
+
await this.memory.conversation.startSession(this.config.sessionId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.config.memory?.semantic) {
|
|
77
|
+
this.memory.semantic = await createSemanticMemory(this.config.memory.semantic);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.config.memory?.episodic) {
|
|
81
|
+
this.memory.episodic = await createEpisodicMemory(this.config.memory.episodic);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize execution engine
|
|
85
|
+
this.execution = await createExecutionEngine(this.config.execution);
|
|
86
|
+
|
|
87
|
+
// Initialize HITL
|
|
88
|
+
if (this.config.hitl?.enabled !== false) {
|
|
89
|
+
this.hitl = await createApprovalGate(this.config.hitl);
|
|
90
|
+
// Update tool registry with HITL
|
|
91
|
+
this.tools.approvalGate = this.hitl;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Initialize budget
|
|
95
|
+
this.budget = createBudgetController(this.config.budget);
|
|
96
|
+
|
|
97
|
+
// Initialize idempotency
|
|
98
|
+
this.idempotency = await IdempotencyStore.create(this.config.idempotency);
|
|
99
|
+
this.tools.idempotencyStore = this.idempotency;
|
|
100
|
+
|
|
101
|
+
// Initialize prompt manager
|
|
102
|
+
this.promptManager = await createPromptManager(this.config.prompts);
|
|
103
|
+
|
|
104
|
+
this.initialized = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run the agent on a task
|
|
109
|
+
*/
|
|
110
|
+
async run(task, options = {}) {
|
|
111
|
+
await this.initialize();
|
|
112
|
+
|
|
113
|
+
const runId = options.runId || `run_${Date.now()}_${uuidv4().slice(0, 8)}`;
|
|
114
|
+
const budgetId = options.budgetId || runId;
|
|
115
|
+
const checkpointId = options.checkpointId;
|
|
116
|
+
const hitlRequired = options.hitlRequired || [];
|
|
117
|
+
const maxSteps = options.maxSteps || 50;
|
|
118
|
+
|
|
119
|
+
// Start episodic memory
|
|
120
|
+
let episodeId = null;
|
|
121
|
+
if (this.memory.episodic) {
|
|
122
|
+
episodeId = await this.memory.episodic.startEpisode(task, { runId, options });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Get system prompt
|
|
126
|
+
const systemPrompt = await this.promptManager.render('system:react', {
|
|
127
|
+
tools: JSON.stringify(this.tools.getDefinitions(), null, 2),
|
|
128
|
+
task,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Build initial messages
|
|
132
|
+
const messages = [
|
|
133
|
+
{ role: 'system', content: systemPrompt },
|
|
134
|
+
{ role: 'user', content: task },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// Add conversation history if available
|
|
138
|
+
if (this.memory.conversation) {
|
|
139
|
+
const history = await this.memory.conversation.getMessagesForLLM(50000);
|
|
140
|
+
messages.splice(1, 0, ...history);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let step = 0;
|
|
144
|
+
let finalAnswer = null;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
while (step < maxSteps) {
|
|
148
|
+
step++;
|
|
149
|
+
|
|
150
|
+
// Check budget before each step
|
|
151
|
+
const budgetCheck = await this.budget.checkBudget(budgetId, 0, 0);
|
|
152
|
+
if (!budgetCheck.allowed) {
|
|
153
|
+
throw new Error(`Budget exceeded: ${budgetCheck.errors.map(e => e.type).join(', ')}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generate response
|
|
157
|
+
const response = await this.llm.generate(messages, {
|
|
158
|
+
tools: this.tools.getDefinitions(),
|
|
159
|
+
toolChoice: 'auto',
|
|
160
|
+
temperature: 0.7,
|
|
161
|
+
maxTokens: 4096,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Record token usage
|
|
165
|
+
if (this.memory.episodic) {
|
|
166
|
+
this.memory.episodic.recordTokens(response.usage?.totalTokens || 0);
|
|
167
|
+
}
|
|
168
|
+
this.budget.recordCost({
|
|
169
|
+
budgetId,
|
|
170
|
+
...response.usage,
|
|
171
|
+
model: this.llm.getModel(),
|
|
172
|
+
provider: this.config.llm?.provider || 'openai',
|
|
173
|
+
cost: this.llm.calculateCost?.(response.usage) || 0,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Add assistant message
|
|
177
|
+
messages.push({
|
|
178
|
+
role: 'assistant',
|
|
179
|
+
content: response.content,
|
|
180
|
+
tool_calls: response.toolCalls,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (this.memory.conversation) {
|
|
184
|
+
await this.memory.conversation.addMessage(messages[messages.length - 1]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check for tool calls
|
|
188
|
+
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
189
|
+
for (const toolCall of response.toolCalls) {
|
|
190
|
+
const toolName = toolCall.function.name;
|
|
191
|
+
const toolArgs = JSON.parse(toolCall.function.arguments);
|
|
192
|
+
|
|
193
|
+
// Check if tool requires HITL
|
|
194
|
+
const tool = this.tools.get(toolName);
|
|
195
|
+
if (tool?.requiresApproval && hitlRequired.includes(toolName)) {
|
|
196
|
+
// Request approval
|
|
197
|
+
try {
|
|
198
|
+
await this.hitl.requestApproval({
|
|
199
|
+
action: toolName,
|
|
200
|
+
context: { params: toolArgs, runId },
|
|
201
|
+
requester: 'agent',
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Approval denied or timeout
|
|
205
|
+
const errorMsg = `Tool ${toolName} requires approval: ${error.message}`;
|
|
206
|
+
messages.push({
|
|
207
|
+
role: 'tool',
|
|
208
|
+
tool_call_id: toolCall.id,
|
|
209
|
+
content: errorMsg,
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Execute tool
|
|
216
|
+
let result;
|
|
217
|
+
try {
|
|
218
|
+
result = await this.tools.execute(toolName, toolArgs, { runId, budgetId });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
result = { error: error.message };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Record tool execution in episodic memory
|
|
224
|
+
if (this.memory.episodic) {
|
|
225
|
+
await this.memory.episodic.addStep({
|
|
226
|
+
type: 'tool',
|
|
227
|
+
tool: toolName,
|
|
228
|
+
params: toolArgs,
|
|
229
|
+
result,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Add tool result
|
|
234
|
+
messages.push({
|
|
235
|
+
role: 'tool',
|
|
236
|
+
tool_call_id: toolCall.id,
|
|
237
|
+
content: JSON.stringify(result),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (this.memory.conversation) {
|
|
241
|
+
await this.memory.conversation.addMessage(messages[messages.length - 1]);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Continue loop for next LLM call
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// No tool calls - check if final answer
|
|
250
|
+
if (response.content && !response.content.includes('THOUGHT:') && !response.content.includes('ACTION:')) {
|
|
251
|
+
finalAnswer = response.content;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Parse ReAct format if present
|
|
256
|
+
if (response.content.includes('THOUGHT:') || response.content.includes('ACTION:')) {
|
|
257
|
+
// This is a ReAct formatted response without tool calls
|
|
258
|
+
// Continue to next iteration
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Default: treat as final answer
|
|
263
|
+
finalAnswer = response.content;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// End episode
|
|
268
|
+
if (this.memory.episodic) {
|
|
269
|
+
await this.memory.episodic.endEpisode(finalAnswer ? 'success' : 'partial', { answer: finalAnswer, steps: step });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
success: !!finalAnswer,
|
|
274
|
+
answer: finalAnswer,
|
|
275
|
+
runId,
|
|
276
|
+
steps: step,
|
|
277
|
+
budget: this.budget.getStatus(budgetId),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
} catch (error) {
|
|
281
|
+
if (this.memory.episodic) {
|
|
282
|
+
await this.memory.episodic.endEpisode('failure', { error: error.message, steps: step });
|
|
283
|
+
}
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Run with durability (checkpointing)
|
|
290
|
+
*/
|
|
291
|
+
async runDurable(task, options = {}) {
|
|
292
|
+
await this.initialize();
|
|
293
|
+
|
|
294
|
+
const workflowId = options.workflowId || `workflow_${Date.now()}`;
|
|
295
|
+
|
|
296
|
+
return this.execution.execute({
|
|
297
|
+
id: workflowId,
|
|
298
|
+
name: task,
|
|
299
|
+
execute: async (context) => {
|
|
300
|
+
return this.run(task, { ...options, ...context });
|
|
301
|
+
},
|
|
302
|
+
}, {
|
|
303
|
+
workflowId,
|
|
304
|
+
checkpointEvery: options.checkpointEvery || 3,
|
|
305
|
+
idempotencyKey: options.idempotencyKey,
|
|
306
|
+
resumeFrom: options.resumeFrom,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Resume from checkpoint
|
|
312
|
+
*/
|
|
313
|
+
async resume(checkpointId) {
|
|
314
|
+
await this.initialize();
|
|
315
|
+
return this.execution.execute({ id: 'resume', execute: async () => {} }, { resumeFrom: checkpointId });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get agent status
|
|
320
|
+
*/
|
|
321
|
+
getStatus() {
|
|
322
|
+
return {
|
|
323
|
+
initialized: this.initialized,
|
|
324
|
+
llm: this.llm?.getModel(),
|
|
325
|
+
tools: this.tools?.getAll().map(t => t.name) || [],
|
|
326
|
+
memory: {
|
|
327
|
+
conversation: this.memory.conversation?.getSummary(),
|
|
328
|
+
semantic: this.memory.semantic ? 'enabled' : 'disabled',
|
|
329
|
+
episodic: this.memory.episodic ? 'enabled' : 'disabled',
|
|
330
|
+
},
|
|
331
|
+
budget: this.budget?.getStatus(),
|
|
332
|
+
hitl: this.hitl ? 'enabled' : 'disabled',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Shutdown
|
|
338
|
+
*/
|
|
339
|
+
async shutdown() {
|
|
340
|
+
if (this.memory.conversation) {
|
|
341
|
+
await this.memory.conversation.clear();
|
|
342
|
+
}
|
|
343
|
+
if (this.execution?.storage) {
|
|
344
|
+
await this.execution.storage.close();
|
|
345
|
+
}
|
|
346
|
+
this.initialized = false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create LLM agent from config
|
|
352
|
+
*/
|
|
353
|
+
export async function createLLMAgent(config = {}) {
|
|
354
|
+
const agent = new LLMAgent(config);
|
|
355
|
+
await agent.initialize();
|
|
356
|
+
return agent;
|
|
357
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Memory
|
|
3
|
+
*
|
|
4
|
+
* Manages conversation history for LLM agents.
|
|
5
|
+
* Supports token-aware truncation and persistence.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createStorage } from '../../index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Conversation message
|
|
12
|
+
* @typedef {Object} Message
|
|
13
|
+
* @property {string} role - 'system' | 'user' | 'assistant' | 'tool'
|
|
14
|
+
* @property {string} content - Message content
|
|
15
|
+
* @property {string} [name] - Name for tool messages
|
|
16
|
+
* @property {string} [tool_call_id] - Tool call ID
|
|
17
|
+
* @property {Array} [tool_calls] - Tool calls from assistant
|
|
18
|
+
* @property {Date} [timestamp] - Message timestamp
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Conversation Memory - Stores and manages conversation history
|
|
23
|
+
*/
|
|
24
|
+
export class ConversationMemory {
|
|
25
|
+
constructor({ storage, maxTokens = 100000, tokenCounter } = {}) {
|
|
26
|
+
this.storage = storage;
|
|
27
|
+
this.maxTokens = maxTokens;
|
|
28
|
+
this.tokenCounter = tokenCounter;
|
|
29
|
+
this.messages = [];
|
|
30
|
+
this.sessionId = null;
|
|
31
|
+
this.initialized = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize conversation table
|
|
36
|
+
*/
|
|
37
|
+
async initialize() {
|
|
38
|
+
if (this.initialized) return;
|
|
39
|
+
|
|
40
|
+
if (this.storage.db) {
|
|
41
|
+
await this.storage.db.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
session_id TEXT NOT NULL,
|
|
45
|
+
role TEXT NOT NULL,
|
|
46
|
+
content TEXT NOT NULL,
|
|
47
|
+
name TEXT,
|
|
48
|
+
tool_call_id TEXT,
|
|
49
|
+
tool_calls TEXT,
|
|
50
|
+
tokens INTEGER,
|
|
51
|
+
created_at TEXT NOT NULL
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
await this.storage.db.exec(`
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_session ON conversations(session_id)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
await this.storage.db.exec(`
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_created ON conversations(created_at)
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.initialized = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Start a new conversation session
|
|
69
|
+
*/
|
|
70
|
+
async startSession(sessionId = null) {
|
|
71
|
+
await this.initialize();
|
|
72
|
+
this.sessionId = sessionId || `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
73
|
+
this.messages = [];
|
|
74
|
+
|
|
75
|
+
if (!sessionId) {
|
|
76
|
+
// Load existing if sessionId provided
|
|
77
|
+
await this.loadSession(this.sessionId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return this.sessionId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load conversation from storage
|
|
85
|
+
*/
|
|
86
|
+
async loadSession(sessionId) {
|
|
87
|
+
await this.initialize();
|
|
88
|
+
|
|
89
|
+
if (this.storage.db) {
|
|
90
|
+
const rows = await this.storage.db.all(
|
|
91
|
+
'SELECT * FROM conversations WHERE session_id = ? ORDER BY created_at ASC',
|
|
92
|
+
[sessionId]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
this.messages = rows.map(row => ({
|
|
96
|
+
role: row.role,
|
|
97
|
+
content: row.content,
|
|
98
|
+
name: row.name,
|
|
99
|
+
tool_call_id: row.tool_call_id,
|
|
100
|
+
tool_calls: row.tool_calls ? JSON.parse(row.tool_calls) : undefined,
|
|
101
|
+
timestamp: row.created_at,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this.messages;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Add a message to conversation
|
|
110
|
+
*/
|
|
111
|
+
async addMessage(message) {
|
|
112
|
+
await this.initialize();
|
|
113
|
+
|
|
114
|
+
const msg = {
|
|
115
|
+
role: message.role,
|
|
116
|
+
content: message.content,
|
|
117
|
+
name: message.name,
|
|
118
|
+
tool_call_id: message.tool_call_id,
|
|
119
|
+
tool_calls: message.tool_calls,
|
|
120
|
+
timestamp: message.timestamp || new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.messages.push(msg);
|
|
124
|
+
|
|
125
|
+
// Persist to storage
|
|
126
|
+
if (this.storage.db && this.sessionId) {
|
|
127
|
+
const tokens = this.tokenCounter
|
|
128
|
+
? await this.tokenCounter.estimateTokens([msg], { provider: 'openai', model: 'gpt-4o-mini' })
|
|
129
|
+
: 0;
|
|
130
|
+
|
|
131
|
+
await this.storage.db.run(
|
|
132
|
+
`INSERT INTO conversations (id, session_id, role, content, name, tool_call_id, tool_calls, tokens, created_at)
|
|
133
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
134
|
+
[
|
|
135
|
+
`msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
136
|
+
this.sessionId,
|
|
137
|
+
msg.role,
|
|
138
|
+
msg.content,
|
|
139
|
+
msg.name || null,
|
|
140
|
+
msg.tool_call_id || null,
|
|
141
|
+
msg.tool_calls ? JSON.stringify(msg.tool_calls) : null,
|
|
142
|
+
tokens,
|
|
143
|
+
msg.timestamp,
|
|
144
|
+
]
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Trim if needed
|
|
149
|
+
await this.trimIfNeeded();
|
|
150
|
+
|
|
151
|
+
return msg;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Add multiple messages
|
|
156
|
+
*/
|
|
157
|
+
async addMessages(messages) {
|
|
158
|
+
for (const msg of messages) {
|
|
159
|
+
await this.addMessage(msg);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get all messages
|
|
165
|
+
*/
|
|
166
|
+
getMessages() {
|
|
167
|
+
return [...this.messages];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get messages for LLM (with token limit)
|
|
172
|
+
*/
|
|
173
|
+
async getMessagesForLLM(maxTokens = null) {
|
|
174
|
+
const limit = maxTokens || this.maxTokens;
|
|
175
|
+
let totalTokens = 0;
|
|
176
|
+
const result = [];
|
|
177
|
+
|
|
178
|
+
// Add messages from newest to oldest until token limit
|
|
179
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
180
|
+
const msg = this.messages[i];
|
|
181
|
+
const msgTokens = this.tokenCounter
|
|
182
|
+
? await this.tokenCounter.estimateTokens([msg], { provider: 'openai', model: 'gpt-4o-mini' })
|
|
183
|
+
: Math.ceil(msg.content.length / 4);
|
|
184
|
+
|
|
185
|
+
if (totalTokens + msgTokens > limit && result.length > 0) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
totalTokens += msgTokens;
|
|
190
|
+
result.unshift(msg);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Trim old messages if over token limit
|
|
198
|
+
*/
|
|
199
|
+
async trimIfNeeded() {
|
|
200
|
+
if (!this.tokenCounter) return;
|
|
201
|
+
|
|
202
|
+
const tokens = await this.tokenCounter.estimateTokens(
|
|
203
|
+
this.messages,
|
|
204
|
+
{ provider: 'openai', model: 'gpt-4o-mini' }
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (tokens > this.maxTokens) {
|
|
208
|
+
// Remove oldest messages (but keep system message)
|
|
209
|
+
const systemMessages = this.messages.filter(m => m.role === 'system');
|
|
210
|
+
const otherMessages = this.messages.filter(m => m.role !== 'system');
|
|
211
|
+
|
|
212
|
+
// Keep removing oldest non-system messages until under limit
|
|
213
|
+
while (otherMessages.length > 0) {
|
|
214
|
+
otherMessages.shift();
|
|
215
|
+
const remainingTokens = await this.tokenCounter.estimateTokens(
|
|
216
|
+
[...systemMessages, ...otherMessages],
|
|
217
|
+
{ provider: 'openai', model: 'gpt-4o-mini' }
|
|
218
|
+
);
|
|
219
|
+
if (remainingTokens <= this.maxTokens) break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.messages = [...systemMessages, ...otherMessages];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Clear conversation
|
|
228
|
+
*/
|
|
229
|
+
async clear() {
|
|
230
|
+
this.messages = [];
|
|
231
|
+
|
|
232
|
+
if (this.storage.db && this.sessionId) {
|
|
233
|
+
await this.storage.db.run(
|
|
234
|
+
'DELETE FROM conversations WHERE session_id = ?',
|
|
235
|
+
[this.sessionId]
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get conversation summary
|
|
242
|
+
*/
|
|
243
|
+
getSummary() {
|
|
244
|
+
return {
|
|
245
|
+
sessionId: this.sessionId,
|
|
246
|
+
messageCount: this.messages.length,
|
|
247
|
+
roles: this.messages.reduce((acc, m) => {
|
|
248
|
+
acc[m.role] = (acc[m.role] || 0) + 1;
|
|
249
|
+
return acc;
|
|
250
|
+
}, {}),
|
|
251
|
+
firstMessage: this.messages[0]?.timestamp,
|
|
252
|
+
lastMessage: this.messages[this.messages.length - 1]?.timestamp,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Create conversation memory from config
|
|
259
|
+
*/
|
|
260
|
+
export async function createConversationMemory(config = {}) {
|
|
261
|
+
const storage = await createStorage(
|
|
262
|
+
config.storage?.type || 'sqlite',
|
|
263
|
+
config.storage?.options || {}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return new ConversationMemory({
|
|
267
|
+
storage,
|
|
268
|
+
maxTokens: config.maxTokens || 100000,
|
|
269
|
+
tokenCounter: config.tokenCounter,
|
|
270
|
+
});
|
|
271
|
+
}
|