diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 28c996b..4d4e804 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,14 +1,5 @@ # Astra Patch Notes -## v2.2.206 (2026-06-05) -### ๐Ÿ“ง Project Astra โ€” ์ด๋ฉ”์ผ ์ž์‚ฐํ™” (Phase 1+2) -- **Gmail ์ฝ๊ธฐ์ „์šฉ ์ˆ˜์ง‘** (`/email-sync [์ผ์ˆ˜]`) โ€” OAuth ์— `gmail.readonly` ์Šค์ฝ”ํ”„ ์ถ”๊ฐ€(๊ณต์œ  ํ† ํฐ), ๋ณธ๋ฌธ/๋ฉ”ํƒ€/์Šค๋ ˆ๋“œ๋ฅผ ๋กœ์ปฌ ์ธ๋ฑ์Šค(`{brainPath}/memory/email_index.json`)์— ์ €์žฅ. ๋ณธ๋ฌธ์€ ๋กœ์ปฌ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š์œผ๋ฉฐ ํ•ฉ์„ฑ์€ ๋กœ์ปฌ LLM only(ํ”„๋ผ์ด๋ฒ„์‹œ). -- **RAG 'email' ์†Œ์Šค** โ€” ์ˆ˜์ง‘๋œ ๋ฉ”์ผ์ด ๊ธฐ์กด ๊ฒ€์ƒ‰ ํŒŒ์ดํ”„๋ผ์ธ์— ์ž๋™ ํ•ฉ๋ฅ˜, ๋‹ต๋ณ€์— **์›๋ฌธ ๋ฉ”์ผ ๋งํฌ** ์ถœ์ฒ˜ ์ œ๊ณต. ๊ธฐ์กด grounding(ํ™•์ธ๋ถˆ๊ฐ€/citation) ๊ทธ๋Œ€๋กœ ์ ์šฉ. -- **ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰** โ€” TF-IDF + ์ž„๋ฒ ๋”ฉ(์„ค์ • ์‹œ) ๋ธ”๋ Œ๋“œ, brain ๊ณผ ๋™์ผ ๊ณต์‹. -- **๋ฏธํšŒ์‹  ์ถ”์ ** (`/email-status [์ผ์ˆ˜]`) โ€” ์Šค๋ ˆ๋“œ์˜ ๋งˆ์ง€๋ง‰์ด ๋‚ด ๋‹ต์žฅ์ด ์•„๋‹Œ ๊ฑด์„ ์ถ”์ถœ(๋…ธ์ด์ฆˆ ์นดํ…Œ๊ณ ๋ฆฌ ์ œ์™ธ, ๐Ÿ”” ์š”์ฒญ ์ถ”์ •). -- **๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™”** โ€” `g1nation.email.autoSync` ์ผœ๋ฉด ์ฃผ๊ธฐ ์ˆ˜์ง‘(์„ค์ • ๋ณ€๊ฒฝ ์‹œ ์žฌ์‹œ์ž‘, unref). ์Šฌ๋ž˜์‹œ ๋ช…๋ น๊ณผ ๋™์ผ ์ฝ”์–ด(`emailSync.ts`) ๊ณต์œ . -- ์‹ ๊ทœ: [features/email/](src/features/email/) (gmailApiยทemailStoreยทemailSyncยทautoSyncยทhandlers) + retrieval 'email' ์†Œ์Šค ํ†ตํ•ฉ. - ## v2.2.205 (2026-06-05) ### ๐Ÿงน ๋ฐฑ์—”๋“œ ๋ถ„๋ฆฌ ์ค€๋น„ โ€” Bridge ํƒ€๊นƒ ํ† ๊ธ€(๋กœ์ปฌ/NAS) + /research ์ œ๊ฑฐ - **Datacollect Bridge ํƒ€๊นƒ ์„ค์ •** ์ถ”๊ฐ€ โ€” Astra Settings ํŒจ๋„์—์„œ `๋กœ์ปฌ/NAS` ์ „ํ™˜ + NAS URL/ํ† ํฐ(`x-bridge-token`). ๊ธฐ๋ณธ `๋กœ์ปฌ` = ํ˜„ํ–‰ ๋™์ž‘ ๊ทธ๋Œ€๋กœ. ([bridgeClient.ts](src/features/datacollect/bridgeClient.ts) ยท [settings-panel](media/settings-panel.html) ยท [settingsPanelProvider.ts](src/features/settings/settingsPanelProvider.ts)) diff --git a/media/settings-panel.html b/media/settings-panel.html index 3eb17c6..0bf76e0 100644 --- a/media/settings-panel.html +++ b/media/settings-panel.html @@ -117,46 +117,6 @@ - -
-

์ด๋ฉ”์ผ (Project Astra)

-

Gmail ์„ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์ˆ˜์ง‘ํ•ด ๋กœ์ปฌ ์ธ๋ฑ์Šค์— ์ €์žฅํ•˜๊ณ , ์ฑ„ํŒ…์ด ๋ฉ”์ผ ๊ทผ๊ฑฐ(์›๋ฌธ ๋งํฌ ํฌํ•จ)๋กœ ๋‹ตํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋ณธ๋ฌธ์€ ๋กœ์ปฌ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š์œผ๋ฉฐ ํ•ฉ์„ฑ์€ ๋กœ์ปฌ LLM ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. (์ตœ์ดˆ 1ํšŒ gmail.readonly ์žฌ์ธ์ฆ ํ•„์š”)

-
- - โ€” -
-
- - - ์ผœ๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ž๋™ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค. -
-
- -
- - -
-
-
- -
- - -
-
-
- -
- - -
-
-
- - -
-
-

๋ฉ”๋ชจ๋ฆฌ

