xiaozuoassistant 0.1.41
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/README.md +90 -0
- package/bin/cli.js +429 -0
- package/config.json +36 -0
- package/dist/client/assets/browser-ponyfill-DNlTAU2D.js +2 -0
- package/dist/client/assets/index-BPATxdcV.js +153 -0
- package/dist/client/assets/index-CgL5gMVL.css +1 -0
- package/dist/client/favicon.svg +4 -0
- package/dist/client/index.html +354 -0
- package/dist/client/locales/en/translation.json +77 -0
- package/dist/client/locales/zh/translation.json +77 -0
- package/dist/server/agents/office.js +23 -0
- package/dist/server/app.js +50 -0
- package/dist/server/channels/base-channel.js +13 -0
- package/dist/server/channels/create-channels.js +18 -0
- package/dist/server/channels/dingtalk.js +83 -0
- package/dist/server/channels/feishu.js +95 -0
- package/dist/server/channels/telegram.js +53 -0
- package/dist/server/channels/terminal.js +49 -0
- package/dist/server/channels/web.js +45 -0
- package/dist/server/channels/wechat.js +107 -0
- package/dist/server/config/loader.js +73 -0
- package/dist/server/config/prompts.js +12 -0
- package/dist/server/core/agents/manager.js +22 -0
- package/dist/server/core/agents/runtime.js +85 -0
- package/dist/server/core/brain.js +131 -0
- package/dist/server/core/event-bus.js +24 -0
- package/dist/server/core/logger.js +71 -0
- package/dist/server/core/memories/manager.js +115 -0
- package/dist/server/core/memories/short-term.js +128 -0
- package/dist/server/core/memories/structured.js +109 -0
- package/dist/server/core/memories/vector.js +138 -0
- package/dist/server/core/memory.js +2 -0
- package/dist/server/core/plugin-manager.js +112 -0
- package/dist/server/core/plugin.js +1 -0
- package/dist/server/core/scheduler.js +24 -0
- package/dist/server/core/types.js +1 -0
- package/dist/server/index.js +318 -0
- package/dist/server/llm/openai.js +23 -0
- package/dist/server/routes/auth.js +28 -0
- package/dist/server/server/create-http.js +17 -0
- package/dist/server/server.js +29 -0
- package/dist/server/skills/base-skill.js +16 -0
- package/dist/server/skills/create-agent.js +58 -0
- package/dist/server/skills/delegate.js +39 -0
- package/dist/server/skills/file-system.js +137 -0
- package/dist/server/skills/list-agents.js +24 -0
- package/dist/server/skills/office-excel.js +84 -0
- package/dist/server/skills/office-ppt.js +58 -0
- package/dist/server/skills/office-word.js +90 -0
- package/dist/server/skills/registry.js +27 -0
- package/dist/server/skills/search.js +31 -0
- package/dist/server/skills/system-time.js +27 -0
- package/package.json +116 -0
- package/public/favicon.svg +4 -0
- package/public/locales/en/translation.json +77 -0
- package/public/locales/zh/translation.json +77 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { config } from '../config/loader.js';
|
|
2
|
+
import { skillRegistry } from '../skills/registry.js';
|
|
3
|
+
import { SYSTEM_PROMPT } from '../config/prompts.js';
|
|
4
|
+
import { createOpenAIClient } from '../llm/openai.js';
|
|
5
|
+
export class Brain {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.openai = createOpenAIClient(config.llm);
|
|
8
|
+
this.updateClient();
|
|
9
|
+
}
|
|
10
|
+
static getInstance() {
|
|
11
|
+
if (!Brain.instance) {
|
|
12
|
+
Brain.instance = new Brain();
|
|
13
|
+
}
|
|
14
|
+
return Brain.instance;
|
|
15
|
+
}
|
|
16
|
+
updateClient() {
|
|
17
|
+
this.openai = createOpenAIClient(config.llm);
|
|
18
|
+
}
|
|
19
|
+
async processMessage(history, newMessage, systemPrompt) {
|
|
20
|
+
if (process.env.DEBUG)
|
|
21
|
+
console.log('[Brain] Processing message:', newMessage);
|
|
22
|
+
const defaultSystemPrompt = systemPrompt || config.systemPrompt || SYSTEM_PROMPT || 'You are xiaozuoAssistant, a helpful AI assistant. You can use tools to help users.';
|
|
23
|
+
// Convert history messages to the format expected by OpenAI
|
|
24
|
+
const messageHistory = history.map(m => {
|
|
25
|
+
const msg = { role: m.role, content: m.content };
|
|
26
|
+
if (m.name)
|
|
27
|
+
msg.name = m.name;
|
|
28
|
+
if (m.tool_call_id)
|
|
29
|
+
msg.tool_call_id = m.tool_call_id;
|
|
30
|
+
if (m.tool_calls)
|
|
31
|
+
msg.tool_calls = m.tool_calls;
|
|
32
|
+
return msg;
|
|
33
|
+
});
|
|
34
|
+
const messages = [
|
|
35
|
+
{ role: 'system', content: defaultSystemPrompt },
|
|
36
|
+
...messageHistory,
|
|
37
|
+
{ role: 'user', content: newMessage }
|
|
38
|
+
];
|
|
39
|
+
try {
|
|
40
|
+
if (process.env.DEBUG)
|
|
41
|
+
console.log('[Brain] Calling LLM...');
|
|
42
|
+
let response = await this.callLLM(messages);
|
|
43
|
+
// Log only the content snippet to avoid flooding logs with full JSON
|
|
44
|
+
const contentSnippet = response.choices[0].message.content ? response.choices[0].message.content.substring(0, 100) + '...' : 'No content';
|
|
45
|
+
if (process.env.DEBUG)
|
|
46
|
+
console.log('[Brain] LLM Response (snippet):', contentSnippet);
|
|
47
|
+
let iterations = 0;
|
|
48
|
+
const MAX_ITERATIONS = 10;
|
|
49
|
+
while (response.choices[0].message.tool_calls && iterations < MAX_ITERATIONS) {
|
|
50
|
+
iterations++;
|
|
51
|
+
const toolCalls = response.choices[0].message.tool_calls;
|
|
52
|
+
const assistantMsg = response.choices[0].message;
|
|
53
|
+
messages.push(assistantMsg); // Add assistant message with tool calls
|
|
54
|
+
for (const toolCall of toolCalls) {
|
|
55
|
+
if (toolCall.type !== 'function')
|
|
56
|
+
continue;
|
|
57
|
+
const functionName = toolCall.function.name;
|
|
58
|
+
const functionArgs = JSON.parse(toolCall.function.arguments);
|
|
59
|
+
if (process.env.DEBUG)
|
|
60
|
+
console.log(`[Brain] Executing tool: ${functionName}`);
|
|
61
|
+
const skill = skillRegistry.getSkill(functionName);
|
|
62
|
+
let toolResult = '';
|
|
63
|
+
if (skill) {
|
|
64
|
+
try {
|
|
65
|
+
const result = await skill.execute(functionArgs);
|
|
66
|
+
toolResult = JSON.stringify(result);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
toolResult = JSON.stringify({ error: error.message });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
toolResult = JSON.stringify({ error: 'Tool not found' });
|
|
74
|
+
}
|
|
75
|
+
// Log tool result snippet
|
|
76
|
+
if (process.env.DEBUG)
|
|
77
|
+
console.log(`[Brain] Tool Result (snippet):`, toolResult.substring(0, 100) + '...');
|
|
78
|
+
messages.push({
|
|
79
|
+
role: 'tool',
|
|
80
|
+
tool_call_id: toolCall.id,
|
|
81
|
+
content: toolResult
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Call LLM again with tool results
|
|
85
|
+
if (process.env.DEBUG)
|
|
86
|
+
console.log('[Brain] Calling LLM with tool results...');
|
|
87
|
+
response = await this.callLLM(messages);
|
|
88
|
+
const nextContentSnippet = response.choices[0].message.content ? response.choices[0].message.content.substring(0, 100) + '...' : 'No content';
|
|
89
|
+
if (process.env.DEBUG)
|
|
90
|
+
console.log('[Brain] LLM Response (after tool, snippet):', nextContentSnippet);
|
|
91
|
+
}
|
|
92
|
+
const finalContent = response.choices[0].message.content || 'I could not generate a response.';
|
|
93
|
+
if (process.env.DEBUG)
|
|
94
|
+
console.log('[Brain] Final Response (snippet):', finalContent.substring(0, 100) + '...');
|
|
95
|
+
return finalContent;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('[Brain] Error in processing:', error);
|
|
99
|
+
return `Error: ${error.message}`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async generateSummary(content) {
|
|
103
|
+
try {
|
|
104
|
+
const response = await this.openai.chat.completions.create({
|
|
105
|
+
model: config.llm.model,
|
|
106
|
+
messages: [
|
|
107
|
+
{ role: 'system', content: 'You are a helpful assistant. Please summarize the following content concisely.' },
|
|
108
|
+
{ role: 'user', content: content }
|
|
109
|
+
],
|
|
110
|
+
temperature: 0.5
|
|
111
|
+
});
|
|
112
|
+
return response.choices[0].message.content || '';
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error('Summary generation failed:', e);
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async callLLM(messages) {
|
|
120
|
+
const tools = skillRegistry.getToolsDefinition();
|
|
121
|
+
// OpenAI SDK expects tools to be undefined if empty array, or valid tools array
|
|
122
|
+
const toolsParam = tools.length > 0 ? tools : undefined;
|
|
123
|
+
return await this.openai.chat.completions.create({
|
|
124
|
+
model: config.llm.model,
|
|
125
|
+
messages: messages,
|
|
126
|
+
tools: toolsParam,
|
|
127
|
+
temperature: config.llm.temperature
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export const brain = Brain.getInstance();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
class EventBus extends EventEmitter {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
}
|
|
6
|
+
static getInstance() {
|
|
7
|
+
if (!EventBus.instance) {
|
|
8
|
+
EventBus.instance = new EventBus();
|
|
9
|
+
}
|
|
10
|
+
return EventBus.instance;
|
|
11
|
+
}
|
|
12
|
+
emitEvent(event) {
|
|
13
|
+
this.emit(event.type, event);
|
|
14
|
+
// 同时也触发一个通用的 '*' 事件,方便日志记录
|
|
15
|
+
this.emit('*', event);
|
|
16
|
+
}
|
|
17
|
+
onEvent(eventType, handler) {
|
|
18
|
+
this.on(eventType, handler);
|
|
19
|
+
}
|
|
20
|
+
offEvent(eventType, handler) {
|
|
21
|
+
this.off(eventType, handler);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const eventBus = EventBus.getInstance();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
import 'winston-daily-rotate-file';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
// Determine log directory
|
|
6
|
+
// In production, logs should be in the directory where the user runs the app (process.cwd()/logs)
|
|
7
|
+
// In development, it might be the project root.
|
|
8
|
+
const logDir = path.join(process.cwd(), 'logs');
|
|
9
|
+
// Ensure log directory exists
|
|
10
|
+
if (!fs.existsSync(logDir)) {
|
|
11
|
+
try {
|
|
12
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
console.error('Failed to create log directory:', e);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Define the custom format
|
|
19
|
+
const logFormat = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Print stack trace for errors
|
|
20
|
+
winston.format.splat(), winston.format.printf(({ timestamp, level, message, stack }) => {
|
|
21
|
+
return `[${timestamp}] ${level.toUpperCase()}: ${message} ${stack || ''}`;
|
|
22
|
+
}));
|
|
23
|
+
// Create the rotating file transport
|
|
24
|
+
const fileTransport = new winston.transports.DailyRotateFile({
|
|
25
|
+
filename: path.join(logDir, 'app-%DATE%.log'),
|
|
26
|
+
datePattern: 'YYYY-MM-DD',
|
|
27
|
+
zippedArchive: true, // Archive old logs (gzip)
|
|
28
|
+
maxSize: '20m', // Rotate if size exceeds 20MB (optional safety)
|
|
29
|
+
maxFiles: '30d', // Keep logs for 30 days
|
|
30
|
+
createSymlink: true, // Create a symlink 'app.log' pointing to current log
|
|
31
|
+
symlinkName: 'app.log',
|
|
32
|
+
level: 'info'
|
|
33
|
+
});
|
|
34
|
+
const transports = [fileTransport];
|
|
35
|
+
// Only add Console transport if NOT in production, or if explicitly requested
|
|
36
|
+
// This prevents double logging to stdout.log in daemon mode.
|
|
37
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
38
|
+
transports.push(new winston.transports.Console({
|
|
39
|
+
format: winston.format.combine(winston.format.colorize(), winston.format.simple())
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
export const logger = winston.createLogger({
|
|
43
|
+
level: 'info',
|
|
44
|
+
format: logFormat,
|
|
45
|
+
transports: transports,
|
|
46
|
+
exceptionHandlers: [
|
|
47
|
+
new winston.transports.File({ filename: path.join(logDir, 'exceptions.log') })
|
|
48
|
+
],
|
|
49
|
+
rejectionHandlers: [
|
|
50
|
+
new winston.transports.File({ filename: path.join(logDir, 'rejections.log') })
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
// Helper to integrate with existing console.log usage
|
|
54
|
+
export const overrideConsole = () => {
|
|
55
|
+
const originalLog = console.log;
|
|
56
|
+
const originalError = console.error;
|
|
57
|
+
const originalWarn = console.warn;
|
|
58
|
+
const originalInfo = console.info;
|
|
59
|
+
console.log = (...args) => {
|
|
60
|
+
logger.info(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' '));
|
|
61
|
+
};
|
|
62
|
+
console.error = (...args) => {
|
|
63
|
+
logger.error(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' '));
|
|
64
|
+
};
|
|
65
|
+
console.warn = (...args) => {
|
|
66
|
+
logger.warn(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' '));
|
|
67
|
+
};
|
|
68
|
+
console.info = (...args) => {
|
|
69
|
+
logger.info(args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' '));
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ShortTermMemory } from './short-term.js';
|
|
2
|
+
import { VectorMemory } from './vector.js';
|
|
3
|
+
import { StructuredMemory } from './structured.js';
|
|
4
|
+
import { brain } from '../brain.js';
|
|
5
|
+
export class MemoryManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.shortTerm = ShortTermMemory.getInstance();
|
|
8
|
+
this.vector = VectorMemory.getInstance();
|
|
9
|
+
this.structured = StructuredMemory.getInstance();
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!MemoryManager.instance) {
|
|
13
|
+
MemoryManager.instance = new MemoryManager();
|
|
14
|
+
}
|
|
15
|
+
return MemoryManager.instance;
|
|
16
|
+
}
|
|
17
|
+
// --- Layer 1: Short-term (Session) ---
|
|
18
|
+
async createSession() {
|
|
19
|
+
return this.shortTerm.createSession();
|
|
20
|
+
}
|
|
21
|
+
async getSession(id) {
|
|
22
|
+
return this.shortTerm.getSession(id);
|
|
23
|
+
}
|
|
24
|
+
async listSessions() {
|
|
25
|
+
return this.shortTerm.listSessions();
|
|
26
|
+
}
|
|
27
|
+
async deleteSession(id) {
|
|
28
|
+
return this.shortTerm.deleteSession(id);
|
|
29
|
+
}
|
|
30
|
+
async addMessage(sessionId, message) {
|
|
31
|
+
// 1. Add to Short-term
|
|
32
|
+
await this.shortTerm.addMessage(sessionId, message);
|
|
33
|
+
// 2. Add to Recent/Long-term (Vector) - Async/Background
|
|
34
|
+
// For now, we only embed user messages or significant assistant responses
|
|
35
|
+
// to avoid cluttering.
|
|
36
|
+
if (message.content.length > 10) { // Simple filter
|
|
37
|
+
this.vector.addMemory(message.content, 'recent', { sessionId, role: message.role })
|
|
38
|
+
.catch(err => console.error('Background vector add failed:', err));
|
|
39
|
+
}
|
|
40
|
+
// 3. Extract Facts (Structured) - This usually requires an LLM call to extract
|
|
41
|
+
// structured info from the message. For MVP, we skip automatic extraction here
|
|
42
|
+
// but provide the API for the Brain to call explicitly.
|
|
43
|
+
}
|
|
44
|
+
async getHistory(sessionId) {
|
|
45
|
+
const session = await this.shortTerm.getSession(sessionId);
|
|
46
|
+
return session ? session.messages : [];
|
|
47
|
+
}
|
|
48
|
+
// --- Retrieval ---
|
|
49
|
+
async getRelevantContext(query, sessionId) {
|
|
50
|
+
// 1. Get Short-term context (recent messages)
|
|
51
|
+
const history = await this.getHistory(sessionId);
|
|
52
|
+
const recentMessages = history.slice(-10).map(m => `${m.role}: ${m.content}`).join('\n');
|
|
53
|
+
// 2. Search Vector Memory (Recent & Long-term)
|
|
54
|
+
const vectorResults = await this.vector.search(query, undefined, 3);
|
|
55
|
+
const vectorContext = vectorResults.map(r => `[Memory]: ${r.text}`).join('\n');
|
|
56
|
+
// 3. Get User Profile (Structured)
|
|
57
|
+
const profile = await this.structured.getUserProfile();
|
|
58
|
+
const profileContext = Object.entries(profile).map(([k, v]) => `[User Info] ${k}: ${v}`).join('\n');
|
|
59
|
+
return `
|
|
60
|
+
=== User Profile ===
|
|
61
|
+
${profileContext}
|
|
62
|
+
|
|
63
|
+
=== Relevant Memories ===
|
|
64
|
+
${vectorContext}
|
|
65
|
+
|
|
66
|
+
=== Recent Conversation ===
|
|
67
|
+
${recentMessages}
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
// --- Maintenance ---
|
|
71
|
+
// Managed by Scheduler
|
|
72
|
+
async runMaintenance(maxAgeDays = 15) {
|
|
73
|
+
console.log(`[MemoryManager] Running maintenance (Max Age: ${maxAgeDays} days)...`);
|
|
74
|
+
try {
|
|
75
|
+
// 1. Clean up old sessions
|
|
76
|
+
const deletedSessions = await this.shortTerm.cleanupOldSessions(maxAgeDays);
|
|
77
|
+
if (deletedSessions > 0) {
|
|
78
|
+
console.log(`[MemoryManager] Deleted ${deletedSessions} old sessions.`);
|
|
79
|
+
}
|
|
80
|
+
// 2. Summarize and Prune Vector Memories
|
|
81
|
+
const oldMemories = await this.vector.getMemoriesOlderThan(maxAgeDays);
|
|
82
|
+
if (oldMemories.length > 0) {
|
|
83
|
+
console.log(`[MemoryManager] Found ${oldMemories.length} old memories to summarize.`);
|
|
84
|
+
// Batch process
|
|
85
|
+
const BATCH_SIZE = 10;
|
|
86
|
+
for (let i = 0; i < oldMemories.length; i += BATCH_SIZE) {
|
|
87
|
+
const batch = oldMemories.slice(i, i + BATCH_SIZE);
|
|
88
|
+
const batchText = batch.map(m => `[${new Date(m.metadata.timestamp).toISOString()}] ${m.text}`).join('\n');
|
|
89
|
+
try {
|
|
90
|
+
const summary = await brain.generateSummary(batchText);
|
|
91
|
+
if (summary) {
|
|
92
|
+
await this.vector.addMemory(summary, 'long_term', {
|
|
93
|
+
original_count: batch.length,
|
|
94
|
+
summary_date: Date.now(),
|
|
95
|
+
source: 'maintenance_summary'
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.error('Failed to summarize batch:', e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Prune after summarization
|
|
104
|
+
await this.vector.pruneOldMemories(maxAgeDays);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// No old memories
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error('[MemoryManager] Maintenance failed:', error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const memoryManager = MemoryManager.getInstance();
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
const SESSIONS_DIR = path.resolve(process.cwd(), 'sessions');
|
|
5
|
+
export class ShortTermMemory {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.sessionCache = new Map();
|
|
8
|
+
fs.ensureDirSync(SESSIONS_DIR);
|
|
9
|
+
this.ensureDefaultSession();
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!ShortTermMemory.instance) {
|
|
13
|
+
ShortTermMemory.instance = new ShortTermMemory();
|
|
14
|
+
}
|
|
15
|
+
return ShortTermMemory.instance;
|
|
16
|
+
}
|
|
17
|
+
async ensureDefaultSession() {
|
|
18
|
+
const defaultSessionId = 'default-session';
|
|
19
|
+
const filePath = path.join(SESSIONS_DIR, `${defaultSessionId}.json`);
|
|
20
|
+
if (!await fs.pathExists(filePath)) {
|
|
21
|
+
const session = {
|
|
22
|
+
id: defaultSessionId,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
updatedAt: Date.now(),
|
|
25
|
+
messages: []
|
|
26
|
+
};
|
|
27
|
+
await this.saveSession(session);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async createSession() {
|
|
31
|
+
const id = uuidv4();
|
|
32
|
+
const session = {
|
|
33
|
+
id,
|
|
34
|
+
createdAt: Date.now(),
|
|
35
|
+
updatedAt: Date.now(),
|
|
36
|
+
messages: []
|
|
37
|
+
};
|
|
38
|
+
await this.saveSession(session);
|
|
39
|
+
return session;
|
|
40
|
+
}
|
|
41
|
+
async getSession(id) {
|
|
42
|
+
if (this.sessionCache.has(id)) {
|
|
43
|
+
return this.sessionCache.get(id);
|
|
44
|
+
}
|
|
45
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
46
|
+
if (await fs.pathExists(filePath)) {
|
|
47
|
+
const data = await fs.readJson(filePath);
|
|
48
|
+
this.sessionCache.set(id, data);
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
async saveSession(session) {
|
|
54
|
+
const filePath = path.join(SESSIONS_DIR, `${session.id}.json`);
|
|
55
|
+
session.updatedAt = Date.now();
|
|
56
|
+
this.sessionCache.set(session.id, session);
|
|
57
|
+
await fs.writeJson(filePath, session, { spaces: 2 });
|
|
58
|
+
}
|
|
59
|
+
async addMessage(sessionId, message) {
|
|
60
|
+
const session = await this.getSession(sessionId);
|
|
61
|
+
if (session) {
|
|
62
|
+
session.messages.push(message);
|
|
63
|
+
// Keep only last 50 messages in memory for "immediate" context if needed,
|
|
64
|
+
// but here we keep full session history for simplicity as per current design.
|
|
65
|
+
// Layer 1 specific requirement: "Recent 20-50 turns".
|
|
66
|
+
// We can implement truncation logic here if we want to save space,
|
|
67
|
+
// but for now let's just save everything to JSON.
|
|
68
|
+
await this.saveSession(session);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async deleteSession(id) {
|
|
75
|
+
if (id === 'default-session') {
|
|
76
|
+
throw new Error('Cannot delete default session');
|
|
77
|
+
}
|
|
78
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
79
|
+
if (await fs.pathExists(filePath)) {
|
|
80
|
+
await fs.remove(filePath);
|
|
81
|
+
this.sessionCache.delete(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async listSessions() {
|
|
85
|
+
const files = await fs.readdir(SESSIONS_DIR);
|
|
86
|
+
const sessions = [];
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
if (file.endsWith('.json')) {
|
|
89
|
+
try {
|
|
90
|
+
const data = await fs.readJson(path.join(SESSIONS_DIR, file));
|
|
91
|
+
sessions.push(data);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
console.error(`Error reading session file ${file}:`, e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
99
|
+
}
|
|
100
|
+
async cleanupOldSessions(maxAgeDays) {
|
|
101
|
+
const files = await fs.readdir(SESSIONS_DIR);
|
|
102
|
+
let deletedCount = 0;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
if (file.endsWith('.json')) {
|
|
107
|
+
const filePath = path.join(SESSIONS_DIR, file);
|
|
108
|
+
try {
|
|
109
|
+
const session = await fs.readJson(filePath);
|
|
110
|
+
// Skip default session
|
|
111
|
+
if (session.id === 'default-session')
|
|
112
|
+
continue;
|
|
113
|
+
const ageDays = (now - session.updatedAt) / msPerDay;
|
|
114
|
+
if (ageDays > maxAgeDays) {
|
|
115
|
+
console.log(`[ShortTermMemory] Deleting old session: ${session.id} (Age: ${ageDays.toFixed(1)} days)`);
|
|
116
|
+
await fs.remove(filePath);
|
|
117
|
+
this.sessionCache.delete(session.id);
|
|
118
|
+
deletedCount++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
console.error(`[ShortTermMemory] Error processing file ${file} for cleanup:`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return deletedCount;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
const DB_DIR = path.resolve(process.cwd(), 'data/sqlite');
|
|
5
|
+
const DB_FILE = path.join(DB_DIR, 'memories.db');
|
|
6
|
+
export class StructuredMemory {
|
|
7
|
+
constructor() {
|
|
8
|
+
fs.ensureDirSync(DB_DIR);
|
|
9
|
+
this.db = new Database(DB_FILE);
|
|
10
|
+
this.initTables();
|
|
11
|
+
}
|
|
12
|
+
static getInstance() {
|
|
13
|
+
if (!StructuredMemory.instance) {
|
|
14
|
+
StructuredMemory.instance = new StructuredMemory();
|
|
15
|
+
}
|
|
16
|
+
return StructuredMemory.instance;
|
|
17
|
+
}
|
|
18
|
+
initTables() {
|
|
19
|
+
// Facts table: key-value or simple structured data
|
|
20
|
+
this.db.prepare(`
|
|
21
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
22
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
23
|
+
category TEXT NOT NULL,
|
|
24
|
+
key TEXT NOT NULL,
|
|
25
|
+
value TEXT NOT NULL,
|
|
26
|
+
confidence REAL DEFAULT 1.0,
|
|
27
|
+
source TEXT,
|
|
28
|
+
timestamp INTEGER
|
|
29
|
+
)
|
|
30
|
+
`).run();
|
|
31
|
+
// User Profile: specialized facts
|
|
32
|
+
this.db.prepare(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
key TEXT UNIQUE NOT NULL,
|
|
36
|
+
value TEXT NOT NULL,
|
|
37
|
+
updated_at INTEGER
|
|
38
|
+
)
|
|
39
|
+
`).run();
|
|
40
|
+
// Graph nodes (Entities)
|
|
41
|
+
this.db.prepare(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
name TEXT UNIQUE NOT NULL,
|
|
45
|
+
type TEXT NOT NULL,
|
|
46
|
+
metadata TEXT
|
|
47
|
+
)
|
|
48
|
+
`).run();
|
|
49
|
+
// Graph edges (Relations)
|
|
50
|
+
this.db.prepare(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
52
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
53
|
+
source_id INTEGER,
|
|
54
|
+
target_id INTEGER,
|
|
55
|
+
relation TEXT NOT NULL,
|
|
56
|
+
weight REAL DEFAULT 1.0,
|
|
57
|
+
FOREIGN KEY(source_id) REFERENCES entities(id),
|
|
58
|
+
FOREIGN KEY(target_id) REFERENCES entities(id)
|
|
59
|
+
)
|
|
60
|
+
`).run();
|
|
61
|
+
}
|
|
62
|
+
addFact(category, key, value, source = 'user') {
|
|
63
|
+
const stmt = this.db.prepare(`
|
|
64
|
+
INSERT INTO facts (category, key, value, source, timestamp)
|
|
65
|
+
VALUES (?, ?, ?, ?, ?)
|
|
66
|
+
`);
|
|
67
|
+
stmt.run(category, key, value, source, Date.now());
|
|
68
|
+
}
|
|
69
|
+
getFacts(category) {
|
|
70
|
+
if (category) {
|
|
71
|
+
return this.db.prepare('SELECT * FROM facts WHERE category = ?').all(category);
|
|
72
|
+
}
|
|
73
|
+
return this.db.prepare('SELECT * FROM facts').all();
|
|
74
|
+
}
|
|
75
|
+
updateUserProfile(key, value) {
|
|
76
|
+
const stmt = this.db.prepare(`
|
|
77
|
+
INSERT INTO user_profile (key, value, updated_at)
|
|
78
|
+
VALUES (?, ?, ?)
|
|
79
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
80
|
+
`);
|
|
81
|
+
stmt.run(key, value, Date.now());
|
|
82
|
+
}
|
|
83
|
+
getUserProfile() {
|
|
84
|
+
const rows = this.db.prepare('SELECT key, value FROM user_profile').all();
|
|
85
|
+
const profile = {};
|
|
86
|
+
rows.forEach((row) => {
|
|
87
|
+
profile[row.key] = row.value;
|
|
88
|
+
});
|
|
89
|
+
return profile;
|
|
90
|
+
}
|
|
91
|
+
// Simple Graph Operations
|
|
92
|
+
addEntity(name, type, metadata = {}) {
|
|
93
|
+
try {
|
|
94
|
+
this.db.prepare('INSERT INTO entities (name, type, metadata) VALUES (?, ?, ?)')
|
|
95
|
+
.run(name, type, JSON.stringify(metadata));
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
// Ignore unique constraint violation, maybe update?
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
addRelation(sourceName, targetName, relation) {
|
|
102
|
+
const source = this.db.prepare('SELECT id FROM entities WHERE name = ?').get(sourceName);
|
|
103
|
+
const target = this.db.prepare('SELECT id FROM entities WHERE name = ?').get(targetName);
|
|
104
|
+
if (source && target) {
|
|
105
|
+
this.db.prepare('INSERT INTO relations (source_id, target_id, relation) VALUES (?, ?, ?)')
|
|
106
|
+
.run(source.id, target.id, relation);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|