Files
connectai/src/config.ts
T
koriweb eeb527c242 feat(datacollect): /youtube 개편·/wikify 신규·출력 위생 (v2.2.48)
- /youtube: 4-렌즈 분석 → 대본(스크립트) 역기획서 포맷으로 개편, 보고서
  앞에 영상 전체 스크립트(Full Script) 출력, 명령어 보조 컨텍스트 지원
- /wikify: 신규 슬래시 명령 — 웹사이트 본문(/api/web-extract)을 P-Reinforce
  v3.0 위키 문서로 합성. 여러 링크 순차 배치 처리, 명세 문서 완전성 규칙,
  위키링크 자동 교정
- Self-Reflector Phase A 기본 비활성화 — [Self-Reflector Check] 내부 검증
  로그가 사용자 답변에 노출되지 않도록
- 슬래시 합성·일반 채팅 시스템 프롬프트에 출력 위생 규칙 추가 — 한·영 토큰
  깨짐 정제, 내부 검증 로그 출력 금지

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:34:07 +09:00

299 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
// ─── 브레인 프로필 인터페이스 ───
export interface BrainProfile {
id: string;
name: string;
localBrainPath: string;
secondBrainRepo?: string;
description?: string;
}
// ─── 에이전트 설정 인터페이스 (통합 버전) ───
export interface IAgentConfig {
ollamaUrl: string;
defaultModel: string;
maxTreeFiles: number;
timeout: number;
localBrainPath: string;
secondBrainRepo: string;
brainProfiles: BrainProfile[];
activeBrainId: string;
maxContextSize: number;
maxAutoSteps: number;
dryRun: boolean;
multiAgentEnabled: boolean;
memoryEnabled: boolean;
memoryShortTermMessages: number;
memoryMediumTermSessions: number;
memoryLongTermFiles: number;
// ─── 컨텍스트 한계 관리 ───
contextLength: number;
maxOutputTokens: number;
contextSafetyMargin: number;
contextOverflowPolicy: 'stopAtLimit' | 'truncateMiddle' | 'rollingWindow';
autoCompactHistory: boolean;
/** 작은 모델(≤4B) 감지 시 예산 계산에 쓸 유효 context window 상한. 0 = 비활성화. */
smallModelContextCap: number;
// ─── 응답 복구 (Thought Quarantine / Auto-Continuation) ───
/** 답변이 출력 토큰 한계에 걸리면 사용자 개입 없이 내부적으로 이어서 생성. */
autoContinueOnOutputLimit: boolean;
/** 자동 이어쓰기 최대 횟수 (무한 반복 방지). 0 = 비활성화. */
maxAutoContinuations: number;
/** 모델이 내부 사고만 출력하고 답변이 없으면 "최종 답변만" 지시로 1회 재생성. */
finalOnlyRetryOnThoughtLeak: boolean;
// ─── Hybrid Semantic Search ───
/**
* Embedding model name as registered in LM Studio / Ollama. Empty disables
* semantic search and the retriever falls back to TF-IDF only. The user
* must load this model in the engine before enabling it here.
*/
embeddingModel: string;
/**
* Blend between TF-IDF (sparse) and embedding cosine (dense) scoring.
* 0 = TF-IDF only (status quo), 1 = embedding only.
* Default 0.5 = equal weight, a reasonable starting point.
*/
embeddingBlendAlpha: number;
/**
* Global Knowledge Mix weight (0100). Controls how much the assistant leans on
* Second Brain evidence vs. model general knowledge when answering.
* 0 → Second Brain disabled; model knowledge only.
* 50 → Balanced (default).
* 100 → Second Brain is primary evidence; model knowledge only fills gaps.
* Per-agent overrides live in AgentKnowledgeEntry.secondBrainWeight and win.
*/
knowledgeMixSecondBrainWeight: number;
/**
* [Self-Reflection] Researcher와 Writer 사이에 메타인지 단계(Reflector)를 삽입할지 여부.
* true(기본): Reflector가 plan/research를 비판적으로 검토한 critique을 Writer에 주입.
* false: 기존 3단계(Planner→Researcher→Writer) 그대로 — 1 LLM 호출 절약 (저성능 모델/저지연 우선 시).
*/
/**
* Model id used by the 1인 기업 mode intent classifier (route message to
* pipeline vs casual chat). Empty → falls back to `defaultModel`. Recommended
* a fast small model (gemma e2b 등) so classification adds <1 s per send.
*/
companyIntentClassifierModel: string;
/**
* Bypass the intent classifier and always run the full pipeline. Legacy
* behaviour. Off by default because chat / question / thanks shouldn't
* dispatch all agents.
*/
companyDisableIntentClassifier: boolean;
/**
* 분류기가 추천한 파이프라인으로 *이번 turn만* 자동 전환할지. 켜면
* 사용자가 명시적으로 활성화해 둔 파이프라인보다 분류기 추천이 우선.
* 끄면 분류기 추천은 채팅 라벨에만 표시되고 dispatch는 사용자 활성
* 파이프라인 그대로. 처음 써 보는 사용자는 끈 채 추천만 보고 점차
* 신뢰 생기면 켜는 흐름을 권장 — 기본 false.
*/
companyAutoSelectPipeline: boolean;
/**
* Intent Alignment 모드. new_task 발생 시 사용자 의도를 C-G-C-F-Q로
* 정리하는 단계를 어떻게 다룰지.
* - 'off' : alignment 비활성. 분류기가 new_task 라고 하면 곧장 pipeline.
* - 'smart' : 기본값. confidence high면 자동 진행, medium/low면 사용자 확인.
* - 'strict' : confidence 무관 항상 사용자에게 contract 확인 카드 띄움.
*/
companyIntentAlignmentMode: 'off' | 'smart' | 'strict';
/** alignment 라운드 최대 횟수 (질문→답변 사이클). 1~5. */
companyIntentAlignmentMaxRounds: number;
/**
* Pixel Office 시각화 패널을 사이드바에 표시할지 여부. UI layer 전용 —
* 끈다고 Agent 행동이 바뀌지 않는다. Off면 webview는 패널을 숨기고
* 백엔드도 broadcast 자체를 skip해서 자원 절약.
*/
/**
* Self-Reflector Phase A — 모든 LLM 응답 끝에 [Self-Reflector Check]
* 자기검증 블록을 자동으로 붙이게 한다. 추가 LLM 콜 없음. 본질적으로
* 응답 품질 안전망이라 끌 이유는 적지만, 잡담 위주 환경에서 노이즈로
* 느껴진다면 꺼둘 수 있다.
*/
selfReflectorEnabled: boolean;
/**
* Self-Reflector Phase B — 회사 모드 specialist 응답 직후 *분리된 콘텍스트*
* 에서 LLM 한 번 더 호출해 외부 시각으로 검증. 실패 시 1회 retry. 비용
* 추가되므로 기본 OFF.
*/
selfReflectorExternalEnabled: boolean;
/**
* Self-Reflector Phase C — 코드 산출물에 한해 syntax/lint를 실제로 돌려
* 실행 기반 검증. Python: py_compile, JS: node --check, TS: tsc --noEmit.
* 실패 시 에러를 응답에 첨부. 기본 OFF — 사용자 환경에 toolchain이
* 깔려 있어야 의미가 있다.
*/
selfReflectorExecutionEnabled: boolean;
companyPixelOfficeEnabled: boolean;
/**
* Pixel Office의 캐릭터 말풍선 연출을 켤지. enabled가 true이고 이 값도
* true일 때만 말풍선이 생성된다. 시끄럽게 느껴지면 사용자가 끌 수 있게.
*/
companyPixelOfficeBubbles: boolean;
enableReflection: boolean;
/**
* [Self-Reflection → Knowledge] Reflector critique 중 의미 있는 발견을 brain의
* `lessons/auto-reflector/`에 lesson 카드로 영속화할지 여부. true(기본)이면 동일/유사 패턴이
* 다음 미션에서 retrieval로 자동 주입되고, 같은 critique이 반복될수록 occurrences/severity가
* 누적됨. false면 critique은 그 미션 한정으로만 사용되고 사라짐.
*/
autoLessonFromReflection: boolean;
}
// ─── 경로 정규화 유틸리티 ───
function normalizePath(p: string): string {
if (!p) return p;
if (p.startsWith('~/')) {
return path.join(os.homedir(), p.substring(2));
}
return p.trim();
}
function toBrainProfile(raw: Partial<BrainProfile> | undefined, fallbackIndex: number): BrainProfile | null {
if (!raw) return null;
const localBrainPath = normalizePath(raw.localBrainPath || '');
if (!localBrainPath) return null;
return {
id: (raw.id || `brain-${fallbackIndex + 1}`).trim(),
name: (raw.name || path.basename(localBrainPath) || `Brain ${fallbackIndex + 1}`).trim(),
localBrainPath,
secondBrainRepo: (raw.secondBrainRepo || '').trim(),
description: (raw.description || '').trim()
};
}
// ─── VS Code 설정에서 읽어오는 값 (통합 구현) ───
export function getConfig(): IAgentConfig {
const cfg = vscode.workspace.getConfiguration('g1nation');
// 브레인 프로필 로직 (utils.ts에서 이관)
const legacyBrainPath = cfg.get<string>('localBrainPath', '');
const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('brainProfiles', []);
const profiles = configuredProfiles
.map((profile, index) => toBrainProfile(profile, index))
.filter((profile): profile is BrainProfile => !!profile);
if (profiles.length === 0) {
const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain');
profiles.push({
id: 'default-brain',
name: 'Local Brain',
localBrainPath: fallbackPath,
secondBrainRepo: legacyBrainRepo.trim(),
description: legacyBrainPath
? 'Migrated from your existing localBrainPath setting'
: 'Auto-created local knowledge folder.'
});
}
const activeBrainId = cfg.get<string>('activeBrainId', profiles[0].id) || profiles[0].id;
const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0];
return {
ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434',
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b') || 'gemma4:e2b',
maxTreeFiles: 200,
timeout: cfg.get<number>('requestTimeout', 300) * 1000,
localBrainPath: activeBrain.localBrainPath,
secondBrainRepo: activeBrain.secondBrainRepo || '',
brainProfiles: profiles,
activeBrainId: activeBrain.id,
maxContextSize: cfg.get<number>('maxContextSize', 12000),
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50),
dryRun: cfg.get<boolean>('dryRun', false),
multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', false),
memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
memoryShortTermMessages: Math.max(0, cfg.get<number>('memoryShortTermMessages', 8)),
memoryMediumTermSessions: Math.max(0, cfg.get<number>('memoryMediumTermSessions', 5)),
memoryLongTermFiles: Math.max(0, cfg.get<number>('memoryLongTermFiles', 6)),
contextLength: Math.max(2048, cfg.get<number>('contextLength', 32768)),
maxOutputTokens: Math.max(256, cfg.get<number>('maxOutputTokens', 4096)),
contextSafetyMargin: Math.max(0, cfg.get<number>('contextSafetyMargin', 2048)),
contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => {
const v = cfg.get<string>('contextOverflowPolicy', 'stopAtLimit');
return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit';
})(),
autoCompactHistory: cfg.get<boolean>('autoCompactHistory', true),
smallModelContextCap: Math.max(0, cfg.get<number>('smallModelContextCap', 0)),
autoContinueOnOutputLimit: cfg.get<boolean>('autoContinueOnOutputLimit', true),
maxAutoContinuations: Math.max(0, Math.min(10, cfg.get<number>('maxAutoContinuations', 4))),
finalOnlyRetryOnThoughtLeak: cfg.get<boolean>('finalOnlyRetryOnThoughtLeak', true),
embeddingModel: (cfg.get<string>('embeddingModel', '') || '').trim(),
embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get<number>('embeddingBlendAlpha', 0.5))),
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
))),
companyIntentClassifierModel: (cfg.get<string>('company.intentClassifierModel', '') || '').trim(),
companyDisableIntentClassifier: cfg.get<boolean>('company.disableIntentClassifier', false),
companyAutoSelectPipeline: cfg.get<boolean>('company.autoSelectPipeline', true),
companyIntentAlignmentMode: ((): 'off' | 'smart' | 'strict' => {
const v = (cfg.get<string>('company.intentAlignmentMode', 'smart') || 'smart').trim().toLowerCase();
return v === 'off' || v === 'strict' ? v : 'smart';
})(),
companyIntentAlignmentMaxRounds: Math.max(1, Math.min(5, cfg.get<number>('company.intentAlignmentMaxRounds', 3))),
selfReflectorEnabled: cfg.get<boolean>('selfReflector.enabled', false),
selfReflectorExternalEnabled: cfg.get<boolean>('selfReflector.externalVerification', false),
selfReflectorExecutionEnabled: cfg.get<boolean>('selfReflector.executionVerification', false),
companyPixelOfficeEnabled: cfg.get<boolean>('company.pixelOffice.enabled', true),
companyPixelOfficeBubbles: cfg.get<boolean>('company.pixelOffice.bubbles', true),
enableReflection: cfg.get<boolean>('enableReflection', true),
autoLessonFromReflection: cfg.get<boolean>('autoLessonFromReflection', true),
};
}
/**
* Config Validator: Validates the current configuration.
*/
export function validateConfig(): { valid: boolean; errors: string[] } {
const config = getConfig();
const errors: string[] = [];
// 1. Ollama URL Validation
try {
new URL(config.ollamaUrl);
} catch (e) {
errors.push(`Invalid Ollama URL: ${config.ollamaUrl}`);
}
// 2. Brain Path Validation
if (config.localBrainPath && !fs.existsSync(config.localBrainPath)) {
errors.push(`Brain path does not exist: ${config.localBrainPath}`);
}
return {
valid: errors.length === 0,
errors
};
}
// ─── 보안 정책 (Security Policy) ───
export const SECURITY_POLICY = {
allowedCommandPrefixes: [
'npm', 'yarn', 'pnpm', 'npx', 'node', 'ts-node', 'git', 'python', 'python3', 'pip', 'pip3',
'docker', 'docker-compose', 'ls', 'dir', 'cat', 'type', 'echo', 'print', 'cargo', 'go', 'rustc',
'java', 'javac', 'mvn', 'gradle', 'flutter', 'dart', 'pub', 'webpack', 'vite', 'esbuild', 'parcel',
'jest', 'mocha', 'vitest', 'cypress', 'tsc', 'vue-tsc',
],
forbiddenCommands: [
'rm -rf', 'rm-rf', 'del /f', 'format', 'mkfs', 'dd if=', ':(){ :|:& };:',
'wget http', 'curl http', 'sudo', 'chmod 777', 'chown root',
],
sensitiveFilePatterns: [
'.env', '.env.*', 'id_rsa', 'id_ed25519', '.gitconfig', '.npmrc', '.pypirc',
'credentials.json', 'service-account.json',
],
maxFileSize: 10 * 1024 * 1024,
maxContextFiles: 200,
};
export const EXCLUDED_DIRS = new Set([
'node_modules', '.git', '.vscode', 'out', 'dist', 'build',
'.next', '.cache', '__pycache__', '.DS_Store', 'coverage',
'.turbo', '.nuxt', '.output', 'vendor', 'target', '.astra'
]);