aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-aiassist.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/module-aiassist.js')
-rw-r--r--modules/core/module-aiassist.js106
1 files changed, 103 insertions, 3 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 70882a9..602ae3e 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -35,7 +35,10 @@
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 ==
+ const optimizationActive = this.terminal?.ollamaManager?.optimizationEnabled !== false;
+ const CR = String.fromCharCode(10);
+ const optNote = CR + CR + '== TOOL EXECUTION RULES (STRICT) ==' + CR + 'STRICT RULE 1: NEVER describe what the filesystem looks like from your training data. ALWAYS call list_files() or read_file() first, then report the ACTUAL result.' + CR + 'STRICT RULE 2: If you return text without calling any tool, you have failed. Use tools for every factual question.' + CR + 'STRICT RULE 3: A response like "/bin/, /etc/, /home/" without calling list_files is ILLEGAL. Call the tool.' + CR + 'STRICT RULE 4: When calling a tool, base your answer ENTIRELY on the tool result. If result is empty, say "empty".' + CR + CR;
+ 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.${optNote}== TOOL REFERENCE ==
You have these tools available. Use them WITHOUT asking permission unless the tool description says (DESTRUCTIVE):
FILESYSTEM:
@@ -436,7 +439,8 @@ RULES:
}
// Auto-load skills relevant to the user's query (awaited, not fire-and-forget)
// Skip skill loading for trivial queries (< 10 chars, no actionable keywords)
- const isTrivial = userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage);
+ const optEnabled = this.terminal?.ollamaManager?.optimizationEnabled !== false;
+ const isTrivial = optEnabled && userMessage.length < 10 && !/\b(search|write|install|edit|create|make|find|delete|remove|change|update|improve|add|fix|build|set|load|save|help)\b/i.test(userMessage);
if (!isTrivial) {
try {
const skillRes = await this.runTool('load_skills', { query: userMessage });
@@ -539,6 +543,42 @@ RULES:
}
return null;
}
+ _parseToolBlocksFromText(text) {
+ if (!text) return [];
+ const toolCalls = [];
+ // Match ```tool name(args) or ```tool\nname(args) blocks
+ const blockRegex = /```tool(?:\s*\n)?\s*(\w+)\s*({[^}]*})?\s*```/gi;
+ let match;
+ while ((match = blockRegex.exec(text)) !== null) {
+ const name = match[1];
+ let args = {};
+ try {
+ if (match[2]) args = JSON.parse(match[2]);
+ } catch (e) {
+ // If JSON parse fails, treat as raw string
+ args = match[2] ? { path: match[2].replace(/[{}]/g,'').trim() } : {};
+ }
+ toolCalls.push({
+ id: 'text_' + Date.now() + '_' + toolCalls.length,
+ type: 'function',
+ function: { name, arguments: args }
+ });
+ }
+ // Also handle single-line format: ```tool get_status{}
+ const simpleRegex = /```tool\s+(\w+)\s*\{([^}]*)\}\s*```/gi;
+ while ((match = simpleRegex.exec(text)) !== null) {
+ // Avoid duplicates — skip if already caught by blockRegex
+ const name = match[1];
+ if (!toolCalls.find(tc => tc.function.name === name)) {
+ toolCalls.push({
+ id: 'text_' + Date.now() + '_' + toolCalls.length,
+ type: 'function',
+ function: { name, arguments: {} }
+ });
+ }
+ }
+ return toolCalls;
+ }
_trackToolFailure(toolName, error) {
try {
const key = 'webx11_tool_failures';
@@ -571,7 +611,67 @@ RULES:
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
- const text = res.message.content || '';
+ let text = res.message.content || '';
+ // Fallback: parse ```tool blocks from text when model writes them instead of native tool_calls
+ let parsedToolCalls = this._parseToolBlocksFromText(text);
+ if (parsedToolCalls.length > 0) {
+ this.terminal.print(`🔧 Parsed ${parsedToolCalls.length} tool call(s) from text block — turn ${toolTurns}/${this.MAX_TOOL_TURNS} (${thinkTime}s)`, 'info');
+ // Extract any non-tool text as pending assistant content
+ const cleanText = text.replace(/```tool[\s\S]*?```/g, '').trim();
+ if (cleanText) messages.push({ role: 'assistant', content: cleanText });
+ messages.push({ role: 'assistant', content: '(calling tool via text block)' });
+ let pendingText = cleanText || null;
+ const destructiveTools = parsedToolCalls.filter(tc => this.isDestructive(tc.function.name));
+ const safeTools = parsedToolCalls.filter(tc => !this.isDestructive(tc.function.name));
+ const safeResults = await Promise.all(safeTools.map(async (tc) => {
+ const fn = tc.function;
+ const t0 = Date.now();
+ const toolResult = await this.runTool(fn.name, fn.arguments);
+ const t1 = Math.round((Date.now() - t0) / 1000);
+ return { fn, toolResult, t1 };
+ }));
+ for (const { fn, toolResult, t1 } of safeResults) {
+ this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
+ const obs = this._summarizeObservation(toolResult, 150);
+ const err = this._isToolError(toolResult);
+ if (err) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${err} (${t1}s)`);
+ this._trackToolFailure(fn.name, err);
+ this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${err}`, 'warning');
+ } else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
+ this.terminal.print(`📊 Result (${t1}s): ${obs}`, 'info');
+ }
+ messages.push({ role: 'tool', content: toolResult });
+ }
+ if (destructiveTools.length > 0) {
+ const approved = await this.confirmActionMulti(destructiveTools);
+ if (approved) {
+ for (const tc of destructiveTools) {
+ const fn = tc.function;
+ const t0 = Date.now();
+ this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
+ const toolResult = await this.runTool(fn.name, fn.arguments);
+ const t1 = Math.round((Date.now() - t0) / 1000);
+ const dObs = this._summarizeObservation(toolResult, 150);
+ const dErr = this._isToolError(toolResult);
+ if (dErr) {
+ this.terminal.log('WARN', 'AI', `Tool ${fn.name} failed: ${dErr} (${t1}s)`);
+ this._trackToolFailure(fn.name, dErr);
+ this.terminal.print(`📊 Result (${t1}s): ⚠️ Error: ${dErr}`, 'warning');
+ } else {
+ this.terminal.log('INFO', 'AI', `Tool ${fn.name} ok (${t1}s)`);
+ this.terminal.print(`📊 Result (${t1}s): ${dObs}`, 'info');
+ }
+ messages.push({ role: 'tool', content: toolResult });
+ }
+ } else {
+ messages.push({ role: 'tool', content: `Destructive actions (${destructiveTools.map(tc => tc.function.name).join(', ')}) denied.` });
+ }
+ }
+ if (pendingText) messages.push({ role: 'assistant', content: pendingText });
+ continue;
+ }
this.terminal.printAIResponse(text || '(no response)');
if (text) this.terminal.ollamaManager.fireResponseHooks(text);
this.terminal.print(`💬 AI answered (turn ${toolTurns}/${this.MAX_TOOL_TURNS}, ${thinkTime}s)`, 'info');