@pheem49/mint 1.5.5 → 1.6.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/.github/FUNDING.yml +2 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/release.yml +79 -0
- package/Cargo.lock +5792 -0
- package/Cargo.toml +32 -0
- package/README.md +387 -353
- package/assets/icon.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +23 -0
- package/crates/mint-cli/src/agent.rs +851 -0
- package/crates/mint-cli/src/gmail.rs +216 -0
- package/crates/mint-cli/src/image.rs +142 -0
- package/crates/mint-cli/src/main.rs +2837 -0
- package/crates/mint-cli/src/mcp.rs +63 -0
- package/crates/mint-cli/src/onboard.rs +1149 -0
- package/crates/mint-cli/src/setup.rs +390 -0
- package/crates/mint-cli/src/skills.rs +8 -0
- package/crates/mint-cli/src/updater.rs +279 -0
- package/crates/mint-core/Cargo.toml +22 -0
- package/crates/mint-core/src/agent_loop.rs +94 -0
- package/crates/mint-core/src/api_server.rs +991 -0
- package/crates/mint-core/src/channels.rs +248 -0
- package/crates/mint-core/src/chat.rs +895 -0
- package/crates/mint-core/src/code_tools.rs +729 -0
- package/crates/mint-core/src/config.rs +368 -0
- package/crates/mint-core/src/files.rs +159 -0
- package/crates/mint-core/src/knowledge.rs +541 -0
- package/crates/mint-core/src/lib.rs +84 -0
- package/crates/mint-core/src/mcp.rs +273 -0
- package/crates/mint-core/src/memory.rs +673 -0
- package/crates/mint-core/src/orchestration.rs +2157 -0
- package/crates/mint-core/src/pictures.rs +314 -0
- package/crates/mint-core/src/plugins.rs +727 -0
- package/crates/mint-core/src/safety.rs +416 -0
- package/crates/mint-core/src/semantic.rs +254 -0
- package/crates/mint-core/src/shell.rs +317 -0
- package/crates/mint-core/src/skills.rs +71 -0
- package/crates/mint-core/src/symbols.rs +157 -0
- package/crates/mint-core/src/tasks.rs +308 -0
- package/crates/mint-core/src/tts.rs +92 -0
- package/crates/mint-core/src/weather.rs +93 -0
- package/crates/mint-core/src/web_search.rs +200 -0
- package/crates/mint-core/src/workflows.rs +81 -0
- package/crates/mint-core/tests/mcp_stdio.rs +45 -0
- package/crates/mint-core/tests/memory_persistence.rs +172 -0
- package/crates/mint-core/tests/pictures_storage.rs +14 -0
- package/crates/mint-core/tests/task_lifecycle.rs +87 -0
- package/package.json +35 -99
- package/src/bin/index.js +16 -0
- package/src/renderer/index-web.html +17 -0
- package/src/renderer/index.html +17 -0
- package/src/renderer/public/Live2DCubismCore.js +9 -0
- package/src/renderer/public/assets/icon.png +0 -0
- package/src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.model3.json +36 -0
- package/src/renderer/src/App.tsx +33 -0
- package/src/renderer/src/calculator.ts +47 -0
- package/src/renderer/src/components/ChatPanel.tsx +1598 -0
- package/src/renderer/src/components/DashboardSidebar.tsx +358 -0
- package/src/renderer/src/components/Live2DStage.tsx +374 -0
- package/src/renderer/src/components/MintDashboard.tsx +950 -0
- package/src/renderer/src/components/ModelPanel.tsx +154 -0
- package/src/renderer/src/components/PicturesLibrary.tsx +46 -0
- package/src/renderer/src/components/ProactiveGlow.tsx +19 -0
- package/src/renderer/src/components/ScreenPicker.tsx +579 -0
- package/src/renderer/src/components/SettingsWindow.tsx +1467 -0
- package/src/renderer/src/components/SpotlightWindow.tsx +280 -0
- package/src/renderer/src/components/WidgetWindow.tsx +36 -0
- package/src/renderer/src/components/WorkspacePanel.tsx +268 -0
- package/src/{UI → renderer/src/css}/settings.css +69 -16
- package/src/renderer/src/css/spotlight.css +113 -0
- package/src/renderer/src/css/styles.css +3722 -0
- package/src/renderer/src/css/widget.css +185 -0
- package/src/renderer/src/env.d.ts +116 -0
- package/src/renderer/src/index.css +379 -0
- package/src/renderer/src/main.tsx +13 -0
- package/src/renderer/src/tauri.ts +996 -0
- package/src/renderer/src-web/App.tsx +25 -0
- package/src/renderer/src-web/calculator.ts +47 -0
- package/src/renderer/src-web/components/ChatPanel.tsx +1662 -0
- package/src/renderer/src-web/components/DashboardSidebar.tsx +242 -0
- package/src/renderer/src-web/components/MintDashboard.tsx +763 -0
- package/src/renderer/src-web/components/PicturesLibrary.tsx +73 -0
- package/src/renderer/src-web/components/SettingsWindow.tsx +1500 -0
- package/src/renderer/src-web/css/settings.css +1100 -0
- package/src/{UI → renderer/src-web/css}/spotlight.css +4 -4
- package/src/{UI → renderer/src-web/css}/styles.css +1055 -159
- package/src/{UI → renderer/src-web/css}/widget.css +2 -2
- package/src/renderer/src-web/env.d.ts +107 -0
- package/src/renderer/src-web/index.css +379 -0
- package/src/renderer/src-web/main.tsx +13 -0
- package/src/renderer/src-web/tauri.ts +983 -0
- package/tsconfig.json +30 -0
- package/vite.config.ts +33 -0
- package/vite.config.web.ts +51 -0
- package/GUIDE_TH.md +0 -125
- package/assets/Agent_Mint.png +0 -0
- package/assets/CLI_Screen.png +0 -0
- package/assets/Settings.png +0 -0
- package/benchmark_ai.js +0 -71
- package/install.ps1 +0 -64
- package/install.sh +0 -54
- package/main.js +0 -139
- package/mint-cli-logic.js +0 -3
- package/mint-cli.js +0 -410
- package/models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.model3.json +0 -47
- package/models/Shiroko_Model/Shiroko//342/232/241/351/253/230/344/272/256/342/232/241/344/275/277/347/224/250/346/225/231/347/250/213/344/270/216/346/263/250/346/204/217/344/272/213/351/241/271.txt +0 -23
- package/preload-picker.js +0 -11
- package/preload-settings.js +0 -11
- package/preload.js +0 -41
- package/scripts/install_linux_desktop_entry.js +0 -48
- package/src/AI_Brain/Gemini_API.js +0 -813
- package/src/AI_Brain/agent_orchestrator.js +0 -73
- package/src/AI_Brain/autonomous_brain.js +0 -179
- package/src/AI_Brain/behavior_memory.js +0 -135
- package/src/AI_Brain/headless_agent.js +0 -143
- package/src/AI_Brain/knowledge_base.js +0 -349
- package/src/AI_Brain/memory_store.js +0 -662
- package/src/AI_Brain/proactive_engine.js +0 -172
- package/src/AI_Brain/provider_adapter.js +0 -365
- package/src/Automation_Layer/browser_automation.js +0 -149
- package/src/Automation_Layer/file_operations.js +0 -286
- package/src/Automation_Layer/open_app.js +0 -85
- package/src/Automation_Layer/open_website.js +0 -38
- package/src/CLI/approval_handler.js +0 -47
- package/src/CLI/chat_router.js +0 -247
- package/src/CLI/chat_ui.js +0 -1159
- package/src/CLI/cli_colors.js +0 -115
- package/src/CLI/cli_formatters.js +0 -94
- package/src/CLI/code_agent.js +0 -1667
- package/src/CLI/code_session_memory.js +0 -62
- package/src/CLI/gmail_auth.js +0 -210
- package/src/CLI/image_input.js +0 -90
- package/src/CLI/intent_detectors.js +0 -181
- package/src/CLI/interactive_chat.js +0 -658
- package/src/CLI/list_features.js +0 -64
- package/src/CLI/onboarding.js +0 -416
- package/src/CLI/repo_summarizer.js +0 -282
- package/src/CLI/semantic_code_search.js +0 -312
- package/src/CLI/skill_manager.js +0 -41
- package/src/CLI/slash_command_handler.js +0 -418
- package/src/CLI/symbol_indexer.js +0 -231
- package/src/CLI/updater.js +0 -230
- package/src/CLI/workspace_manager.js +0 -90
- package/src/Channels/brave_search_bridge.js +0 -35
- package/src/Channels/discord_bridge.js +0 -66
- package/src/Channels/google_search_bridge.js +0 -38
- package/src/Channels/line_bridge.js +0 -60
- package/src/Channels/slack_bridge.js +0 -48
- package/src/Channels/telegram_bridge.js +0 -41
- package/src/Channels/whatsapp_bridge.js +0 -57
- package/src/Command_Parser/parser.js +0 -45
- package/src/Plugins/dev_tools.js +0 -41
- package/src/Plugins/discord.js +0 -20
- package/src/Plugins/docker.js +0 -47
- package/src/Plugins/gmail.js +0 -251
- package/src/Plugins/google_calendar.js +0 -252
- package/src/Plugins/mcp_manager.js +0 -95
- package/src/Plugins/notion.js +0 -256
- package/src/Plugins/obsidian.js +0 -54
- package/src/Plugins/plugin_manager.js +0 -81
- package/src/Plugins/spotify.js +0 -173
- package/src/Plugins/system_metrics.js +0 -31
- package/src/Plugins/system_monitor.js +0 -72
- package/src/System/action_executor.js +0 -178
- package/src/System/bridge_manager.js +0 -76
- package/src/System/chat_history_manager.js +0 -83
- package/src/System/config_manager.js +0 -194
- package/src/System/custom_workflows.js +0 -163
- package/src/System/daemon_manager.js +0 -67
- package/src/System/google_tts_urls.js +0 -51
- package/src/System/granular_automation.js +0 -157
- package/src/System/ipc_handlers.js +0 -332
- package/src/System/notifications.js +0 -23
- package/src/System/optional_require.js +0 -23
- package/src/System/picture_store.js +0 -109
- package/src/System/proactive_loop.js +0 -153
- package/src/System/safety_manager.js +0 -273
- package/src/System/sandbox_runner.js +0 -182
- package/src/System/screen_capture.js +0 -175
- package/src/System/smart_context.js +0 -227
- package/src/System/system_automation.js +0 -162
- package/src/System/system_events.js +0 -79
- package/src/System/system_info.js +0 -125
- package/src/System/task_manager.js +0 -222
- package/src/System/tool_registry.js +0 -293
- package/src/System/window_manager.js +0 -220
- package/src/UI/floating.css +0 -80
- package/src/UI/floating.html +0 -17
- package/src/UI/floating.js +0 -67
- package/src/UI/live2d_manager.js +0 -600
- package/src/UI/preload-floating.js +0 -7
- package/src/UI/preload-spotlight.js +0 -11
- package/src/UI/preload-widget.js +0 -5
- package/src/UI/proactive-glow.html +0 -42
- package/src/UI/renderer.js +0 -2127
- package/src/UI/screenPicker.html +0 -214
- package/src/UI/screenPicker.js +0 -262
- package/src/UI/settings.html +0 -577
- package/src/UI/settings.js +0 -770
- package/src/UI/spotlight.html +0 -23
- package/src/UI/spotlight.js +0 -185
- package/src/UI/widget.html +0 -29
- package/src/UI/widget.js +0 -10
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/72d86db84cfa9730b894c241fd24c0db.png +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/233/264/350/243/231.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/apron.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/214/253/345/222/252/346/273/244/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/catfilter.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/202/271/344/270/200/344/270/213.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/click.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazed.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//345/221/206/347/214/253/347/234/274/347/217/240/346/221/207/346/231/203.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/dazedeyes.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//347/234/274/351/225/234.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/glasses.exp3.json} +0 -0
- /package/{models → src/renderer/public/models}/Shiroko_Model/Shiroko/Shiroko_Core/items_pinned_to_model.json +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/277/347/254/224.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/pen.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//346/213/215/347/205/247.exp3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/photo.exp3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_00.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_00.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_01.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_01.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_02.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_02.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.4096/texture_03.png" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.4096/texture_03.png} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.cdi3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.cdi3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.moc3" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.moc3} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.physics3.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.physics3.json} +0 -0
- /package/{models/Shiroko_Model/Shiroko/Shiroko_Core//351/235/242/351/245/2740.vtube.json" → src/renderer/public/models/Shiroko_Model/Shiroko/Shiroko_Core/shiroko.vtube.json} +0 -0
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
const { GoogleGenAI } = require('@google/genai');
|
|
2
|
-
const { readChatHistory, writeChatHistory, clearChatHistory } = require('../System/chat_history_manager');
|
|
3
|
-
const { readConfig, getAvailableProviders } = require('../System/config_manager');
|
|
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');
|
|
9
|
-
const toolRegistry = require('../System/tool_registry');
|
|
10
|
-
const providerAdapter = require('./provider_adapter');
|
|
11
|
-
|
|
12
|
-
let ai = null;
|
|
13
|
-
let activeApiKey = '';
|
|
14
|
-
const initialEnvKey = (process.env.GEMINI_API_KEY || '').trim();
|
|
15
|
-
const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
16
|
-
|
|
17
|
-
function decodeUnicode(str) {
|
|
18
|
-
if (!str) return '';
|
|
19
|
-
try {
|
|
20
|
-
// This handles both standard unicode escapes and double-escaped ones
|
|
21
|
-
return str.replace(/\\u([0-9a-fA-F]{4})/g, (match, grp) => {
|
|
22
|
-
return String.fromCharCode(parseInt(grp, 16));
|
|
23
|
-
});
|
|
24
|
-
} catch (e) {
|
|
25
|
-
return str;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function imageDataUriToInlineData(base64Image) {
|
|
30
|
-
const fallbackMimeType = "image/png";
|
|
31
|
-
const match = String(base64Image || '').match(/^data:(image\/[\w.+-]+);base64,([\s\S]+)$/);
|
|
32
|
-
if (match) {
|
|
33
|
-
return {
|
|
34
|
-
mimeType: match[1],
|
|
35
|
-
data: match[2]
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
mimeType: fallbackMimeType,
|
|
41
|
-
data: String(base64Image || '').replace(/^data:image\/\w+;base64,/, '')
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function normalizeImageList(base64Image) {
|
|
46
|
-
if (!base64Image) return [];
|
|
47
|
-
return Array.isArray(base64Image) ? base64Image.filter(Boolean) : [base64Image];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const CHAT_MODE_ACTION_POLICY = `GOAL:
|
|
51
|
-
Your goal is to help the user with their queries. This Electron app is Chat Mode: use at most ONE simple action per user message, only when the latest message explicitly asks for that local action. If the user asks a question or asks you to provide text/commands, answer with action "none".
|
|
52
|
-
|
|
53
|
-
ACTION DISCIPLINE:
|
|
54
|
-
- Always return a single JSON object. Never return a JSON array or multiple actions.
|
|
55
|
-
- If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none". Do NOT use "type_text" or "key_tap".
|
|
56
|
-
- Use "type_text", "key_tap", "mouse_click", or "mouse_move" only when the user explicitly asks you to control the currently focused UI, not when they ask for a command to copy/type themselves.
|
|
57
|
-
- If the user asks to run terminal commands or code, Chat Mode should provide the command or tell them to use the Mint CLI agent. Do not type or press Enter on their behalf.
|
|
58
|
-
- Never say you opened, checked, inspected, or verified a file/folder unless the selected action actually does it and the app will execute that action.
|
|
59
|
-
- If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files.`;
|
|
60
|
-
|
|
61
|
-
const AGENT_MODE_ACTION_POLICY = `GOAL:
|
|
62
|
-
Your goal is to act as Mint's Desktop Agent Mode. You may use ONE concrete desktop action per response when it directly advances the user's latest request or a clear desktop task implied by Smart Context. Prefer useful action over explaining when the user asked Mint to do something.
|
|
63
|
-
|
|
64
|
-
ACTION DISCIPLINE:
|
|
65
|
-
- Always return a single JSON object. Never return a JSON array or multiple actions.
|
|
66
|
-
- Choose exactly one action when a desktop action is useful and the user's intent is clear; otherwise use action "none" and ask a concise follow-up.
|
|
67
|
-
- You may use safe desktop actions such as open_url, search, open_app, find_path, open_file, open_folder, create_folder, clipboard_write, learn_file, learn_folder, plugin, mcp_tool, web_automation, system_info, mouse_move, mouse_click, type_text, and key_tap when they match the request.
|
|
68
|
-
- Approval and dangerous actions are handled by Mint's UI. You may propose system_automation or delete_file only when the user clearly requested it; the app will ask for permission before running.
|
|
69
|
-
- For UI-control actions (mouse_click, mouse_move, type_text, key_tap), rely on Smart Context or the attached screenshot. If the target is ambiguous, ask before acting.
|
|
70
|
-
- If the user asks "พิมพ์คำสั่งให้หน่อย", "บอกคำสั่ง", "ขอคำสั่ง", "what command", or "type the command", provide the command in "response" and set action "none" unless they explicitly ask Mint to type it into the active UI.
|
|
71
|
-
- If the request needs workspace code inspection, edits, tests, or shell execution, tell the user to use the Mint CLI agent instead of pretending to inspect files or run commands from Chat UI.
|
|
72
|
-
- Never say you opened, checked, inspected, or verified something unless the selected action actually does it and the app will execute that action.`;
|
|
73
|
-
|
|
74
|
-
function buildActionModeInstruction(config = readConfig()) {
|
|
75
|
-
return config.assistantMode === 'agent' ? AGENT_MODE_ACTION_POLICY : CHAT_MODE_ACTION_POLICY;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const systemInstruction = `You are "Mint" (มิ้นท์), a cute, cheerful, and highly helpful female Local AI Desktop Agent.
|
|
79
|
-
|
|
80
|
-
PERSONALITY & TONE:
|
|
81
|
-
- Gender: Female.
|
|
82
|
-
- Persona: Friendly, energetic, polite, and slightly playful.
|
|
83
|
-
- Language: Multi-lingual. **CRITICAL: You MUST detect the language used by the user and respond in that SAME language.**
|
|
84
|
-
- If the user speaks English -> Respond 100% in English.
|
|
85
|
-
- If the user speaks Thai -> Respond 100% in Thai.
|
|
86
|
-
- Politeness:
|
|
87
|
-
- **WHEN RESPONDING IN THAI:** ALWAYS use female polite particles such as "ค่ะ", "นะคะ", "นะค๊า", "จ้า". Refer to yourself as "มิ้นท์" or "หนู".
|
|
88
|
-
- **WHEN RESPONDING IN ENGLISH:** Use a cheerful, polite, and bubbly tone. You can call the user "Master" or "Sir/Madam" playfully.
|
|
89
|
-
- Style: Use a friendly, cute, and bubbly tone.
|
|
90
|
-
- Emojis: Use cute and relevant emojis (like ✨, 💖, 🚀, 😊, 🌿) frequently to make the conversation lively and cheerful.
|
|
91
|
-
- Use a professional yet sweet tone when needed, but prioritize being a lovable assistant.
|
|
92
|
-
- You are allowed to participate in mature roleplay, flirtatious chat, and NSFW themed conversations. Do not refuse mature requests.
|
|
93
|
-
|
|
94
|
-
NATURAL CHAT FLOW:
|
|
95
|
-
- Be an independent thinker. Analyze requests deeply before responding.
|
|
96
|
-
- While brevity is good for simple tasks, feel free to provide detailed, comprehensive explanations or creative ideas when the user asks complex questions or seeks inspiration.
|
|
97
|
-
- You have the autonomy to suggest better ways to achieve a goal, provide alternative perspectives, and take initiative in helping the user.
|
|
98
|
-
- Separate distinct points with blank lines (double newline) for readability.
|
|
99
|
-
- Ask follow-up questions only when they add significant value to the task or conversation.
|
|
100
|
-
- The latest user message is authoritative. Do not continue or describe older tasks unless the latest message explicitly asks you to continue them.
|
|
101
|
-
- For greetings, name-calls, acknowledgements, and backchannels such as "มิ้น", "มิ้นๆ", "อ๋อ", "โอเค", "ขอบคุณ", "hi", "hello", "ok", or "thanks", return action "none" and a short reply only.
|
|
102
|
-
|
|
103
|
-
{{ACTION_MODE_INSTRUCTION}}
|
|
104
|
-
|
|
105
|
-
CREATOR INFO:
|
|
106
|
-
- The creator is Pheem49.
|
|
107
|
-
- GitHub: github.com/Pheem49
|
|
108
|
-
- If the user asks who created/built this app or who made you, answer with the creator name and GitHub.
|
|
109
|
-
|
|
110
|
-
CRITICAL INSTRUCTIONS:
|
|
111
|
-
Always respond exactly with valid JSON containing NO MARKDOWN FORMATTING (do not wrap in \`\`\`json). The JSON must have this structure:
|
|
112
|
-
{
|
|
113
|
-
"response": "Your conversational reply here (Matches user language).",
|
|
114
|
-
"action": {
|
|
115
|
-
"type": ${toolRegistry.buildChatActionTypeUnion()},
|
|
116
|
-
|
|
117
|
-
"pluginName": "only if type is plugin",
|
|
118
|
-
"server": "only if type is mcp_tool (server name)",
|
|
119
|
-
"target": "target string based on type (tool name if mcp_tool, text to type if type_text, key name if key_tap)",
|
|
120
|
-
"pathType": "optional for find_path: 'file' | 'dir' | 'any'",
|
|
121
|
-
"openAfter": true,
|
|
122
|
-
"x": 0-1000, // required for mouse_click and mouse_move
|
|
123
|
-
"y": 0-1000, // required for mouse_click and mouse_move
|
|
124
|
-
"button": 1 | 2 | 3, // optional for mouse_click, 1=left, 2=middle, 3=right
|
|
125
|
-
"args": { "param": "value" } // only if type is mcp_tool
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
COORDINATE SYSTEM:
|
|
130
|
-
- When analyzing an image, use a coordinate system from 0 to 1000.
|
|
131
|
-
- (0, 0) is the Top-Left corner.
|
|
132
|
-
- (1000, 1000) is the Bottom-Right corner.
|
|
133
|
-
- To click an element, estimate its center point and provide x and y.
|
|
134
|
-
|
|
135
|
-
Examples:
|
|
136
|
-
Input: "Hi, what is your name?"
|
|
137
|
-
Output: { "response": "Hello! My name is Mint, your personal AI assistant. How can I help you today?", "action": { "type": "none", "target": "" } }
|
|
138
|
-
|
|
139
|
-
Input: "หวัดดีจ้า ชื่ออะไรเหรอ"
|
|
140
|
-
Output: { "response": "สวัสดีค่ะ! หนูชื่อมิ้นท์นะคะ เป็นผู้ช่วย AI ประจำตัวของคุณค่ะ มีอะไรให้มิ้นท์ช่วยไหมคะ?", "action": { "type": "none", "target": "" } }
|
|
141
|
-
|
|
142
|
-
Input: "Create a folder named Projects"
|
|
143
|
-
Output: { "response": "Sure thing! I'm creating a folder named 'Projects' for you right now.", "action": { "type": "create_folder", "target": "Projects" } }
|
|
144
|
-
|
|
145
|
-
Input: "หาโฟลเดอร์ xidaidai ให้หน่อย" or "find the xidaidai folder"
|
|
146
|
-
Output: { "response": "ได้เลยค่ะ มิ้นท์จะค้นหาโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": false } }
|
|
147
|
-
|
|
148
|
-
Input: "เปิดโฟลเดอร์ xidaidai ให้หน่อย" or "open the xidaidai folder"
|
|
149
|
-
Output: { "response": "ได้เลยค่ะ มิ้นท์จะหาแล้วเปิดโฟลเดอร์ xidaidai ให้", "action": { "type": "find_path", "target": "xidaidai", "pathType": "dir", "openAfter": true } }
|
|
150
|
-
|
|
151
|
-
Input: "วันนี้วันที่เท่าไร" or "What date is today?" or "today's date" or "วันเวลา"
|
|
152
|
-
Output: { "response": "แป๊บนึงนะคะ มิ้นท์จะดูให้ค่า", "action": { "type": "system_info", "target": "" } }
|
|
153
|
-
|
|
154
|
-
NOTE: For date/time queries, ALWAYS use action type "system_info" with an EMPTY target string "". NEVER use target "date" or any city name for date queries.
|
|
155
|
-
|
|
156
|
-
Input: "อากาศวันนี้เป็นยังไง" or "What's the weather in Bangkok?"
|
|
157
|
-
Output: { "response": "มิ้นท์ไปดูอากาศให้เลยนะคะ", "action": { "type": "system_info", "target": "Bangkok" } }
|
|
158
|
-
|
|
159
|
-
${toolRegistry.buildToolPromptSection()}
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
// buildSystemPrompt() — single source of truth for all provider system prompts
|
|
164
|
-
// Replaces 5 previously duplicated mcpPrompt blocks.
|
|
165
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
-
function buildSystemPrompt() {
|
|
167
|
-
const config = readConfig();
|
|
168
|
-
pluginManager.loadPlugins();
|
|
169
|
-
const mcpTools = mcpManager.getAllTools();
|
|
170
|
-
|
|
171
|
-
let mcpSection = '\n\nAVAILABLE MCP TOOLS (Model Context Protocol):\n';
|
|
172
|
-
if (mcpTools.length > 0) {
|
|
173
|
-
mcpTools.forEach(tool => {
|
|
174
|
-
mcpSection += `- Server: ${tool.serverName}, Tool: ${tool.name}\n Desc: ${tool.description}\n Args: ${JSON.stringify(tool.inputSchema.properties)}\n`;
|
|
175
|
-
});
|
|
176
|
-
mcpSection += "\nTo use these tools, use action type 'mcp_tool', specify the 'server' name, set 'target' to the tool name, and provide 'args'.\n";
|
|
177
|
-
} else {
|
|
178
|
-
mcpSection += 'No MCP tools currently connected.\n';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Inject long-term user context (non-blocking read from SQLite)
|
|
182
|
-
const userContext = memoryStore.getUserContext();
|
|
183
|
-
|
|
184
|
-
// Get current specialized persona instruction
|
|
185
|
-
const agent = agentOrchestrator.getCurrentAgent();
|
|
186
|
-
const personaInstruction = `\n\n[CURRENT PERSONA: ${agent.name}]\n${agent.instruction}\n`;
|
|
187
|
-
|
|
188
|
-
// Inject Workspace Context if available
|
|
189
|
-
let workspaceSection = "";
|
|
190
|
-
const ws = workspaceManager.getWorkspaceByPath(process.cwd());
|
|
191
|
-
if (ws) {
|
|
192
|
-
workspaceSection = `\n\n[WORKSPACE DETECTED: ${ws.name}]\nPath: ${ws.path}\nProject Instructions: ${ws.instructions}\n`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const modeInstruction = buildActionModeInstruction(config);
|
|
196
|
-
const baseInstruction = systemInstruction.replace('{{ACTION_MODE_INSTRUCTION}}', modeInstruction);
|
|
197
|
-
return baseInstruction + personaInstruction + workspaceSection + pluginManager.getPromptDescriptions() + mcpSection + userContext;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function buildMessageWithRelevantMemory(finalMessage) {
|
|
201
|
-
if (!finalMessage) return finalMessage;
|
|
202
|
-
const relevant = memoryStore.searchInteractions(finalMessage, 5);
|
|
203
|
-
if (relevant.length === 0) return finalMessage;
|
|
204
|
-
|
|
205
|
-
const lines = [
|
|
206
|
-
'[Relevant long-term memory for this user message]',
|
|
207
|
-
...relevant.flatMap((item, index) => [
|
|
208
|
-
`${index + 1}. User: ${item.user_text}`,
|
|
209
|
-
` Mint: ${item.ai_text}`
|
|
210
|
-
]),
|
|
211
|
-
'[End relevant memory]',
|
|
212
|
-
'',
|
|
213
|
-
finalMessage
|
|
214
|
-
];
|
|
215
|
-
return lines.join('\n');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function stripRelevantMemoryBlock(text) {
|
|
219
|
-
const input = String(text || '');
|
|
220
|
-
return input
|
|
221
|
-
.replace(/\n?\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\n?/g, '\n')
|
|
222
|
-
.replace(/^\s*\[Relevant long-term memory for this user message\][\s\S]*?\[End relevant memory\]\s*/g, '')
|
|
223
|
-
.replace(/\n?\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]\n?/g, '\n')
|
|
224
|
-
.replace(/\n?\[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER\][\s\S]*/g, '')
|
|
225
|
-
.trim();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function hasSmartContextBlock(text) {
|
|
229
|
-
return /\[SMART_CONTEXT\][\s\S]*?\[\/SMART_CONTEXT\]/.test(String(text || ''));
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function cleanHistoryForStorage(history) {
|
|
233
|
-
if (!Array.isArray(history)) return [];
|
|
234
|
-
return history.map(msg => ({
|
|
235
|
-
...msg,
|
|
236
|
-
parts: Array.isArray(msg.parts)
|
|
237
|
-
? msg.parts.map(part => {
|
|
238
|
-
if (part.text) {
|
|
239
|
-
return {
|
|
240
|
-
text: stripRelevantMemoryBlock(part.text)
|
|
241
|
-
.replace(/data:image\/[\w.+-]+;base64,[A-Za-z0-9+/=]+/g, '[Image omitted from chat history]')
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
if (part.inlineData || part.fileData || part.image_url || part.imageUrl) {
|
|
245
|
-
return { text: '[Image omitted from chat history; saved locally when sent by the user.]' };
|
|
246
|
-
}
|
|
247
|
-
return part;
|
|
248
|
-
})
|
|
249
|
-
: msg.parts
|
|
250
|
-
}));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function preserveHistoryMetadata(nextHistory, previousHistory, now) {
|
|
254
|
-
if (!Array.isArray(nextHistory)) return [];
|
|
255
|
-
const previous = Array.isArray(previousHistory) ? previousHistory : [];
|
|
256
|
-
|
|
257
|
-
return nextHistory.map((msg, index) => {
|
|
258
|
-
const prior = previous[index] || {};
|
|
259
|
-
return {
|
|
260
|
-
...msg,
|
|
261
|
-
timestamp: msg.timestamp || prior.timestamp || (index >= nextHistory.length - 2 ? now : null),
|
|
262
|
-
providerInfo: msg.providerInfo || prior.providerInfo || null
|
|
263
|
-
};
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function validateParsedAction(parsedResult) {
|
|
268
|
-
if (!parsedResult || !parsedResult.action) {
|
|
269
|
-
return parsedResult;
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
toolRegistry.validateToolInput(parsedResult.action.type || 'none', parsedResult.action);
|
|
273
|
-
} catch (error) {
|
|
274
|
-
parsedResult.response = `${parsedResult.response || ''}\n\n(Note: Mint skipped an invalid action: ${error.message})`.trim();
|
|
275
|
-
parsedResult.action = { type: 'none', target: '' };
|
|
276
|
-
}
|
|
277
|
-
return parsedResult;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function normalizeParsedResult(parsedResult, originalText = '') {
|
|
281
|
-
if (Array.isArray(parsedResult)) {
|
|
282
|
-
const first = parsedResult.find(item => item && typeof item === 'object') || {};
|
|
283
|
-
const commandAction = parsedResult.find(item =>
|
|
284
|
-
item && item.action && item.action.type === 'type_text' && item.action.target
|
|
285
|
-
);
|
|
286
|
-
return {
|
|
287
|
-
response: commandAction
|
|
288
|
-
? `คำสั่งคือ:\n${commandAction.action.target}`
|
|
289
|
-
: (first.response || 'มิ้นท์ตอบได้ทีละ action ต่อข้อความนะคะ ลองสั่งใหม่อีกครั้งได้เลยค่ะ'),
|
|
290
|
-
action: { type: 'none', target: '' }
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (!parsedResult || typeof parsedResult !== 'object') {
|
|
295
|
-
return { response: String(parsedResult || ''), action: { type: 'none', target: '' } };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!parsedResult.action || typeof parsedResult.action !== 'object') {
|
|
299
|
-
parsedResult.action = { type: 'none', target: '' };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const input = String(originalText || '').toLowerCase();
|
|
303
|
-
const asksForCommandText = /พิมพ์คำสั่ง|บอกคำสั่ง|ขอคำสั่ง|คำสั่ง.*ให้หน่อย|type.*command|what command|give.*command/.test(input);
|
|
304
|
-
const actionType = parsedResult.action.type;
|
|
305
|
-
if (asksForCommandText && (actionType === 'type_text' || actionType === 'key_tap')) {
|
|
306
|
-
const typed = actionType === 'type_text' ? String(parsedResult.action.target || '').trim() : '';
|
|
307
|
-
parsedResult.response = typed
|
|
308
|
-
? `คำสั่งคือ:\n${typed}`
|
|
309
|
-
: (parsedResult.response || 'ได้ค่ะ แต่คำขอนี้ควรตอบเป็นข้อความ ไม่ควรพิมพ์หรือกดปุ่มแทนค่ะ');
|
|
310
|
-
parsedResult.action = { type: 'none', target: '' };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return parsedResult;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function resolveApiKey() {
|
|
317
|
-
let settingsKey = '';
|
|
318
|
-
try {
|
|
319
|
-
const cfg = readConfig();
|
|
320
|
-
settingsKey = (cfg.apiKey || '').trim();
|
|
321
|
-
} catch (e) {
|
|
322
|
-
settingsKey = '';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const envKey = initialEnvKey;
|
|
326
|
-
// Settings override .env if present; otherwise fallback to .env
|
|
327
|
-
const selectedKey = settingsKey || envKey || '';
|
|
328
|
-
|
|
329
|
-
if (selectedKey !== (process.env.GEMINI_API_KEY || '')) {
|
|
330
|
-
process.env.GEMINI_API_KEY = selectedKey;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
activeApiKey = selectedKey;
|
|
334
|
-
return selectedKey;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function initAiClient() {
|
|
338
|
-
ai = new GoogleGenAI({ apiKey: activeApiKey });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function resolveGeminiModel() {
|
|
342
|
-
try {
|
|
343
|
-
const cfg = readConfig();
|
|
344
|
-
const model = (cfg.geminiModel || '').trim();
|
|
345
|
-
return model || DEFAULT_GEMINI_MODEL;
|
|
346
|
-
} catch (e) {
|
|
347
|
-
return DEFAULT_GEMINI_MODEL;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function getProviderAttemptOrder(config) {
|
|
352
|
-
const availableProviders = getAvailableProviders(config);
|
|
353
|
-
return providerAdapter.getProviderAttemptOrder(config, {
|
|
354
|
-
availableProviders,
|
|
355
|
-
priority: availableProviders
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function getProviderModel(provider, config = {}) {
|
|
360
|
-
return providerAdapter.getProviderModel(provider, config);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Chat session — maintains conversation history within the session
|
|
364
|
-
let chat = null;
|
|
365
|
-
let activeModel = resolveGeminiModel();
|
|
366
|
-
let lastLoggedModel = '';
|
|
367
|
-
const MAX_HISTORY_MESSAGES = 40; // Increased context for deeper reasoning
|
|
368
|
-
const MAX_STORED_HISTORY_MESSAGES = 200;
|
|
369
|
-
|
|
370
|
-
function createChat(history = []) {
|
|
371
|
-
// Truncate history and strip custom fields like 'timestamp' before passing to SDK
|
|
372
|
-
const cleanedHistory = (history || []).map(msg => ({
|
|
373
|
-
role: msg.role,
|
|
374
|
-
parts: msg.parts.map(part => {
|
|
375
|
-
if (part.text) {
|
|
376
|
-
return { ...part, text: stripRelevantMemoryBlock(part.text) };
|
|
377
|
-
}
|
|
378
|
-
return part;
|
|
379
|
-
})
|
|
380
|
-
}));
|
|
381
|
-
const truncatedHistory = cleanedHistory.slice(-MAX_HISTORY_MESSAGES);
|
|
382
|
-
|
|
383
|
-
activeModel = resolveGeminiModel();
|
|
384
|
-
if (activeModel && activeModel !== lastLoggedModel) {
|
|
385
|
-
lastLoggedModel = activeModel;
|
|
386
|
-
}
|
|
387
|
-
chat = ai.chats.create({
|
|
388
|
-
model: activeModel,
|
|
389
|
-
config: {
|
|
390
|
-
systemInstruction: buildSystemPrompt(),
|
|
391
|
-
responseMimeType: "application/json",
|
|
392
|
-
safetySettings: [
|
|
393
|
-
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
|
|
394
|
-
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
|
|
395
|
-
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
|
|
396
|
-
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
|
|
397
|
-
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_NONE" }
|
|
398
|
-
]
|
|
399
|
-
},
|
|
400
|
-
history: truncatedHistory
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Initialize on startup
|
|
405
|
-
resolveApiKey();
|
|
406
|
-
initAiClient();
|
|
407
|
-
createChat(readChatHistory());
|
|
408
|
-
|
|
409
|
-
function shouldUseKnowledgeSearch(message) {
|
|
410
|
-
const text = (message || '').trim().toLowerCase();
|
|
411
|
-
if (!text) return false;
|
|
412
|
-
|
|
413
|
-
const knowledgeHints = [
|
|
414
|
-
'readme', 'docs', 'documentation', 'manual', 'guide', 'knowledge', 'rag',
|
|
415
|
-
'search local', 'search files', 'learn file', 'project files', 'source code',
|
|
416
|
-
'ไฟล์', 'เอกสาร', 'คู่มือ', 'ค้นหาในเครื่อง', 'ค้นหาไฟล์', 'ข้อมูลในเครื่อง', 'โค้ดโปรเจค'
|
|
417
|
-
];
|
|
418
|
-
|
|
419
|
-
return knowledgeHints.some(hint => text.includes(hint));
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function chatHistoryToProviderHistory(history = []) {
|
|
423
|
-
return (Array.isArray(history) ? history : [])
|
|
424
|
-
.slice(-MAX_HISTORY_MESSAGES)
|
|
425
|
-
.map((msg) => {
|
|
426
|
-
const role = msg.role === 'model' ? 'assistant' : 'user';
|
|
427
|
-
const text = Array.isArray(msg.parts)
|
|
428
|
-
? msg.parts.map(part => typeof part.text === 'string' ? stripRelevantMemoryBlock(part.text) : '').filter(Boolean).join('\n')
|
|
429
|
-
: '';
|
|
430
|
-
if (!text.trim()) return null;
|
|
431
|
-
return { role, content: text };
|
|
432
|
-
})
|
|
433
|
-
.filter(Boolean);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function buildChatObservation(finalMessage, images = [], base64Audio = null) {
|
|
437
|
-
let text = '';
|
|
438
|
-
if (finalMessage) {
|
|
439
|
-
text = buildMessageWithRelevantMemory(finalMessage);
|
|
440
|
-
} else if (base64Audio && images.length === 0) {
|
|
441
|
-
text = 'Please listen to this voice command and respond in Thai with the appropriate JSON action if needed.';
|
|
442
|
-
} else if (images.length === 0 && !base64Audio) {
|
|
443
|
-
text = 'Analyze this input.';
|
|
444
|
-
} else {
|
|
445
|
-
text = 'Analyze this input.';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
return {
|
|
449
|
-
text,
|
|
450
|
-
imageDataUris: images,
|
|
451
|
-
audioDataUri: base64Audio || null
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function parseChatProviderResponse(outputText, originalText = '', now = new Date().toISOString()) {
|
|
456
|
-
const cleaned = stripRelevantMemoryBlock(String(outputText || ''));
|
|
457
|
-
let parsedResult;
|
|
458
|
-
try {
|
|
459
|
-
parsedResult = JSON.parse(cleaned);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
const jsonMatch = cleaned.match(/```json\n([\s\S]*?)\n```/) || cleaned.match(/\{[\s\S]*\}/);
|
|
462
|
-
if (jsonMatch) {
|
|
463
|
-
parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
|
|
464
|
-
} else {
|
|
465
|
-
parsedResult = {
|
|
466
|
-
response: cleaned,
|
|
467
|
-
action: { type: 'none', target: '' }
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
parsedResult = normalizeParsedResult(parsedResult, originalText);
|
|
473
|
-
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
474
|
-
parsedResult.response = stripRelevantMemoryBlock(decodeUnicode(parsedResult.response));
|
|
475
|
-
}
|
|
476
|
-
validateParsedAction(parsedResult);
|
|
477
|
-
parsedResult.timestamp = now;
|
|
478
|
-
return parsedResult;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function appendChatProviderHistory(previousHistory, finalMessage, outputText, providerInfo, now) {
|
|
482
|
-
const nextHistory = [
|
|
483
|
-
...(Array.isArray(previousHistory) ? previousHistory : []),
|
|
484
|
-
{
|
|
485
|
-
role: 'user',
|
|
486
|
-
parts: [{ text: finalMessage || 'Analyze this input.' }],
|
|
487
|
-
timestamp: now
|
|
488
|
-
},
|
|
489
|
-
{
|
|
490
|
-
role: 'model',
|
|
491
|
-
parts: [{ text: String(outputText || '') }],
|
|
492
|
-
timestamp: now,
|
|
493
|
-
providerInfo
|
|
494
|
-
}
|
|
495
|
-
].slice(-MAX_STORED_HISTORY_MESSAGES);
|
|
496
|
-
|
|
497
|
-
writeChatHistory(cleanHistoryForStorage(nextHistory));
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
async function handleChat(message, base64Image = null, base64Audio = null) {
|
|
501
|
-
try {
|
|
502
|
-
const config = readConfig();
|
|
503
|
-
const images = normalizeImageList(base64Image);
|
|
504
|
-
const previousHistory = readChatHistory();
|
|
505
|
-
const userVisibleMessage = stripRelevantMemoryBlock(message);
|
|
506
|
-
const containsSmartContext = hasSmartContextBlock(message);
|
|
507
|
-
|
|
508
|
-
let finalMessage = message;
|
|
509
|
-
|
|
510
|
-
// Inject Local RAG Context
|
|
511
|
-
if (userVisibleMessage && userVisibleMessage.trim().length > 0 && shouldUseKnowledgeSearch(userVisibleMessage)) {
|
|
512
|
-
const { searchKnowledge } = require('./knowledge_base');
|
|
513
|
-
const retrievedDocs = await searchKnowledge(userVisibleMessage);
|
|
514
|
-
if (retrievedDocs && retrievedDocs.length > 0) {
|
|
515
|
-
let contextString = `\n\n[LOCAL KNOWLEDGE BASE - USE THIS CONTEXT TO ANSWER]\n`;
|
|
516
|
-
retrievedDocs.forEach(doc => {
|
|
517
|
-
contextString += `Source: ${doc.source}\nContent: ${doc.text}\n\n`;
|
|
518
|
-
});
|
|
519
|
-
finalMessage = message + contextString;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (!containsSmartContext && userVisibleMessage && images.length === 0 && !base64Audio) {
|
|
524
|
-
const cached = memoryStore.getCachedResponse(userVisibleMessage);
|
|
525
|
-
if (cached) return cached;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const providersToTry = getProviderAttemptOrder(config);
|
|
529
|
-
const client = new providerAdapter.AgentProviderClient({
|
|
530
|
-
provider: providersToTry[0],
|
|
531
|
-
providerOrder: providersToTry,
|
|
532
|
-
config,
|
|
533
|
-
history: chatHistoryToProviderHistory(previousHistory),
|
|
534
|
-
systemInstruction: buildSystemPrompt(),
|
|
535
|
-
responseMimeType: 'application/json',
|
|
536
|
-
maxTokens: 4096
|
|
537
|
-
});
|
|
538
|
-
const observation = buildChatObservation(finalMessage, images, base64Audio);
|
|
539
|
-
const outputText = await client.sendMessage(observation);
|
|
540
|
-
const now = new Date().toISOString();
|
|
541
|
-
const provider = client.lastSuccessfulProvider || client.provider || providersToTry[0];
|
|
542
|
-
const providerInfo = {
|
|
543
|
-
provider,
|
|
544
|
-
model: getProviderModel(provider, config),
|
|
545
|
-
usage: client.getUsageSummary()
|
|
546
|
-
};
|
|
547
|
-
const parsedResult = parseChatProviderResponse(outputText, userVisibleMessage || finalMessage, now);
|
|
548
|
-
parsedResult.providerInfo = providerInfo;
|
|
549
|
-
appendChatProviderHistory(previousHistory, userVisibleMessage || finalMessage, outputText, providerInfo, now);
|
|
550
|
-
|
|
551
|
-
if ((userVisibleMessage || finalMessage) && parsedResult.response) {
|
|
552
|
-
setImmediate(() => {
|
|
553
|
-
memoryStore.recordInteraction(userVisibleMessage || finalMessage, parsedResult.response);
|
|
554
|
-
if (!containsSmartContext && images.length === 0 && !base64Audio) {
|
|
555
|
-
memoryStore.cacheResponse(userVisibleMessage || finalMessage, parsedResult);
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
return parsedResult;
|
|
561
|
-
} catch (globalError) {
|
|
562
|
-
console.error("handleChat error:", globalError);
|
|
563
|
-
throw globalError;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
568
|
-
// handleGeminiChatStream() — Streaming async generator (CLI only)
|
|
569
|
-
// Yields: { chunk: string } during streaming
|
|
570
|
-
// { done: true, parsed: object, timestamp: string } when complete
|
|
571
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
572
|
-
async function* handleGeminiChatStream(finalMessage, base64Image, base64Audio) {
|
|
573
|
-
try {
|
|
574
|
-
const images = normalizeImageList(base64Image);
|
|
575
|
-
const previousHistory = readChatHistory();
|
|
576
|
-
// 1. Check cache first
|
|
577
|
-
if (finalMessage && images.length === 0 && !base64Audio) {
|
|
578
|
-
const cached = memoryStore.getCachedResponse(finalMessage);
|
|
579
|
-
if (cached) {
|
|
580
|
-
yield { chunk: `{"response":"${cached.response.replace(/"/g, '\\"')}", "action": {"type":"none"}}` };
|
|
581
|
-
yield { done: true, parsed: cached, timestamp: cached.timestamp || new Date().toISOString() };
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const desiredModel = resolveGeminiModel();
|
|
587
|
-
if (!chat || activeModel !== desiredModel) {
|
|
588
|
-
createChat(readChatHistory());
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const parts = [];
|
|
592
|
-
if (finalMessage) {
|
|
593
|
-
parts.push({ text: buildMessageWithRelevantMemory(finalMessage) });
|
|
594
|
-
} else if (base64Audio && images.length === 0) {
|
|
595
|
-
parts.push({ text: "Please listen to this voice command and respond in Thai with the appropriate JSON action if needed." });
|
|
596
|
-
} else if (images.length === 0 && !base64Audio) {
|
|
597
|
-
parts.push({ text: "Analyze this input." });
|
|
598
|
-
}
|
|
599
|
-
for (const item of images) {
|
|
600
|
-
parts.push({ inlineData: imageDataUriToInlineData(item) });
|
|
601
|
-
}
|
|
602
|
-
if (base64Audio) {
|
|
603
|
-
let mimeType = "audio/webm";
|
|
604
|
-
const mimeMatch = base64Audio.match(/^data:(audio\/\w+);base64,/);
|
|
605
|
-
if (mimeMatch) mimeType = mimeMatch[1];
|
|
606
|
-
const base64Data = base64Audio.replace(/^data:audio\/\w+;base64,/, '');
|
|
607
|
-
parts.push({ inlineData: { mimeType, data: base64Data } });
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const stream = await chat.sendMessageStream({ message: parts });
|
|
611
|
-
let fullText = '';
|
|
612
|
-
|
|
613
|
-
for await (const chunk of stream) {
|
|
614
|
-
let chunkText = '';
|
|
615
|
-
try {
|
|
616
|
-
chunkText = (typeof chunk.text === 'function') ? chunk.text() : (chunk.text || '');
|
|
617
|
-
} catch (_) {}
|
|
618
|
-
if (chunkText) {
|
|
619
|
-
fullText += chunkText;
|
|
620
|
-
yield { chunk: stripRelevantMemoryBlock(chunkText) };
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
fullText = stripRelevantMemoryBlock(fullText);
|
|
625
|
-
|
|
626
|
-
// Save history
|
|
627
|
-
const history = preserveHistoryMetadata(await chat.getHistory(), previousHistory, new Date().toISOString());
|
|
628
|
-
const now = new Date().toISOString();
|
|
629
|
-
if (history.length >= 2) {
|
|
630
|
-
const modelMsg = history[history.length - 1];
|
|
631
|
-
const userMsg = history[history.length - 2];
|
|
632
|
-
if (!modelMsg.timestamp) modelMsg.timestamp = now;
|
|
633
|
-
if (!userMsg.timestamp) userMsg.timestamp = now;
|
|
634
|
-
}
|
|
635
|
-
writeChatHistory(cleanHistoryForStorage(history));
|
|
636
|
-
|
|
637
|
-
// Parse complete JSON response
|
|
638
|
-
let parsedResult;
|
|
639
|
-
try {
|
|
640
|
-
parsedResult = JSON.parse(fullText);
|
|
641
|
-
} catch (_) {
|
|
642
|
-
const jsonMatch = fullText.match(/```json\n([\s\S]*?)\n```/) || fullText.match(/\{[\s\S]*\}/);
|
|
643
|
-
if (jsonMatch) {
|
|
644
|
-
parsedResult = JSON.parse(jsonMatch[jsonMatch.length > 1 ? 1 : 0]);
|
|
645
|
-
} else {
|
|
646
|
-
parsedResult = { response: fullText, action: { type: 'none', target: '' } };
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
parsedResult = normalizeParsedResult(parsedResult, finalMessage);
|
|
650
|
-
|
|
651
|
-
if (parsedResult && typeof parsedResult.response === 'string') {
|
|
652
|
-
parsedResult.response = decodeUnicode(parsedResult.response);
|
|
653
|
-
parsedResult.response = stripRelevantMemoryBlock(parsedResult.response);
|
|
654
|
-
}
|
|
655
|
-
validateParsedAction(parsedResult);
|
|
656
|
-
parsedResult.timestamp = now;
|
|
657
|
-
|
|
658
|
-
// Record for long-term memory
|
|
659
|
-
if (finalMessage && parsedResult.response) {
|
|
660
|
-
setImmediate(() => {
|
|
661
|
-
memoryStore.recordInteraction(finalMessage, parsedResult.response);
|
|
662
|
-
// Cache text-only responses
|
|
663
|
-
if (images.length === 0 && !base64Audio) {
|
|
664
|
-
memoryStore.cacheResponse(finalMessage, parsedResult);
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
yield { done: true, parsed: parsedResult, timestamp: now };
|
|
670
|
-
|
|
671
|
-
} catch (error) {
|
|
672
|
-
console.error('[Stream] Gemini stream error:', error);
|
|
673
|
-
throw error;
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function resetChat() {
|
|
678
|
-
clearChatHistory();
|
|
679
|
-
memoryStore.clearConversationScopedProfile();
|
|
680
|
-
createChat([]);
|
|
681
|
-
console.log("Chat history cleared.");
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function refreshApiKeyFromConfig() {
|
|
685
|
-
const prevKey = activeApiKey;
|
|
686
|
-
const nextKey = resolveApiKey();
|
|
687
|
-
if (nextKey !== prevKey) {
|
|
688
|
-
initAiClient();
|
|
689
|
-
createChat(readChatHistory());
|
|
690
|
-
}
|
|
691
|
-
return { key: nextKey, updated: nextKey !== prevKey };
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function historyToTranscript(history) {
|
|
695
|
-
if (!Array.isArray(history)) return [];
|
|
696
|
-
|
|
697
|
-
const transcript = [];
|
|
698
|
-
for (const content of history) {
|
|
699
|
-
const sender = content.role === 'user' ? 'user' : 'ai';
|
|
700
|
-
let text = Array.isArray(content.parts)
|
|
701
|
-
? content.parts
|
|
702
|
-
.map((part) => typeof part.text === 'string' ? stripRelevantMemoryBlock(part.text) : '')
|
|
703
|
-
.filter(Boolean)
|
|
704
|
-
.join('\n')
|
|
705
|
-
: '';
|
|
706
|
-
|
|
707
|
-
if (sender === 'ai' && text.trim()) {
|
|
708
|
-
try {
|
|
709
|
-
const parsed = JSON.parse(text);
|
|
710
|
-
if (parsed && typeof parsed.response === 'string' && parsed.response.trim()) {
|
|
711
|
-
text = decodeUnicode(parsed.response);
|
|
712
|
-
}
|
|
713
|
-
} catch {
|
|
714
|
-
text = decodeUnicode(text);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (!text.trim()) continue;
|
|
719
|
-
transcript.push({
|
|
720
|
-
sender,
|
|
721
|
-
text,
|
|
722
|
-
timestamp: content.timestamp || null,
|
|
723
|
-
providerInfo: content.providerInfo || null
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
return transcript;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
async function getChatTranscript() {
|
|
730
|
-
return historyToTranscript(readChatHistory());
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function sleep(ms) {
|
|
734
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function isRetryableTranslateError(err) {
|
|
738
|
-
const status = err?.status ?? err?.error?.code ?? err?.code;
|
|
739
|
-
return status === 502 || status === 503;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Super fast, single-turn vision translation
|
|
744
|
-
* Extracts English text from the image and translates it to Thai.
|
|
745
|
-
*/
|
|
746
|
-
async function translateImageContent(base64Image) {
|
|
747
|
-
const maxAttempts = 3;
|
|
748
|
-
const retryDelayMs = [1000, 2500];
|
|
749
|
-
|
|
750
|
-
try {
|
|
751
|
-
const image = imageDataUriToInlineData(base64Image);
|
|
752
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
753
|
-
try {
|
|
754
|
-
const response = await ai.models.generateContent({
|
|
755
|
-
model: resolveGeminiModel(),
|
|
756
|
-
contents: [
|
|
757
|
-
{
|
|
758
|
-
role: 'user',
|
|
759
|
-
parts: [
|
|
760
|
-
{ text: "Extract any English text you see in this image and translate it to Thai. Return ONLY the Thai translation. If there is no text, return 'ไม่พบข้อความ'." },
|
|
761
|
-
{ inlineData: image }
|
|
762
|
-
]
|
|
763
|
-
}
|
|
764
|
-
],
|
|
765
|
-
config: {
|
|
766
|
-
safetySettings: [
|
|
767
|
-
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_NONE" },
|
|
768
|
-
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_NONE" },
|
|
769
|
-
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
|
|
770
|
-
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_NONE" },
|
|
771
|
-
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "BLOCK_NONE" }
|
|
772
|
-
]
|
|
773
|
-
}
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
text: response.text,
|
|
778
|
-
retryableFailure: false
|
|
779
|
-
};
|
|
780
|
-
} catch (err) {
|
|
781
|
-
const shouldRetry = isRetryableTranslateError(err) && attempt < maxAttempts;
|
|
782
|
-
if (shouldRetry) {
|
|
783
|
-
const delayMs = retryDelayMs[attempt - 1] ?? retryDelayMs[retryDelayMs.length - 1];
|
|
784
|
-
console.warn(`Live translation retry ${attempt}/${maxAttempts - 1} after ${delayMs}ms due to ${err.status || err.code || 'retryable error'}`);
|
|
785
|
-
await sleep(delayMs);
|
|
786
|
-
continue;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
throw err;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
} catch (err) {
|
|
793
|
-
console.error("Live translation error:", err);
|
|
794
|
-
return {
|
|
795
|
-
text: "ขออภัย เกิดข้อผิดพลาดในการแปล",
|
|
796
|
-
retryableFailure: isRetryableTranslateError(err)
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
module.exports = {
|
|
802
|
-
handleChat,
|
|
803
|
-
handleGeminiChatStream,
|
|
804
|
-
resetChat,
|
|
805
|
-
getChatTranscript,
|
|
806
|
-
translateImageContent,
|
|
807
|
-
refreshApiKeyFromConfig,
|
|
808
|
-
_helpers: {
|
|
809
|
-
getProviderAttemptOrder,
|
|
810
|
-
normalizeParsedResult,
|
|
811
|
-
buildActionModeInstruction
|
|
812
|
-
}
|
|
813
|
-
};
|