chore: sync connectai
This commit is contained in:
+114
-68
@@ -9,13 +9,19 @@ import {
|
||||
EXCLUDED_DIRS,
|
||||
SYSTEM_PROMPT,
|
||||
shouldAutoPushBrain,
|
||||
getSecondBrainRepo
|
||||
getSecondBrainRepo,
|
||||
buildApiUrl,
|
||||
logError,
|
||||
logInfo,
|
||||
resolveEngine,
|
||||
summarizeText
|
||||
} from './utils';
|
||||
import { validatePath, sanitizeCommand } from './security';
|
||||
|
||||
export interface ChatMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string | any[];
|
||||
internal?: boolean;
|
||||
}
|
||||
|
||||
export class AgentExecutor {
|
||||
@@ -32,7 +38,7 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
public getHistory() {
|
||||
return this.chatHistory;
|
||||
return this.chatHistory.filter(message => !message.internal);
|
||||
}
|
||||
|
||||
public setHistory(history: ChatMessage[]) {
|
||||
@@ -74,6 +80,10 @@ export class AgentExecutor {
|
||||
if (!this.webview) return;
|
||||
|
||||
try {
|
||||
if (loopDepth === 0) {
|
||||
await this.context.workspaceState.update('lastActionStr', undefined);
|
||||
}
|
||||
|
||||
// 1. Prepare Context
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders;
|
||||
const rootPath = workspaceFolders ? workspaceFolders[0].uri.fsPath : '';
|
||||
@@ -91,8 +101,12 @@ export class AgentExecutor {
|
||||
|
||||
// 2. Setup History
|
||||
if (prompt !== null) {
|
||||
this.chatHistory.push({ role: 'user', content: prompt });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
||||
if (loopDepth === 0) {
|
||||
this.chatHistory.push({ role: 'user', content: prompt });
|
||||
this.webview.postMessage({ type: 'streamChunk', value: '' }); // Trigger UI update if needed
|
||||
} else {
|
||||
this.chatHistory.push({ role: 'system', content: prompt, internal: true });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. API Request Setup
|
||||
@@ -108,60 +122,24 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
// Inject System Directives
|
||||
if (reqMessages.length > 0) {
|
||||
const internetCtx = internetEnabled
|
||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||
: '';
|
||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}\n${internetCtx}`;
|
||||
|
||||
const firstUserIdx = reqMessages.findIndex(m => m.role === 'user');
|
||||
if (firstUserIdx >= 0) {
|
||||
let content = reqMessages[firstUserIdx].content;
|
||||
if (typeof content === 'string') {
|
||||
reqMessages[firstUserIdx].content = `${fullSystemPrompt}\n\n[USER QUERY]\n${content}`;
|
||||
if (loopDepth > 0) {
|
||||
reqMessages[firstUserIdx].content = `[Autonomous Step ${loopDepth}/${config.maxAutoSteps}]\n${reqMessages[firstUserIdx].content}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const internetCtx = internetEnabled
|
||||
? `\n\n[CRITICAL: INTERNET ACCESS ENABLED]\nYou can use <read_url> to search. Current time: ${new Date().toLocaleString()}`
|
||||
: '';
|
||||
const fullSystemPrompt = `${systemPrompt}${internetCtx}\n\n[CONTEXT]\n${contextBlock}`;
|
||||
const messagesForRequest: ChatMessage[] = [
|
||||
{ role: 'system', content: fullSystemPrompt, internal: true },
|
||||
...reqMessages
|
||||
];
|
||||
|
||||
// 4. Call AI Engine
|
||||
const isLMStudio = ollamaUrl.includes('1234') || ollamaUrl.includes('v1') || ollamaUrl.includes('localhost');
|
||||
// Note: Many users use LM Studio on localhost, we'll try to be smart or fallback to Ollama format if it fails.
|
||||
|
||||
const apiUrl = isLMStudio ?
|
||||
(ollamaUrl.endsWith('/v1') ? `${ollamaUrl}/chat/completions` : `${ollamaUrl}/v1/chat/completions`) :
|
||||
`${ollamaUrl}/api/chat`;
|
||||
|
||||
this.abortController = new AbortController();
|
||||
|
||||
const streamBody = {
|
||||
model: modelName || defaultModel,
|
||||
messages: reqMessages,
|
||||
stream: true,
|
||||
...(isLMStudio
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
body: JSON.stringify(streamBody),
|
||||
signal: this.abortController.signal,
|
||||
keepalive: true
|
||||
const request = await this.createStreamingRequest({
|
||||
baseUrl: ollamaUrl,
|
||||
modelName: modelName || defaultModel,
|
||||
reqMessages: messagesForRequest,
|
||||
temperature
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`AI Engine error: ${response.status} - ${errText}`);
|
||||
}
|
||||
const { response, engine, apiUrl } = request;
|
||||
|
||||
let aiResponseText = '';
|
||||
const reader = response.body?.getReader();
|
||||
@@ -185,19 +163,21 @@ export class AgentExecutor {
|
||||
try {
|
||||
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
||||
const json = JSON.parse(raw);
|
||||
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
if (token) {
|
||||
aiResponseText += token;
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e: any) {
|
||||
logError('Failed to parse streaming chunk.', { engine, apiUrl, chunk: summarizeText(trimmed, 300), error: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === 'AbortError') {
|
||||
console.log('[Agent] Generation aborted by user.');
|
||||
logInfo('Generation aborted by user.');
|
||||
} else {
|
||||
console.error('[Agent] Stream reading error:', err);
|
||||
logError('Stream reading error.', { engine, apiUrl, error: err?.message || String(err) });
|
||||
this.webview?.postMessage({ type: 'error', value: `Connection lost: ${err.message}` });
|
||||
}
|
||||
}
|
||||
@@ -208,19 +188,25 @@ export class AgentExecutor {
|
||||
const trimmed = buffer.trim();
|
||||
const raw = trimmed.startsWith('data: ') ? trimmed.slice(6) : trimmed;
|
||||
const json = JSON.parse(raw);
|
||||
const token = isLMStudio ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
const token = engine === 'lmstudio' ? json.choices?.[0]?.delta?.content || '' : json.message?.content || json.response || '';
|
||||
if (token) {
|
||||
aiResponseText += token;
|
||||
this.webview?.postMessage({ type: 'streamChunk', value: token });
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e: any) {
|
||||
logError('Failed to parse final streaming buffer.', { engine, apiUrl, buffer: summarizeText(buffer, 300), error: e?.message || String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' });
|
||||
this.chatHistory.push({ role: 'assistant', content: aiResponseText });
|
||||
|
||||
// 5. Execute Actions
|
||||
const report = await this.executeActions(aiResponseText, rootPath);
|
||||
if (!aiResponseText.trim() && report.length === 0) {
|
||||
logError('Model returned an empty response without actions.', { model: modelName || defaultModel, loopDepth });
|
||||
this.webview.postMessage({ type: 'error', value: 'AI model returned an empty response.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.length > 0) {
|
||||
const reportMsg = `\n\n> ⚙️ **System Action Report** (${loopDepth + 1}/${config.maxAutoSteps})\n> ${report.join("\n> ")}\n\n`;
|
||||
@@ -233,11 +219,11 @@ export class AgentExecutor {
|
||||
|
||||
if (currentActionStr === lastActionStr) {
|
||||
this.webview.postMessage({ type: 'streamChunk', value: "\n⚠️ *Stopping to prevent infinite loop.*" });
|
||||
if (loopDepth === 0) this.webview.postMessage({ type: 'streamEnd' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.context.workspaceState.update('lastActionStr', currentActionStr);
|
||||
logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report });
|
||||
|
||||
// Explicitly tell the AI to look at the results and continue
|
||||
const continuationPrompt = "I have executed your actions. Above is the result. Please analyze it and provide the next step or the final answer.";
|
||||
@@ -249,10 +235,70 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
logError('Agent prompt failed.', { error: error?.message || String(error), promptPreview: summarizeText(prompt || '', 200) });
|
||||
this.webview.postMessage({ type: "error", value: `[Agent Error]: ${error.message}` });
|
||||
} finally {
|
||||
if (loopDepth === 0) {
|
||||
this.webview.postMessage({ type: 'streamEnd' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createStreamingRequest(params: {
|
||||
baseUrl: string;
|
||||
modelName: string;
|
||||
reqMessages: ChatMessage[];
|
||||
temperature: number;
|
||||
}): Promise<{ response: Response; engine: 'lmstudio' | 'ollama'; apiUrl: string }> {
|
||||
const { baseUrl, modelName, reqMessages, temperature } = params;
|
||||
const primaryEngine = resolveEngine(baseUrl);
|
||||
const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (const engine of engines) {
|
||||
const apiUrl = buildApiUrl(baseUrl, engine, 'chat');
|
||||
const streamBody = {
|
||||
model: modelName,
|
||||
messages: reqMessages,
|
||||
stream: true,
|
||||
...(engine === 'lmstudio'
|
||||
? { max_tokens: 4096, temperature }
|
||||
: { options: { num_ctx: 32768, num_predict: 4096, temperature } }),
|
||||
};
|
||||
|
||||
try {
|
||||
logInfo('AI streaming request started.', { engine, apiUrl, model: modelName, messageCount: reqMessages.length });
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
},
|
||||
body: JSON.stringify(streamBody),
|
||||
signal: this.abortController?.signal,
|
||||
keepalive: true
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
lastError = new Error(`AI Engine error (${engine}): ${response.status} - ${summarizeText(errText, 300)}`);
|
||||
logError('AI streaming request returned non-OK status.', { engine, apiUrl, status: response.status, body: summarizeText(errText, 500) });
|
||||
continue;
|
||||
}
|
||||
|
||||
logInfo('AI streaming request connected.', { engine, apiUrl });
|
||||
return { response, engine, apiUrl };
|
||||
} catch (error: any) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logError('AI streaming request failed.', { engine, apiUrl, error: lastError.message });
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Unable to connect to AI engine.');
|
||||
}
|
||||
|
||||
private async executeActions(aiMessage: string, rootPath: string): Promise<string[]> {
|
||||
const report: string[] = [];
|
||||
let brainModified = false;
|
||||
@@ -330,7 +376,7 @@ export class AgentExecutor {
|
||||
const content = fs.readFileSync(absPath, 'utf-8');
|
||||
const preview = content.length > 8000 ? content.slice(0, 8000) + "\n... (truncated)" : content;
|
||||
report.push(`📖 Read: ${relPath}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\`` });
|
||||
this.chatHistory.push({ role: 'system', content: `[Result of read_file ${relPath}]\n\`\`\`\n${preview}\n\`\`\``, internal: true });
|
||||
} else {
|
||||
report.push(`❌ Read failed: ${relPath} not found`);
|
||||
}
|
||||
@@ -368,7 +414,7 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
report.push(`📂 Listed: ${relPath}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of list_files ${relPath}]\n${listing}` });
|
||||
this.chatHistory.push({ role: 'system', content: `[Result of list_files ${relPath}]\n${listing}`, internal: true });
|
||||
}
|
||||
} catch (err: any) { report.push(`❌ Listing failed: ${err.message}`); }
|
||||
}
|
||||
@@ -392,7 +438,7 @@ export class AgentExecutor {
|
||||
}
|
||||
|
||||
report.push(`🧠 Brain Listed: ${relPath}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of list_brain ${relPath}]\n${listing}` });
|
||||
this.chatHistory.push({ role: 'system', content: `[Result of list_brain ${relPath}]\n${listing}`, internal: true });
|
||||
} else {
|
||||
report.push(`❌ Brain List failed: ${relPath} not found`);
|
||||
}
|
||||
@@ -411,7 +457,7 @@ export class AgentExecutor {
|
||||
if (targetFile && fs.existsSync(targetFile)) {
|
||||
const content = fs.readFileSync(targetFile, 'utf-8');
|
||||
report.push(`🧠 Brain Read: ${fileName}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\`` });
|
||||
this.chatHistory.push({ role: 'system', content: `[Result of read_brain ${fileName}]\n\`\`\`\n${content}\n\`\`\``, internal: true });
|
||||
} else {
|
||||
report.push(`❌ Brain Read failed: ${fileName} not found in Second Brain`);
|
||||
}
|
||||
@@ -429,7 +475,7 @@ export class AgentExecutor {
|
||||
const content = text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
const preview = content.length > 5000 ? content.slice(0, 5000) + "\n... (truncated)" : content;
|
||||
report.push(`🌐 Read URL: ${url}`);
|
||||
this.chatHistory.push({ role: 'user', content: `[Result of read_url ${url}]\n${preview}` });
|
||||
this.chatHistory.push({ role: 'system', content: `[Result of read_url ${url}]\n${preview}`, internal: true });
|
||||
} catch (err: any) { report.push(`❌ URL Read failed: ${err.message}`); }
|
||||
}
|
||||
|
||||
@@ -453,7 +499,7 @@ export class AgentExecutor {
|
||||
execSync(`git commit -m "[G1nation] Knowledge Update"`, { cwd: brainDir });
|
||||
execSync(`git push`, { cwd: brainDir });
|
||||
} catch (err) {
|
||||
console.error('[Agent] Sync failed:', err);
|
||||
logError('Second Brain sync failed.', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user