@pheem49/mint 1.2.4 → 1.4.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/README.md +55 -23
- package/index.html +16 -0
- package/main.js +36 -83
- package/mint-cli-logic.js +19 -0
- package/mint-cli.js +316 -39
- package/package.json +20 -3
- package/src/AI_Brain/Gemini_API.js +439 -20
- package/src/AI_Brain/agent_orchestrator.js +73 -0
- package/src/AI_Brain/autonomous_brain.js +2 -0
- package/src/AI_Brain/knowledge_base.js +199 -125
- package/src/AI_Brain/memory_store.js +318 -0
- package/src/Automation_Layer/file_operations.js +41 -19
- package/src/CLI/chat_router.js +181 -0
- package/src/CLI/chat_ui.js +321 -110
- package/src/CLI/code_agent.js +556 -0
- package/src/CLI/code_session_memory.js +62 -0
- package/src/CLI/list_features.js +1 -0
- package/src/CLI/onboarding.js +53 -6
- package/src/CLI/workspace_manager.js +81 -0
- package/src/Plugins/mcp_manager.js +95 -0
- package/src/Plugins/plugin_manager.js +2 -2
- package/src/Plugins/spotify.js +168 -40
- package/src/Plugins/system_monitor.js +72 -0
- package/src/System/config_manager.js +61 -8
- package/src/System/granular_automation.js +88 -0
- package/src/System/notifications.js +23 -0
- package/src/UI/settings.html +167 -65
- package/src/UI/settings.js +253 -42
- package/tests/agent_orchestrator.test.js +41 -0
- package/tests/config_manager.test.js +141 -0
- package/tests/memory_store.test.js +185 -0
- package/tests/spotify.test.js +201 -0
- package/tests/system_monitor.test.js +37 -0
- package/tests/workspace_manager.test.js +41 -0
|
@@ -1,12 +1,17 @@
|
|
|
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
|
+
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');
|
|
5
9
|
|
|
6
10
|
let ai = null;
|
|
7
11
|
let activeApiKey = '';
|
|
8
12
|
const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
|
|
9
|
-
const
|
|
13
|
+
const axios = require('axios');
|
|
14
|
+
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
10
15
|
|
|
11
16
|
function decodeUnicode(str) {
|
|
12
17
|
if (!str) return '';
|
|
@@ -53,12 +58,24 @@ Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not
|
|
|
53
58
|
{
|
|
54
59
|
"response": "Your conversational reply here (Matches user language).",
|
|
55
60
|
"action": {
|
|
56
|
-
"type": "none" | "open_url" | "open_app" | "search" | "web_automation" | "create_folder" | "open_file" | "delete_file" | "clipboard_write" | "system_info" | "plugin" | "learn_file" | "system_automation",
|
|
61
|
+
"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",
|
|
62
|
+
|
|
57
63
|
"pluginName": "only if type is plugin",
|
|
58
|
-
"
|
|
64
|
+
"server": "only if type is mcp_tool (server name)",
|
|
65
|
+
"target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
|
|
66
|
+
"x": 0-1000, // required for mouse_click and mouse_move
|
|
67
|
+
"y": 0-1000, // required for mouse_click and mouse_move
|
|
68
|
+
"button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
|
|
69
|
+
"args": { "param": "value" } // only if type is mcp_tool
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
73
|
+
COORDINATE SYSTEM:
|
|
74
|
+
- When analyzing an image, use a coordinate system from 0 to 1000.
|
|
75
|
+
- (0, 0) is the Top-Left corner.
|
|
76
|
+
- (1000, 1000) is the Bottom-Right corner.
|
|
77
|
+
- To click an element, estimate its center point and provide x and y.
|
|
78
|
+
|
|
62
79
|
Examples:
|
|
63
80
|
Input: "Hi, what is your name?"
|
|
64
81
|
Output: { "response": "Hello! My name is Mint, your personal AI assistant. How can I help you today?", "action": { "type": "none", "target": "" } }
|
|
@@ -78,6 +95,41 @@ Input: "อากาศวันนี้เป็นยังไง" or "What's
|
|
|
78
95
|
Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
|
|
79
96
|
`;
|
|
80
97
|
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// buildSystemPrompt() — single source of truth for all provider system prompts
|
|
100
|
+
// Replaces 5 previously duplicated mcpPrompt blocks.
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
function buildSystemPrompt() {
|
|
103
|
+
pluginManager.loadPlugins();
|
|
104
|
+
const mcpTools = mcpManager.getAllTools();
|
|
105
|
+
|
|
106
|
+
let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
|
|
107
|
+
if (mcpTools.length > 0) {
|
|
108
|
+
mcpTools.forEach(tool => {
|
|
109
|
+
mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
110
|
+
});
|
|
111
|
+
mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
|
|
112
|
+
} else {
|
|
113
|
+
mcpSection += 'No MCP tools currently connected.\n';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Inject long-term user context (non-blocking read from SQLite)
|
|
117
|
+
const userContext = memoryStore.getUserContext();
|
|
118
|
+
|
|
119
|
+
// Get current specialized persona instruction
|
|
120
|
+
const agent = agentOrchestrator.getCurrentAgent();
|
|
121
|
+
const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
|
|
122
|
+
|
|
123
|
+
// Inject Workspace Context if available
|
|
124
|
+
let workspaceSection = "";
|
|
125
|
+
const ws = workspaceManager.getWorkspaceByPath(process.cwd());
|
|
126
|
+
if (ws) {
|
|
127
|
+
workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return systemInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
|
|
131
|
+
}
|
|
132
|
+
|
|
81
133
|
function resolveApiKey() {
|
|
82
134
|
let settingsKey = '';
|
|
83
135
|
try {
|
|
@@ -120,10 +172,6 @@ let lastLoggedModel = '';
|
|
|
120
172
|
const MAX_HISTORY_MESSAGES = 20; // Keep only the last 20 messages (approx 10 turns)
|
|
121
173
|
|
|
122
174
|
function createChat(history = []) {
|
|
123
|
-
// Load plugins and get dynamic description for the prompt
|
|
124
|
-
pluginManager.loadPlugins();
|
|
125
|
-
const dynamicPrompt = systemInstruction + pluginManager.getPromptDescriptions();
|
|
126
|
-
|
|
127
175
|
// Truncate history and strip custom fields like 'timestamp' before passing to SDK
|
|
128
176
|
const cleanedHistory = (history || []).map(msg => ({
|
|
129
177
|
role: msg.role,
|
|
@@ -133,13 +181,12 @@ function createChat(history = []) {
|
|
|
133
181
|
|
|
134
182
|
activeModel = resolveGeminiModel();
|
|
135
183
|
if (activeModel && activeModel !== lastLoggedModel) {
|
|
136
|
-
// console.log(`[Gemini] Using model: ${activeModel}`);
|
|
137
184
|
lastLoggedModel = activeModel;
|
|
138
185
|
}
|
|
139
186
|
chat = ai.chats.create({
|
|
140
187
|
model: activeModel,
|
|
141
188
|
config: {
|
|
142
|
-
systemInstruction:
|
|
189
|
+
systemInstruction: buildSystemPrompt(),
|
|
143
190
|
responseMimeType: "application/json"
|
|
144
191
|
},
|
|
145
192
|
history: truncatedHistory
|
|
@@ -151,7 +198,18 @@ resolveApiKey();
|
|
|
151
198
|
initAiClient();
|
|
152
199
|
createChat(readChatHistory());
|
|
153
200
|
|
|
154
|
-
|
|
201
|
+
function shouldUseKnowledgeSearch(message) {
|
|
202
|
+
const text = (message || '').trim().toLowerCase();
|
|
203
|
+
if (!text) return false;
|
|
204
|
+
|
|
205
|
+
const knowledgeHints = [
|
|
206
|
+
'readme', 'docs', 'documentation', 'manual', 'guide', 'knowledge', 'rag',
|
|
207
|
+
'search local', 'search files', 'learn file', 'project files', 'source code',
|
|
208
|
+
'ไฟล์', 'เอกสาร', 'คู่มือ', 'ค้นหาในเครื่อง', 'ค้นหาไฟล์', 'ข้อมูลในเครื่อง', 'โค้ดโปรเจค'
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
return knowledgeHints.some(hint => text.includes(hint));
|
|
212
|
+
}
|
|
155
213
|
|
|
156
214
|
async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
157
215
|
try {
|
|
@@ -175,7 +233,8 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
175
233
|
let finalMessage = message;
|
|
176
234
|
|
|
177
235
|
// Inject Local RAG Context
|
|
178
|
-
if (message && message.trim().length > 0) {
|
|
236
|
+
if (message && message.trim().length > 0 && shouldUseKnowledgeSearch(message)) {
|
|
237
|
+
const { searchKnowledge } = require('./knowledge_base');
|
|
179
238
|
const retrievedDocs = await searchKnowledge(message);
|
|
180
239
|
if (retrievedDocs && retrievedDocs.length > 0) {
|
|
181
240
|
let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
|
|
@@ -186,9 +245,56 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
186
245
|
}
|
|
187
246
|
}
|
|
188
247
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
248
|
+
const { getAvailableProviders } = require('../System/config_manager');
|
|
249
|
+
const availableProviders = getAvailableProviders(config);
|
|
250
|
+
|
|
251
|
+
// Ensure the requested provider is prioritized. If not available, fallback to the first available.
|
|
252
|
+
let providersToTry = [provider];
|
|
253
|
+
const alternates = availableProviders.filter(p => p !== provider);
|
|
254
|
+
providersToTry = providersToTry.concat(alternates);
|
|
255
|
+
|
|
256
|
+
for (let i = 0; i < providersToTry.length; i++) {
|
|
257
|
+
const currentProv = providersToTry[i];
|
|
258
|
+
try {
|
|
259
|
+
if (currentProv === 'ollama') {
|
|
260
|
+
return await handleOllamaChat(finalMessage, base64Image, base64Audio, config);
|
|
261
|
+
}
|
|
262
|
+
if (currentProv === 'anthropic') {
|
|
263
|
+
return await handleAnthropicChat(finalMessage, base64Image, config);
|
|
264
|
+
}
|
|
265
|
+
if (currentProv === 'openai') {
|
|
266
|
+
return await handleOpenAIChat(finalMessage, base64Image, config);
|
|
267
|
+
}
|
|
268
|
+
if (currentProv === 'local_openai') {
|
|
269
|
+
return await handleLocalOpenAIChat(finalMessage, base64Image, config);
|
|
270
|
+
}
|
|
271
|
+
if (currentProv === 'huggingface') {
|
|
272
|
+
return await handleHuggingFaceChat(finalMessage, base64Image, config);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return await handleGeminiChat(finalMessage, base64Image, base64Audio);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`[Fallback System] Provider '${currentProv}' failed:`, error.message);
|
|
278
|
+
if (i === providersToTry.length - 1) {
|
|
279
|
+
console.error("[Fallback System] All available providers failed.");
|
|
280
|
+
throw error; // No more providers to fallback to
|
|
281
|
+
}
|
|
282
|
+
console.log(`[Fallback System] Switching to next available provider: '${providersToTry[i+1]}'`);
|
|
283
|
+
// Continue the loop to try the next provider
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (globalError) {
|
|
287
|
+
console.error("handleChat error:", globalError);
|
|
288
|
+
throw globalError;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function handleGeminiChat(finalMessage, base64Image, base64Audio) {
|
|
293
|
+
try {
|
|
294
|
+
// 1. Check cache first for text-only messages
|
|
295
|
+
if (finalMessage && !base64Image && !base64Audio) {
|
|
296
|
+
const cached = memoryStore.getCachedResponse(finalMessage);
|
|
297
|
+
if (cached) return cached;
|
|
192
298
|
}
|
|
193
299
|
|
|
194
300
|
const desiredModel = resolveGeminiModel();
|
|
@@ -272,7 +378,7 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
272
378
|
}
|
|
273
379
|
}
|
|
274
380
|
|
|
275
|
-
//
|
|
381
|
+
// Decode any remaining unicode escapes in the response text
|
|
276
382
|
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
277
383
|
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
278
384
|
}
|
|
@@ -280,6 +386,17 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
280
386
|
// Attach timestamp to the result
|
|
281
387
|
parsedResult.timestamp = now;
|
|
282
388
|
|
|
389
|
+
// Record interaction for long-term memory (non-blocking)
|
|
390
|
+
if (finalMessage && parsedResult.response) {
|
|
391
|
+
setImmediate(() => {
|
|
392
|
+
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
393
|
+
// Cache text-only responses
|
|
394
|
+
if (!base64Image && !base64Audio) {
|
|
395
|
+
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
283
400
|
return parsedResult;
|
|
284
401
|
|
|
285
402
|
} catch (error) {
|
|
@@ -288,12 +405,312 @@ async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
|
288
405
|
}
|
|
289
406
|
}
|
|
290
407
|
|
|
291
|
-
|
|
408
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
409
|
+
// handleGeminiChatStream() — Streaming async generator (CLI only)
|
|
410
|
+
// Yields: { chunk: string } during streaming
|
|
411
|
+
// { done: true, parsed: object, timestamp: string } when complete
|
|
412
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
413
|
+
async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
|
|
414
|
+
try {
|
|
415
|
+
// 1. Check cache first
|
|
416
|
+
if (finalMessage && !base64Image && !base64Audio) {
|
|
417
|
+
const cached = memoryStore.getCachedResponse(finalMessage);
|
|
418
|
+
if (cached) {
|
|
419
|
+
yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
|
|
420
|
+
yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const desiredModel = resolveGeminiModel();
|
|
426
|
+
if (!chat || activeModel !== desiredModel) {
|
|
427
|
+
createChat(readChatHistory());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (finalMessage) {
|
|
432
|
+
parts.push({ text: finalMessage });
|
|
433
|
+
} else if (base64Audio && !base64Image) {
|
|
434
|
+
parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
|
|
435
|
+
} else if (!base64Image && !base64Audio) {
|
|
436
|
+
parts.push({ text: "Analyze this input." });
|
|
437
|
+
}
|
|
438
|
+
if (base64Image) {
|
|
439
|
+
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
|
|
440
|
+
parts.push({ inlineData: { mimeType: "image/png", data: base64Data } });
|
|
441
|
+
}
|
|
442
|
+
if (base64Audio) {
|
|
443
|
+
let mimeType = "audio/webm";
|
|
444
|
+
const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
|
|
445
|
+
if (mimeMatch) mimeType = mimeMatch[1];
|
|
446
|
+
const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
|
|
447
|
+
parts.push({ inlineData: { mimeType, data: base64Data } });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const stream = await chat.sendMessageStream({ message: parts });
|
|
451
|
+
let fullText = '';
|
|
452
|
+
|
|
453
|
+
for await (const chunk of stream) {
|
|
454
|
+
let chunkText = '';
|
|
455
|
+
try {
|
|
456
|
+
chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
|
|
457
|
+
} catch (_) {}
|
|
458
|
+
if (chunkText) {
|
|
459
|
+
fullText += chunkText;
|
|
460
|
+
yield { chunk: chunkText };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Save history
|
|
465
|
+
const history = await chat.getHistory();
|
|
466
|
+
const now = new Date().toISOString();
|
|
467
|
+
if (history.length >= 2) {
|
|
468
|
+
const modelMsg = history[history.length - 1];
|
|
469
|
+
const userMsg = history[history.length - 2];
|
|
470
|
+
if (!modelMsg.timestamp) modelMsg.timestamp = now;
|
|
471
|
+
if (!userMsg.timestamp) userMsg.timestamp = now;
|
|
472
|
+
}
|
|
473
|
+
writeChatHistory(history);
|
|
474
|
+
|
|
475
|
+
// Parse complete JSON response
|
|
476
|
+
let parsedResult;
|
|
477
|
+
try {
|
|
478
|
+
parsedResult = JSON.parse(fullText);
|
|
479
|
+
} catch (_) {
|
|
480
|
+
const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
|
|
481
|
+
if (jsonMatch) {
|
|
482
|
+
parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
|
|
483
|
+
} else {
|
|
484
|
+
parsedResult = { response: fullText, action: { type: 'none', target: '' } };
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
488
|
+
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
489
|
+
}
|
|
490
|
+
parsedResult.timestamp = now;
|
|
491
|
+
|
|
492
|
+
// Record for long-term memory
|
|
493
|
+
if (finalMessage && parsedResult.response) {
|
|
494
|
+
setImmediate(() => {
|
|
495
|
+
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
496
|
+
// Cache text-only responses
|
|
497
|
+
if (!base64Image && !base64Audio) {
|
|
498
|
+
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
yield { done: true, parsed: parsedResult, timestamp: now };
|
|
504
|
+
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error('[Stream] Gemini stream error:', error);
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function handleAnthropicChat(finalMessage, base64Image, config) {
|
|
512
|
+
const history = readChatHistory() || [];
|
|
513
|
+
const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
514
|
+
if (!apiKey) return { response: "กรุณาใส่ Anthropic API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
515
|
+
|
|
516
|
+
const systemPrompt = buildSystemPrompt();
|
|
517
|
+
|
|
518
|
+
const messages = [];
|
|
519
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
520
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
521
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
522
|
+
if (text) messages.push({ role, content: text });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const content = [];
|
|
526
|
+
if (base64Image) {
|
|
527
|
+
const base64Data = base64Image.replace(/^data:image\/\w+;base64,/, '');
|
|
528
|
+
const mimeType = base64Image.match(/^data:(image\/\w+);base64,/)[1];
|
|
529
|
+
content.push({
|
|
530
|
+
type: "image",
|
|
531
|
+
source: { type: "base64", media_type: mimeType, data: base64Data }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
content.push({ type: "text", text: finalMessage || "Analyze this." });
|
|
535
|
+
messages.push({ role: "user", content });
|
|
536
|
+
|
|
537
|
+
const response = await axios.post('https://api.anthropic.com/v1/messages', {
|
|
538
|
+
model: config.anthropicModel || 'claude-3-5-sonnet-latest',
|
|
539
|
+
max_tokens: 4096,
|
|
540
|
+
system: systemPrompt,
|
|
541
|
+
messages: messages
|
|
542
|
+
}, {
|
|
543
|
+
headers: {
|
|
544
|
+
'x-api-key': apiKey,
|
|
545
|
+
'anthropic-version': '2023-06-01',
|
|
546
|
+
'content-type': 'application/json'
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const outputText = response.data.content[0].text;
|
|
551
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
552
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
553
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
554
|
+
|
|
555
|
+
return parseAiResponse(outputText);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function handleOpenAIChat(finalMessage, base64Image, config) {
|
|
559
|
+
const history = readChatHistory() || [];
|
|
560
|
+
const apiKey = config.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
561
|
+
if (!apiKey) return { response: "กรุณาใส่ OpenAI API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
562
|
+
|
|
563
|
+
const systemPrompt = buildSystemPrompt();
|
|
564
|
+
|
|
565
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
566
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
567
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
568
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
569
|
+
if (text) messages.push({ role, content: text });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const content = [{ type: "text", text: finalMessage || "Analyze this." }];
|
|
573
|
+
if (base64Image) {
|
|
574
|
+
content.push({
|
|
575
|
+
type: "image_url",
|
|
576
|
+
image_url: { url: base64Image }
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
messages.push({ role: "user", content });
|
|
580
|
+
|
|
581
|
+
const response = await axios.post('https://api.openai.com/v1/chat/completions', {
|
|
582
|
+
model: config.openaiModel || 'gpt-4o',
|
|
583
|
+
messages: messages,
|
|
584
|
+
response_format: { type: "json_object" }
|
|
585
|
+
}, {
|
|
586
|
+
headers: {
|
|
587
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
588
|
+
'Content-Type': 'application/json'
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const outputText = response.data.choices[0].message.content;
|
|
593
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
594
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
595
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
596
|
+
|
|
597
|
+
return parseAiResponse(outputText);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function handleLocalOpenAIChat(finalMessage, base64Image, config) {
|
|
601
|
+
const history = readChatHistory() || [];
|
|
602
|
+
const apiKey = 'lm-studio';
|
|
603
|
+
const baseUrl = config.localApiBaseUrl || 'http://localhost:1234/v1';
|
|
604
|
+
|
|
605
|
+
const systemPrompt = buildSystemPrompt();
|
|
606
|
+
|
|
607
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
608
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
609
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
610
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
611
|
+
if (text) messages.push({ role, content: text });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const content = [{ type: "text", text: finalMessage || "Analyze this." }];
|
|
615
|
+
if (base64Image) {
|
|
616
|
+
content.push({
|
|
617
|
+
type: "image_url",
|
|
618
|
+
image_url: { url: base64Image }
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
messages.push({ role: "user", content });
|
|
622
|
+
|
|
623
|
+
const response = await axios.post(`${baseUrl.replace(/\/$/, '')}/chat/completions`, {
|
|
624
|
+
model: config.localModelName || 'local-model',
|
|
625
|
+
messages: messages,
|
|
626
|
+
// response_format json_object is sometimes problematic on weak local models, but required by our prompt.
|
|
627
|
+
// We'll keep it as some local servers like LM Studio support it for specific models.
|
|
628
|
+
// If not supported, the system prompt usually coerces it anyway.
|
|
629
|
+
response_format: { type: "json_object" }
|
|
630
|
+
}, {
|
|
631
|
+
headers: {
|
|
632
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
633
|
+
'Content-Type': 'application/json'
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const outputText = response.data.choices[0].message.content;
|
|
638
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
639
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
640
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
641
|
+
|
|
642
|
+
return parseAiResponse(outputText);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function handleHuggingFaceChat(finalMessage, base64Image, config) {
|
|
646
|
+
const history = readChatHistory() || [];
|
|
647
|
+
const apiKey = config.hfApiKey || process.env.HF_API_KEY;
|
|
648
|
+
if (!apiKey) return { response: "กรุณาใส่ Hugging Face API Key ในการตั้งค่าก่อนนะคะ", action: { type: "none" } };
|
|
649
|
+
|
|
650
|
+
const modelId = config.hfModel || 'meta-llama/Meta-Llama-3-8B-Instruct';
|
|
651
|
+
const baseUrl = `https://api-inference.huggingface.co/models/${modelId}/v1/chat/completions`;
|
|
652
|
+
|
|
653
|
+
const systemPrompt = buildSystemPrompt();
|
|
654
|
+
|
|
655
|
+
const messages = [{ role: "system", content: systemPrompt }];
|
|
656
|
+
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
657
|
+
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
658
|
+
let text = Array.isArray(msg.parts) ? msg.parts.map(p => p.text || '').join('\n') : '';
|
|
659
|
+
if (text) messages.push({ role, content: text });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const content = [{ type: "text", text: finalMessage || "Analyze this." }];
|
|
663
|
+
if (base64Image) {
|
|
664
|
+
content.push({
|
|
665
|
+
type: "image_url",
|
|
666
|
+
image_url: { url: base64Image }
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
messages.push({ role: "user", content });
|
|
670
|
+
|
|
671
|
+
const response = await axios.post(baseUrl, {
|
|
672
|
+
model: modelId,
|
|
673
|
+
messages: messages,
|
|
674
|
+
max_tokens: 4096
|
|
675
|
+
}, {
|
|
676
|
+
headers: {
|
|
677
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
678
|
+
'Content-Type': 'application/json'
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const outputText = response.data.choices[0].message.content;
|
|
683
|
+
history.push({ role: 'user', parts: [{ text: finalMessage }] });
|
|
684
|
+
history.push({ role: 'model', parts: [{ text: outputText }] });
|
|
685
|
+
writeChatHistory(history.slice(-MAX_HISTORY_MESSAGES));
|
|
686
|
+
|
|
687
|
+
return parseAiResponse(outputText);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function parseAiResponse(outputText) {
|
|
691
|
+
let parsedResult;
|
|
692
|
+
try {
|
|
693
|
+
parsedResult = JSON.parse(outputText);
|
|
694
|
+
} catch (e) {
|
|
695
|
+
const jsonMatch = outputText.match(/```json\n([\s\S]*?)\n```/) || outputText.match(/\{[\s\S]*\}/);
|
|
696
|
+
if (jsonMatch) {
|
|
697
|
+
parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
|
|
698
|
+
} else {
|
|
699
|
+
parsedResult = { response: outputText, action: { type: "none", target: "" } };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
703
|
+
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
704
|
+
}
|
|
705
|
+
parsedResult.timestamp = new Date().toISOString();
|
|
706
|
+
return parsedResult;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function handleOllamaChat(finalMessage, base64Image, base64Audio, config) {
|
|
292
710
|
const history = readChatHistory() || [];
|
|
293
|
-
pluginManager.loadPlugins();
|
|
294
711
|
|
|
295
712
|
const ollamaMessages = [
|
|
296
|
-
{ role: 'system', content:
|
|
713
|
+
{ role: 'system', content: buildSystemPrompt() }
|
|
297
714
|
];
|
|
298
715
|
|
|
299
716
|
for (const msg of history.slice(-MAX_HISTORY_MESSAGES)) {
|
|
@@ -320,7 +737,8 @@ async function handleOllamaChat(finalMessage, base64Image, base64Audio, config,
|
|
|
320
737
|
|
|
321
738
|
ollamaMessages.push(userMessage);
|
|
322
739
|
|
|
323
|
-
const
|
|
740
|
+
const ollamaBaseUrl = (config.ollamaHost || 'http://localhost:11434').replace(/\/$/, '');
|
|
741
|
+
const response = await axios.post(`${ollamaBaseUrl}/api/chat`, {
|
|
324
742
|
model: config.ollamaModel || 'llama3:latest',
|
|
325
743
|
messages: ollamaMessages,
|
|
326
744
|
format: 'json',
|
|
@@ -465,6 +883,7 @@ async function translateImageContent(base64Image) {
|
|
|
465
883
|
|
|
466
884
|
module.exports = {
|
|
467
885
|
handleChat,
|
|
886
|
+
handleGeminiChatStream,
|
|
468
887
|
resetChat,
|
|
469
888
|
getChatTranscript,
|
|
470
889
|
translateImageContent,
|
|
@@ -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:
|