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.
Files changed (56) hide show
  1. package/README.md +90 -0
  2. package/bin/cli.js +429 -0
  3. package/config.json +36 -0
  4. package/dist/client/assets/browser-ponyfill-DNlTAU2D.js +2 -0
  5. package/dist/client/assets/index-BPATxdcV.js +153 -0
  6. package/dist/client/assets/index-CgL5gMVL.css +1 -0
  7. package/dist/client/favicon.svg +4 -0
  8. package/dist/client/index.html +354 -0
  9. package/dist/client/locales/en/translation.json +77 -0
  10. package/dist/client/locales/zh/translation.json +77 -0
  11. package/dist/server/agents/office.js +23 -0
  12. package/dist/server/app.js +50 -0
  13. package/dist/server/channels/base-channel.js +13 -0
  14. package/dist/server/channels/create-channels.js +18 -0
  15. package/dist/server/channels/dingtalk.js +83 -0
  16. package/dist/server/channels/feishu.js +95 -0
  17. package/dist/server/channels/telegram.js +53 -0
  18. package/dist/server/channels/terminal.js +49 -0
  19. package/dist/server/channels/web.js +45 -0
  20. package/dist/server/channels/wechat.js +107 -0
  21. package/dist/server/config/loader.js +73 -0
  22. package/dist/server/config/prompts.js +12 -0
  23. package/dist/server/core/agents/manager.js +22 -0
  24. package/dist/server/core/agents/runtime.js +85 -0
  25. package/dist/server/core/brain.js +131 -0
  26. package/dist/server/core/event-bus.js +24 -0
  27. package/dist/server/core/logger.js +71 -0
  28. package/dist/server/core/memories/manager.js +115 -0
  29. package/dist/server/core/memories/short-term.js +128 -0
  30. package/dist/server/core/memories/structured.js +109 -0
  31. package/dist/server/core/memories/vector.js +138 -0
  32. package/dist/server/core/memory.js +2 -0
  33. package/dist/server/core/plugin-manager.js +112 -0
  34. package/dist/server/core/plugin.js +1 -0
  35. package/dist/server/core/scheduler.js +24 -0
  36. package/dist/server/core/types.js +1 -0
  37. package/dist/server/index.js +318 -0
  38. package/dist/server/llm/openai.js +23 -0
  39. package/dist/server/routes/auth.js +28 -0
  40. package/dist/server/server/create-http.js +17 -0
  41. package/dist/server/server.js +29 -0
  42. package/dist/server/skills/base-skill.js +16 -0
  43. package/dist/server/skills/create-agent.js +58 -0
  44. package/dist/server/skills/delegate.js +39 -0
  45. package/dist/server/skills/file-system.js +137 -0
  46. package/dist/server/skills/list-agents.js +24 -0
  47. package/dist/server/skills/office-excel.js +84 -0
  48. package/dist/server/skills/office-ppt.js +58 -0
  49. package/dist/server/skills/office-word.js +90 -0
  50. package/dist/server/skills/registry.js +27 -0
  51. package/dist/server/skills/search.js +31 -0
  52. package/dist/server/skills/system-time.js +27 -0
  53. package/package.json +116 -0
  54. package/public/favicon.svg +4 -0
  55. package/public/locales/en/translation.json +77 -0
  56. 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
+ }