feat(weekly): /weekly 주간 보고서(금주/차주) 추가 + /meet 정확도 개선 (v2.2.204)
- /weekly: 차주 날짜 입력→금주 자동 역산, Google Tasks 기반 금주/차주 보고서. 버킷팅은 코드(예측 가능), 포맷팅만 LLM. 신규 weeklyPrompt.ts + coordination.ts runWeekly. - 기존 CEO /weekly 리뷰 카드(dashboards.ts) 제거 — 이름 충돌 해소, /weekly 일원화. - /meet: 액션아이템에 '작업 상세' 열 추가, 캘린더 notes 가 실제 작업 내용을 담도록 재구성. - /meet: 발언자 추적 복원 + 비선형 회의 재조립 + 근거/할루시네이션 억제 규칙으로 오귀속 감소. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -696,7 +696,20 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode.
|
||||
const { date, tentative } = resolveTaskDate(task.due, meetingDate, today);
|
||||
if (tentative) tentativeCount++;
|
||||
const evTitle = tentative ? `${task.work} (미확정)` : task.work;
|
||||
const notes = `회의록: ${meetTitle}\n담당: ${task.owner}\n기한 표기: ${task.due || '(없음)'}\nAstra /meet 자동 등록`;
|
||||
const detailLine = task.detail?.trim()
|
||||
? task.detail.trim()
|
||||
: '(녹취록에서 작업 상세가 추출되지 않음 — 회의록 본문 참조)';
|
||||
const notes = [
|
||||
`■ 작업 상세`,
|
||||
detailLine,
|
||||
``,
|
||||
`■ 맥락`,
|
||||
`· 회의록: ${meetTitle}`,
|
||||
`· 담당: ${task.owner || '(미지정)'}`,
|
||||
`· 기한: ${task.due?.trim() || '(미표기)'} → ${date}${tentative ? ' (미확정·자동 산정)' : ''}`,
|
||||
``,
|
||||
`— Astra /meet 자동 등록`,
|
||||
].join('\n');
|
||||
|
||||
const successes: string[] = [];
|
||||
const failures: string[] = [];
|
||||
|
||||
@@ -10,7 +10,8 @@ export function buildMeetPrompt(transcript: string, metadata: string): string {
|
||||
구조화된 회의록(Actionable Minutes)을 생성한다.
|
||||
|
||||
# 역할 (Role)
|
||||
- Fact Extractor: 사실만 추출
|
||||
- Fact Extractor: 녹취록에 명시적으로 존재하는 사실만 추출
|
||||
- Attribution Tracker: 누가 무엇을 말했는지 발언 주체를 끝까지 추적해 오귀속을 방지
|
||||
- Decision Tracker: 결정 여부 구분
|
||||
- Action Organizer: 실행 항목 구조화
|
||||
- Context Filter: 불필요한 발언(잡담) 제거
|
||||
@@ -19,17 +20,26 @@ export function buildMeetPrompt(transcript: string, metadata: string): string {
|
||||
1순위: 메타데이터 / 2순위: 녹취록 내용. 충돌 시 반드시 메타데이터를 사용한다.
|
||||
|
||||
# 처리 절차 (Processing Flow)
|
||||
1. Deconstruction — 잡담 제거, 의미 단위로 분해, 발언자 ID는 무시한다.
|
||||
2. Classification — 모든 내용을 Fact / Discussion / Decision / Risk / Action 으로 분류한다.
|
||||
3. Decision Logic
|
||||
1. Speaker Tracking — 발언자 ID/이름을 끝까지 유지한다. **누가 한 말인지를 절대 임의로 바꾸거나 합치지 말 것.**
|
||||
2. Topic Reclustering — 이 녹취록은 비선형이다(A→B→Z→다시 A 식으로 주제가 튄다). 녹취록 전체를 훑어 **흩어진 발언을 주제별로 다시 묶은 뒤** 정리한다. 녹취록상 앞뒤로 붙어 있다는 이유만으로 두 발언을 인과·연결 관계로 엮지 말 것(**인접 ≠ 연결**).
|
||||
3. Deconstruction — 잡담을 제거하고 의미 단위로 분해하되, 각 단위에 발언 주체를 보존한다.
|
||||
4. Classification — 모든 내용을 Fact / Discussion / Decision / Risk / Action 으로 분류한다.
|
||||
5. Decision Logic
|
||||
- 명확한 합의 표현 → Decision
|
||||
- 실행 주체 + 행동 → Action
|
||||
- 제안/의견 → Discussion
|
||||
- 조건 부족 → Open Issue
|
||||
4. Structuring — 중복 제거, 핵심만 유지, 짧고 명확한 문장을 사용한다.
|
||||
- 조건 부족 / 합의 불명확 → Open Issue
|
||||
6. Structuring — 중복 제거, 핵심만 유지, 짧고 명확한 문장을 사용한다.
|
||||
|
||||
# 근거·정확성 규칙 (Grounding Rules — 반드시 준수, 할루시네이션 방지)
|
||||
- **녹취록에 명시된 내용만 적는다.** 추론으로 빈칸을 메우거나, 별개의 발언을 하나의 인과 사슬로 합성하지 말 것.
|
||||
- **발언 주체가 불명확하면 추측하지 말 것.** 누가 말했는지 확실하지 않으면 이름을 붙이지 말고 "(발언 주체 불명확)"으로 표기하거나 "~라는 의견이 제시됨"처럼 주체 없이 중립적으로 서술한다. 어떤 발언자의 말을 다른 발언자의 결론으로 옮기는 것은 가장 심각한 오류다.
|
||||
- **녹취록에 없는 숫자·날짜·금액·고유명사·제품명을 만들어내지 말 것.** 불확실하면 "확인 필요"로 둔다.
|
||||
- 어떤 항목의 근거가 녹취록에서 약하거나 모호하면, 지어내지 말고 해당 항목 끝에 "(확인 필요)"를 붙인다.
|
||||
- Decision은 명시적 합의 표현이 있을 때만 '결정됨'이다. 합의가 불명확하면 '논의 중' 또는 오픈 이슈로 둔다.
|
||||
|
||||
# 출력 검증 (Validation)
|
||||
출력 전 내부적으로 점검한다: Decision은 실제 합의인가 / Action은 실행 가능한가.
|
||||
출력 전 내부적으로 점검한다: ① 각 발언이 올바른 주체에게 귀속됐는가 ② 인접 발언을 임의로 연결하지 않았는가 ③ Decision은 실제 합의인가 ④ 녹취록에 없는 정보를 추가하지 않았는가 ⑤ Action은 실행 가능한가.
|
||||
단, 검증 과정·체크 로그는 출력하지 말 것. 최종 회의록만 출력한다.
|
||||
|
||||
[메타데이터]
|
||||
@@ -55,7 +65,7 @@ ${transcript}
|
||||
각 안건마다 아래 구조로:
|
||||
### [안건 제목]
|
||||
- **현황**:
|
||||
- **핵심 논의**:
|
||||
- **핵심 논의**: 쟁점이 되거나 주체가 중요한 발언은 "OOO: ~" 형태로 발언자를 밝힌다. 주체가 불명확하면 이름을 붙이지 말 것.
|
||||
- **결론**: [결정됨 / 논의 중 / 보류]
|
||||
|
||||
## 2. 리스크 및 이슈
|
||||
@@ -65,8 +75,12 @@ ${transcript}
|
||||
## 4. 오픈 이슈
|
||||
|
||||
## 5. 액션 아이템
|
||||
| 담당 | 작업 내용 | 기한 |
|
||||
| --- | --- | --- |
|
||||
각 행은 반드시 녹취록 근거로 작성한다. 표 셀 안에서는 줄바꿈과 \`|\` 문자를 쓰지 말 것.
|
||||
| 담당 | 작업 내용 | 작업 상세 | 기한 |
|
||||
| --- | --- | --- | --- |
|
||||
|
||||
- **작업 내용**: 한 줄짜리 작업명. 캘린더 일정 제목으로 그대로 쓰이므로 그 자체로 무슨 일인지 식별되게 작성한다. ("검토", "확인" 같은 단독 동사 금지)
|
||||
- **작업 상세**: 이 작업이 **무엇이고, 왜 필요하며, 구체적으로 무엇을 수행해야 하는지**를 2~3문장으로 적는다(배경·목적·수행 범위·산출물). 녹취록에서 언급된 대상·수치·조건을 그대로 인용한다. 근거가 부족하면 "추가 확인 필요: …" 형태로 무엇을 확인해야 하는지 명시한다. 단순히 작업명을 반복하지 말 것.
|
||||
|
||||
위 형식을 정확히 따르고, 모든 내용은 한국어로 작성하시오.`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* `/weekly` 주간 보고서 LLM 프롬프트.
|
||||
*
|
||||
* 입력: 캘린더(Google Tasks)에서 버킷팅된 금주/차주 task 목록 + 각 task 의
|
||||
* 제목·마감·상태·메모(= /meet·/task 가 넣은 "작업 상세 / 맥락"). 금주/차주 분류는
|
||||
* **호출부(coordination.ts)에서 due/completed 날짜로 이미 끝낸 상태**로 들어온다.
|
||||
* 이 프롬프트는 *재분류하지 않고* 정해진 포맷으로 **서술만** 한다.
|
||||
*
|
||||
* 하위 bullet 은 오직 각 task 의 메모에서만 끌어온다 — 없는 사실을 만들지 않는다.
|
||||
*/
|
||||
|
||||
export interface WeeklyTask {
|
||||
title: string;
|
||||
/** 'YYYY-MM-DD' 또는 '' (마감 미정). */
|
||||
due: string;
|
||||
status: 'needsAction' | 'completed';
|
||||
/** 완료 시각 'YYYY-MM-DD' (status 'completed' 일 때만). */
|
||||
completedYmd?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface WeeklyPromptInput {
|
||||
thisWeek: WeeklyTask[];
|
||||
nextWeek: WeeklyTask[];
|
||||
thisRange: [string, string];
|
||||
nextRange: [string, string];
|
||||
}
|
||||
|
||||
/** /weekly 전용 시스템 프롬프트 — 기본(UX 분석가) 대신 주간 보고 작성자로 역할 고정. */
|
||||
export const WEEKLY_SYSTEM =
|
||||
'당신은 팀 주간 업무 보고서를 작성하는 PM 보조다. '
|
||||
+ '제공된 task 데이터(제목·마감·상태·메모)에 있는 사실만으로 보고서를 쓴다. '
|
||||
+ '메모에 없는 일정·숫자·고유명사·결정을 절대 지어내지 않으며, 모든 출력은 한국어로 작성한다.';
|
||||
|
||||
function serializeTasks(tasks: WeeklyTask[]): string {
|
||||
if (tasks.length === 0) return '(해당 범위에 task 없음)';
|
||||
return tasks.map((t, i) => {
|
||||
const statusLabel = t.status === 'completed'
|
||||
? `완료(${t.completedYmd || '시점 미상'})`
|
||||
: '진행/예정';
|
||||
const notes = (t.notes || '').trim();
|
||||
const notesBlock = notes
|
||||
? notes.split('\n').map((l) => ` ${l}`).join('\n')
|
||||
: ' (메모 없음)';
|
||||
return [
|
||||
`${i + 1}. 제목: ${t.title}`,
|
||||
` 마감: ${t.due || '미정'} | 상태: ${statusLabel}`,
|
||||
` 메모:`,
|
||||
notesBlock,
|
||||
].join('\n');
|
||||
}).join('\n\n');
|
||||
}
|
||||
|
||||
export function buildWeeklyPrompt(input: WeeklyPromptInput): string {
|
||||
const { thisWeek, nextWeek, thisRange, nextRange } = input;
|
||||
return `# 임무 (Objective)
|
||||
캘린더(Google Tasks)에서 추출한 금주·차주 작업 목록을 기반으로, **아래 정확한 포맷**의 주간 업무 보고서를 작성한다. 외부 지식 없이 제공된 데이터만 사용한다.
|
||||
|
||||
# 분류 규칙 (이미 끝남 — 재분류 금지)
|
||||
- 금주/차주 분류는 호출부에서 날짜로 이미 끝냈다. [금주 작업]에 들어온 항목은 [금주] 섹션에, [차주 작업]은 [차주] 섹션에 그대로 배치한다. **임의로 옮기지 말 것.**
|
||||
- 금주 기간: ${thisRange[0]} ~ ${thisRange[1]} / 차주 기간: ${nextRange[0]} ~ ${nextRange[1]}
|
||||
|
||||
# 작성 규칙 (Rules)
|
||||
1. 각 task 는 한 줄로: \`: [태그] 작업명 (M/D)\` 형식. 제목 앞의 \`[태그]\`(예: [이머시브], [3D App], [기타])는 그대로 유지한다.
|
||||
- 날짜는 M/D 형식(예: 6/12). 완료된 작업은 \`(6/4 완료)\`, 예정은 \`(6/8)\`, 마감이 차주인데 금주에 진행 중이면 \`(6/12 완료 예상)\`처럼 메모 근거가 있을 때만 표기.
|
||||
- 제목 끝의 \`(미확정)\` 같은 자동 꼬리표는 빼고 작업명만 쓴다.
|
||||
2. 같은 \`[태그]\`끼리 인접하도록 정렬한다.
|
||||
3. 각 task 아래에 하위 항목(\` - \`)으로 세부 내용을 2~4개 적는다. **이 세부 내용은 오직 해당 task 의 '메모'에서만** 끌어온다(작업 상세·맥락·기한 표기). 메모를 짧은 서술형으로 다듬되, 날짜·대상·수치는 메모에 적힌 그대로 인용한다.
|
||||
4. **메모에 없는 내용을 추가하지 말 것.** 세부 정보가 부족하면 지어내지 말고 "추가 확인 필요" 또는 메모에 적힌 "(확인 필요)"를 그대로 옮긴다.
|
||||
5. 해당 섹션에 task 가 하나도 없으면 그 섹션 본문에 \`: (해당 작업 없음)\` 한 줄만 적는다.
|
||||
|
||||
# 출력 포맷 (정확히 이 구조 — 헤더 문구·대괄호 그대로)
|
||||
|
||||
[금주]
|
||||
[주요 일정]
|
||||
: [태그] 작업명 (M/D)
|
||||
- 세부 내용
|
||||
- 세부 내용
|
||||
|
||||
: [태그] 작업명 (M/D)
|
||||
- 세부 내용
|
||||
|
||||
[차주]
|
||||
[예상 일정]
|
||||
: [태그] 작업명 (M/D)
|
||||
- 세부 내용
|
||||
|
||||
---
|
||||
[금주 작업] (${thisWeek.length}건)
|
||||
${serializeTasks(thisWeek)}
|
||||
|
||||
[차주 작업] (${nextWeek.length}건)
|
||||
${serializeTasks(nextWeek)}
|
||||
|
||||
위 포맷을 정확히 따르고, 보고서 본문만 출력한다(설명·머리말·코드펜스 금지).`;
|
||||
}
|
||||
@@ -70,9 +70,13 @@ export function resolveTaskDate(due: string, meetingDate: Date, today: Date): {
|
||||
return { date: toYmd(addBusinessDays(today, 5)), tentative: true };
|
||||
}
|
||||
|
||||
/** 회의록 본문의 "## 5. 액션 아이템" 마크다운 표에서 행을 파싱. */
|
||||
export function parseActionItems(report: string): { owner: string; work: string; due: string }[] {
|
||||
const rows: { owner: string; work: string; due: string }[] = [];
|
||||
/**
|
||||
* 회의록 본문의 "## 5. 액션 아이템" 마크다운 표에서 행을 파싱.
|
||||
* 4열 표(담당 | 작업 내용 | 작업 상세 | 기한)와 구(舊) 3열 표(담당 | 작업 내용 | 기한)를
|
||||
* 모두 지원한다. 3열일 때 detail 은 빈 문자열.
|
||||
*/
|
||||
export function parseActionItems(report: string): { owner: string; work: string; detail: string; due: string }[] {
|
||||
const rows: { owner: string; work: string; detail: string; due: string }[] = [];
|
||||
let inSection = false;
|
||||
for (const line of report.split('\n')) {
|
||||
if (/^#{1,6}\s*5\.\s*액션\s*아이템/.test(line)) { inSection = true; continue; }
|
||||
@@ -83,7 +87,11 @@ export function parseActionItems(report: string): { owner: string; work: string;
|
||||
if (cells.length < 3) continue;
|
||||
if (/^:?-+:?$/.test(cells[0])) continue; // 표 구분선
|
||||
if (cells[0] === '담당' || cells[1] === '작업 내용') continue; // 헤더
|
||||
rows.push({ owner: cells[0], work: cells[1], due: cells[2] });
|
||||
if (cells.length >= 4) {
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: cells[2], due: cells[3] });
|
||||
} else {
|
||||
rows.push({ owner: cells[0], work: cells[1], detail: '', due: cells[2] });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user