/** * Requirement Graph — 업무 유형별 필수 요소 정의 + 감지 + 커버리지 검사. * * Self-Evolving Digital Employee OS 마스터 플랜(docs/SELF_EVOLVING_OS_MASTER_PLAN.md) * Phase 1 / Track 2-1. 신뢰 조건 T3 "품질이 일관적이다 — 필수 요소 누락 없음" 담당. * * 동작 2단계: * 1. *Instructional* — 사용자 요청에서 업무 유형(회의록/시장조사/업무조사/일정) 감지 시 * [TASK REQUIREMENTS] 블록을 시스템 프롬프트에 주입 → 모델이 필수 요소를 빠짐없이 작성. * 정보가 없어 채울 수 없는 요소는 "(확인 필요)" 로 명시하게 강제 — 조용한 생략 금지 * (Anti-Hallucination T1 과 연결). * 2. *Deterministic* — 답변 완료 후 post-answer hook 이 필수 요소 커버리지를 정규식으로 * 스캔, 누락 가능 요소를 footer 로 표시 (termValidator 와 같은 패턴, LLM 호출 없음). * * Gap Detector (Phase 3) 가 이 모듈의 Requirement 정의를 입력으로 사용한다: * Gap = Requirement − Knowledge. */ export interface RequirementElement { /** 안정적 식별자 (Failure Pattern DB 가 누락 카운트 키로 사용 예정). */ id: string; /** 사람이 읽는 요소명 — 블록·footer 에 표시. */ label: string; /** 모델에게 주는 작성 힌트. */ hint: string; /** 커버리지 검사용 정규식 소스 (OR 결합, i+u 플래그). */ detectPatterns: string[]; } export interface TaskRequirement { /** 업무 유형 ID (예: 'meeting-minutes'). */ id: string; /** 사람이 읽는 업무명 (예: '회의록'). */ label: string; /** 사용자 요청에서 업무 유형을 감지하는 정규식 소스 (OR). */ detectKeywords: string[]; /** * 답변 커버리지 검사 여부. 일정 등 짧은 확인형 응답이 정상인 업무는 false — * footer 노이즈(false-positive) 방지. 블록 주입은 항상 수행. */ coverageCheck: boolean; elements: RequirementElement[]; } export interface CoverageResult { ran: boolean; taskId?: string; taskLabel?: string; covered: string[]; // element labels missing: string[]; // element labels } /** * 기본 업무 정의 4종. 배열 순서 = 감지 우선순위 (구체적 유형 먼저, 범용 '업무조사' 마지막 — * "조사" 류 키워드가 시장조사를 가로채지 않도록). */ export const DEFAULT_TASK_REQUIREMENTS: TaskRequirement[] = [ { id: 'meeting-minutes', label: '회의록', detectKeywords: ['회의록', '회의 ?(내용|결과)? ?정리', '미팅 ?(노트|정리)', 'meeting (minutes|notes)'], coverageCheck: true, elements: [ { id: 'attendees', label: '참석자', hint: '회의 참석 인원 전원. 불명확하면 "(확인 필요)".', detectPatterns: ['참석자', '참석 ?인원', 'attendees?'], }, { id: 'decisions', label: '결정사항', hint: '회의에서 합의·확정된 사항. 논의만 되고 미결인 항목과 구분.', detectPatterns: ['결정 ?사항', '결정된', '합의', '확정', 'decisions?'], }, { id: 'action-items', label: '액션 아이템', hint: '후속 실행 항목. 각 항목에 담당자·기한 연결.', detectPatterns: ['액션 ?아이템', 'action ?items?', '할 ?일', '후속 ?(조치|작업)', 'to-?do'], }, { id: 'owners', label: '담당자', hint: '액션 아이템별 책임자. 미정이면 "(담당자 미정)" 명시.', detectPatterns: ['담당자?', '책임자', 'owner'], }, { id: 'due-dates', label: '기한', hint: '액션 아이템별 마감일. 미정이면 "(기한 미정)" 명시.', detectPatterns: ['기한', '마감', '까지', 'due', '\\d{1,2}\\s*월\\s*\\d{1,2}\\s*일'], }, ], }, { id: 'market-research', label: '시장조사', detectKeywords: ['시장 ?조사', '시장 ?분석', '시장 ?(규모|동향|현황)', 'market (research|analysis)'], coverageCheck: true, elements: [ { id: 'market-size', label: '시장 규모', hint: '금액/수량 기준 규모. 수치 출처 필수, 없으면 "(확인 필요)".', detectPatterns: ['시장 ?규모', 'market ?size', '\\d+\\s*(억|조|만\\s*달러|billion|million)'], }, { id: 'growth', label: '성장률', hint: '연 성장률(CAGR 등) 또는 성장 추세.', detectPatterns: ['성장률', '성장세', 'CAGR', 'growth', '연평균'], }, { id: 'competitors', label: '경쟁사', hint: '주요 플레이어와 각자의 포지션.', detectPatterns: ['경쟁사', '경쟁 ?업체', '주요 ?(업체|기업|플레이어)', 'competitors?'], }, { id: 'pricing', label: '가격', hint: '가격대·요금 구조.', detectPatterns: ['가격', '요금', '단가', 'pricing', '원대', '달러'], }, { id: 'customer-needs', label: '고객 니즈', hint: '고객 요구·페인 포인트.', detectPatterns: ['니즈', '고객 ?(요구|수요)', '페인 ?포인트', 'needs', 'pain ?points?'], }, { id: 'trends', label: '트렌드', hint: '시장 동향·변화 방향.', detectPatterns: ['트렌드', '동향', '추세', 'trends?'], }, { id: 'sources', label: '출처', hint: '핵심 수치·주장의 출처. 모델 일반 지식이면 그렇게 명시.', detectPatterns: ['출처', '근거', 'source', '자료:', '참고'], }, ], }, { id: 'schedule', label: '일정 관리', detectKeywords: ['일정 ?(등록|추가|확인|조회|정리|관리)', '스케줄', '캘린더', '미팅 ?잡', '약속 ?(등록|추가|잡)'], coverageCheck: false, // 짧은 확인형 응답이 정상 — footer 검사는 노이즈 elements: [ { id: 'datetime', label: '일시', hint: '날짜와 시간을 명시. 모호하면 되묻기.', detectPatterns: ['\\d{1,2}\\s*[:시]', '날짜', '일시'], }, { id: 'title', label: '일정 제목', hint: '무엇을 위한 일정인지.', detectPatterns: ['제목', '일정명', '건명'], }, { id: 'conflict-check', label: '충돌 확인', hint: '기존 일정과 겹침 여부 확인 결과 명시.', detectPatterns: ['충돌', '겹치', '겹침'], }, ], }, { id: 'work-research', label: '업무조사', detectKeywords: ['업무 ?조사', '조사해', '리서치', '알아봐\\s*줘?', '서치해', 'research'], coverageCheck: true, elements: [ { id: 'purpose', label: '조사 목적', hint: '무엇을 알기 위한 조사인지 한 줄 명시.', detectPatterns: ['목적', '배경', '알아보기 위해'], }, { id: 'summary', label: '핵심 요약', hint: '결론 먼저 — 3줄 이내 요약.', detectPatterns: ['요약', '핵심', '결론부터', 'TL;?DR', 'summary'], }, { id: 'details', label: '세부 내용', hint: '요약을 뒷받침하는 상세 조사 내용.', detectPatterns: ['상세', '세부', '구체적', '자세히'], }, { id: 'sources', label: '출처', hint: '핵심 주장의 출처. 모델 일반 지식이면 그렇게 명시.', detectPatterns: ['출처', '근거', 'source', '참고'], }, { id: 'implications', label: '시사점·다음 단계', hint: '조사 결과가 의미하는 것과 권장 다음 행동.', detectPatterns: ['시사점', '다음 ?단계', '권장', '제안', '결론'], }, ], }, ]; function toRegex(sources: string[]): RegExp { return new RegExp(sources.join('|'), 'iu'); } /** * 사용자 요청에서 업무 유형 감지. 배열 순서대로 첫 매치 반환, 없으면 null. * 짧은 인사·일반 잡담은 키워드 미매치로 자연스럽게 제외. */ export function detectTaskType( userPrompt: string, requirements: TaskRequirement[] = DEFAULT_TASK_REQUIREMENTS, ): TaskRequirement | null { if (!userPrompt || !userPrompt.trim()) return null; for (const req of requirements) { if (toRegex(req.detectKeywords).test(userPrompt)) return req; } return null; } /** * [TASK REQUIREMENTS] 시스템 프롬프트 블록 생성. 업무 유형 미감지 시 빈 문자열 — * memoryContext 의 dynamicBlocks join 에서 자동 제외. */ export function buildRequirementGraphBlock( userPrompt: string, requirements: TaskRequirement[] = DEFAULT_TASK_REQUIREMENTS, /** 과거 자주 누락된 요소 label — Reflection/Failure Pattern 이 공급 (T5: 같은 실수 반복 방지). */ emphasizeLabels: string[] = [], ): string { const req = detectTaskType(userPrompt, requirements); if (!req) return ''; const emphasize = new Set(emphasizeLabels); const lines: string[] = []; lines.push(`[TASK REQUIREMENTS — ${req.label}]`); lines.push(`이 요청은 '${req.label}' 업무로 감지됨. 아래 필수 요소를 *모두* 포함해 작성할 것.`); lines.push('정보가 없어 채울 수 없는 요소는 조용히 생략하지 말고 "(확인 필요)" 로 명시 후 사용자에게 질문.'); lines.push(''); for (const el of req.elements) { const mark = emphasize.has(el.label) ? ' ⚠️ *과거에 자주 누락된 요소 — 특히 주의*' : ''; lines.push(`- [ ] **${el.label}** — ${el.hint}${mark}`); } lines.push(''); lines.push('제출 전 위 체크리스트를 스스로 점검하고, 누락 요소가 있으면 보완 후 답변할 것.'); lines.push('[/TASK REQUIREMENTS]'); return lines.join('\n'); } /** * 답변 커버리지 결정론적 검사 — 각 필수 요소의 detectPatterns 가 답변에 하나도 안 나타나면 * missing. LLM 호출 없음 (정규식), 매 turn 안전. * * 한계(의도된 보수성): 패턴 매치 = "요소가 언급됨" 이지 "내용이 충실함" 이 아님. * 내용 충실도 평가는 Phase 3 Self Evaluation 담당. */ export function checkRequirementCoverage( userPrompt: string, assistantAnswer: string, requirements: TaskRequirement[] = DEFAULT_TASK_REQUIREMENTS, ): CoverageResult { const req = detectTaskType(userPrompt, requirements); if (!req || !req.coverageCheck || !assistantAnswer || !assistantAnswer.trim()) { return { ran: false, covered: [], missing: [] }; } const covered: string[] = []; const missing: string[] = []; for (const el of req.elements) { if (toRegex(el.detectPatterns).test(assistantAnswer)) covered.push(el.label); else missing.push(el.label); } return { ran: true, taskId: req.id, taskLabel: req.label, covered, missing }; } /** * 커버리지 footer — 누락 있을 때만 문자열 반환 (전부 충족 시 빈 문자열, 노이즈 방지). * termValidator footer 와 같은 위치(답변 아래 streamChunk)에 표시. */ export function formatRequirementCoverageFooter(result: CoverageResult): string { if (!result.ran || result.missing.length === 0) return ''; const miss = result.missing.join(', '); return `\n\n> ⚠️ **Requirement Check (${result.taskLabel})** — 누락 가능 요소: ${miss}. 해당 내용이 없었다면 "(확인 필요)" 로 표시하거나 추가 정보를 요청하세요.`; }