diff options
Diffstat (limited to 'modules/core')
| -rw-r--r-- | modules/core/module-aiassist.js | 121 | ||||
| -rw-r--r-- | modules/core/ollama-manager.js | 83 |
2 files changed, 142 insertions, 62 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index e983e6a..70882a9 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -32,9 +32,10 @@ } catch (e) {} } buildSystemPrompt() { - return `You are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise. - -== TOOL REFERENCE == + const now = new Date(); + const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); + return `Current date: ${dateStr}\nCurrent time: ${timeStr}\n\nYou are WebX11AI, the AI assistant embedded in the WebX11 browser terminal. You have direct access to read/write files, search the web, install packages, edit modules, and run terminal commands — all via tool calls. Use tools proactively. Keep responses concise.\n\n== TOOL REFERENCE == You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE): FILESYSTEM: @@ -141,240 +142,240 @@ RULES: type: 'function', function: { name: 'list_files', - description: 'List files and directories in the current or specified virtual filesystem directory', - parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory path to list, e.g. "" for root, "notes" for notes dir' } }, required: [] } + description: 'List VFS directory contents', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir path ("" for root)' } }, required: [] } } }, { type: 'function', function: { name: 'read_file', - description: 'Read contents of a file from the virtual filesystem', - parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to read, e.g. "hello.txt" or "notes/todo.md"' } }, required: ['path'] } + description: 'Read VFS file', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' } }, required: ['path'] } } }, { type: 'function', function: { name: 'write_file', - description: 'Write content to a file in the virtual filesystem (DESTRUCTIVE)', - parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path to write' }, content: { type: 'string', description: 'Content to write' } }, required: ['path', 'content'] } + description: 'Write file (DESTRUCTIVE)', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'File path' }, content: { type: 'string', description: 'Content' } }, required: ['path', 'content'] } } }, { type: 'function', function: { name: 'make_directory', - description: 'Create a new directory in the virtual filesystem', - parameters: { type: 'object', properties: { path: { type: 'string', description: 'Directory name to create' } }, required: ['path'] } + description: 'Create VFS directory', + parameters: { type: 'object', properties: { path: { type: 'string', description: 'Dir name' } }, required: ['path'] } } }, { type: 'function', function: { name: 'delete_file', - description: 'Delete a file or empty directory from the virtual filesystem (DESTRUCTIVE)', + description: 'Delete file or empty dir (DESTRUCTIVE)', parameters: { type: 'object', properties: { path: { type: 'string', description: 'Path to delete' } }, required: ['path'] } } }, { type: 'function', function: { name: 'get_status', - description: 'Get system status: filesystem, modules, Ollama connection, memory', + description: 'System status: FS, modules, memory', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'get_help', - description: 'Get full command reference for WebX11', + description: 'Command reference', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'web_search', - description: 'Search the web using the WebX11 proxy API (SearXNG + DDG fallback). Returns structured results with title, URL, and content snippet.', + description: 'Search web via SearXNG + DDG', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' } }, required: ['query'] } } }, { type: 'function', function: { name: 'web_fetch', - description: 'Fetch and extract clean readable content from a URL using the WebX11 proxy API. Returns title, clean text content, and word count. Use for reading articles, documentation, or web pages.', - parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL to fetch' } }, required: ['url'] } + description: 'Fetch & extract URL content', + parameters: { type: 'object', properties: { url: { type: 'string', description: 'Full URL' } }, required: ['url'] } } }, { type: 'function', function: { name: 'wpm_install', - description: 'Install a WebX11 package from the package index', - parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name to install, e.g. "widget" or "calc"' } }, required: ['package'] } + description: 'Install WPM package', + parameters: { type: 'object', properties: { package: { type: 'string', description: 'Package name' } }, required: ['package'] } } }, { type: 'function', function: { name: 'wpm_search', - description: 'Search available WebX11 packages by keyword', - parameters: { type: 'object', properties: { keyword: { type: 'string', description: 'Search keyword' } }, required: ['keyword'] } + description: 'Search WPM packages', + parameters: { type: 'object', properties: { keyword: { type: 'string', description: 'Keyword' } }, required: ['keyword'] } } }, { type: 'function', function: { name: 'run_command', - description: 'Execute any WebX11 terminal command directly and return its output', - parameters: { type: 'object', properties: { command: { type: 'string', description: 'Full command to execute, e.g. "grep error" or "wc -l"' } }, required: ['command'] } + description: 'Run terminal command', + parameters: { type: 'object', properties: { command: { type: 'string', description: 'Command to run' } }, required: ['command'] } } }, { type: 'function', function: { name: 'get_current_dir', - description: 'Get current working directory path in virtual filesystem', + description: 'Current VFS directory', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'view_module', - description: 'View the source code of any loaded WebX11 module', - parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name, e.g. ThemeSwitcherModule' } }, required: ['name'] } + description: 'View module source code', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'edit_module', - description: 'Copy a module source into the editor for modification (DESTRUCTIVE)', - parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name to edit' } }, required: ['name'] } + description: 'Edit module source (DESTRUCTIVE)', + parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'test_module', - description: 'Load an edited module from /_mods/ for live testing', + description: 'Test edited module from /_mods/', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'reload_module', - description: 'Reload a module from its original URL, discarding local edits', + description: 'Reload module from original URL', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Module class name' } }, required: ['name'] } } }, { type: 'function', function: { name: 'list_modified_modules', - description: 'List which modules have local edits pending test', + description: 'List modules with local edits', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'get_module_status', - description: 'Full status of all modules: loaded, modified, URLs', + description: 'Module status: loaded, modified, URLs', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'load_skills', - description: 'Search available skills by relevance to a query. Skills are stored procedures with step-by-step approaches for recurring tasks. Returns matching skill content to help you work efficiently.', - parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query describing the task — e.g. "search the web" or "organize files"' } }, required: ['query'] } + description: 'Search skills relevant to task', + parameters: { type: 'object', properties: { query: { type: 'string', description: 'Task description' } }, required: ['query'] } } }, { type: 'function', function: { name: 'create_skill', - description: 'Save a new skill (stored procedure) so you can reuse this approach next time. Call after completing a complex multi-step task. Skills persist across sessions.', + description: 'Save a new skill', parameters: { type: 'object', properties: { - name: { type: 'string', description: 'Short kebab-case name, e.g. "web-scrape-and-summarize"' }, - description: { type: 'string', description: 'One-line description of what this skill does' }, - content: { type: 'string', description: 'Markdown content with YAML frontmatter. Must start with "---\\nname: <name>\\n..."' }, - tags: { type: 'string', description: 'Comma-separated tags, e.g. "search,web,scraping"' }, - category: { type: 'string', description: 'Category, e.g. "research", "system", "dev"' } + name: { type: 'string', description: 'kebab-case name' }, + description: { type: 'string', description: 'One-line description' }, + content: { type: 'string', description: 'Markdown with YAML frontmatter' }, + tags: { type: 'string', description: 'Comma-separated' }, + category: { type: 'string', description: 'e.g. research, system, dev' } }, required: ['name', 'description', 'content'] } } }, { type: 'function', function: { name: 'patch_skill', - description: 'Update an existing skill file. Use when a stored procedure is outdated or has better steps. Replaces the entire skill content.', + description: 'Replace skill content', parameters: { type: 'object', properties: { - name: { type: 'string', description: 'Skill name to patch (without .md)' }, - content: { type: 'string', description: 'Complete new markdown content including YAML frontmatter' } + name: { type: 'string', description: 'Skill name' }, + content: { type: 'string', description: 'New markdown content' } }, required: ['name', 'content'] } } }, { type: 'function', function: { name: 'list_skills', - description: 'List all available skills. Optionally filter by category.', - parameters: { type: 'object', properties: { category: { type: 'string', description: 'Optional category filter, e.g. "research" or "system"' } }, required: [] } + description: 'Browse skills (optional filter)', + parameters: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category' } }, required: [] } } }, { type: 'function', function: { name: 'check_updates', - description: 'Check if loaded modules are reachable at their source URLs. Reports any that appear outdated or unreachable.', + description: 'Check module URLs are reachable', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_get', - description: 'Read a value from the persistent user profile. Returns null if key not found.', - parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key to read, e.g. "preferredModel", "fontSize", "knownFacts.name"' } }, required: ['key'] } + description: 'Read user profile key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'e.g. preferredModel' } }, required: ['key'] } } }, { type: 'function', function: { name: 'user_set', - description: 'Write a value to the persistent user profile. Saves across sessions.', - parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key, e.g. "preferredModel"' }, value: { type: 'string', description: 'Value to store' } }, required: ['key', 'value'] } + description: 'Write user profile key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'Profile key' }, value: { type: 'string', description: 'Value' } }, required: ['key', 'value'] } } }, { type: 'function', function: { name: 'user_list', - description: 'List all stored keys in the user profile.', + description: 'List stored profile keys', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'user_forget', - description: 'Delete a key from the user profile.', + description: 'Delete profile key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'Key to delete' } }, required: ['key'] } } }, { type: 'function', function: { name: 'session_save', - description: 'Save a summary of the current session to /memory/. Use after complex tasks. Creates a daily log file.', - parameters: { type: 'object', properties: { summary: { type: 'string', description: 'Brief summary of what was accomplished this session' } }, required: ['summary'] } + description: 'Save session summary', + parameters: { type: 'object', properties: { summary: { type: 'string', description: 'What was accomplished' } }, required: ['summary'] } } }, { type: 'function', function: { name: 'session_load', - description: 'Load a past session log by date. Format: YYYY-MM-DD.', - parameters: { type: 'object', properties: { date: { type: 'string', description: 'Date string like "2026-05-23"' } }, required: ['date'] } + description: 'Load past session by date', + parameters: { type: 'object', properties: { date: { type: 'string', description: 'YYYY-MM-DD' } }, required: ['date'] } } }, { type: 'function', function: { name: 'session_recent', - description: 'Show summaries from the most recent sessions.', - parameters: { type: 'object', properties: { count: { type: 'number', description: 'Number of recent sessions to show (default 3)' } }, required: [] } + description: 'Show recent sessions', + parameters: { type: 'object', properties: { count: { type: 'number', description: 'Count (default 3)' } }, required: [] } } }, { type: 'function', function: { name: 'session_terminal', - description: "Read the last ~50 lines of terminal output from the auto-saved session. Use this when the user references something they did in the terminal that you don't remember. Shows commands, outputs, and timestamps.", + description: 'Read last terminal output', parameters: { type: 'object', properties: {}, required: [] } } }, { type: 'function', function: { name: 'session_stats', - description: 'Aggregate statistics across all sessions: total tasks, most-used tools, common topics.', + description: 'Session aggregate stats', parameters: { type: 'object', properties: {}, required: [] } } }]; diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js index c790e6f..3f46b13 100644 --- a/modules/core/ollama-manager.js +++ b/modules/core/ollama-manager.js @@ -592,6 +592,12 @@ class OllamaManager { // ─── Chat With Tools ───────────────────────────────────────────────────── + _isModelType(modelName, type) { + if (!modelName) return false; + const lower = modelName.toLowerCase(); + return lower.includes(type); + } + async chatWithTools(messages, tools, model = null) { if (!this.connected) { this.terminal.print('Ollama not connected. Use ollamaconnect first.', 'error'); @@ -658,7 +664,8 @@ class OllamaManager { error: null }; } else { - // OpenAI-compatible provider + // OpenAI-compatible provider (OpenRouter, DeepSeek, etc.) + const isDeepSeek = this._isModelType(useModel, 'deepseek'); const headers = { 'Content-Type': 'application/json' }; if (provider.apiKey) { headers['Authorization'] = 'Bearer ' + provider.apiKey; @@ -668,6 +675,11 @@ class OllamaManager { messages: messages, stream: false }; + if (isDeepSeek) { + body.temperature = 0.3; + body.max_tokens = 4096; + body.stop = ['<|end|>']; + } if (tools && tools.length > 0) { body.tools = tools; } @@ -688,8 +700,75 @@ class OllamaManager { const choice = data.choices?.[0]; const message = choice?.message || null; const toolCalls = message?.tool_calls || null; + const content = message?.content || ''; + + // DeepSeek: handle null content with tool calls — send empty string + if (isDeepSeek && content === '' && toolCalls && toolCalls.length > 0) { + return { + message: { role: 'assistant', content: '' }, + tool_calls: toolCalls.map(tc => ({ + id: tc.id, + type: tc.type || 'function', + function: { + name: tc.function?.name || '', + arguments: tc.function?.arguments || '{}' + } + })), + error: null + }; + } + + // DeepSeek: empty response with no tools — retry once with stream forcing + if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) { + this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning'); + body.stream = true; + delete body.stop; + const retryResponse = await fetch(`${provider.endpoint}/v1/chat/completions`, { + method: 'POST', + headers, + body: JSON.stringify(body), + signal: this._abortController ? undefined : undefined + }); + if (retryResponse.ok) { + const reader = retryResponse.body.getReader(); + const decoder = new TextDecoder(); + let fullText = ''; + let retryBuffer = ''; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + retryBuffer += decoder.decode(value, { stream: true }); + const lines = retryBuffer.split('\n'); + retryBuffer = lines.pop() || ''; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed === 'data: [DONE]') continue; + if (trimmed.startsWith('data: ')) { + try { + const parsed = JSON.parse(trimmed.substring(6)); + const chunk = parsed.choices?.[0]?.delta?.content || ''; + if (chunk) fullText += chunk; + } catch (e) {} + } + } + } + if (fullText) { + return { + message: { role: 'assistant', content: fullText }, + tool_calls: null, + error: null + }; + } + } + return { + message: { role: 'assistant', content: '' }, + tool_calls: null, + error: null + }; + } + return { - message: message ? { role: message.role || 'assistant', content: message.content || '' } : null, + message: message ? { role: message.role || 'assistant', content: content } : null, tool_calls: toolCalls ? toolCalls.map(tc => ({ id: tc.id, type: tc.type || 'function', |