ded3eea7ce
주요 변경: [chunked writer 아키텍처 (v2.2.74~v2.2.75)] - 5-stage 다중 에이전트(planner/researcher/reflector/writer/synthesizer) 파이프라인 제거 → 단일 ChunkedWriter 의 outline → section[N] → polish 3-step 으로 교체. 본문 분석에서 추상화 손실 / 토큰 폭증 문제 해소 - 답변 길이 자동 분기: 짧은 prompt 는 fast-path direct 1회 호출, 본문 분석은 chunked. outline 빈 배열도 direct 폴백 [코드 리뷰 9개 항목 일괄 패치 (v2.2.76)] - /research polling hang 방어 (heartbeat + status 정규화 + 연속 실패 abort) - 회사 모드 dispatcher abort 신호를 AIService.chat 까지 전달 - bridgeFetch 에 onHeartbeat 콜백 도입 (slow endpoint 사용자 친화적) - dead code 정리: reflectionPersister.ts 제거 + enableReflection 등 좀비 config 키 - parseOutline 의 empty vs fallback reason 명시적 분리 - chatHandlers 의 회사 모드 케이스 ~325줄을 src/sidebar/companyHandlers.ts 로 분리 - Intent Alignment 라운드 한도 도달 시 smart 모드 자동 진행 - LM Studio doSwitch unload 실패 시 currentModel 정리 + load 강행 - retrieval informationDensity → queryCoverage 정합화 [/youtube 채널 지원 (v2.2.77~v2.2.82)] - 채널/플레이리스트 URL 자동 감지 + n:N 으로 영상 개수 지정 (최대 50) - 채널 루트 URL 에 /videos 탭 자동 append (yt-dlp enumeration 정상화) - 영상별 순차 처리 (queue 패턴) + i/N 진행 표시 + 마지막 통계 요약 - mode:info / mode:benchmark / mode:both 분석 모드 분기 - info: 영상 내용을 지식 카드로 추출 (튜토리얼·강의·뉴스용) - benchmark: 4-렌즈 대본 역기획서 (콘텐츠 제작 벤치마크용) - both: 둘 다 (기본) - bare keyword 도 허용: /youtube <url> n:1 info - bridge 에러 메시지 [object Object] 깨짐 수정 (구조화 에러 추출) - "패키지 없음" 등 환경 의존성 에러에 자동 가이드 첨부 [Astra: Setup Datacollect Dependencies 명령 추가 (v2.2.80)] - Python 자동 감지 + yt-dlp / youtube-transcript-api 자동 설치 - macOS PEP 668 환경 자동 폴백 (--user --break-system-packages) - /youtube 등에서 패키지 미설치 감지 시 "Install Now" 버튼 notification [테스트] - tests/agentEngine.test.ts 를 chunked flow 에 맞춰 전체 재작성 - tests/resilience_stress.test.ts Scenario B/D 를 role-aware mock 으로 갱신 - 399/399 통과 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
221 lines
8.8 KiB
TypeScript
221 lines
8.8 KiB
TypeScript
import { logInfo } from '../utils';
|
|
|
|
export class AgentDataValidator {
|
|
/**
|
|
* 에이전트 간 핸드오프(Handoff) 시 데이터 무결성을 검증하고 품질 점수를 반환합니다.
|
|
*/
|
|
public static validateHandoff(stage: string, data: string): { score: number, conflictRisk: number } {
|
|
if (!data || data.trim().length === 0) {
|
|
throw new Error(`[IntegrityError] 데이터 누락: ${stage} 에이전트의 출력이 비어 있습니다.`);
|
|
}
|
|
|
|
const minLength = process.env.NODE_ENV === 'test' ? 5 : 50;
|
|
|
|
switch (stage.toLowerCase()) {
|
|
case 'planner':
|
|
if (data.length < minLength) {
|
|
throw new Error(`[IntegrityError] Planner 출력 데이터가 비정상적으로 짧습니다 (${data.length} chars). 계획 누락 의심.`);
|
|
}
|
|
break;
|
|
case 'researcher':
|
|
if (data.length < minLength) {
|
|
throw new Error(`[IntegrityError] Researcher 출력 데이터가 부족합니다 (${data.length} chars). 근거 데이터 누락 의심.`);
|
|
}
|
|
break;
|
|
case 'writer':
|
|
if (data.length < minLength) {
|
|
throw new Error(`[IntegrityError] Writer 결과물이 불완전합니다 (${data.length} chars). 최종 보고서 작성 실패 의심.`);
|
|
}
|
|
break;
|
|
case 'outline':
|
|
// Outline 응답은 JSON 배열 — 빈 배열 `[]` (single-pass 신호) 도 유효.
|
|
// 길이 검증을 우회하고, 형식 검사는 호출자(`parseOutline`) 가 별도로 한다.
|
|
break;
|
|
case 'direct':
|
|
// 짧은 질문에 대한 짧은 답을 허용. 한 문장(≈ 10자) 까지 OK.
|
|
if (data.length < 4) {
|
|
throw new Error(`[IntegrityError] Direct 답변이 비정상적으로 짧습니다 (${data.length} chars).`);
|
|
}
|
|
break;
|
|
default:
|
|
if (data.length < 10) {
|
|
throw new Error(`[IntegrityError] ${stage} 에이전트로부터 유효한 응답을 받지 못했습니다.`);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 품질 점수화 (Quality Scoring)
|
|
const score = QualityScorer.evaluate(data);
|
|
if (score < 50) {
|
|
logInfo(`[QualityWarning] ${stage} 결과 품질 낮음 (Score: ${score}). 내용 보강이 필요할 수 있습니다.`);
|
|
}
|
|
|
|
// [New] 지능형 충돌 감지 및 위험도 평가
|
|
const conflictRisk = ConflictDetector.analyzeSemanticDivergence(data);
|
|
if (conflictRisk > 40) {
|
|
logInfo(`[ConflictAlert] ${stage} 단계에서 지식 충돌 위험 감지 (Risk: ${conflictRisk}).`);
|
|
}
|
|
|
|
return { score, conflictRisk };
|
|
}
|
|
|
|
/**
|
|
* [New] 오류 발생 시 안전하게 데이터를 진단합니다 (에러를 던지지 않음).
|
|
*/
|
|
public static audit(stage: string, data: string): { score: number, conflictRisk: number, issues: string[] } {
|
|
const issues: string[] = [];
|
|
if (!data || data.trim().length === 0) {
|
|
issues.push('데이터가 완전히 비어 있음');
|
|
return { score: 0, conflictRisk: 100, issues };
|
|
}
|
|
|
|
if (data.length < 50) issues.push('데이터 길이가 너무 짧음 (잠재적 누락)');
|
|
|
|
const score = QualityScorer.evaluate(data);
|
|
const conflictRisk = ConflictDetector.analyzeSemanticDivergence(data);
|
|
|
|
if (score < 40) issues.push('품질 점수 매우 낮음');
|
|
if (conflictRisk > 60) issues.push('심각한 지식 충돌 감지');
|
|
|
|
return { score, conflictRisk, issues };
|
|
}
|
|
}
|
|
|
|
export class QualityScorer {
|
|
/**
|
|
* 수집된 데이터의 품질을 휴리스틱하게 점수화합니다 (0-100).
|
|
*/
|
|
public static evaluate(data: string): number {
|
|
if (!data) return 0;
|
|
let score = 0;
|
|
|
|
// 1. 길이 기반 점수 (최대 40점)
|
|
score += Math.min(40, data.length / 50);
|
|
|
|
// 2. 구조화 기반 점수 (최대 30점)
|
|
if (data.includes('##') || data.includes('**') || data.includes('- ')) {
|
|
score += 30;
|
|
}
|
|
|
|
// 3. 밀도/정보량 기반 점수 (최대 30점)
|
|
// 코드 블록이나 구체적 링크가 있으면 가산점
|
|
if (data.includes('```')) score += 15;
|
|
if (data.includes('## 💻 실전 구현') || data.includes('Implementation')) score += 10;
|
|
if (data.includes('[[') || data.includes('http')) score += 5;
|
|
|
|
// 감점 요소: Placeholder 나 Empty 상태
|
|
if (data.includes('[Placeholder]') || data.includes('TODO')) {
|
|
score -= 20;
|
|
}
|
|
|
|
|
|
return Math.max(0, Math.min(100, score));
|
|
}
|
|
}
|
|
|
|
export class PerformanceProfiler {
|
|
/**
|
|
* LLM 호출 시 발생하는 비동기 오버헤드와 생성 시간을 측정합니다.
|
|
* 이 지표를 통해 병목(Bottleneck) 현상을 정밀하게 추적할 수 있습니다.
|
|
*/
|
|
public static logLLMLatency(agentName: string, durationMs: number, outputLength: number): void {
|
|
// 영어권 기준 대략적인 토큰 수 산정 (1 token ≈ 4 characters)
|
|
const tokensApprox = Math.floor(outputLength / 4);
|
|
const tokensPerSecond = durationMs > 0 ? tokensApprox / (durationMs / 1000) : 0;
|
|
|
|
logInfo(
|
|
`[Profiler] [${agentName}] LLM Latency: ${durationMs}ms | ` +
|
|
`Output: ${outputLength} chars (~${tokensApprox} tokens) | ` +
|
|
`Speed: ${tokensPerSecond.toFixed(2)} tok/s`
|
|
);
|
|
}
|
|
}
|
|
|
|
export class ConflictDetector {
|
|
/**
|
|
* [Astra v4.1] 도메인 맥락 기반의 의미적 괴리(Semantic Divergence)를 분석합니다.
|
|
* 데이터 내의 논리적 모순이나 상충하는 지표를 탐지하여 위험도를 산출합니다.
|
|
*/
|
|
public static analyzeSemanticDivergence(data: string): number {
|
|
if (!data) return 0;
|
|
let riskScore = 0;
|
|
|
|
// 1. 수치적 모순 탐지 (Metric Contradiction)
|
|
// 동일 문맥 내에서 서로 다른 수치가 발견되는 패턴 탐지
|
|
const metricPatterns = [
|
|
/(\d+%)\s+vs\s+(\d+%)/g,
|
|
/(\d+ms)\s+대비\s+(\d+ms)/g,
|
|
/상충되는\s+(데이터|정보|결과)/i
|
|
];
|
|
|
|
for (const pattern of metricPatterns) {
|
|
if (pattern.test(data)) riskScore += 25;
|
|
}
|
|
|
|
// 2. 논리적 상충 용어 탐지 (Logical Divergence)
|
|
const conflictTerms = [
|
|
['최적화', '성능 저하'],
|
|
['안정적', '불안정'],
|
|
['증가', '감소'],
|
|
['승인', '반려/경고'],
|
|
['임상 등급', '비승인/소비자용']
|
|
];
|
|
|
|
for (const [pos, neg] of conflictTerms) {
|
|
if (data.includes(pos) && data.includes(neg)) {
|
|
const posIdx = data.indexOf(pos);
|
|
const negIdx = data.indexOf(neg);
|
|
if (Math.abs(posIdx - negIdx) < 400) {
|
|
riskScore += 15;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. 기술적 상충 (Technical Divergence)
|
|
const technicalConflicts = [
|
|
/동기(식)?.*비동기(식)?/g,
|
|
/로컬.*클라우드/g,
|
|
/정적.*동적/g,
|
|
/Success.*Fail/i
|
|
];
|
|
for (const pattern of technicalConflicts) {
|
|
if (pattern.test(data)) riskScore += 10;
|
|
}
|
|
|
|
// 4. 명시적 경고 태그 확인
|
|
if (data.includes('[CONFLICT WARNING]') || data.includes('[ERROR]')) riskScore += 30;
|
|
|
|
return Math.min(100, riskScore);
|
|
}
|
|
}
|
|
|
|
export class CognitionAudit {
|
|
/**
|
|
* [Astra v4.0] 지능적 판단 및 정책 준수 여부를 감사(Audit)합니다.
|
|
* 시스템이 얼마나 '생각'하고 '경고'했는지 정량적으로 측정합니다.
|
|
*/
|
|
public static auditPolicyCompliance(stage: string, content: string): void {
|
|
const findings = [];
|
|
|
|
// 1. 충돌 경고 감지 (Conflict Warning)
|
|
if (content.includes('[CONFLICT WARNING]')) {
|
|
findings.push('Conflict Detected & Warning Issued');
|
|
}
|
|
|
|
// 2. 선제적 제안 감지 (Proactive Advice)
|
|
if (content.includes('## 💡 Astra의 선제적 제안')) {
|
|
findings.push('Proactive Advice Generated');
|
|
}
|
|
|
|
// 3. 신뢰도 인용 패턴 확인 (Credibility Usage)
|
|
const highTrustCitationCount = (content.match(/\[\[/g) || []).length;
|
|
if (highTrustCitationCount > 0) {
|
|
findings.push(`Knowledge Density: ${highTrustCitationCount} connections used`);
|
|
}
|
|
|
|
if (findings.length > 0) {
|
|
logInfo(`[CognitionAudit] [${stage}] ${findings.join(' | ')}`);
|
|
}
|
|
}
|
|
}
|