feat(chat): 일정/할일 질의에 Google Calendar·Tasks 실데이터 주입 (v2.2.221)
"오늘 업무 목록 알려줘" 류 질문에 채팅이 캘린더를 못 읽던 문제 수정. 채팅 경로(RAG)는 두뇌·기억만 검색해 캘린더가 연결돼 있어도 일정 질의에 모른다고 하거나 지어내던 구조적 공백. - scheduleContext.ts: 일정/할일 질의 감지(isScheduleRequest) 시 iCal 캐시 새로고침 + 기간 내 일정 + Tasks(기간 마감/기한 경과/조건부) 실데이터 블록을 contextBlock 에 주입. "데이터에 없으면 없다고 답하라" 지시 포함 (환각 방지). 기간 해석: 기본 오늘 · "내일" · "이번 주". - 미연결이면 연결 명령 안내 블록 — 지어내지 않고 정직하게 안내. - Tasks 조회 실패(토큰 만료 등) 시 에러를 그대로 사용자에게 전달. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-1
@@ -19,6 +19,7 @@ import { TransactionManager } from './core/transaction';
|
||||
import { SessionManager } from './core/session';
|
||||
import { AgentWorkflowManager } from './agents/AgentWorkflowManager';
|
||||
import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraModeArchitecture';
|
||||
import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext';
|
||||
import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting';
|
||||
import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract';
|
||||
import { buildDroppedHistorySummary } from './lib/contextBuilders/droppedHistorySummary';
|
||||
@@ -505,7 +506,7 @@ export class AgentExecutor {
|
||||
: getActiveBrainProfile();
|
||||
// Per-turn context blocks → src/agent/handlePrompt/buildTurnContextBlocks.ts
|
||||
const {
|
||||
contextBlock,
|
||||
contextBlock: baseContextBlock,
|
||||
brainContext,
|
||||
brainInventoryCtx,
|
||||
brainFiles,
|
||||
@@ -523,6 +524,17 @@ export class AgentExecutor {
|
||||
rootPath,
|
||||
});
|
||||
void brainPreview;
|
||||
// [일정/할일 실데이터] "오늘 업무 목록" 류 질의는 RAG(두뇌)가 아니라
|
||||
// Google Calendar/Tasks 가 진실의 원천 — 감지 시 실데이터 블록을 주입.
|
||||
// 미주입 시 모델이 모른다고 하거나 지어내는 문제의 수정.
|
||||
let contextBlock = baseContextBlock;
|
||||
if (prompt && loopDepth === 0 && !isCasualConversation && isScheduleRequest(prompt)) {
|
||||
try {
|
||||
contextBlock += `\n\n${await buildScheduleContext(this.context, prompt)}`;
|
||||
} catch (e: any) {
|
||||
logError('Schedule context 주입 실패 (계속 진행).', { error: e?.message ?? String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Setup History
|
||||
if (prompt !== null) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 일정/할일 질의 컨텍스트 빌더 — "오늘 업무 목록 알려줘" 류 질문에
|
||||
* 연결된 Google Calendar(iCal 캐시) + Google Tasks 의 *실데이터*를 주입한다.
|
||||
*
|
||||
* 문제: 채팅 경로(RAG)는 두뇌·기억만 검색해서, 캘린더가 연결돼 있어도
|
||||
* "오늘 일정"을 묻면 모델이 모르거나 지어냈다. 데일리 브리핑(텔레그램)과
|
||||
* 같은 소스를 채팅에도 노출한다.
|
||||
*
|
||||
* 신뢰성 규칙: 데이터에 없는 일정·할일을 지어내지 말라는 지시를 블록에 포함.
|
||||
* 미연결이면 연결 방법을 안내하는 블록을 반환 — 모델이 환각으로 메꾸지 않게.
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { logInfo } from '../../utils';
|
||||
import { refreshCalendarCache, readCalendarEventsCache, readCalendarConfig } from '../../features/calendar/calendarCache';
|
||||
import { listTasks } from '../../features/calendar/tasksApi';
|
||||
|
||||
const SCHEDULE_RE = /(오늘|내일|이번\s*주|금주|다음\s*주)?\s*(업무\s*목록|할\s*일|할일|일정|스케줄|미팅|회의\s*일정|투두|to-?do)|캘린더|calendar|tasks\s*목록/i;
|
||||
|
||||
/** 일정/할일을 묻는 질의인지. 짧은 인사·코드 질문에 오탐하지 않게 보수적으로. */
|
||||
export function isScheduleRequest(prompt: string): boolean {
|
||||
const p = (prompt || '').trim();
|
||||
if (!p || p.length > 200) return false; // 긴 작업 지시문은 제외
|
||||
return SCHEDULE_RE.test(p);
|
||||
}
|
||||
|
||||
function kstYmd(offsetDays = 0): string {
|
||||
const now = new Date(Date.now() + offsetDays * 86_400_000);
|
||||
const parts = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
||||
return parts; // en-CA → YYYY-MM-DD
|
||||
}
|
||||
|
||||
/**
|
||||
* 일정 데이터 블록 생성. 미연결/실패 시에도 모델이 정직하게 답하도록 상태를 명시.
|
||||
*/
|
||||
export async function buildScheduleContext(context: vscode.ExtensionContext, prompt: string): Promise<string> {
|
||||
const cfg = readCalendarConfig(context);
|
||||
if (!cfg.icalUrl && !cfg.refreshToken) {
|
||||
return [
|
||||
'[SCHEDULE DATA — Google Calendar/Tasks]',
|
||||
'상태: 캘린더가 연결되어 있지 않음.',
|
||||
'→ 사용자에게 "Astra: Google Calendar (iCal) 연결" 또는 "Astra: Google Calendar OAuth 연결 (쓰기)" 명령으로 연결하라고 안내하라. 일정을 지어내지 말 것.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// 질의의 기간 해석 — 기본 오늘, "내일"/"이번 주" 키워드 반영.
|
||||
const p = prompt || '';
|
||||
const wantTomorrow = /내일/.test(p);
|
||||
const wantWeek = /이번\s*주|금주|다음\s*주/.test(p);
|
||||
const startYmd = wantTomorrow ? kstYmd(1) : kstYmd(0);
|
||||
const endYmd = wantWeek ? kstYmd(7) : startYmd;
|
||||
const todayYmd = kstYmd(0);
|
||||
const rangeLabel = wantWeek ? `${startYmd} ~ ${endYmd}` : startYmd;
|
||||
|
||||
const lines: string[] = [
|
||||
`[SCHEDULE DATA — Google Calendar/Tasks 실데이터 · 기준 ${rangeLabel} (KST)]`,
|
||||
'아래 데이터만 근거로 답하라. 여기에 없는 일정·할일을 지어내지 말고, 비어 있으면 "없다"고 답하라.',
|
||||
];
|
||||
|
||||
// (1) 캘린더 일정 — 캐시 새로고침 best-effort.
|
||||
try { await refreshCalendarCache(context); } catch { /* 기존 캐시 사용 */ }
|
||||
const events = readCalendarEventsCache(context)
|
||||
.filter(e => { const d = (e.startIso || '').slice(0, 10); return d >= startYmd && d <= endYmd; })
|
||||
.sort((a, b) => a.startIso.localeCompare(b.startIso));
|
||||
lines.push('', `■ 일정 (${events.length}건)`);
|
||||
for (const e of events.slice(0, 20)) {
|
||||
const d = e.startIso.slice(0, 10);
|
||||
const time = e.allDay ? '종일' : e.startIso.slice(11, 16);
|
||||
lines.push(`- ${wantWeek ? d + ' ' : ''}${time} ${e.summary}${e.location ? ` @${e.location}` : ''}`);
|
||||
}
|
||||
if (!events.length) lines.push('- (없음)');
|
||||
|
||||
// (2) Google Tasks — 미완료: 기간 내 마감 + 기한 경과 + 조건부(무기한).
|
||||
const tr = await listTasks(context, { showCompleted: false, maxResults: 100 });
|
||||
if (tr.ok) {
|
||||
const open = tr.tasks.filter(t => t.status === 'needsAction');
|
||||
const inRange = open.filter(t => t.due && t.due >= startYmd && t.due <= endYmd);
|
||||
const overdue = open.filter(t => t.due && t.due < todayYmd);
|
||||
const conditional = open.filter(t => !t.due && /^\[조건부\]/.test(t.title));
|
||||
lines.push('', `■ 할 일 — 기간 내 마감 (${inRange.length}건)`);
|
||||
for (const t of inRange.slice(0, 20)) lines.push(`- ${t.due} ${t.title}`);
|
||||
if (!inRange.length) lines.push('- (없음)');
|
||||
if (overdue.length) {
|
||||
lines.push('', `■ 기한 지난 미완료 (${overdue.length}건)`);
|
||||
for (const t of overdue.slice(0, 10)) lines.push(`- ${t.due} ${t.title}`);
|
||||
}
|
||||
if (conditional.length) {
|
||||
lines.push('', `■ 조건부 대기 (${conditional.length}건)`);
|
||||
for (const t of conditional.slice(0, 10)) lines.push(`- ${t.title}`);
|
||||
}
|
||||
} else {
|
||||
lines.push('', `■ 할 일: 조회 실패 — ${tr.error}`);
|
||||
lines.push('→ 이 오류 메시지를 사용자에게 그대로 전달하라 (할일을 지어내지 말 것).');
|
||||
}
|
||||
|
||||
logInfo('Schedule context 주입.', { events: events.length, range: rangeLabel, tasksOk: tr.ok });
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user