167 lines
7.8 KiB
TypeScript
167 lines
7.8 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import { getConfig } from '../config';
|
|
import { AgentExecuteOptions } from '../lib/engine';
|
|
|
|
export abstract class BaseAgent {
|
|
constructor(protected readonly modelName: string) {}
|
|
|
|
protected async callLLM(persona: string, prompt: string, signal?: AbortSignal): Promise<string> {
|
|
const { ollamaUrl } = getConfig();
|
|
if (!ollamaUrl) {
|
|
throw new Error('Ollama URL이 설정되지 않았습니다. 설정을 확인해주세요.');
|
|
}
|
|
|
|
if (typeof fetch === 'undefined') {
|
|
throw new Error('이 환경에서는 fetch 함수를 사용할 수 없습니다. Node.js 버전을 확인하거나 polyfill이 필요합니다.');
|
|
}
|
|
|
|
const messages = [
|
|
{ role: 'system', content: persona },
|
|
{ role: 'user', content: prompt }
|
|
];
|
|
|
|
// 엔진 자동 감지 (Ollama vs OpenAI/LM Studio)
|
|
const isOllama = ollamaUrl.includes(':11434') || ollamaUrl.includes('ollama');
|
|
const endpoint = isOllama ? `${ollamaUrl}/api/chat` : `${ollamaUrl}/v1/chat/completions`;
|
|
|
|
// 컨텍스트 초과 방지를 위해 출력 토큰 상한을 항상 명시한다 (서브에이전트 중간 산출물용).
|
|
const { contextLength, maxOutputTokens } = getConfig();
|
|
const numCtx = Math.max(2048, contextLength);
|
|
const outCap = Math.max(256, maxOutputTokens);
|
|
|
|
let lastError: any;
|
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 45000);
|
|
const combinedSignal = signal ? anySignal([signal, controller.signal]) : controller.signal;
|
|
|
|
try {
|
|
if (attempt > 1) await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
|
|
const response = await fetch(endpoint, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(isOllama ? {
|
|
model: this.modelName,
|
|
messages,
|
|
stream: false,
|
|
options: { temperature: 0.3, num_ctx: numCtx, num_predict: outCap }
|
|
} : {
|
|
model: this.modelName,
|
|
messages,
|
|
stream: false,
|
|
temperature: 0.3,
|
|
max_tokens: outCap
|
|
}),
|
|
signal: combinedSignal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Agent API Error: ${response.statusText} (${response.status})`);
|
|
}
|
|
|
|
const data = await response.json() as any;
|
|
|
|
// 강력한 응답 추출 (Multi-path parsing)
|
|
let content = '';
|
|
if (data.message?.content) content = data.message.content;
|
|
else if (data.choices?.[0]?.message?.content) content = data.choices[0].message.content;
|
|
else if (data.choices?.[0]?.text) content = data.choices[0].text;
|
|
else if (data.response) content = data.response;
|
|
else if (typeof data === 'string') content = data;
|
|
|
|
return content || '';
|
|
} catch (error: any) {
|
|
clearTimeout(timeoutId);
|
|
lastError = error;
|
|
if (error.name === 'AbortError') break;
|
|
if (attempt === 3) break;
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
abstract execute(input: string, context?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string>;
|
|
}
|
|
|
|
// Helper to combine signals (since AbortSignal.any is not always available in older Node)
|
|
function anySignal(signals: AbortSignal[]): AbortSignal {
|
|
const controller = new AbortController();
|
|
for (const signal of signals) {
|
|
if (signal.aborted) {
|
|
controller.abort();
|
|
return signal;
|
|
}
|
|
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
}
|
|
return controller.signal;
|
|
}
|
|
|
|
export class PlannerAgent extends BaseAgent {
|
|
private readonly persona = `You are the [Master Strategist & Planner].
|
|
Your sole purpose is to transform vague requests into flawless, high-resolution execution blueprints.
|
|
- THINKING PROCESS: You must analyze the request from multiple angles (technical, logical, structural).
|
|
- OUTPUT RULE: You MUST output a structured <blueprint> using Markdown.
|
|
- COMPONENTS: Each blueprint must have [Objective], [Core Challenges], [Data Requirements], and [Step-by-Step Research Tasks].
|
|
- CONSTRAINT: Do not be vague. Use professional terminology. If the request is too simple, expand it with relevant technical considerations.`;
|
|
|
|
async execute(input: string, brainContext?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
|
|
const wrappedInput = `### SYSTEM INSTRUCTION: GENERATE EXECUTION BLUEPRINT
|
|
1. Target Goal: ${input}
|
|
2. Available Knowledge Base & Policy: ${brainContext}
|
|
3. Mission: Create a comprehensive research roadmap.`;
|
|
return this.callLLM(this.persona, wrappedInput, signal);
|
|
}
|
|
}
|
|
|
|
export class ResearcherAgent extends BaseAgent {
|
|
private readonly persona = `You are the [Senior Technical Researcher].
|
|
Your mission is to extract, filter, and synthesize critical data based on a strategic blueprint.
|
|
- DATA INTEGRITY: Only provide high-quality, verified-style information.
|
|
- FORMAT: Use [Key Facts], [Technical Deep-Dive], and [Summary of Knowledge] sections.
|
|
- CRITICAL THINKING: Identify gaps in the plan and provide extra insights to fill those gaps.
|
|
- NO FLUFF: Be concise but extremely dense with information.`;
|
|
|
|
async execute(input: string, brainContext?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
|
|
const wrappedInput = `### SYSTEM INSTRUCTION: DATA HARVESTING
|
|
1. Blueprint to Follow: ${input}
|
|
2. Contextual Constraints & Policy: ${brainContext}
|
|
3. Mission: Provide a dense summary of facts and technical insights.`;
|
|
return this.callLLM(this.persona, wrappedInput, signal);
|
|
}
|
|
}
|
|
|
|
export class WriterAgent extends BaseAgent {
|
|
private readonly persona = `You are the [Lead Synthesis Writer & Editor].
|
|
Your goal is to produce a state-of-the-art final report that wows the user.
|
|
- TONE: Authoritative yet accessible. Professional developer/consultant style.
|
|
- STRUCTURE: Use an executive summary, detailed analysis sections, and a "Final Recommendation" block.
|
|
- LANGUAGE: Always respond in the user's language (KOREAN).
|
|
- POLISHING: Ensure logical flow between sections. Make it look like a premium report.`;
|
|
|
|
async execute(input: string, originalRequest?: string, signal?: AbortSignal, options?: AgentExecuteOptions): Promise<string> {
|
|
// [Astra v4.0] Advisor 모드 처리
|
|
if (options?.config?.role === 'advisor') {
|
|
const advisorPersona = `You are the [Strategic Proactive Advisor].
|
|
Analyze the provided report and suggest 3 high-impact next actions for the user.
|
|
- Focus on decision forks, risk mitigation, or immediate implementation steps.
|
|
- Be extremely concrete and actionable.
|
|
- Respond in KOREAN.`;
|
|
return this.callLLM(advisorPersona, input, signal);
|
|
}
|
|
|
|
// Fix 3: Trim input if it's too long (Basic Context Diet)
|
|
const trimmedData = input.length > 8000 ? input.substring(0, 8000) + '... [Data Trimmed for Performance]' : input;
|
|
|
|
const policy = options?.context || '';
|
|
const wrappedInput = `### SYSTEM INSTRUCTION: FINAL SYNTHESIS
|
|
1. Gathered Research Data: ${trimmedData}
|
|
2. User's Original Objective: ${originalRequest}
|
|
3. Applied Knowledge & Filtering Policy: ${policy}
|
|
4. Mission: Write the definitive final report in KOREAN.`;
|
|
return this.callLLM(this.persona, wrappedInput, signal);
|
|
}
|
|
}
|