@jungjaehoon/mama-server 1.0.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 +53 -0
- package/src/mama/config-loader.js +218 -0
- package/src/mama/db-adapter/README.md +105 -0
- package/src/mama/db-adapter/base-adapter.js +91 -0
- package/src/mama/db-adapter/index.js +31 -0
- package/src/mama/db-adapter/sqlite-adapter.js +342 -0
- package/src/mama/db-adapter/statement.js +127 -0
- package/src/mama/db-manager.js +584 -0
- package/src/mama/debug-logger.js +78 -0
- package/src/mama/decision-formatter.js +1180 -0
- package/src/mama/decision-tracker.js +565 -0
- package/src/mama/embedding-cache.js +221 -0
- package/src/mama/embeddings.js +265 -0
- package/src/mama/hook-metrics.js +403 -0
- package/src/mama/mama-api.js +913 -0
- package/src/mama/memory-inject.js +243 -0
- package/src/mama/memory-store.js +89 -0
- package/src/mama/ollama-client.js +387 -0
- package/src/mama/outcome-tracker.js +349 -0
- package/src/mama/query-intent.js +236 -0
- package/src/mama/relevance-scorer.js +283 -0
- package/src/mama/time-formatter.js +82 -0
- package/src/mama/transparency-banner.js +301 -0
- package/src/server.js +290 -0
- package/src/tools/checkpoint-tools.js +76 -0
- package/src/tools/index.js +54 -0
- package/src/tools/list-decisions.js +76 -0
- package/src/tools/recall-decision.js +75 -0
- package/src/tools/save-decision.js +113 -0
- package/src/tools/suggest-decision.js +84 -0
- package/src/tools/update-outcome.js +128 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MAMA (Memory-Augmented MCP Architecture) - Ollama Client Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Simple wrapper for Ollama API with EXAONE 3.5 support
|
|
5
|
+
* Tasks: 9.2-9.5 (HTTP client, EXAONE wrapper, Error handling, Testing)
|
|
6
|
+
* AC #1: LLM integration ready
|
|
7
|
+
*
|
|
8
|
+
* @module ollama-client
|
|
9
|
+
* @version 1.0
|
|
10
|
+
* @date 2025-11-14
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { info, error: logError } = require('./debug-logger');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
|
|
16
|
+
// Ollama configuration
|
|
17
|
+
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'localhost';
|
|
18
|
+
const OLLAMA_PORT = process.env.OLLAMA_PORT || 11434;
|
|
19
|
+
const DEFAULT_MODEL = 'exaone3.5:2.4b';
|
|
20
|
+
const FALLBACK_MODEL = 'gemma:2b';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Call Ollama API
|
|
24
|
+
*
|
|
25
|
+
* Task 9.2: Implement HTTP client for Ollama API
|
|
26
|
+
* AC #1: LLM API callable
|
|
27
|
+
*
|
|
28
|
+
* @param {string} endpoint - API endpoint (e.g., '/api/generate')
|
|
29
|
+
* @param {Object} payload - Request payload
|
|
30
|
+
* @param {number} timeout - Timeout in milliseconds (default: 30000)
|
|
31
|
+
* @returns {Promise<Object>} API response
|
|
32
|
+
* @throws {Error} If request fails
|
|
33
|
+
*/
|
|
34
|
+
function callOllamaAPI(endpoint, payload, timeout = 30000) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const postData = JSON.stringify(payload);
|
|
37
|
+
|
|
38
|
+
const options = {
|
|
39
|
+
hostname: OLLAMA_HOST,
|
|
40
|
+
port: OLLAMA_PORT,
|
|
41
|
+
path: endpoint,
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const req = http.request(options, (res) => {
|
|
50
|
+
let data = '';
|
|
51
|
+
|
|
52
|
+
res.on('data', (chunk) => {
|
|
53
|
+
data += chunk;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
58
|
+
try {
|
|
59
|
+
// Ollama returns NDJSON (newline-delimited JSON)
|
|
60
|
+
// For non-streaming, we only get one line
|
|
61
|
+
const lines = data.trim().split('\n');
|
|
62
|
+
const response = JSON.parse(lines[lines.length - 1]);
|
|
63
|
+
resolve(response);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
reject(new Error(`Failed to parse Ollama response: ${error.message}`));
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
reject(new Error(`Ollama API error: ${res.statusCode} - ${data}`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
req.on('error', (error) => {
|
|
74
|
+
reject(new Error(`Ollama connection failed: ${error.message}`));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.setTimeout(timeout, () => {
|
|
78
|
+
req.destroy();
|
|
79
|
+
reject(new Error(`Ollama request timeout (${timeout}ms)`));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
req.write(postData);
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate text with EXAONE 3.5
|
|
89
|
+
*
|
|
90
|
+
* Task 9.3: Implement EXAONE wrapper (with fallback to Gemma)
|
|
91
|
+
* AC #1: Decision detection LLM ready
|
|
92
|
+
*
|
|
93
|
+
* @param {string} prompt - Input prompt
|
|
94
|
+
* @param {Object} options - Generation options
|
|
95
|
+
* @param {string} options.model - Model name (default: EXAONE 3.5)
|
|
96
|
+
* @param {string} options.format - Response format ('json' or null)
|
|
97
|
+
* @param {number} options.temperature - Temperature (default: 0.7)
|
|
98
|
+
* @param {number} options.max_tokens - Max tokens (default: 500)
|
|
99
|
+
* @returns {Promise<string|Object>} Generated text or JSON object
|
|
100
|
+
* @throws {Error} If generation fails
|
|
101
|
+
*/
|
|
102
|
+
async function generate(prompt, options = {}) {
|
|
103
|
+
const { model = DEFAULT_MODEL, format = null, temperature = 0.7, max_tokens = 500 } = options;
|
|
104
|
+
|
|
105
|
+
const payload = {
|
|
106
|
+
model,
|
|
107
|
+
prompt,
|
|
108
|
+
stream: false,
|
|
109
|
+
options: {
|
|
110
|
+
temperature,
|
|
111
|
+
num_predict: max_tokens,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (format === 'json') {
|
|
116
|
+
payload.format = 'json';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await callOllamaAPI('/api/generate', payload);
|
|
121
|
+
|
|
122
|
+
// Extract response text
|
|
123
|
+
const responseText = response.response;
|
|
124
|
+
|
|
125
|
+
// Parse JSON if requested
|
|
126
|
+
if (format === 'json') {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(responseText);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`Failed to parse JSON response: ${responseText}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return responseText;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Task 9.4: Try fallback model if EXAONE fails
|
|
137
|
+
if (model === DEFAULT_MODEL && error.message.includes('not found')) {
|
|
138
|
+
console.warn(`[MAMA] EXAONE not found, trying fallback (${FALLBACK_MODEL})...`);
|
|
139
|
+
|
|
140
|
+
return generate(prompt, {
|
|
141
|
+
...options,
|
|
142
|
+
model: FALLBACK_MODEL,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Analyze decision from tool execution
|
|
152
|
+
*
|
|
153
|
+
* Wrapper for decision detection (used in Story 014.7.3)
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} toolExecution - Tool execution data
|
|
156
|
+
* @param {Object} sessionContext - Session context
|
|
157
|
+
* @returns {Promise<Object>} Decision analysis result
|
|
158
|
+
*/
|
|
159
|
+
async function analyzeDecision(toolExecution, sessionContext) {
|
|
160
|
+
const prompt = `
|
|
161
|
+
Analyze if this represents a DECISION (not just an action):
|
|
162
|
+
|
|
163
|
+
Session Context:
|
|
164
|
+
- Latest User Message: ${sessionContext.latest_user_message || 'N/A'}
|
|
165
|
+
- Recent Exchange: ${sessionContext.recent_exchange || 'N/A'}
|
|
166
|
+
|
|
167
|
+
Tool Execution:
|
|
168
|
+
- Tool: ${toolExecution.tool_name}
|
|
169
|
+
- Input: ${JSON.stringify(toolExecution.tool_input)}
|
|
170
|
+
- Result: ${toolExecution.exit_code === 0 ? 'SUCCESS' : 'FAILED'}
|
|
171
|
+
|
|
172
|
+
Decision Indicators:
|
|
173
|
+
1. User explicitly chose between alternatives?
|
|
174
|
+
Example: "Let's use JWT" (not "Use JWT" - that's just action)
|
|
175
|
+
|
|
176
|
+
2. User changed previous approach?
|
|
177
|
+
Example: "Complex → Simple approach"
|
|
178
|
+
|
|
179
|
+
3. User expressed preference?
|
|
180
|
+
Example: "Let's do it this way from now", "This approach is better"
|
|
181
|
+
|
|
182
|
+
4. Significant architectural choice?
|
|
183
|
+
Example: "Mesh structure: COMPLEX", "Authentication: JWT"
|
|
184
|
+
|
|
185
|
+
Is this a DECISION? Return JSON with "topic" as a short snake_case identifier:
|
|
186
|
+
{
|
|
187
|
+
"is_decision": boolean,
|
|
188
|
+
"topic": string or null (extract main technical topic in snake_case, e.g., "mesh_structure", "database_choice", "auth_strategy"),
|
|
189
|
+
"decision": string or null (the actual choice made, e.g., "COMPLEX", "PostgreSQL", "JWT"),
|
|
190
|
+
"reasoning": "Why this is/isn't a decision",
|
|
191
|
+
"confidence": 0.0-1.0
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
IMPORTANT: Generate "topic" freely based on context. Do NOT limit to predefined values.
|
|
195
|
+
`;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const response = await generate(prompt, {
|
|
199
|
+
format: 'json',
|
|
200
|
+
temperature: 0.3, // Lower temperature for structured output
|
|
201
|
+
max_tokens: 300,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return response;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// CLAUDE.md Rule #1: NO FALLBACK
|
|
207
|
+
// Errors must be thrown for debugging
|
|
208
|
+
logError(`[MAMA] Decision analysis FAILED: ${error.message}`);
|
|
209
|
+
throw new Error(`Decision analysis failed: ${error.message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Analyze query intent
|
|
215
|
+
*
|
|
216
|
+
* Wrapper for query intent detection (used in Story 014.7.2)
|
|
217
|
+
*
|
|
218
|
+
* @param {string} userMessage - User's message
|
|
219
|
+
* @returns {Promise<Object>} Query intent analysis
|
|
220
|
+
*/
|
|
221
|
+
async function analyzeQueryIntent(userMessage) {
|
|
222
|
+
const prompt = `
|
|
223
|
+
Analyze this user message to determine if it involves past decisions:
|
|
224
|
+
|
|
225
|
+
User Message: "${userMessage}"
|
|
226
|
+
|
|
227
|
+
Questions to answer:
|
|
228
|
+
1. Does this query reference past decisions or choices?
|
|
229
|
+
2. Is the user asking about previous approaches?
|
|
230
|
+
3. What topic is being discussed? (e.g., "mesh_structure", "authentication", "testing")
|
|
231
|
+
|
|
232
|
+
Return JSON:
|
|
233
|
+
{
|
|
234
|
+
"involves_decision": boolean,
|
|
235
|
+
"topic": "topic_name" | null,
|
|
236
|
+
"query_type": "recall" | "evolution" | "none",
|
|
237
|
+
"reasoning": "Why this involves/doesn't involve decisions"
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const response = await generate(prompt, {
|
|
243
|
+
format: 'json',
|
|
244
|
+
temperature: 0.3,
|
|
245
|
+
max_tokens: 200,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return response;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// CLAUDE.md Rule #1: NO FALLBACK
|
|
251
|
+
// Errors must be thrown for debugging
|
|
252
|
+
logError(`[MAMA] Query intent analysis FAILED: ${error.message}`);
|
|
253
|
+
throw new Error(`Query intent analysis failed: ${error.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Check if Ollama is available
|
|
259
|
+
*
|
|
260
|
+
* Utility for health checks
|
|
261
|
+
*
|
|
262
|
+
* @returns {Promise<boolean>} True if Ollama is accessible
|
|
263
|
+
*/
|
|
264
|
+
async function isAvailable() {
|
|
265
|
+
return new Promise((resolve) => {
|
|
266
|
+
const options = {
|
|
267
|
+
hostname: OLLAMA_HOST,
|
|
268
|
+
port: OLLAMA_PORT,
|
|
269
|
+
path: '/api/tags',
|
|
270
|
+
method: 'GET', // Use GET for health check
|
|
271
|
+
timeout: 2000,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const req = http.request(options, (res) => {
|
|
275
|
+
resolve(res.statusCode >= 200 && res.statusCode < 300);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
req.on('error', () => resolve(false));
|
|
279
|
+
req.on('timeout', () => {
|
|
280
|
+
req.destroy();
|
|
281
|
+
resolve(false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
req.end();
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* List available models
|
|
290
|
+
*
|
|
291
|
+
* Utility for setup script
|
|
292
|
+
*
|
|
293
|
+
* @returns {Promise<Array<string>>} Array of model names
|
|
294
|
+
*/
|
|
295
|
+
async function listModels() {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const options = {
|
|
298
|
+
hostname: OLLAMA_HOST,
|
|
299
|
+
port: OLLAMA_PORT,
|
|
300
|
+
path: '/api/tags',
|
|
301
|
+
method: 'GET', // Use GET for listing models
|
|
302
|
+
timeout: 5000,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const req = http.request(options, (res) => {
|
|
306
|
+
let data = '';
|
|
307
|
+
|
|
308
|
+
res.on('data', (chunk) => {
|
|
309
|
+
data += chunk;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
res.on('end', () => {
|
|
313
|
+
try {
|
|
314
|
+
const response = JSON.parse(data);
|
|
315
|
+
resolve(response.models?.map((m) => m.name) || []);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
reject(new Error(`Failed to parse models response: ${error.message}`));
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
req.on('error', (error) => {
|
|
323
|
+
reject(new Error(`Failed to list models: ${error.message}`));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
req.on('timeout', () => {
|
|
327
|
+
req.destroy();
|
|
328
|
+
reject(new Error('List models timeout'));
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
req.end();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Export API
|
|
336
|
+
module.exports = {
|
|
337
|
+
generate,
|
|
338
|
+
analyzeDecision,
|
|
339
|
+
analyzeQueryIntent,
|
|
340
|
+
isAvailable,
|
|
341
|
+
listModels,
|
|
342
|
+
DEFAULT_MODEL,
|
|
343
|
+
FALLBACK_MODEL,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// CLI execution for testing
|
|
347
|
+
if (require.main === module) {
|
|
348
|
+
info('🧠 MAMA Ollama Client - Test\n');
|
|
349
|
+
|
|
350
|
+
// Task 9.5: Test Ollama connection and generation
|
|
351
|
+
(async () => {
|
|
352
|
+
try {
|
|
353
|
+
info('📋 Test 1: Check Ollama availability...');
|
|
354
|
+
const available = await isAvailable();
|
|
355
|
+
if (!available) {
|
|
356
|
+
throw new Error('Ollama is not available');
|
|
357
|
+
}
|
|
358
|
+
info('✅ Ollama is available\n');
|
|
359
|
+
|
|
360
|
+
info('📋 Test 2: List available models...');
|
|
361
|
+
const models = await listModels();
|
|
362
|
+
info(`✅ Found ${models.length} models:`, models.join(', '), '\n');
|
|
363
|
+
|
|
364
|
+
info('📋 Test 3: Generate text...');
|
|
365
|
+
const text = await generate('What is 2+2?', {
|
|
366
|
+
temperature: 0.1,
|
|
367
|
+
max_tokens: 50,
|
|
368
|
+
});
|
|
369
|
+
info(`✅ Generated: ${text.trim()}\n`);
|
|
370
|
+
|
|
371
|
+
info('📋 Test 4: Generate JSON...');
|
|
372
|
+
const json = await generate('Return {"test": true, "value": 42}', {
|
|
373
|
+
format: 'json',
|
|
374
|
+
temperature: 0.1,
|
|
375
|
+
max_tokens: 50,
|
|
376
|
+
});
|
|
377
|
+
info('✅ Generated JSON:', json, '\n');
|
|
378
|
+
|
|
379
|
+
info('═══════════════════════════');
|
|
380
|
+
info('✅ All tests passed!');
|
|
381
|
+
info('═══════════════════════════');
|
|
382
|
+
} catch (error) {
|
|
383
|
+
logError(`❌ Test failed: ${error.message}`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
})();
|
|
387
|
+
}
|