From c7c84702af490dd9f2b6306da8739c462be68dc4 Mon Sep 17 00:00:00 2001 From: g1nation Date: Thu, 11 Jun 2026 18:34:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=EC=9D=BC=EC=A0=95/=ED=95=A0?= =?UTF-8?q?=EC=9D=BC=20=EC=A7=88=EC=9D=98=EC=97=90=20Google=20Calendar?= =?UTF-8?q?=C2=B7Tasks=20=EC=8B=A4=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20(v2.2.221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "오늘 업무 목록 알려줘" 류 질문에 채팅이 캘린더를 못 읽던 문제 수정. 채팅 경로(RAG)는 두뇌·기억만 검색해 캘린더가 연결돼 있어도 일정 질의에 모른다고 하거나 지어내던 구조적 공백. - scheduleContext.ts: 일정/할일 질의 감지(isScheduleRequest) 시 iCal 캐시 새로고침 + 기간 내 일정 + Tasks(기간 마감/기한 경과/조건부) 실데이터 블록을 contextBlock 에 주입. "데이터에 없으면 없다고 답하라" 지시 포함 (환각 방지). 기간 해석: 기본 오늘 · "내일" · "이번 주". - 미연결이면 연결 명령 안내 블록 — 지어내지 않고 정직하게 안내. - Tasks 조회 실패(토큰 만료 등) 시 에러를 그대로 사용자에게 전달. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 4 +- package.json | 2 +- src/agent.ts | 14 +++- src/lib/contextBuilders/scheduleContext.ts | 97 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 src/lib/contextBuilders/scheduleContext.ts diff --git a/package-lock.json b/package-lock.json index 04516bc..9aadbe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "astra", - "version": "2.2.220", + "version": "2.2.221", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "astra", - "version": "2.2.220", + "version": "2.2.221", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", diff --git a/package.json b/package.json index 921ffd4..e219c4d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.220", + "version": "2.2.221", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 2b036f2..53e8b7f 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -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) { diff --git a/src/lib/contextBuilders/scheduleContext.ts b/src/lib/contextBuilders/scheduleContext.ts new file mode 100644 index 0000000..51eed41 --- /dev/null +++ b/src/lib/contextBuilders/scheduleContext.ts @@ -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 { + 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'); +}