diff --git a/media/settings-panel.js b/media/settings-panel.js index 5b7f8ac..5196c03 100644 --- a/media/settings-panel.js +++ b/media/settings-panel.js @@ -34,15 +34,6 @@ const dcMaxPages = $('dcMaxPages'); const dcSynthTemp = $('dcSynthTemp'); - // ---- Email (Project Astra) ---- - const emailStatus = $('emailStatus'); - const emailAutoSync = $('emailAutoSync'); - const emailInterval = $('emailInterval'); - const emailDays = $('emailDays'); - const emailMax = $('emailMax'); - const emailSyncNow = $('emailSyncNow'); - const emailSyncMsg = $('emailSyncMsg'); - // ---- Memory ---- const memEnabled = $('memEnabled'); const memShort = $('memShort'); @@ -162,23 +153,6 @@ vscode.postMessage({ type: 'datacollect.update', synthesisTemperature: Number(dcSynthTemp.value) }) ); - // ---- Email listeners ---- - emailAutoSync.addEventListener('change', () => - vscode.postMessage({ type: 'email.update', autoSync: emailAutoSync.checked }) - ); - document.querySelector('[data-save="email.interval"]').addEventListener('click', () => - vscode.postMessage({ type: 'email.update', autoSyncIntervalMinutes: Number(emailInterval.value) }) - ); - document.querySelector('[data-save="email.days"]').addEventListener('click', () => - vscode.postMessage({ type: 'email.update', syncDays: Number(emailDays.value) }) - ); - document.querySelector('[data-save="email.max"]').addEventListener('click', () => - vscode.postMessage({ type: 'email.update', syncMaxMessages: Number(emailMax.value) }) - ); - emailSyncNow.addEventListener('click', () => - vscode.postMessage({ type: 'email.syncNow' }) - ); - // ---- Memory listeners ---- memEnabled.addEventListener('change', (e) => vscode.postMessage({ type: 'memory.update', memoryEnabled: e.target.checked }) @@ -435,21 +409,6 @@ setIfNotFocused(dcSynthTemp, dc.synthesisTemperature); } - // ---- Email (Project Astra) ---- - const em = state.email; - if (em) { - if (document.activeElement !== emailAutoSync) emailAutoSync.checked = !!em.autoSync; - setIfNotFocused(emailInterval, em.autoSyncIntervalMinutes); - setIfNotFocused(emailDays, em.syncDays); - setIfNotFocused(emailMax, em.syncMaxMessages); - emailStatus.textContent = em.indexedCount > 0 - ? `${em.indexedCount}๊ฑด ์ €์žฅ๋จ${em.newestDate ? ` ยท ์ตœ์‹  ${em.newestDate}` : ''}` - : '์ˆ˜์ง‘๋œ ๋ฉ”์ผ ์—†์Œ โ€” "์ง€๊ธˆ ๋™๊ธฐํ™”" ๋˜๋Š” /email-sync ์‹คํ–‰'; - emailSyncMsg.textContent = em.lastSyncMessage || ''; - emailSyncNow.disabled = !!em.syncing; - emailSyncNow.textContent = em.syncing ? '๋™๊ธฐํ™” ์ค‘โ€ฆ' : '์ง€๊ธˆ ๋™๊ธฐํ™”'; - } - // ---- Memory ---- const mem = state.memory; memEnabled.checked = !!mem.memoryEnabled; diff --git a/package.json b/package.json index a59e1fc..c07c028 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.206", + "version": "2.2.205", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -230,32 +230,6 @@ "default": "", "markdownDescription": "`/benchmark` ๋“ฑ Datacollect slash ๋ช…๋ น ๊ฒฐ๊ณผ๋ฌผ(markdown)์„ ์ €์žฅํ•  ํด๋”. **๋น„์›Œ๋‘๋ฉด** Bridge ๊ธฐ๋ณธ ์œ„์น˜(Bridge์˜ `WIKI_RAW_PATH` ํ™˜๊ฒฝ๋ณ€์ˆ˜)์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค โ€” ์ฝ”๋“œ/์„ค์ • ์–ด๋””์—๋„ ์ ˆ๋Œ€๊ฒฝ๋กœ๊ฐ€ ๋ฐ•ํžˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํŠน์ • ํด๋”๋กœ ์ €์žฅํ•˜๋ ค๋ฉด ์ ˆ๋Œ€๊ฒฝ๋กœ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. Astra Settings ํŒจ๋„์˜ 'Datacollect' ์„น์…˜์—์„œ๋„ ํŽธ์ง‘ ๊ฐ€๋Šฅ." }, - "g1nation.email.syncDays": { - "type": "number", - "default": 7, - "minimum": 1, - "maximum": 365, - "markdownDescription": "[Project Astra] `/email-sync` ๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ์ˆ˜์ง‘ํ•  ์ตœ๊ทผ ์ผ์ˆ˜. ๋ช…๋ น์—์„œ `/email-sync 30` ์ฒ˜๋Ÿผ ๊ทธ๋•Œ๊ทธ๋•Œ ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ˆ˜์ง‘๋œ ๋ฉ”์ผ์€ ๋กœ์ปฌ ์ธ๋ฑ์Šค(`{brainPath}/memory/email_index.json`)์— ์ €์žฅ๋˜์–ด ์ฑ„ํŒ… ๋‹ต๋ณ€์˜ ๊ทผ๊ฑฐ๋กœ ์“ฐ์ž…๋‹ˆ๋‹ค(์ฝ๊ธฐ ์ „์šฉ)." - }, - "g1nation.email.syncMaxMessages": { - "type": "number", - "default": 200, - "minimum": 1, - "maximum": 2000, - "markdownDescription": "[Project Astra] `/email-sync` 1ํšŒ ์‹คํ–‰ ์‹œ ๊ฐ€์ ธ์˜ฌ ์ตœ๋Œ€ ๋ฉ”์ผ ์ˆ˜. ๋ฉ”์ผ ๋ณธ๋ฌธ์€ ๋กœ์ปฌ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š์œผ๋ฉฐ, ํ•ฉ์„ฑ์€ ๋กœ์ปฌ LLM ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค." - }, - "g1nation.email.autoSync": { - "type": "boolean", - "default": false, - "markdownDescription": "[Project Astra] ์ผœ๋ฉด ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ Gmail ์„ ์ž๋™ ์ˆ˜์ง‘ํ•ฉ๋‹ˆ๋‹ค(`/email-sync` ์™€ ๋™์ผ ๋™์ž‘). ๋„๋ฉด ์ˆ˜๋™ `/email-sync` ๋งŒ. ๊ธฐ๋ณธ off." - }, - "g1nation.email.autoSyncIntervalMinutes": { - "type": "number", - "default": 30, - "minimum": 5, - "maximum": 1440, - "markdownDescription": "[Project Astra] ์ž๋™ ๋™๊ธฐํ™” ๊ฐ„๊ฒฉ(๋ถ„). `g1nation.email.autoSync` ๊ฐ€ ์ผœ์ ธ ์žˆ์„ ๋•Œ๋งŒ ์ ์šฉ. ์ตœ์†Œ 5๋ถ„." - }, "g1nation.datacollectCrawlDepth": { "type": "number", "default": 1, diff --git a/src/extension.ts b/src/extension.ts index 907a74e..8c01430 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,8 +6,6 @@ import * as path from 'path'; import './features/teamops/handlers'; import './features/system/handlers'; import './features/datacollect/handlers'; -import './features/email/handlers'; // Project Astra โ€” /email-sync -import { startEmailAutoSync } from './features/email/autoSync'; // axios removed in favor of native fetch import { _getBrainDir, @@ -121,9 +119,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push({ dispose: () => activityTracker.dispose() }); context.subscriptions.push({ dispose: () => lifecycle.dispose() }); - // Project Astra โ€” ์ด๋ฉ”์ผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™” (g1nation.email.autoSync ์ผœ์ ธ ์žˆ์„ ๋•Œ๋งŒ). - startEmailAutoSync(context); - // React to engine URL changes โ€” re-target the SDK and reset state. context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { diff --git a/src/features/calendar/oauth.ts b/src/features/calendar/oauth.ts index 29baa1d..38c16f9 100644 --- a/src/features/calendar/oauth.ts +++ b/src/features/calendar/oauth.ts @@ -19,14 +19,12 @@ import * as http from 'http'; import * as crypto from 'crypto'; import * as vscode from 'vscode'; -// Calendar/Sheets/Tasks/Gmail ๊ถŒํ•œ์„ ํ•œ ๋ฒˆ์— ์š”์ฒญ โ€” ์‚ฌ์šฉ์ž๊ฐ€ OAuth ํ•œ ๋ฒˆ ํ•˜๋ฉด ๋ชจ๋‘ ๋™์ž‘. -// ์˜› ์‚ฌ์šฉ์ž(์ด์ „ ์Šค์ฝ”ํ”„๋กœ ์—ฐ๊ฒฐ)๋Š” ์ƒˆ ๊ธฐ๋Šฅ(Gmail ๋“ฑ) ์‚ฌ์šฉ ์‹œ ๊ถŒํ•œ ๋ถ€์กฑ ์—๋Ÿฌ โ†’ ์žฌ์—ฐ๊ฒฐ ํ•„์š”. -// gmail.readonly ๋Š” ์ฝ๊ธฐ ์ „์šฉ โ€” ์‚ญ์ œ/๋‹ต์žฅ/์ „๋‹ฌ ๋ถˆ๊ฐ€(๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑยท๋ณด์•ˆ). +// Calendar ์™€ Sheets ์–‘์ชฝ ๊ถŒํ•œ์„ ํ•œ ๋ฒˆ์— ์š”์ฒญ โ€” ์‚ฌ์šฉ์ž๊ฐ€ OAuth ํ•œ ๋ฒˆ ํ•˜๋ฉด ๋‘˜ ๋‹ค ๋™์ž‘. +// ์˜› ์‚ฌ์šฉ์ž(Calendar ๋งŒ ์—ฐ๊ฒฐ)๋Š” Sheets ์‚ฌ์šฉ ์‹œ ๊ถŒํ•œ ๋ถ€์กฑ ์—๋Ÿฌ โ†’ ์žฌ์—ฐ๊ฒฐ ํ•„์š”. const SCOPE = [ 'https://www.googleapis.com/auth/calendar.events', 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/tasks', - 'https://www.googleapis.com/auth/gmail.readonly', 'openid', 'email', ].join(' '); diff --git a/src/features/email/autoSync.ts b/src/features/email/autoSync.ts deleted file mode 100644 index 8fdaa4c..0000000 --- a/src/features/email/autoSync.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * ============================================================ - * Email Auto-Sync Scheduler (Project Astra, Phase 2) - * - * g1nation.email.autoSync ๊ฐ€ ์ผœ์ ธ ์žˆ์œผ๋ฉด ์ฃผ๊ธฐ์ ์œผ๋กœ syncEmails ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. - * - ๊ธฐ๋™ ์งํ›„ ๊ณง์žฅ ๋Œ๋ฆฌ์ง€ ์•Š๊ณ  60s ๋’ค ์ฒซ ์‹คํ–‰(ํ™œ์„ฑํ™” ๋ธ”๋กœํ‚น ๋ฐฉ์ง€) โ†’ ์ดํ›„ ๊ฐ„๊ฒฉ ๋ฐ˜๋ณต. - * - ๋งค tick ๋งˆ๋‹ค enabled ๋ฅผ ์žฌํ™•์ธ โ†’ ์„ค์ •์—์„œ ๋„๋ฉด ๋‹ค์Œ tick ๋ถ€ํ„ฐ ๋ฉˆ์ถค. - * - ์„ค์ •(autoSync / ๊ฐ„๊ฒฉ) ๋ณ€๊ฒฝ ์‹œ ํƒ€์ด๋จธ ์žฌ์‹œ์ž‘(onDidChangeConfiguration). - * - ๋™์‹œ ์‹คํ–‰ ๊ฐ€๋“œ(running) + timer.unref() ๋กœ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒ๋ฅผ ๋ง‰์ง€ ์•Š์Œ. - * - * ์Šฌ๋ž˜์‹œ ๋ช…๋ น๊ณผ *๊ฐ™์€* syncEmails ์ฝ”์–ด๋ฅผ ์“ด๋‹ค โ€” ์ˆ˜์ง‘ ๋™์ž‘ ๋‹จ์ผ ์ถœ์ฒ˜. - * ============================================================ - */ - -import * as vscode from 'vscode'; -import { logInfo, logError } from '../../utils'; -import { syncEmails } from './emailSync'; - -let timer: ReturnType | null = null; -let kickoff: ReturnType | null = null; -let configListener: vscode.Disposable | null = null; -let running = false; - -function readSettings() { - const c = vscode.workspace.getConfiguration('g1nation'); - return { - enabled: c.get('email.autoSync', false), - intervalMin: Math.max(5, c.get('email.autoSyncIntervalMinutes', 30) ?? 30), - days: c.get('email.syncDays', 7) ?? 7, - maxResults: Math.max(1, Math.min(2000, c.get('email.syncMaxMessages', 200) ?? 200)), - }; -} - -async function tick(context: vscode.ExtensionContext): Promise { - const s = readSettings(); - if (!s.enabled || running) return; // ๋„๋ฉด ์ฆ‰์‹œ ์ค‘๋‹จ / ๊ฒน์น˜๋ฉด ์Šคํ‚ต - running = true; - try { - const r = await syncEmails(context, { days: s.days, maxResults: s.maxResults }); - if (r.ok) logInfo('[email] auto-sync ์™„๋ฃŒ', { added: r.added, total: r.total, embedded: r.embedded, failed: r.failed }); - else logError('[email] auto-sync ์‹คํŒจ', { error: r.error }); - } catch (e: any) { - logError('[email] auto-sync ์˜ˆ์™ธ', { error: e?.message || String(e) }); - } finally { - running = false; - } -} - -function stopTimers(): void { - if (timer) { clearInterval(timer); timer = null; } - if (kickoff) { clearTimeout(kickoff); kickoff = null; } -} - -function restartTimers(context: vscode.ExtensionContext): void { - stopTimers(); - const s = readSettings(); - if (!s.enabled) { logInfo('[email] auto-sync ๋น„ํ™œ์„ฑ(์„ค์ • off)'); return; } - kickoff = setTimeout(() => { void tick(context); }, 60_000); - timer = setInterval(() => { void tick(context); }, s.intervalMin * 60_000); - if (typeof (kickoff as any).unref === 'function') (kickoff as any).unref(); - if (typeof (timer as any).unref === 'function') (timer as any).unref(); - logInfo(`[email] auto-sync ํ™œ์„ฑ โ€” ${s.intervalMin}๋ถ„ ๊ฐ„๊ฒฉ`); -} - -/** extension activate ์—์„œ 1ํšŒ ํ˜ธ์ถœ. ์„ค์ • ๋ณ€๊ฒฝ ๊ฐ์‹œ + ํƒ€์ด๋จธ ์‹œ์ž‘. */ -export function startEmailAutoSync(context: vscode.ExtensionContext): void { - if (!configListener) { - configListener = vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('g1nation.email.autoSync') || e.affectsConfiguration('g1nation.email.autoSyncIntervalMinutes')) { - restartTimers(context); - } - }); - context.subscriptions.push(configListener, { dispose: stopTimers }); - } - restartTimers(context); -} diff --git a/src/features/email/emailStore.ts b/src/features/email/emailStore.ts deleted file mode 100644 index 067612b..0000000 --- a/src/features/email/emailStore.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * ============================================================ - * Email Store โ€” ์ˆ˜์ง‘๋œ ์ด๋ฉ”์ผ์˜ ๋กœ์ปฌ ์˜์† ์ €์žฅ์†Œ (Project Astra, Phase 1) - * - * ์ €์žฅ ์œ„์น˜: {brainPath}/memory/email_index.json (LongTermMemory ์™€ ๋™์ผ ๊ทœ์•ฝ) - * - ์ด๋ฉ”์ผ ๋ณธ๋ฌธ์€ ๋กœ์ปฌ์„ ์ ˆ๋Œ€ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค(ํ”„๋ผ์ด๋ฒ„์‹œ ๋ถˆ๋ณ€์‹). ํ•ฉ์„ฑ์€ ๋กœ์ปฌ LLM only. - * - ๊ฐ ๋ ˆ์ฝ”๋“œ๋Š” *์ˆ˜์ง‘ ์‹œ์ ์—* ํ† ํฐํ™”๋ผ ๋“ค์–ด์˜ค๋ฏ€๋กœ(handlers.ts), ๊ฒ€์ƒ‰ ๋•Œ ์žฌํ† ํฐํ™”ํ•˜์ง€ - * ์•Š๋Š”๋‹ค โ€” brainIndex ์˜ mtime ์บ์‹œ์™€ ๊ฐ™์€ ์ •์‹ . - * - ๋ชจ๋“ˆ ๋ ˆ๋ฒจ ์บ์‹œ(path+mtime ํ‚ค)๋กœ ๋งค ์งˆ์˜๋งˆ๋‹ค ์ „์ฒด ํŒŒ์ผ์„ ๋‹ค์‹œ ์ฝ์ง€ ์•Š๋Š”๋‹ค. - * (Phase 2 ์—์„œ brainIndex ์ˆ˜์ค€์˜ ์ฆ๋ถ„ ์ธ๋ฑ์Šค + ์ž„๋ฒ ๋”ฉ์œผ๋กœ ํ™•์žฅ.) - * ============================================================ - */ - -import * as fs from 'fs'; -import * as path from 'path'; - -export interface EmailRecord { - /** Gmail message id (๋ถˆ๋ณ€ โ€” ์ค‘๋ณต ์ œ๊ฑฐ ํ‚ค). */ - messageId: string; - threadId: string; - from: string; - to: string; - subject: string; - /** ์ˆ˜์‹ /๋ฐœ์‹  ์‹œ๊ฐ epoch ms. */ - date: number; - snippet: string; - /** ํ”Œ๋ ˆ์ธ ํ…์ŠคํŠธ ๋ณธ๋ฌธ(๋กœ์ปฌ ์ €์žฅ). */ - bodyText: string; - /** ์›๋ฌธ ๋ฉ”์ผ๋กœ ์ ํ”„ํ•˜๋Š” ๋”ฅ๋งํฌ (์ถœ์ฒ˜ ์ถ”์ ์šฉ). */ - permalink: string; - labels: string[]; - /** ๊ฒ€์ƒ‰์šฉ ํ† ํฐ(๋ณธ๋ฌธ+์ œ๋ชฉ+๋ฐœ์‹ ์ž) โ€” ์ˆ˜์ง‘ ์‹œ 1ํšŒ ๊ณ„์‚ฐ. */ - tokens: string[]; - /** ์ œ๋ชฉ ํ† ํฐ(ํƒ€์ดํ‹€ ๊ฐ€์ค‘์น˜). */ - subjectTokens: string[]; - /** (Phase 2) ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ โ€” ์ˆ˜์ง‘ ์‹œ 1ํšŒ ๊ณ„์‚ฐ. ๋ชจ๋ธ ๋ถˆ์ผ์น˜ ์‹œ ๊ฒ€์ƒ‰์—์„œ ๋ฌด์‹œ. */ - embedding?: number[]; - /** ์ž„๋ฒ ๋”ฉ์„ ๋งŒ๋“  ๋ชจ๋ธ๋ช…(๋ชจ๋ธ ๊ต์ฒด ์‹œ ๋ฌดํšจํ™” ํŒ๋‹จ). */ - embeddingModel?: string; -} - -interface EmailStore { - version: number; - updatedAt: number; - records: EmailRecord[]; -} - -const STORE_VERSION = 1; - -export function getEmailStorePath(brainPath: string): string { - return path.join(brainPath, 'memory', 'email_index.json'); -} - -// path -> { mtimeMs, records } : ๊ฐ™์€ ํŒŒ์ผ์ด ์•ˆ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด ๋””์Šคํฌ ์žฌ๋… ์ƒ๋žต. -const _cache = new Map(); - -/** ์ €์žฅ๋œ ์ด๋ฉ”์ผ ๋ ˆ์ฝ”๋“œ ๋กœ๋“œ. ํŒŒ์ผ์ด ์—†๊ฑฐ๋‚˜ ๊นจ์กŒ์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด. mtime ์บ์‹œ ์ ์šฉ. */ -export function loadEmailRecords(brainPath: string): EmailRecord[] { - const filePath = getEmailStorePath(brainPath); - let mtimeMs = 0; - try { mtimeMs = fs.statSync(filePath).mtimeMs; } catch { return []; } - const cached = _cache.get(filePath); - if (cached && cached.mtimeMs === mtimeMs) return cached.records; - try { - const raw = fs.readFileSync(filePath, 'utf-8'); - const store = JSON.parse(raw) as EmailStore; - const records = Array.isArray(store?.records) ? store.records : []; - _cache.set(filePath, { mtimeMs, records }); - return records; - } catch { - return []; - } -} - -/** ๋ ˆ์ฝ”๋“œ ์ €์žฅ(์›์ž์  ์“ฐ๊ธฐ). ๋””๋ ‰ํ„ฐ๋ฆฌ ์ž๋™ ์ƒ์„ฑ, ์บ์‹œ ๊ฐฑ์‹ . */ -export function saveEmailRecords(brainPath: string, records: EmailRecord[]): void { - const filePath = getEmailStorePath(brainPath); - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - const store: EmailStore = { version: STORE_VERSION, updatedAt: Date.now(), records }; - const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`; - fs.writeFileSync(tmp, JSON.stringify(store), 'utf-8'); - fs.renameSync(tmp, filePath); - try { - _cache.set(filePath, { mtimeMs: fs.statSync(filePath).mtimeMs, records }); - } catch { /* ์บ์‹œ ๊ฐฑ์‹  ์‹คํŒจ๋Š” ๋ฌดํ•ด โ€” ๋‹ค์Œ ๋กœ๋“œ๊ฐ€ ๋””์Šคํฌ์—์„œ ๋‹ค์‹œ ์ฝ์Œ */ } -} - -/** - * ์‹ ๊ทœ ๋ ˆ์ฝ”๋“œ๋ฅผ messageId ๊ธฐ์ค€์œผ๋กœ ๋จธ์ง€(์ค‘๋ณต์€ ์ƒˆ ๊ฐ’์œผ๋กœ ๋ฎ์–ด์”€) ํ›„ ์ €์žฅ. - * ์ตœ์‹ ์ˆœ ์ •๋ ฌ, ์ƒํ•œ(maxRetained) ์ดˆ๊ณผ ์‹œ ์˜ค๋ž˜๋œ ๊ฒƒ๋ถ€ํ„ฐ ์ œ๊ฑฐ. - * ๋ฐ˜ํ™˜: { total, added }. - */ -export function upsertEmailRecords( - brainPath: string, - incoming: EmailRecord[], - maxRetained = 50000, -): { total: number; added: number } { - const existing = loadEmailRecords(brainPath); - const byId = new Map(); - for (const r of existing) byId.set(r.messageId, r); - let added = 0; - for (const r of incoming) { - if (!byId.has(r.messageId)) added++; - byId.set(r.messageId, r); - } - let merged = Array.from(byId.values()).sort((a, b) => b.date - a.date); - if (merged.length > maxRetained) merged = merged.slice(0, maxRetained); - saveEmailRecords(brainPath, merged); - return { total: merged.length, added }; -} diff --git a/src/features/email/emailSync.ts b/src/features/email/emailSync.ts deleted file mode 100644 index eaa8290..0000000 --- a/src/features/email/emailSync.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * ============================================================ - * Email Sync Core โ€” ์ˆ˜์ง‘ ๋กœ์ง ๋‹จ์ผ ์ถœ์ฒ˜ (Project Astra, Phase 2) - * - * ์Šฌ๋ž˜์‹œ ๋ช…๋ น(/email-sync)๊ณผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž๋™ ๋™๊ธฐํ™”(autoSync.ts)๊ฐ€ *๊ฐ™์€* ์ด ํ•จ์ˆ˜๋ฅผ - * ํ˜ธ์ถœํ•œ๋‹ค โ€” ์ˆ˜์ง‘ ๋™์ž‘์ด ํ•œ ๊ณณ์—๋งŒ ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฝ๋‹ค. - * onProgress ์ฝœ๋ฐฑ์œผ๋กœ UI ์ŠคํŠธ๋ฆฌ๋ฐ(์ˆ˜๋™) / ๋ฌด์Œ(๋ฐฑ๊ทธ๋ผ์šด๋“œ)์„ ๋ชจ๋‘ ์ง€์›. - * - * ํ”„๋ผ์ด๋ฒ„์‹œ ๋ถˆ๋ณ€์‹: ๋ฉ”์ผ ๋ณธ๋ฌธ์€ ๋กœ์ปฌ ์ธ๋ฑ์Šค์—๋งŒ ์ €์žฅ. ํ•ฉ์„ฑ์€ ๋กœ์ปฌ LLM only. - * ============================================================ - */ - -import * as vscode from 'vscode'; -import { getConfig } from '../../config'; -import { tokenize } from '../../retrieval/scoring'; -import { embedTexts } from '../../retrieval/embeddings'; -import { listMessageIds, getMessage } from './gmailApi'; -import { upsertEmailRecords, loadEmailRecords, type EmailRecord } from './emailStore'; - -/** getMessage ๋™์‹œ ํ˜ธ์ถœ ์ˆ˜ โ€” Gmail rate limit ๊ณผ ์†๋„์˜ ๊ท ํ˜•. */ -const FETCH_CONCURRENCY = 6; - -export interface SyncResult { - ok: boolean; - /** ์ธ๋ฑ์Šค ์ด ๋ฉ”์ผ ์ˆ˜(๋จธ์ง€ ํ›„). */ - total: number; - /** ์‹ ๊ทœ ์ถ”๊ฐ€ ์ˆ˜. */ - added: number; - /** ๋ณธ๋ฌธ fetch ์‹คํŒจ ์ˆ˜. */ - failed: number; - /** ์ž„๋ฒ ๋”ฉ๋œ ์ˆ˜(0 = ์ž„๋ฒ ๋”ฉ ๋น„ํ™œ์„ฑ/์‹คํŒจ). */ - embedded: number; - error?: string; -} - -export async function syncEmails( - context: vscode.ExtensionContext, - opts: { days: number; maxResults: number; onProgress?: (msg: string) => void }, -): Promise { - const log = opts.onProgress || (() => { /* silent */ }); - const base: SyncResult = { ok: false, total: 0, added: 0, failed: 0, embedded: 0 }; - - const brainPath = (getConfig().localBrainPath || '').trim(); - if (!brainPath) return { ...base, error: 'ํ™œ์„ฑ ๋ธŒ๋ ˆ์ธ ๊ฒฝ๋กœ๊ฐ€ ์„ค์ •๋ผ ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.' }; - - const list = await listMessageIds(context, { query: `newer_than:${opts.days}d`, maxResults: opts.maxResults }); - if (!list.ok) return { ...base, error: list.error }; - if (list.data.length === 0) { - return { ok: true, total: loadEmailRecords(brainPath).length, added: 0, failed: 0, embedded: 0 }; - } - - log(`๋ณธ๋ฌธ ๊ฐ€์ ธ์˜ค๋Š” ์ค‘โ€ฆ (${list.data.length}๊ฑด)`); - const records: EmailRecord[] = []; - let fetched = 0; - let failed = 0; - - for (let i = 0; i < list.data.length; i += FETCH_CONCURRENCY) { - const batch = list.data.slice(i, i + FETCH_CONCURRENCY); - const results = await Promise.all(batch.map((m) => getMessage(context, m.id))); - for (const r of results) { - if (!r.ok) { failed++; continue; } - const m = r.data; - const searchable = `${m.subject}\n${m.from}\n${m.bodyText || m.snippet}`; - records.push({ - messageId: m.messageId, - threadId: m.threadId, - from: m.from, - to: m.to, - subject: m.subject, - date: m.date, - snippet: m.snippet, - bodyText: m.bodyText, - permalink: m.permalink, - labels: m.labels, - tokens: tokenize(searchable), - subjectTokens: tokenize(m.subject), - }); - } - fetched += batch.length; - if (fetched % 30 === 0 || fetched >= list.data.length) { - log(` ยท ${Math.min(fetched, list.data.length)}/${list.data.length}`); - } - } - - if (records.length === 0) { - return { ...base, failed, error: `๋ณธ๋ฌธ์„ ํ•˜๋‚˜๋„ ๊ฐ€์ ธ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค (์‹คํŒจ ${failed}๊ฑด).` }; - } - - // ์ž„๋ฒ ๋”ฉ(best-effort) โ€” ๋ชจ๋ธ ์„ค์ • ์‹œ ์ˆ˜์ง‘๊ณผ ๋™์‹œ์— ๋ฒกํ„ฐํ™”. - let embedded = 0; - const acfg = getConfig(); - if (acfg.embeddingModel) { - log(`์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ค‘โ€ฆ (๋ชจ๋ธ: ${acfg.embeddingModel})`); - try { - const texts = records.map((r) => `${r.subject}\n${r.bodyText || r.snippet}`); - const vectors = await embedTexts(texts, { baseUrl: acfg.ollamaUrl, model: acfg.embeddingModel }); - if (vectors.length === records.length) { - records.forEach((r, idx) => { r.embedding = vectors[idx]; r.embeddingModel = acfg.embeddingModel; }); - embedded = vectors.length; - } - } catch { /* ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์œผ๋กœ graceful fallback */ } - } - - const { total, added } = upsertEmailRecords(brainPath, records); - return { ok: true, total, added, failed, embedded }; -} diff --git a/src/features/email/gmailApi.ts b/src/features/email/gmailApi.ts deleted file mode 100644 index 79e6981..0000000 --- a/src/features/email/gmailApi.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * ============================================================ - * Gmail API v1 โ€” ์ฝ๊ธฐ ์ „์šฉ(read-only) ๋ฉ”์‹œ์ง€ ์ˆ˜์ง‘ (Project Astra, Phase 1) - * - * Calendar/Tasks/Sheets ์™€ *๊ฐ™์€ Google OAuth ํ† ํฐ*์„ ๊ณต์œ ํ•œ๋‹ค - * (`getFreshAccessToken`). scope ์— `gmail.readonly` ๊ฐ€ ํฌํ•จ๋ผ์•ผ ํ•จ(oauth.ts). - * ์‚ญ์ œ/๋‹ต์žฅ/์ „๋‹ฌ ๋“ฑ ์“ฐ๊ธฐ ๊ธฐ๋Šฅ์€ ์ผ์ ˆ ์—†๋‹ค(์ฝ๊ธฐ ์ „์šฉ โ€” ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑยท๋ณด์•ˆ). - * - * ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—†์Œ โ€” REST + native fetch. tasksApi.ts ์™€ ๋™์ผํ•œ ์…ฐ์ดํ”„: - * (context, ...) -> getFreshAccessToken -> fetch -> { ok, data } | { ok:false, error } - * ============================================================ - */ - -import * as vscode from 'vscode'; -import { getFreshAccessToken } from '../calendar/calendarApi'; - -const API_BASE = 'https://gmail.googleapis.com/gmail/v1/users/me'; - -export interface ParsedMessage { - messageId: string; - threadId: string; - from: string; - to: string; - subject: string; - /** epoch ms */ - date: number; - snippet: string; - bodyText: string; - permalink: string; - labels: string[]; -} - -type ApiResult = { ok: true; data: T } | { ok: false; error: string }; - -function authError(status: number, msg: string): boolean { - return status === 401 || status === 403 || /insufficient|scope|disabled|enable/i.test(msg); -} - -const REAUTH_HINT = - 'Gmail API ๊ถŒํ•œ ๋ถ€์กฑ โ€” "Astra: Google Calendar OAuth ์—ฐ๊ฒฐ" ๋ช…๋ น์„ ๋‹ค์‹œ ์‹คํ–‰ํ•ด gmail.readonly ์Šค์ฝ”ํ”„ ๋™์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. (Google Cloud Console ์—์„œ Gmail API ํ™œ์„ฑํ™”๋„ ํ™•์ธ)'; - -/** - * ์ตœ๊ทผ ๋ฉ”์‹œ์ง€ id ๋ชฉ๋ก์„ ์กฐํšŒ. q ๋Š” Gmail ๊ฒ€์ƒ‰ ๋ฌธ๋ฒ•(์˜ˆ: `newer_than:7d`). - * maxResults ๊นŒ์ง€ pageToken ์œผ๋กœ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•œ๋‹ค. - */ -export async function listMessageIds( - context: vscode.ExtensionContext, - opts: { query?: string; maxResults?: number } = {}, -): Promise>> { - const tok = await getFreshAccessToken(context); - if (!tok.ok) return { ok: false, error: tok.error }; - - const target = Math.max(1, Math.min(2000, opts.maxResults ?? 200)); - const out: Array<{ id: string; threadId: string }> = []; - let pageToken: string | undefined; - - try { - while (out.length < target) { - const params = new URLSearchParams({ maxResults: String(Math.min(100, target - out.length)) }); - if (opts.query) params.set('q', opts.query); - if (pageToken) params.set('pageToken', pageToken); - const res = await fetch(`${API_BASE}/messages?${params.toString()}`, { - headers: { Authorization: `Bearer ${tok.accessToken}` }, - signal: AbortSignal.timeout(20000), - }); - const json: any = await res.json().catch(() => ({})); - if (!res.ok) { - const msg: string = json?.error?.message || `HTTP ${res.status}`; - return { ok: false, error: authError(res.status, msg) ? REAUTH_HINT : msg }; - } - const items: any[] = Array.isArray(json.messages) ? json.messages : []; - for (const m of items) { - if (m?.id) out.push({ id: String(m.id), threadId: String(m.threadId || '') }); - } - pageToken = json.nextPageToken; - if (!pageToken || items.length === 0) break; - } - return { ok: true, data: out.slice(0, target) }; - } catch (e: any) { - return { ok: false, error: e?.message || String(e) }; - } -} - -/** ๋‹จ์ผ ๋ฉ”์‹œ์ง€ ์ „์ฒด(format=full)๋ฅผ ์กฐํšŒํ•ด ํŒŒ์‹ฑ. */ -export async function getMessage( - context: vscode.ExtensionContext, - id: string, -): Promise> { - const tok = await getFreshAccessToken(context); - if (!tok.ok) return { ok: false, error: tok.error }; - try { - const res = await fetch(`${API_BASE}/messages/${encodeURIComponent(id)}?format=full`, { - headers: { Authorization: `Bearer ${tok.accessToken}` }, - signal: AbortSignal.timeout(20000), - }); - const json: any = await res.json().catch(() => ({})); - if (!res.ok) { - const msg: string = json?.error?.message || `HTTP ${res.status}`; - return { ok: false, error: authError(res.status, msg) ? REAUTH_HINT : msg }; - } - return { ok: true, data: parseMessage(json) }; - } catch (e: any) { - return { ok: false, error: e?.message || String(e) }; - } -} - -// โ”€โ”€โ”€ ํŒŒ์‹ฑ ํ—ฌํผ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -function header(headers: any[], name: string): string { - const h = (headers || []).find((x) => String(x?.name || '').toLowerCase() === name.toLowerCase()); - return h ? String(h.value || '') : ''; -} - -function decodeBase64Url(data: string): string { - try { - return Buffer.from(String(data).replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8'); - } catch { - return ''; - } -} - -/** payload ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ text/plain ๋ณธ๋ฌธ์„ ์ถ”์ถœ. ์—†์œผ๋ฉด text/html ์„ ํƒœ๊ทธ ์ œ๊ฑฐํ•ด ์‚ฌ์šฉ. */ -function extractBody(payload: any): string { - if (!payload) return ''; - const plain = findPart(payload, 'text/plain'); - if (plain) return plain; - const html = findPart(payload, 'text/html'); - if (html) return html.replace(//gi, ' ').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+\n/g, '\n'); - return ''; -} - -function findPart(node: any, mime: string): string { - if (!node) return ''; - if (node.mimeType === mime && node.body?.data) return decodeBase64Url(node.body.data); - const parts: any[] = Array.isArray(node.parts) ? node.parts : []; - for (const p of parts) { - const found = findPart(p, mime); - if (found) return found; - } - return ''; -} - -function parseMessage(msg: any): ParsedMessage { - const headers: any[] = msg?.payload?.headers || []; - const internal = Number(msg?.internalDate); - const dateHeader = Date.parse(header(headers, 'Date')); - const date = Number.isFinite(internal) && internal > 0 - ? internal - : (Number.isFinite(dateHeader) ? dateHeader : Date.now()); - return { - messageId: String(msg?.id || ''), - threadId: String(msg?.threadId || ''), - from: header(headers, 'From'), - to: header(headers, 'To'), - subject: header(headers, 'Subject'), - date, - snippet: String(msg?.snippet || ''), - bodyText: extractBody(msg?.payload).trim(), - permalink: `https://mail.google.com/mail/u/0/#all/${String(msg?.id || '')}`, - labels: Array.isArray(msg?.labelIds) ? msg.labelIds.map((x: any) => String(x)) : [], - }; -} diff --git a/src/features/email/handlers.ts b/src/features/email/handlers.ts deleted file mode 100644 index 8599a78..0000000 --- a/src/features/email/handlers.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * ============================================================ - * Email handlers โ€” /email-sync (Project Astra, Phase 1) - * - * Gmail ์„ ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์ˆ˜์ง‘ํ•ด {brainPath}/memory/email_index.json ์— ์ €์žฅํ•œ๋‹ค. - * ์ €์žฅ๋œ ๋ฉ”์ผ์€ RetrievalOrchestrator ์˜ 'email' ์†Œ์Šค๋กœ ์ž๋™ ๊ฒ€์ƒ‰๋˜๋ฏ€๋กœ(๋ณ„๋„ QA - * ๋ช…๋ น ๋ถˆํ•„์š”), ์ดํ›„ ์ผ๋ฐ˜ ์ฑ„ํŒ… ์งˆ๋ฌธ์ด ๋ฉ”์ผ ๊ทผ๊ฑฐ+์›๋ฌธ ๋งํฌ๋กœ ๋‹ตํ•˜๊ฒŒ ๋œ๋‹ค. - * - * import ๋งŒ์œผ๋กœ ๋“ฑ๋ก๋˜๋„๋ก module scope ์—์„œ registerSlashCommand ํ˜ธ์ถœ โ€” ๋ฐฐ๋Ÿด - * (extension.ts)์—์„œ `import './features/email/handlers'` ํ•œ ์ค„ ์ถ”๊ฐ€. - * ============================================================ - */ - -import * as vscode from 'vscode'; -import { registerSlashCommand, chunk } from '../datacollect/slashRouter'; -import { getConfig } from '../../config'; -import { syncEmails } from './emailSync'; -import { loadEmailRecords, getEmailStorePath, type EmailRecord } from './emailStore'; - -async function runEmailSync(arg: string, view: any, context?: vscode.ExtensionContext): Promise { - if (!context) { chunk(view, '\nโŒ ExtensionContext ์—†์Œ โ€” /email-sync ์‹คํ–‰ ๋ถˆ๊ฐ€.\n'); return true; } - - const cfg = vscode.workspace.getConfiguration('g1nation'); - const argDays = parseInt(arg.trim().split(/\s+/)[0] || '', 10); - const days = Number.isFinite(argDays) && argDays > 0 ? Math.min(365, argDays) : (cfg.get('email.syncDays', 7) ?? 7); - const maxResults = Math.max(1, Math.min(2000, cfg.get('email.syncMaxMessages', 200) ?? 200)); - - const brainPath = (getConfig().localBrainPath || '').trim(); - if (!brainPath) { - chunk(view, '\nโŒ ํ™œ์„ฑ ๋ธŒ๋ ˆ์ธ ๊ฒฝ๋กœ๊ฐ€ ์„ค์ •๋ผ ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Astra Settings ์—์„œ Brain ์„ ๋จผ์ € ์ง€์ •ํ•˜์„ธ์š”.\n'); - return true; - } - - chunk(view, `\n๐Ÿ“ง **์ด๋ฉ”์ผ ์ˆ˜์ง‘ (์ฝ๊ธฐ ์ „์šฉ)** โ€” ์ตœ๊ทผ ${days}์ผ, ์ตœ๋Œ€ ${maxResults}๊ฑด\n ยท ์ €์žฅ ์œ„์น˜: \`${getEmailStorePath(brainPath)}\`\n`); - const r = await syncEmails(context, { days, maxResults, onProgress: (m) => chunk(view, ` ${m}\n`) }); - if (!r.ok) { chunk(view, `\nโŒ ์ˆ˜์ง‘ ์‹คํŒจ: ${r.error}\n`); return true; } - - chunk(view, `\nโœ… ์ˆ˜์ง‘ ์™„๋ฃŒ โ€” ์‹ ๊ทœ ${r.added}๊ฑด / ์ธ๋ฑ์Šค ์ด ${r.total}๊ฑด${r.embedded ? ` ยท ์ž„๋ฒ ๋”ฉ ${r.embedded}๊ฑด` : ''}${r.failed ? ` ยท ์‹คํŒจ ${r.failed}๊ฑด` : ''}\n`); - chunk(view, '์ด์ œ ์ผ๋ฐ˜ ์ฑ„ํŒ…์œผ๋กœ ๋ฉ”์ผ ๋‚ด์šฉ์„ ๋ฌผ์–ด๋ณด๋ฉด ๊ทผ๊ฑฐ ๋ฉ”์ผ๊ณผ ์›๋ฌธ ๋งํฌ๋กœ ๋‹ตํ•ฉ๋‹ˆ๋‹ค. (์˜ˆ: "A ํ”„๋กœ์ ํŠธ ๊ณ„์•ฝ ์กฐ๊ฑด ์–ด๋–ป๊ฒŒ ๋ฐ”๋€Œ์—ˆ์ง€?")\n'); - return true; -} - -// โ”€โ”€โ”€ /email-status โ€” ๋ฏธํšŒ์‹ /๋†“์นœ ์š”์ฒญ ์ถ”์  (Scenario 2) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -// ์ˆ˜์ง‘๋œ ๋ฉ”์ผ์„ ์Šค๋ ˆ๋“œ๋กœ ๋ฌถ์–ด, ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€๊ฐ€ '๋‚ด๊ฐ€ ๋ณด๋‚ธ ๊ฒƒ(SENT)'์ด ์•„๋‹Œ ์Šค๋ ˆ๋“œ๋ฅผ -// '๋ฏธํšŒ์‹ '์œผ๋กœ ์ถ”์ถœํ•œ๋‹ค. ํ”„๋กœ๋ชจ์…˜/์†Œ์…œ/์—…๋ฐ์ดํŠธ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ๋…ธ์ด์ฆˆ๋กœ ์ œ์™ธ. - -const NOISE_LABELS = ['CATEGORY_PROMOTIONS', 'CATEGORY_SOCIAL', 'CATEGORY_UPDATES', 'CATEGORY_FORUMS', 'SPAM', 'TRASH']; -const REQUEST_HINT = /์š”์ฒญ|๋ฌธ์˜|๋ถ€ํƒ|๊ฒ€ํ† |ํšŒ์‹ |๋‹ต์žฅ|ํ™•์ธ\s*(?:๋ถ€ํƒ|์š”๋ง)|please|could you|kindly|๋ฐ˜๋ ค|์Šน์ธ|\?\s*$/i; - -async function runEmailStatus(arg: string, view: any, context?: vscode.ExtensionContext): Promise { - if (!context) { chunk(view, '\nโŒ ExtensionContext ์—†์Œ โ€” /email-status ์‹คํ–‰ ๋ถˆ๊ฐ€.\n'); return true; } - const brainPath = (getConfig().localBrainPath || '').trim(); - if (!brainPath) { chunk(view, '\nโŒ ํ™œ์„ฑ ๋ธŒ๋ ˆ์ธ ๊ฒฝ๋กœ ๋ฏธ์„ค์ •.\n'); return true; } - - const argDays = parseInt(arg.trim().split(/\s+/)[0] || '', 10); - const days = Number.isFinite(argDays) && argDays > 0 ? Math.min(90, argDays) : 7; - - const all = loadEmailRecords(brainPath); - if (all.length === 0) { - chunk(view, '\nโ„น๏ธ ์ˆ˜์ง‘๋œ ๋ฉ”์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋จผ์ € `/email-sync` ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”.\n'); - return true; - } - - const since = Date.now() - days * 24 * 60 * 60 * 1000; - const recent = all.filter((r) => r.date >= since); - - // ์Šค๋ ˆ๋“œ๋ณ„ ์ตœ์‹  ๋ฉ”์‹œ์ง€ - const latestByThread = new Map(); - for (const r of recent) { - const key = r.threadId || r.messageId; - const cur = latestByThread.get(key); - if (!cur || r.date > cur.date) latestByThread.set(key, r); - } - - const unanswered = Array.from(latestByThread.values()) - .filter((r) => !r.labels.includes('SENT')) // ๋‚ด๊ฐ€ ๋งˆ์ง€๋ง‰์— ๋‹ตํ•˜์ง€ ์•Š์Œ - .filter((r) => !r.labels.some((l) => NOISE_LABELS.includes(l))) // ํ”„๋กœ๋ชจ์…˜/์†Œ์…œ ๋“ฑ ์ œ์™ธ - .sort((a, b) => a.date - b.date); // ์˜ค๋ž˜๋œ ๋ฏธํšŒ์‹  ๋จผ์ € - - chunk(view, `\n๐Ÿ“ญ **๋ฏธํšŒ์‹  / ๋†“์นœ ์š”์ฒญ โ€” ์ตœ๊ทผ ${days}์ผ** (${unanswered.length}๊ฑด)\n`); - if (unanswered.length === 0) { - chunk(view, '\nโœ… ๋ฏธํšŒ์‹  ์Šค๋ ˆ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊น”๋”ํ•˜๋„ค์š”.\n'); - return true; - } - - const MAX = 20; - const today = Date.now(); - for (const r of unanswered.slice(0, MAX)) { - const ageDays = Math.floor((today - r.date) / (24 * 60 * 60 * 1000)); - const dateStr = new Date(r.date).toISOString().slice(0, 10); - const isRequest = REQUEST_HINT.test(r.subject) || REQUEST_HINT.test(r.snippet); - const flag = isRequest ? '๐Ÿ”” ' : ''; - const fromShort = r.from.replace(/\s*<[^>]+>/, '').trim() || r.from; - chunk(view, `- ${flag}**${r.subject || '(์ œ๋ชฉ ์—†์Œ)'}** โ€” ${fromShort} ยท ${dateStr} (${ageDays}์ผ ๊ฒฝ๊ณผ)\n โ†ณ ${r.permalink}\n`); - } - if (unanswered.length > MAX) chunk(view, `\n_โ€ฆ+${unanswered.length - MAX}๊ฑด ๋” (\`/email-status ${days}\` ๋ฒ”์œ„๋ฅผ ์ขํ˜€ ๋ณด์„ธ์š”)_\n`); - chunk(view, '\n๐Ÿ”” = ํšŒ์‹ ยท๊ฒ€ํ†  ์š”์ฒญ์œผ๋กœ ๋ณด์ด๋Š” ๋ฉ”์ผ. ์ž์„ธํ•œ ๋‚ด์šฉ์€ ์ฑ„ํŒ…์œผ๋กœ ๋ฌผ์–ด๋ณด์„ธ์š”.\n'); - return true; -} - -registerSlashCommand({ - name: '/email-sync', - description: 'Gmail ์ฝ๊ธฐ์ „์šฉ ์ˆ˜์ง‘ โ†’ ๋กœ์ปฌ ์ธ๋ฑ์Šค (์ดํ›„ ์ฑ„ํŒ…์ด ๋ฉ”์ผ ๊ทผ๊ฑฐ๋กœ ๋‹ต๋ณ€). ์‚ฌ์šฉ๋ฒ•: /email-sync [์ผ์ˆ˜]', - handler: runEmailSync, -}); -registerSlashCommand({ - name: '/email-status', - description: '๋ฏธํšŒ์‹ /๋†“์นœ ์š”์ฒญ ์ถ”์  โ€” ๋งˆ์ง€๋ง‰์ด ๋‚ด ๋‹ต์žฅ์ด ์•„๋‹Œ ์Šค๋ ˆ๋“œ ์ถ”์ถœ. ์‚ฌ์šฉ๋ฒ•: /email-status [์ผ์ˆ˜]', - handler: runEmailStatus, -}); diff --git a/src/features/settings/settingsPanelProvider.ts b/src/features/settings/settingsPanelProvider.ts index 5ceaee5..8d402d4 100644 --- a/src/features/settings/settingsPanelProvider.ts +++ b/src/features/settings/settingsPanelProvider.ts @@ -6,9 +6,6 @@ import type { TelegramBot } from '../../integrations/telegram/telegramBot'; import { logError, logInfo } from '../../utils'; import { discoverModels } from '../../lib/discoverModels'; import { pickConfigTarget } from '../../lib/paths'; -import { getConfig } from '../../config'; -import { loadEmailRecords } from '../email/emailStore'; -import { syncEmails } from '../email/emailSync'; /** * Astra Settings webview. @@ -104,20 +101,6 @@ interface SettingsState { maxPages: number; synthesisTemperature: number; }; - email: { - autoSync: boolean; - autoSyncIntervalMinutes: number; - syncDays: number; - syncMaxMessages: number; - /** ๋กœ์ปฌ ์ธ๋ฑ์Šค์— ์ €์žฅ๋œ ๋ฉ”์ผ ์ˆ˜. */ - indexedCount: number; - /** ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฉ”์ผ ๋‚ ์งœ(YYYY-MM-DD) โ€” ์—†์œผ๋ฉด ''. */ - newestDate: string; - /** ๋งˆ์ง€๋ง‰ '์ง€๊ธˆ ๋™๊ธฐํ™”' ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€. */ - lastSyncMessage: string; - /** ๋™๊ธฐํ™” ์ง„ํ–‰ ์ค‘. */ - syncing: boolean; - }; google: { clientId: string; /** secret ์ž์ฒด๋Š” client ์— echo ์•ˆ ํ•จ โ€” *์„ค์ • ์—ฌ๋ถ€* ๋งŒ. true ๋ฉด input placeholder ๊ฐ€ "์ €์žฅ๋จ" ์œผ๋กœ ๋ฐ”๋€œ. */ @@ -165,10 +148,6 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { constructor(private readonly _deps: SettingsPanelDeps) {} - // Project Astra โ€” ์ด๋ฉ”์ผ '์ง€๊ธˆ ๋™๊ธฐํ™”' ์ƒํƒœ(ํŒจ๋„ ํ‘œ์‹œ์šฉ). - private _emailSyncing = false; - private _emailLastSyncMsg = ''; - public resolveWebviewView(view: vscode.WebviewView): void { this._view = view; this._setupWebview(view.webview); @@ -279,12 +258,6 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { case 'datacollect.update': await this._handleDatacollectUpdate(msg); return; - case 'email.update': - await this._handleEmailUpdate(msg); - return; - case 'email.syncNow': - await this._handleEmailSyncNow(); - return; case 'google.update': await this._handleGoogleUpdate(msg); return; @@ -665,70 +638,6 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { } } - // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Email (Project Astra) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - private async _handleEmailUpdate(msg: any): Promise { - if (typeof msg.autoSync === 'boolean') { - await this._safeConfigUpdate('email.autoSync', msg.autoSync); - } - if (typeof msg.autoSyncIntervalMinutes === 'number' && Number.isFinite(msg.autoSyncIntervalMinutes)) { - await this._safeConfigUpdate('email.autoSyncIntervalMinutes', Math.max(5, Math.min(1440, Math.floor(msg.autoSyncIntervalMinutes)))); - } - if (typeof msg.syncDays === 'number' && Number.isFinite(msg.syncDays)) { - await this._safeConfigUpdate('email.syncDays', Math.max(1, Math.min(365, Math.floor(msg.syncDays)))); - } - if (typeof msg.syncMaxMessages === 'number' && Number.isFinite(msg.syncMaxMessages)) { - await this._safeConfigUpdate('email.syncMaxMessages', Math.max(1, Math.min(2000, Math.floor(msg.syncMaxMessages)))); - } - } - - /** ํŒจ๋„์˜ '์ง€๊ธˆ ๋™๊ธฐํ™”' ๋ฒ„ํŠผ โ€” ์Šฌ๋ž˜์‹œ ๋ช…๋ น๊ณผ ๋™์ผํ•œ syncEmails ์ฝ”์–ด ํ˜ธ์ถœ. */ - private async _handleEmailSyncNow(): Promise { - if (this._emailSyncing) return; - const c = vscode.workspace.getConfiguration('g1nation'); - const days = c.get('email.syncDays', 7) ?? 7; - const maxResults = Math.max(1, Math.min(2000, c.get('email.syncMaxMessages', 200) ?? 200)); - this._emailSyncing = true; - this._emailLastSyncMsg = '๋™๊ธฐํ™” ์ค‘โ€ฆ'; - await this._refreshState(); - try { - const r = await syncEmails(this._deps.context, { days, maxResults }); - this._emailLastSyncMsg = r.ok - ? `์™„๋ฃŒ โ€” ์‹ ๊ทœ ${r.added} / ์ด ${r.total}${r.embedded ? ` ยท ์ž„๋ฒ ๋”ฉ ${r.embedded}` : ''}${r.failed ? ` ยท ์‹คํŒจ ${r.failed}` : ''}` - : `์‹คํŒจ โ€” ${r.error}`; - } catch (e: any) { - this._emailLastSyncMsg = `์˜ค๋ฅ˜ โ€” ${e?.message || String(e)}`; - } finally { - this._emailSyncing = false; - await this._refreshState(); - } - } - - private _buildEmailState(): SettingsState['email'] { - const c = vscode.workspace.getConfiguration('g1nation'); - let indexedCount = 0; - let newestDate = ''; - try { - const brainPath = (getConfig().localBrainPath || '').trim(); - if (brainPath) { - const records = loadEmailRecords(brainPath); - indexedCount = records.length; - const newest = records.reduce((m, r) => (r.date > m ? r.date : m), 0); - if (newest > 0) newestDate = new Date(newest).toISOString().slice(0, 10); - } - } catch { /* status ํ‘œ์‹œ ์‹คํŒจ๋Š” ๋ฌดํ•ด */ } - return { - autoSync: c.get('email.autoSync', false), - autoSyncIntervalMinutes: c.get('email.autoSyncIntervalMinutes', 30) ?? 30, - syncDays: c.get('email.syncDays', 7) ?? 7, - syncMaxMessages: c.get('email.syncMaxMessages', 200) ?? 200, - indexedCount, - newestDate, - lastSyncMessage: this._emailLastSyncMsg, - syncing: this._emailSyncing, - }; - } - private async _refreshState(): Promise { if (!this._view && !this._panel) return; const cfg = vscode.workspace.getConfiguration('g1nation'); @@ -791,7 +700,6 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { maxPages: cfg.get('datacollectMaxPages', 8) ?? 8, synthesisTemperature: cfg.get('datacollectSynthesisTemperature', 0.1) ?? 0.1, }, - email: this._buildEmailState(), google: this._buildGoogleState(), providers: await this._buildProvidersState(), devilAgent: { enabled: cfg.get('devilAgent.enabled', false) }, diff --git a/src/retrieval/contextBudget.ts b/src/retrieval/contextBudget.ts index 109d9d4..bf14bd9 100644 --- a/src/retrieval/contextBudget.ts +++ b/src/retrieval/contextBudget.ts @@ -119,8 +119,7 @@ export function assembleContext(chunks: RetrievalChunk[]): string { 'procedural-memory': '๐Ÿ“‹ Procedural Memory (๋ฐ˜๋ณต ์ ˆ์ฐจ)', 'episodic-memory': '๐Ÿ“– Episodic Memory (๊ณผ๊ฑฐ ๋Œ€ํ™” ํ๋ฆ„)', 'project-scan': '๐Ÿ” Project Scan', - 'recent-knowledge': '๐Ÿ“„ Recent Project Knowledge', - 'email': '๐Ÿ“ง ์ด๋ฉ”์ผ (์ˆ˜์ง‘๋œ ๋ฉ”์ผ ๊ทผ๊ฑฐ โ€” ์›๋ฌธ ๋งํฌ ํฌํ•จ)' + 'recent-knowledge': '๐Ÿ“„ Recent Project Knowledge' }; // Group by source diff --git a/src/retrieval/index.ts b/src/retrieval/index.ts index 75f7276..6f4c2ac 100644 --- a/src/retrieval/index.ts +++ b/src/retrieval/index.ts @@ -26,7 +26,6 @@ import { extractLessonEssence } from './lessonHelpers'; import { cosineSimilarity } from './embeddings'; import { applyActionabilityBoost, WorkStateSignals, ActionabilityWeights } from './actionabilityScoring'; import { applyHierarchicalReweight, classifyQueryLevel, AbstractionLevel, HierarchicalWeights } from './hierarchicalLevel'; -import { loadEmailRecords } from '../features/email/emailStore'; export { tokenize, expandQuery, scoreTfIdf, scoreTfIdfPreTokenized, extractBestExcerpt } from './scoring'; export { selectWithinBudget, assembleContext, estimateTokens } from './contextBudget'; @@ -84,8 +83,6 @@ interface RetrievalOptions { embeddingModel?: string; /** Blend weight: 0 = TF-IDF only, 1 = cosine only. Default 0.5. */ embeddingBlendAlpha?: number; - /** Project Astra โ€” email ์†Œ์Šค์—์„œ ๊ฐ€์ ธ์˜ฌ ์ตœ๋Œ€ ๋ฉ”์ผ ์ˆ˜. ๊ธฐ๋ณธ 6. 0 ์ด๋ฉด ์Šคํ‚ต. */ - emailLimit?: number; /** * Actionability โ€” "ํ˜„์žฌ ์ž‘์—… ์ƒํƒœ" ์‹ ํ˜ธ(์ตœ๊ทผ ์Šฌ๋ž˜์‹œ ๋ช…๋ น + ์—ด๋ฆฐ ํŒŒ์ผ) ๋กœ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์žฌ๊ฐ€์ค‘. * undefined ๋ฉด actionability re-rank ์•ˆ ํ•จ (legacy ๋™์ž‘). @@ -154,19 +151,6 @@ export class RetrievalOrchestrator { allChunks.push(...memoryChunks); fusionLog.push(`Memory search: ${memoryChunks.length} chunks found`); - // โ”€โ”€ โ‘ -b Email Search (Project Astra) โ€” ์ˆ˜์ง‘๋œ ๋ฉ”์ผ์—์„œ ๊ทผ๊ฑฐ ๊ฒ€์ƒ‰ โ”€โ”€ - // ์ธ๋ฑ์Šค๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด(๋ฏธ์ˆ˜์ง‘) ์ฆ‰์‹œ [] ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ๋น„์šฉ 0. - const emailChunks = this.searchEmailIndex( - expandedTokens, - options.brain, - options.emailLimit ?? 6, - options.queryEmbedding, - options.embeddingModel, - options.embeddingBlendAlpha, - ); - allChunks.push(...emailChunks); - if (emailChunks.length > 0) fusionLog.push(`Email search: ${emailChunks.length} chunks found`); - // โ”€โ”€ โ‘ก-b Medium-Term Memory (recent sessions) โ”€โ”€ const mediumChunks = this.scoreRecentSessions( expandedTokens, @@ -359,84 +343,6 @@ export class RetrievalOrchestrator { } } - // โ”€โ”€โ”€ Email Search (Project Astra) โ”€โ”€โ”€ - - /** - * ์ˆ˜์ง‘๋œ ์ด๋ฉ”์ผ ์ธ๋ฑ์Šค({brainPath}/memory/email_index.json)์—์„œ TF-IDF ๋กœ ๊ทผ๊ฑฐ ๋ฉ”์ผ์„ - * ์ฐพ๋Š”๋‹ค. ๋ธŒ๋ ˆ์ธ ํŒŒ์ผ ๊ฒ€์ƒ‰๊ณผ *๋™์ผํ•œ* scoreTfIdfPreTokenized ๋ฅผ ์จ์„œ ์ผ๊ด€์„ฑ ์œ ์ง€. - * ๊ฐ ์ฒญํฌ๋Š” messageId/permalink ๋ฉ”ํƒ€๋ฅผ ์‹ค์–ด ๋‹ต๋ณ€์ด ์›๋ฌธ ๋ฉ”์ผ๋กœ ์ ํ”„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค. - * Phase 1 ์€ ํ‚ค์›Œ๋“œ(TF-IDF)๋งŒ โ€” ์ž„๋ฒ ๋”ฉ ๋ธ”๋ Œ๋“œ๋Š” Phase 2. - */ - private searchEmailIndex( - expandedTokens: string[], - brain: BrainProfile, - limit: number, - queryEmbedding?: number[], - embeddingModel?: string, - embeddingBlendAlpha?: number, - ): RetrievalChunk[] { - if (limit <= 0) return []; - try { - const records = loadEmailRecords(brain.localBrainPath); - if (records.length === 0) return []; - const scored = scoreTfIdfPreTokenized( - expandedTokens, - records.map((r) => ({ - tokens: r.tokens, - titleTokens: r.subjectTokens, - lastModified: r.date, - conflictCount: 0, - })), - ); - - // (Phase 2) ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ธ”๋ Œ๋“œ โ€” ๋ธŒ๋ ˆ์ธ ๊ฒ€์ƒ‰๊ณผ ๋™์ผ ๊ณต์‹. ๊ฐ™์€ ๋ชจ๋ธ๋กœ ์ž„๋ฒ ๋”ฉ๋œ - // ๋ ˆ์ฝ”๋“œ๋งŒ ์ฝ”์‚ฌ์ธ ๊ฐ€์‚ฐ, ์—†์œผ๋ฉด ์ˆœ์ˆ˜ TF-IDF ์œ ์ง€(graceful fallback). - if (queryEmbedding && embeddingModel && (embeddingBlendAlpha ?? 0) > 0) { - const alpha = Math.max(0, Math.min(1, embeddingBlendAlpha!)); - const maxTfidf = scored.reduce((m, s) => s.score > m ? s.score : m, 0) || 1; - for (const s of scored) { - const rec = records[s.index]; - if (!rec.embedding || rec.embeddingModel !== embeddingModel) continue; - const cos = cosineSimilarity(queryEmbedding, rec.embedding); - const tfidfNorm = s.score / maxTfidf; - s.score = (1 - alpha) * tfidfNorm + alpha * Math.max(0, cos); - } - } - - const ranked = scored.filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, limit); - const out: RetrievalChunk[] = []; - for (const s of ranked) { - const r = records[s.index]; - const dateStr = new Date(r.date).toISOString().slice(0, 10); - const title = `๋ฉ”์ผ: "${r.subject || '(์ œ๋ชฉ ์—†์Œ)'}" (${dateStr}, from ${r.from})`; - const body = summarizeText(r.bodyText || r.snippet || '', 700); - const content = `${body}${r.permalink ? `\n[์›๋ฌธ ๋งํฌ] ${r.permalink}` : ''}`; - out.push({ - id: `email-${r.messageId}`, - source: 'email' as const, - title, - content, - score: s.score, - tokenEstimate: estimateTokens(content), - metadata: { - category: 'email', - lastUpdated: r.date, - queryCoverage: s.queryCoverage, - emailMessageId: r.messageId, - emailThreadId: r.threadId, - emailFrom: r.from, - emailSubject: r.subject, - emailDate: r.date, - emailPermalink: r.permalink, - }, - }); - } - return out; - } catch { - return []; - } - } - // โ”€โ”€โ”€ Memory Layer Search โ”€โ”€โ”€ private searchMemoryLayers( @@ -607,8 +513,7 @@ export class RetrievalOrchestrator { 'medium-term-memory': 0.78, // recent sessions: useful when the user references "last time / yesterday" 'episodic-memory': 0.7, 'project-scan': 0.6, - 'recent-knowledge': 0.75, - 'email': 0.9 // ๋ฉ”์ผ์€ ์ง์ ‘ ๊ทผ๊ฑฐ โ€” ๋ธŒ๋ ˆ์ธ ๋…ธํŠธ์™€ ๋™๊ธ‰์œผ๋กœ ์ทจ๊ธ‰ + 'recent-knowledge': 0.75 }; for (const chunk of chunks) { diff --git a/src/retrieval/types.ts b/src/retrieval/types.ts index 186cb2a..e6c6864 100644 --- a/src/retrieval/types.ts +++ b/src/retrieval/types.ts @@ -16,8 +16,7 @@ export type RetrievalSource = | 'procedural-memory' // Procedural Memory | 'episodic-memory' // Episodic Memory | 'project-scan' // Local Project Path scan - | 'recent-knowledge' // Recent Project Knowledge record - | 'email'; // Project Astra โ€” ์ˆ˜์ง‘๋œ Gmail/์ด๋ฉ”์ผ + | 'recent-knowledge'; // Recent Project Knowledge record export type ConflictSeverity = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH'; @@ -44,15 +43,6 @@ export interface RetrievalChunk { isLesson?: boolean; /** 'lesson' | 'playbook' | 'qa-finding' when isLesson is true. */ lessonKind?: string; - - // --- Email (Project Astra) โ€” ์ถœ์ฒ˜ ์ถ”์ : ์›๋ฌธ ๋ฉ”์ผ๋กœ ์ ํ”„ --- - emailMessageId?: string; - emailThreadId?: string; - emailFrom?: string; - emailSubject?: string; - emailDate?: number; - /** ์›๋ฌธ ๋ฉ”์ผ ๋”ฅ๋งํฌ. */ - emailPermalink?: string; }; }