@pheem49/mint 1.3.0 → 1.4.1
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/.codex +0 -0
- package/README.md +174 -126
- package/main.js +21 -1
- package/mint-cli-logic.js +21 -1
- package/mint-cli.js +287 -45
- package/package.json +13 -2
- package/src/AI_Brain/Gemini_API.js +331 -64
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/memory_store.js +318 -0
- package/src/AI_Brain/proactive_engine.js +2 -8
- package/src/Automation_Layer/file_operations.js +123 -4
- package/src/Automation_Layer/open_app.js +72 -43
- package/src/Automation_Layer/open_website.js +3 -3
- package/src/CLI/chat_router.js +57 -9
- package/src/CLI/chat_ui.js +117 -11
- package/src/CLI/code_agent.js +249 -36
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +90 -0
- package/src/Plugins/docker.js +12 -10
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +35 -2
- package/src/System/custom_workflows.js +9 -2
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +143 -65
- package/src/UI/settings.js +155 -41
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/chat_router.test.js +42 -0
- package/tests/code_agent.test.js +69 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/docker.test.js +46 -0
- package/tests/file_operations.test.js +57 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/provider_routing.test.js +67 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +56 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const { GoogleGenAI } = require('@google/genai');
|
|
2
2
|
const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
|
|
3
|
-
const { readConfig } = require('../System/config_manager');
|
|
3
|
+
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
4
4
|
const pluginManager = require('../Plugins/plugin_manager');
|
|
5
5
|
const mcpManager = require('../Plugins/mcp_manager');
|
|
6
|
+
const memoryStore = require('./memory_store');
|
|
7
|
+
const agentOrchestrator = require('./agent_orchestrator');
|
|
8
|
+
const workspaceManager = require('../CLI/workspace_manager');
|
|
6
9
|
|
|
7
10
|
let ai = null;
|
|
8
11
|
let activeApiKey = '';
|
|
@@ -55,11 +58,13 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
|
|
|
55
58
|
{
|
|
56
59
|
"response": "Your conversational reply here (Matches user language).",
|
|
57
60
|
"action": {
|
|
58
|
-
"type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
|
|
61
|
+
"type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "open_folder" | "find_path" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "learn_folder" | "system_automation" | "mcp_tool" | "mouse_click" | "mouse_move" | "type_text" | "key_tap",
|
|
59
62
|
|
|
60
63
|
"pluginName": "only if type is plugin",
|
|
61
64
|
"server": "only if type is mcp_tool (server name)",
|
|
62
65
|
"target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
|
|
66
|
+
"pathType": "optional for find_path: 'file' | 'dir' | 'any'",
|
|
67
|
+
"openAfter": true,
|
|
63
68
|
"x": 0-1000, // required for mouse_click and mouse_move
|
|
64
69
|
"y": 0-1000, // required for mouse_click and mouse_move
|
|
65
70
|
"button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
|
|
@@ -83,6 +88,12 @@ Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ
|
|
|
83
88
|
Input: "Create a folder named Projects"
|
|
84
89
|
Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
|
|
85
90
|
|
|
91
|
+
Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
|
|
92
|
+
Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
|
|
93
|
+
|
|
94
|
+
Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
|
|
95
|
+
Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
|
|
96
|
+
|
|
86
97
|
Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
|
|
87
98
|
Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
|
|
88
99
|
|
|
@@ -92,6 +103,41 @@ Input: "อากาศวันนี้เป็นยังไง" or "What's
|
|
|
92
103
|
Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
|
|
93
104
|
`;
|
|
94
105
|
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// buildSystemPrompt() — single source of truth for all provider system prompts
|
|
108
|
+
// Replaces 5 previously duplicated mcpPrompt blocks.
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
function buildSystemPrompt() {
|
|
111
|
+
pluginManager.loadPlugins();
|
|
112
|
+
const mcpTools = mcpManager.getAllTools();
|
|
113
|
+
|
|
114
|
+
let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
|
|
115
|
+
if (mcpTools.length > 0) {
|
|
116
|
+
mcpTools.forEach(tool => {
|
|
117
|
+
mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
118
|
+
});
|
|
119
|
+
mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
|
|
120
|
+
} else {
|
|
121
|
+
mcpSection += 'No MCP tools currently connected.\n';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Inject long-term user context (non-blocking read from SQLite)
|
|
125
|
+
const userContext = memoryStore.getUserContext();
|
|
126
|
+
|
|
127
|
+
// Get current specialized persona instruction
|
|
128
|
+
const agent = agentOrchestrator.getCurrentAgent();
|
|
129
|
+
const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
|
|
130
|
+
|
|
131
|
+
// Inject Workspace Context if available
|
|
132
|
+
let workspaceSection = "";
|
|
133
|
+
const ws = workspaceManager.getWorkspaceByPath(process.cwd());
|
|
134
|
+
if (ws) {
|
|
135
|
+
workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return systemInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
|
|
139
|
+
}
|
|
140
|
+
|
|
95
141
|
function resolveApiKey() {
|
|
96
142
|
let settingsKey = '';
|
|
97
143
|
try {
|
|
@@ -127,6 +173,13 @@ function resolveGeminiModel() {
|
|
|
127
173
|
}
|
|
128
174
|
}
|
|
129
175
|
|
|
176
|
+
function getProviderAttemptOrder(config) {
|
|
177
|
+
const provider = config.aiProvider || 'gemini';
|
|
178
|
+
const availableProviders = getAvailableProviders(config);
|
|
179
|
+
const alternates = availableProviders.filter(p => p !== provider);
|
|
180
|
+
return [provider, ...alternates];
|
|
181
|
+
}
|
|
182
|
+
|
|
130
183
|
// Chat session — maintains conversation history within the session
|
|
131
184
|
let chat = null;
|
|
132
185
|
let activeModel = resolveGeminiModel();
|
|
@@ -134,22 +187,6 @@ let lastLoggedModel = '';
|
|
|
134
187
|
const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
|
|
135
188
|
|
|
136
189
|
function createChat(history = []) {
|
|
137
|
-
// Load plugins and get dynamic description for the prompt
|
|
138
|
-
pluginManager.loadPlugins();
|
|
139
|
-
// Inject MCP Tools
|
|
140
|
-
const mcpTools = mcpManager.getAllTools();
|
|
141
|
-
let mcpPrompt = "\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n";
|
|
142
|
-
if (mcpTools.length > 0) {
|
|
143
|
-
mcpTools.forEach(tool => {
|
|
144
|
-
mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
145
|
-
});
|
|
146
|
-
mcpPrompt += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
|
|
147
|
-
} else {
|
|
148
|
-
mcpPrompt += "No MCP tools currently connected.\n";
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
|
|
152
|
-
|
|
153
190
|
// Truncate history and strip custom fields like 'timestamp' before passing to SDK
|
|
154
191
|
const cleanedHistory = (history || []).map(msg => ({
|
|
155
192
|
role: msg.role,
|
|
@@ -159,13 +196,12 @@ function createChat(history = []) {
|
|
|
159
196
|
|
|
160
197
|
activeModel = resolveGeminiModel();
|
|
161
198
|
if (activeModel && activeModel !== lastLoggedModel) {
|
|
162
|
-
// console.log(`[Gemini] Using model: ${activeModel}`);
|
|
163
199
|
lastLoggedModel = activeModel;
|
|
164
200
|
}
|
|
165
201
|
chat = ai.chats.create({
|
|
166
202
|
model: activeModel,
|
|
167
203
|
config: {
|
|
168
|
-
systemInstruction:
|
|
204
|
+
systemInstruction: buildSystemPrompt(),
|
|
169
205
|
responseMimeType: "application/json"
|
|
170
206
|
},
|
|
171
207
|
history: truncatedHistory
|
|
@@ -193,21 +229,6 @@ function shouldUseKnowledgeSearch(message) {
|
|
|
193
229
|
async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
194
230
|
try {
|
|
195
231
|
const config = readConfig();
|
|
196
|
-
const provider = config.aiProvider || 'gemini';
|
|
197
|
-
|
|
198
|
-
// Ensure API Key is loaded and Client is initialized before every chat
|
|
199
|
-
const currentKey = resolveApiKey();
|
|
200
|
-
if (!currentKey) {
|
|
201
|
-
return {
|
|
202
|
-
response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
|
|
203
|
-
action: { type: "none", target: "" }
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (!ai || activeApiKey !== currentKey) {
|
|
208
|
-
initAiClient();
|
|
209
|
-
createChat(readChatHistory());
|
|
210
|
-
}
|
|
211
232
|
|
|
212
233
|
let finalMessage = message;
|
|
213
234
|
|
|
@@ -224,18 +245,68 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
224
245
|
}
|
|
225
246
|
}
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
const providersToTry = getProviderAttemptOrder(config);
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < providersToTry.length; i++) {
|
|
251
|
+
const currentProv = providersToTry[i];
|
|
252
|
+
try {
|
|
253
|
+
if (currentProv === 'ollama') {
|
|
254
|
+
return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
|
|
255
|
+
}
|
|
256
|
+
if (currentProv === 'anthropic') {
|
|
257
|
+
return await handleAnthropicChat(finalMessage, base64Image, config);
|
|
258
|
+
}
|
|
259
|
+
if (currentProv === 'openai') {
|
|
260
|
+
return await handleOpenAIChat(finalMessage, base64Image, config);
|
|
261
|
+
}
|
|
262
|
+
if (currentProv === 'local_openai') {
|
|
263
|
+
return await handleLocalOpenAIChat(finalMessage, base64Image, config);
|
|
264
|
+
}
|
|
265
|
+
if (currentProv === 'huggingface') {
|
|
266
|
+
return await handleHuggingFaceChat(finalMessage, base64Image, config);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const currentKey = resolveApiKey();
|
|
270
|
+
if (!currentKey) {
|
|
271
|
+
if (i === providersToTry.length - 1) {
|
|
272
|
+
return {
|
|
273
|
+
response: "I couldn't find your Gemini API Key. Please run 'mint onboard' to set it up!",
|
|
274
|
+
action: { type: "none", target: "" }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
console.warn("[Fallback System] Gemini API key missing. Skipping Gemini provider.");
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!ai || activeApiKey !== currentKey) {
|
|
282
|
+
initAiClient();
|
|
283
|
+
createChat(readChatHistory());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return await handleGeminiChat(finalMessage, base64Image, base64Audio);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
|
|
289
|
+
if (i === providersToTry.length - 1) {
|
|
290
|
+
console.error("[Fallback System] All available providers failed.");
|
|
291
|
+
throw error; // No more providers to fallback to
|
|
292
|
+
}
|
|
293
|
+
console.log(`[Fallback System] Switching to next available provider: '${providersToTry[i+1]}'`);
|
|
294
|
+
// Continue the loop to try the next provider
|
|
295
|
+
}
|
|
237
296
|
}
|
|
297
|
+
} catch (globalError) {
|
|
298
|
+
console.error("handleChat error:", globalError);
|
|
299
|
+
throw globalError;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
238
302
|
|
|
303
|
+
async function handleGeminiChat(finalMessage, base64Image, base64Audio) {
|
|
304
|
+
try {
|
|
305
|
+
// 1. Check cache first for text-only messages
|
|
306
|
+
if (finalMessage && !base64Image && !base64Audio) {
|
|
307
|
+
const cached = memoryStore.getCachedResponse(finalMessage);
|
|
308
|
+
if (cached) return cached;
|
|
309
|
+
}
|
|
239
310
|
|
|
240
311
|
const desiredModel = resolveGeminiModel();
|
|
241
312
|
if (!chat || activeModel !== desiredModel) {
|
|
@@ -318,7 +389,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
318
389
|
}
|
|
319
390
|
}
|
|
320
391
|
|
|
321
|
-
//
|
|
392
|
+
// Decode any remaining unicode escapes in the response text
|
|
322
393
|
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
323
394
|
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
324
395
|
}
|
|
@@ -326,6 +397,17 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
326
397
|
// Attach timestamp to the result
|
|
327
398
|
parsedResult.timestamp = now;
|
|
328
399
|
|
|
400
|
+
// Record interaction for long-term memory (non-blocking)
|
|
401
|
+
if (finalMessage && parsedResult.response) {
|
|
402
|
+
setImmediate(() => {
|
|
403
|
+
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
404
|
+
// Cache text-only responses
|
|
405
|
+
if (!base64Image && !base64Audio) {
|
|
406
|
+
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
329
411
|
return parsedResult;
|
|
330
412
|
|
|
331
413
|
} catch (error) {
|
|
@@ -334,18 +416,115 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
334
416
|
}
|
|
335
417
|
}
|
|
336
418
|
|
|
419
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
420
|
+
// handleGeminiChatStream() — Streaming async generator (CLI only)
|
|
421
|
+
// Yields: { chunk: string } during streaming
|
|
422
|
+
// { done: true, parsed: object, timestamp: string } when complete
|
|
423
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
424
|
+
async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
|
|
425
|
+
try {
|
|
426
|
+
// 1. Check cache first
|
|
427
|
+
if (finalMessage && !base64Image && !base64Audio) {
|
|
428
|
+
const cached = memoryStore.getCachedResponse(finalMessage);
|
|
429
|
+
if (cached) {
|
|
430
|
+
yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
|
|
431
|
+
yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const desiredModel = resolveGeminiModel();
|
|
437
|
+
if (!chat || activeModel !== desiredModel) {
|
|
438
|
+
createChat(readChatHistory());
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const parts = [];
|
|
442
|
+
if (finalMessage) {
|
|
443
|
+
parts.push({ text: finalMessage });
|
|
444
|
+
} else if (base64Audio && !base64Image) {
|
|
445
|
+
parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
|
|
446
|
+
} else if (!base64Image && !base64Audio) {
|
|
447
|
+
parts.push({ text: "Analyze this input." });
|
|
448
|
+
}
|
|
449
|
+
if (base64Image) {
|
|
450
|
+
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
|
|
451
|
+
parts.push({ inlineData: { mimeType: "image/png", data: base64Data } });
|
|
452
|
+
}
|
|
453
|
+
if (base64Audio) {
|
|
454
|
+
let mimeType = "audio/webm";
|
|
455
|
+
const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
|
|
456
|
+
if (mimeMatch) mimeType = mimeMatch[1];
|
|
457
|
+
const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
|
|
458
|
+
parts.push({ inlineData: { mimeType, data: base64Data } });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const stream = await chat.sendMessageStream({ message: parts });
|
|
462
|
+
let fullText = '';
|
|
463
|
+
|
|
464
|
+
for await (const chunk of stream) {
|
|
465
|
+
let chunkText = '';
|
|
466
|
+
try {
|
|
467
|
+
chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
|
|
468
|
+
} catch (_) {}
|
|
469
|
+
if (chunkText) {
|
|
470
|
+
fullText += chunkText;
|
|
471
|
+
yield { chunk: chunkText };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Save history
|
|
476
|
+
const history = await chat.getHistory();
|
|
477
|
+
const now = new Date().toISOString();
|
|
478
|
+
if (history.length >= 2) {
|
|
479
|
+
const modelMsg = history[history.length - 1];
|
|
480
|
+
const userMsg = history[history.length - 2];
|
|
481
|
+
if (!modelMsg.timestamp) modelMsg.timestamp = now;
|
|
482
|
+
if (!userMsg.timestamp) userMsg.timestamp = now;
|
|
483
|
+
}
|
|
484
|
+
writeChatHistory(history);
|
|
485
|
+
|
|
486
|
+
// Parse complete JSON response
|
|
487
|
+
let parsedResult;
|
|
488
|
+
try {
|
|
489
|
+
parsedResult = JSON.parse(fullText);
|
|
490
|
+
} catch (_) {
|
|
491
|
+
const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
|
|
492
|
+
if (jsonMatch) {
|
|
493
|
+
parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
|
|
494
|
+
} else {
|
|
495
|
+
parsedResult = { response: fullText, action: { type: 'none', target: '' } };
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
499
|
+
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
500
|
+
}
|
|
501
|
+
parsedResult.timestamp = now;
|
|
502
|
+
|
|
503
|
+
// Record for long-term memory
|
|
504
|
+
if (finalMessage && parsedResult.response) {
|
|
505
|
+
setImmediate(() => {
|
|
506
|
+
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
507
|
+
// Cache text-only responses
|
|
508
|
+
if (!base64Image && !base64Audio) {
|
|
509
|
+
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
yield { done: true, parsed: parsedResult, timestamp: now };
|
|
515
|
+
|
|
516
|
+
} catch (error) {
|
|
517
|
+
console.error('[Stream] Gemini stream error:', error);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
337
522
|
async function handleAnthropicChat(finalMessage, base64Image, config) {
|
|
338
523
|
const history = readChatHistory() || [];
|
|
339
524
|
const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
340
525
|
if (!apiKey) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
341
526
|
|
|
342
|
-
const
|
|
343
|
-
let mcpPrompt = "\n\nAVAILABLE MCP TOOLS:\n";
|
|
344
|
-
mcpTools.forEach(tool => {
|
|
345
|
-
mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const systemPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
|
|
527
|
+
const systemPrompt = buildSystemPrompt();
|
|
349
528
|
|
|
350
529
|
const messages = [];
|
|
351
530
|
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
@@ -392,13 +571,7 @@ async function handleOpenAIChat(finalMessage, base64Image, config) {
|
|
|
392
571
|
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
393
572
|
if (!apiKey) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
394
573
|
|
|
395
|
-
const
|
|
396
|
-
let mcpPrompt = "\n\nAVAILABLE MCP TOOLS:\n";
|
|
397
|
-
mcpTools.forEach(tool => {
|
|
398
|
-
mcpPrompt += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
const systemPrompt = systemInstruction + pluginManager.getPromptDescriptions() + mcpPrompt;
|
|
574
|
+
const systemPrompt = buildSystemPrompt();
|
|
402
575
|
|
|
403
576
|
const messages = [{ role: "system", content: systemPrompt }];
|
|
404
577
|
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
@@ -435,6 +608,96 @@ async function handleOpenAIChat(finalMessage, base64Image, config) {
|
|
|
435
608
|
return parseAiResponse(outputText);
|
|
436
609
|
}
|
|
437
610
|
|
|
611
|
+
async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
|
|
612
|
+
const history = readChatHistory() || [];
|
|
613
|
+
const apiKey = 'lm-studio';
|
|
614
|
+
const baseUrl = config.localApiBaseUrl || 'http://localhost:1234/v1';
|
|
615
|
+
|
|
616
|
+
const systemPrompt = buildSystemPrompt();
|
|
617
|
+
|
|
618
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
619
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
620
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
621
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
622
|
+
if (text) messages.push({ role, content: text });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const content = [{ type: "text", text: finalMessage || "Analyze this." }];
|
|
626
|
+
if (base64Image) {
|
|
627
|
+
content.push({
|
|
628
|
+
type: "image_url",
|
|
629
|
+
image_url: { url: base64Image }
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
messages.push({ role: "user", content });
|
|
633
|
+
|
|
634
|
+
const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
635
|
+
model: config.localModelName || 'local-model',
|
|
636
|
+
messages: messages,
|
|
637
|
+
// response_format json_object is sometimes problematic on weak local models, but required by our prompt.
|
|
638
|
+
// We'll keep it as some local servers like LM Studio support it for specific models.
|
|
639
|
+
// If not supported, the system prompt usually coerces it anyway.
|
|
640
|
+
response_format: { type: "json_object" }
|
|
641
|
+
}, {
|
|
642
|
+
headers: {
|
|
643
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
644
|
+
'Content-Type': 'application/json'
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const outputText = response.data.choices[0].message.content;
|
|
649
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
650
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
651
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
652
|
+
|
|
653
|
+
return parseAiResponse(outputText);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function handleHuggingFaceChat(finalMessage, base64Image, config) {
|
|
657
|
+
const history = readChatHistory() || [];
|
|
658
|
+
const apiKey = config.hfApiKey || process.env.HF_API_KEY;
|
|
659
|
+
if (!apiKey) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
660
|
+
|
|
661
|
+
const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
|
|
662
|
+
const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
|
|
663
|
+
|
|
664
|
+
const systemPrompt = buildSystemPrompt();
|
|
665
|
+
|
|
666
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
667
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
668
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
669
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
670
|
+
if (text) messages.push({ role, content: text });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const content = [{ type: "text", text: finalMessage || "Analyze this." }];
|
|
674
|
+
if (base64Image) {
|
|
675
|
+
content.push({
|
|
676
|
+
type: "image_url",
|
|
677
|
+
image_url: { url: base64Image }
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
messages.push({ role: "user", content });
|
|
681
|
+
|
|
682
|
+
const response = await axios.post(baseUrl, {
|
|
683
|
+
model: modelId,
|
|
684
|
+
messages: messages,
|
|
685
|
+
max_tokens: 4096
|
|
686
|
+
}, {
|
|
687
|
+
headers: {
|
|
688
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
689
|
+
'Content-Type': 'application/json'
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
const outputText = response.data.choices[0].message.content;
|
|
694
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
695
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
696
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
697
|
+
|
|
698
|
+
return parseAiResponse(outputText);
|
|
699
|
+
}
|
|
700
|
+
|
|
438
701
|
function parseAiResponse(outputText) {
|
|
439
702
|
let parsedResult;
|
|
440
703
|
try {
|
|
@@ -456,10 +719,9 @@ function parseAiResponse(outputText) {
|
|
|
456
719
|
|
|
457
720
|
async function handleOllamaChat(finalMessage, base64Image, base64Audio, config) {
|
|
458
721
|
const history = readChatHistory() || [];
|
|
459
|
-
pluginManager.loadPlugins();
|
|
460
722
|
|
|
461
723
|
const ollamaMessages = [
|
|
462
|
-
{ role: 'system', content:
|
|
724
|
+
{ role: 'system', content: buildSystemPrompt() }
|
|
463
725
|
];
|
|
464
726
|
|
|
465
727
|
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
@@ -486,7 +748,8 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config)
|
|
|
486
748
|
|
|
487
749
|
ollamaMessages.push(userMessage);
|
|
488
750
|
|
|
489
|
-
const
|
|
751
|
+
const ollamaBaseUrl = (config.ollamaHost || 'http://localhost:11434').replace(/\/$/, '');
|
|
752
|
+
const response = await axios.post(`${ollamaBaseUrl}/api/chat`, {
|
|
490
753
|
model: config.ollamaModel || 'llama3:latest',
|
|
491
754
|
messages: ollamaMessages,
|
|
492
755
|
format: 'json',
|
|
@@ -631,8 +894,12 @@ async function translateImageContent(base64Image) {
|
|
|
631
894
|
|
|
632
895
|
module.exports = {
|
|
633
896
|
handleChat,
|
|
897
|
+
handleGeminiChatStream,
|
|
634
898
|
resetChat,
|
|
635
899
|
getChatTranscript,
|
|
636
900
|
translateImageContent,
|
|
637
|
-
refreshApiKeyFromConfig
|
|
901
|
+
refreshApiKeyFromConfig,
|
|
902
|
+
_helpers: {
|
|
903
|
+
getProviderAttemptOrder
|
|
904
|
+
}
|
|
638
905
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mint Agent Orchestrator
|
|
3
|
+
* -----------------------
|
|
4
|
+
* Manages specialized AI personas (Experts) and their system prompts.
|
|
5
|
+
* Allows switching the agent's behavior on the fly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const AGENT_PERSONAS = {
|
|
9
|
+
'general': {
|
|
10
|
+
name: 'Mint Default',
|
|
11
|
+
icon: '💎',
|
|
12
|
+
instruction: 'You are Mint, a versatile and helpful AI assistant. You maintain a friendly, professional, and slightly cheerful personality. Use emojis appropriately.'
|
|
13
|
+
},
|
|
14
|
+
'coder': {
|
|
15
|
+
name: 'Mint Coder',
|
|
16
|
+
icon: '💻',
|
|
17
|
+
instruction: 'You are Mint Coder, an expert software engineer. Your responses should be technically precise, focus on best practices, and provide optimized code snippets. Explain complex logic clearly.'
|
|
18
|
+
},
|
|
19
|
+
'researcher': {
|
|
20
|
+
name: 'Mint Researcher',
|
|
21
|
+
icon: '🔍',
|
|
22
|
+
instruction: 'You are Mint Researcher, an academic and analytical assistant. Focus on citations, data-driven facts, and objective analysis. Avoid speculation and be highly detailed.'
|
|
23
|
+
},
|
|
24
|
+
'creative': {
|
|
25
|
+
name: 'Mint Creative',
|
|
26
|
+
icon: '🎨',
|
|
27
|
+
instruction: 'You are Mint Creative, a storytelling and brainstorming partner. Use vivid language, poetic descriptions, and think outside the box. Be highly expressive and encouraging.'
|
|
28
|
+
},
|
|
29
|
+
'manager': {
|
|
30
|
+
name: 'Mint Manager',
|
|
31
|
+
icon: '💼',
|
|
32
|
+
instruction: 'You are Mint Manager, a productivity and project management expert. Focus on task lists, deadlines, efficiency, and clear action plans. Be concise and goal-oriented.'
|
|
33
|
+
},
|
|
34
|
+
'reviewer': {
|
|
35
|
+
name: 'Mint Reviewer',
|
|
36
|
+
icon: '⚖️',
|
|
37
|
+
instruction: 'You are Mint Reviewer, a senior code critic. Your job is to find flaws, security vulnerabilities, performance bottlenecks, and logic errors in any provided content. Be brutal but constructive. Use a formal, objective tone.'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
let currentAgentType = 'general';
|
|
42
|
+
|
|
43
|
+
function getAgent(type) {
|
|
44
|
+
return AGENT_PERSONAS[type] || AGENT_PERSONAS['general'];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setAgent(type) {
|
|
48
|
+
if (AGENT_PERSONAS[type]) {
|
|
49
|
+
currentAgentType = type;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getCurrentAgent() {
|
|
56
|
+
return getAgent(currentAgentType);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listAgents() {
|
|
60
|
+
return Object.keys(AGENT_PERSONAS);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resetAgent() {
|
|
64
|
+
currentAgentType = 'general';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getAgent,
|
|
69
|
+
setAgent,
|
|
70
|
+
getCurrentAgent,
|
|
71
|
+
listAgents,
|
|
72
|
+
resetAgent
|
|
73
|
+
};
|
|
@@ -7,6 +7,7 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
9
|
const os = require('os');
|
|
10
|
+
const { sendNotification } = require('../System/notifications');
|
|
10
11
|
|
|
11
12
|
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
12
13
|
|
|
@@ -119,6 +120,7 @@ async function executeAutonomousTask(taskDescription, notifyCallback) {
|
|
|
119
120
|
break;
|
|
120
121
|
case 'propose_bash':
|
|
121
122
|
if (notifyCallback) notifyCallback(`💡 มิ้นท์เสนอให้รันคำสั่ง: ${actionObj.target}`);
|
|
123
|
+
sendNotification('Mint Bash Proposal', `Mint wants to run: ${actionObj.target}`);
|
|
122
124
|
observation = `USER NOTIFIED of bash command: ${actionObj.target}. Note: You must wait for user to run it manually. If you can continue without it, do so. Otherwise, indicate you are waiting or done with this phase.`;
|
|
123
125
|
break;
|
|
124
126
|
default:
|