diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 8299d89..4d4e804 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,18 @@ # Astra Patch Notes +## 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)) +- **`/research`(NotebookLM) ์ œ๊ฑฐ** โ€” Chrome/Google ๋กœ๊ทธ์ธ ์˜์กด์ด๋ผ ๋กœ์ปฌ Datacollect ์•ฑ ์ „์šฉ์œผ๋กœ ๋ถ„๋ฆฌ. benchmark/youtube/wikify/blog/meet ๋Š” ์œ ์ง€. ([handlers.ts](src/features/datacollect/handlers.ts)) + +### ๐Ÿ›ก๏ธ ํ™˜๊ฐยท์˜ค์—ผ ๋ฐฉ์ง€ ๊ฐ•ํ™” (์ฝ”๋“œ ๊ฒ€ํ†  ๊ธฐ๋ฐ˜) +- **์—๋Ÿฌ๋กœ๊ทธ ์˜ค์—ผ ์ฐจ๋‹จ** โ€” STT/์ŠคํƒํŠธ๋ ˆ์ด์Šค/์—๋Ÿฌ๋คํ”„๋ฅผ ์žฅ๊ธฐ๊ธฐ์–ต ์ฑ„๊ตด์—์„œ ์ œ์™ธ(`looksLikeErrorLog`, `ERROR_NOISE`) + ์ž๋™ ์ถ”์ถœ ํ•ญ๋ชฉ์— 14์ผ TTL(์ฐธ์กฐ ์‹œ ์Šฌ๋ผ์ด๋”ฉ ์—ฐ์žฅ). ๊ธฐ์กดยท์ˆ˜๋™ ํ•ญ๋ชฉ ๋ฌด์˜ํ–ฅ. ([LongTermMemory.ts](src/memory/LongTermMemory.ts) ยท [MemoryExtractor.ts](src/memory/MemoryExtractor.ts)) +- **์ปจํ…์ŠคํŠธ ์ฃผ์ œ ํƒœ๊น…** โ€” ๊ฒ€์ƒ‰ ์ฒญํฌ์— `[์ฃผ์ œ]` ํƒœ๊ทธ + "๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธยท์ฃผ์ œ ์„ž์ง€ ๋ง๋ผ" ๊ฒฝ๊ณ„ ์ง€์นจ์œผ๋กœ ๋ฌด์„ฑ ๊ต์ฐจ์˜ค์—ผ ๋ฐฉ์ง€. ([contextBudget.ts](src/retrieval/contextBudget.ts)) +- **"ํ™•์ธ ๋ถˆ๊ฐ€" ๋ธ”๋žญํ‚ท ๊ทœ์น™** โ€” ๊ทผ๊ฑฐ ์—†๋Š” ์‚ฌ์‹ค ๋‚ ์กฐ ๊ธˆ์ง€(์ˆ˜์น˜/๋‚ ์งœ/๊ณ ์œ ๋ช…์‚ฌ/๊ฒฐ์ •), R7(๊ฐ€์ • ํ›„ ์ง„ํ–‰)๊ณผ ๊ตฌ๋ถ„. ([utils.ts](src/utils.ts)) + +### ๐ŸŽ™๏ธ /meet STT ์˜คํƒ€ ๋ณด์ • +- ์Œ์„ฑโ†’ํ…์ŠคํŠธ ์˜คํƒ€๋ฅผ ๋ฌธ๋งฅยท๋„๋ฉ”์ธ ์ง€์‹์œผ๋กœ ์ •๊ทœํ™”ํ•˜๋˜ **"์ฒ ์ž ๋ณด์ • โ‰  ์‚ฌ์‹ค ๋‚ ์กฐ"** ๋ช…์‹œ โ€” ์˜คํƒ€ ํ•˜๋‚˜๋กœ ์ „์ฒด๋ฅผ "ํ™•์ธ ๋ถˆ๊ฐ€"๋กœ ๋ง‰์ง€ ์•Š๊ฒŒ. metadata ๋ฅผ ์ฆ‰์„ ์šฉ์–ด์ง‘์œผ๋กœ ํ™œ์šฉ. ([meetPrompt.ts](src/features/datacollect/prompts/meetPrompt.ts)) + ## v2.2.204 (2026-06-04) ### โœจ `/weekly` ์ „๋ฉด ๊ต์ฒด โ€” ์บ˜๋ฆฐ๋” task ๊ธฐ๋ฐ˜ ์ฃผ๊ฐ„ ๋ณด๊ณ ์„œ (๊ธˆ์ฃผ/์ฐจ์ฃผ) - **๊ธฐ์กด `/weekly`(๋Œ€ํ‘œ์šฉ CEO ์ฃผ๊ฐ„ ๋ฆฌ๋ทฐ ์นด๋“œ โ€” ๊ณ ๊ฐ/์ฑ„์šฉ/๋Ÿฐ์›จ์ด ์ง‘๊ณ„)๋Š” ์ œ๊ฑฐ**ํ•˜๊ณ , `/weekly` ๋ฅผ task ๊ธฐ๋ฐ˜ ๊ธˆ์ฃผ/์ฐจ์ฃผ ๋ณด๊ณ ์„œ๋กœ ์ผ์›ํ™”. (์ œ๊ฑฐ: [dashboards.ts](src/features/teamops/handlers/dashboards.ts) `runWeekly` + weekly ์ „์šฉ ํ—ฌํผ) diff --git a/media/settings-panel.html b/media/settings-panel.html index aa0dde9..0bf76e0 100644 --- a/media/settings-panel.html +++ b/media/settings-panel.html @@ -48,14 +48,41 @@

Datacollect (slash ๋ช…๋ น)

-

์ฑ„ํŒ…์—์„œ /research ยท /benchmark ยท /youtube ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด Datacollect Bridge๋กœ ์œ„์ž„๋ฉ๋‹ˆ๋‹ค. Bridge๋Š” Datacollect ํ”„๋กœ์ ํŠธ์—์„œ npm run bridge ๋กœ ์‹คํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

+

์ฑ„ํŒ…์—์„œ /research ยท /benchmark ยท /youtube ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด Datacollect Bridge๋กœ ์œ„์ž„๋ฉ๋‹ˆ๋‹ค. ํƒ€๊นƒ์œผ๋กœ ๋กœ์ปฌ(npm run bridge) ๋˜๋Š” NAS์˜ ๊ฒฝ๋Ÿ‰ Bridge ์ค‘ ์–ด๋””๋ฅผ ํ˜ธ์ถœํ• ์ง€ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

- + +
+ + +
+ local = ์•„๋ž˜ ๋กœ์ปฌ Bridge URL ์‚ฌ์šฉ. nas = NAS Bridge URL(+ํ† ํฐ) ์‚ฌ์šฉ. nas์ธ๋ฐ URL์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ์ปฌ๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค. +
+
+
+
+ +
+ + +
+ ํƒ€๊นƒ์ด nas์ผ ๋•Œ ํ˜ธ์ถœํ•  NAS ๊ฒฝ๋Ÿ‰ Bridge ์ฃผ์†Œ. +
+
+ +
+ + +
+ NAS Bridge์˜ x-bridge-token. nas ํƒ€๊นƒ์ผ ๋•Œ๋งŒ ์š”์ฒญ ํ—ค๋”์— ์‹ค๋ฆฝ๋‹ˆ๋‹ค. +
diff --git a/media/settings-panel.js b/media/settings-panel.js index 7f8afe3..5196c03 100644 --- a/media/settings-panel.js +++ b/media/settings-panel.js @@ -25,7 +25,10 @@ const cnModelHint = $('cnModelHint'); // ---- Datacollect ---- + const dcBridgeTarget = $('dcBridgeTarget'); const dcBridgeUrl = $('dcBridgeUrl'); + const dcBridgeNasUrl = $('dcBridgeNasUrl'); + const dcBridgeNasToken = $('dcBridgeNasToken'); const dcSavePath = $('dcSavePath'); const dcCrawlDepth = $('dcCrawlDepth'); const dcMaxPages = $('dcMaxPages'); @@ -125,9 +128,18 @@ ); // ---- Datacollect listeners ---- + document.querySelector('[data-save="datacollect.bridgeTarget"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', bridgeTarget: dcBridgeTarget.value }) + ); document.querySelector('[data-save="datacollect.bridgeUrl"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', bridgeUrl: dcBridgeUrl.value }) ); + document.querySelector('[data-save="datacollect.bridgeNasUrl"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', bridgeNasUrl: dcBridgeNasUrl.value }) + ); + document.querySelector('[data-save="datacollect.bridgeNasToken"]').addEventListener('click', () => + vscode.postMessage({ type: 'datacollect.update', bridgeNasToken: dcBridgeNasToken.value }) + ); document.querySelector('[data-save="datacollect.savePath"]').addEventListener('click', () => vscode.postMessage({ type: 'datacollect.update', savePath: dcSavePath.value }) ); @@ -385,7 +397,12 @@ // ---- Datacollect ---- const dc = state.datacollect; if (dc) { + if (dcBridgeTarget && document.activeElement !== dcBridgeTarget && (dc.bridgeTarget === 'local' || dc.bridgeTarget === 'nas')) { + dcBridgeTarget.value = dc.bridgeTarget; + } setIfNotFocused(dcBridgeUrl, dc.bridgeUrl); + setIfNotFocused(dcBridgeNasUrl, dc.bridgeNasUrl); + setIfNotFocused(dcBridgeNasToken, dc.bridgeNasToken); setIfNotFocused(dcSavePath, dc.savePath); setIfNotFocused(dcCrawlDepth, dc.crawlDepth); setIfNotFocused(dcMaxPages, dc.maxPages); diff --git a/package.json b/package.json index f3b6691..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.204", + "version": "2.2.205", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -204,10 +204,26 @@ "default": false, "description": "Enable Multi-Agent Workflow (Planner -> Researcher -> Writer) for complex tasks." }, + "g1nation.datacollectBridgeTarget": { + "type": "string", + "enum": ["local", "nas"], + "default": "local", + "markdownDescription": "Datacollect ๋ฐฑ์—”๋“œ(Bridge)๋ฅผ ์–ด๋””๋กœ ๋ณด๋‚ผ์ง€ ์„ ํƒ. **`local`**(๊ธฐ๋ณธ) = `g1nation.datacollectBridgeUrl`(๋กœ์ปฌ `npm run bridge`). **`nas`** = `g1nation.datacollectBridgeNasUrl`(NAS์˜ ๊ฒฝ๋Ÿ‰ Bridge). `nas`์ธ๋ฐ URL์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ ๋กœ์ปฌ๋กœ ํด๋ฐฑํ•ฉ๋‹ˆ๋‹ค." + }, "g1nation.datacollectBridgeUrl": { "type": "string", "default": "http://127.0.0.1:3002", - "description": "Wiki/Datacollect MCP Bridge URL. /research, /benchmark, /youtube chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project)." + "description": "[local ํƒ€๊นƒ] Wiki/Datacollect MCP Bridge URL. /benchmark, /youtube, /wikify chat slash commands route here. The Bridge must be running (`npm run bridge` in the Datacollect project)." + }, + "g1nation.datacollectBridgeNasUrl": { + "type": "string", + "default": "", + "markdownDescription": "[nas ํƒ€๊นƒ] NAS์—์„œ ๋„๋Š” ๊ฒฝ๋Ÿ‰ Bridge URL (์˜ˆ: `https://your-nas-domain` ๋˜๋Š” `http://nas-ip:3002`). `datacollectBridgeTarget`์„ `nas`๋กœ ๋‘๋ฉด ์—ฌ๊ธฐ๋กœ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋น„์›Œ๋‘๋ฉด ๋กœ์ปฌ๋กœ ํด๋ฐฑ." + }, + "g1nation.datacollectBridgeNasToken": { + "type": "string", + "default": "", + "markdownDescription": "[nas ํƒ€๊นƒ] NAS Bridge๊ฐ€ ์š”๊ตฌํ•˜๋Š” `x-bridge-token` ๊ฐ’(Bridge์˜ `BRIDGE_AUTH_TOKEN`๊ณผ ์ผ์น˜). `nas` ํƒ€๊นƒ์ผ ๋•Œ๋งŒ ์š”์ฒญ ํ—ค๋”์— ์‹ค๋ฆฝ๋‹ˆ๋‹ค. ๋กœ์ปฌ ํƒ€๊นƒ์—๋Š” ์˜ํ–ฅ ์—†์Œ." }, "g1nation.datacollectSavePath": { "type": "string", diff --git a/src/features/datacollect/bridgeClient.ts b/src/features/datacollect/bridgeClient.ts index e891d5a..f268706 100644 --- a/src/features/datacollect/bridgeClient.ts +++ b/src/features/datacollect/bridgeClient.ts @@ -4,19 +4,36 @@ import * as vscode from 'vscode'; * Datacollect (Wiki/Datacollect ํ”„๋กœ์ ํŠธ)์˜ MCP Bridge HTTP ํด๋ผ์ด์–ธํŠธ. * * Bridge๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ณ„๋„๋กœ ๋„์šฐ๋Š” Node Express ์„œ๋ฒ„(`npm run bridge`)์ด๊ณ  - * ๊ธฐ๋ณธ ํฌํŠธ๋Š” 3002. Research(NotebookLM)/Web Benchmark(Playwright)/YouTube - * (yt-dlp+transcript) ๊ฐ™์€ ๋ฌด๊ฑฐ์šด ๊ธฐ๋Šฅ์„ ๋…ธ์ถœํ•˜๋ฏ€๋กœ, Astra๋Š” ์ด endpoint๋ฅผ - * thin client๋กœ ํ˜ธ์ถœ๋งŒ ํ•œ๋‹ค โ€” Playwright/Chrome/NotebookLM-MCP ์˜์กด์„ฑ์„ - * Astra๊ฐ€ ์ง์ ‘ ๋“ค๊ณ  ๊ฐˆ ํ•„์š” ์—†์Œ. + * ๊ธฐ๋ณธ ํฌํŠธ๋Š” 3002. Web Benchmark(Playwright)/YouTube(yt-dlp+transcript)/Wikify + * ๊ฐ™์€ ๋ฌด๊ฑฐ์šด ๊ธฐ๋Šฅ์„ ๋…ธ์ถœํ•˜๋ฏ€๋กœ, Astra๋Š” ์ด endpoint๋ฅผ thin client๋กœ ํ˜ธ์ถœ๋งŒ ํ•œ๋‹ค + * โ€” Playwright/Chrome/Python ์˜์กด์„ฑ์„ Astra๊ฐ€ ์ง์ ‘ ๋“ค๊ณ  ๊ฐˆ ํ•„์š” ์—†์Œ. + * (NotebookLM Deep Research ๋Š” ASTRA ์—์„œ ์ œ๊ฑฐ โ€” ๋กœ์ปฌ Datacollect ์•ฑ ์ „์šฉ.) * - * URL์€ `astra.datacollectBridgeUrl` VS Code ์„ค์ •์œผ๋กœ override ๊ฐ€๋Šฅ, ๊ธฐ๋ณธ๊ฐ’ - * `http://127.0.0.1:3002`. ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค๋ฅธ ๋จธ์‹ /ํฌํŠธ์—์„œ ๋„์šฐ๋ฉด ๊ทธ์ชฝ์œผ๋กœ ๊ฐ€๊ฒŒ. + * ํƒ€๊นƒ์€ `g1nation.datacollectBridgeTarget`(`local`|`nas`)์œผ๋กœ ์ „ํ™˜ํ•œ๋‹ค. + * - local(๊ธฐ๋ณธ): `g1nation.datacollectBridgeUrl` (๊ธฐ๋ณธ `http://127.0.0.1:3002`) + * - nas: `g1nation.datacollectBridgeNasUrl` (+ `datacollectBridgeNasToken` ํ—ค๋”) + * nas ์ธ๋ฐ URL ์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์•ˆ์ „ํ•˜๊ฒŒ local ๋กœ ํด๋ฐฑํ•œ๋‹ค(์ ˆ๋Œ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ). */ export function getBridgeBaseUrl(): string { - const raw = vscode.workspace.getConfiguration('g1nation').get('datacollectBridgeUrl'); - const url = (raw && raw.trim()) || 'http://127.0.0.1:3002'; - return url.replace(/\/$/, ''); + const cfg = vscode.workspace.getConfiguration('g1nation'); + const localUrl = (cfg.get('datacollectBridgeUrl')?.trim()) || 'http://127.0.0.1:3002'; + if (cfg.get('datacollectBridgeTarget', 'local') === 'nas') { + const nasUrl = cfg.get('datacollectBridgeNasUrl')?.trim(); + if (nasUrl) return nasUrl.replace(/\/$/, ''); + // nas ์„ ํƒํ–ˆ์œผ๋‚˜ URL ๋ฏธ์„ค์ • โ†’ ๋กœ์ปฌ๋กœ ํด๋ฐฑ (๊ตฌ๋™ ๋Š๊ธฐ์ง€ ์•Š๊ฒŒ). + } + return localUrl.replace(/\/$/, ''); +} + +/** + * nas ํƒ€๊นƒ์ผ ๋•Œ NAS Bridge ์˜ `x-bridge-token` ๊ฐ’. local ์ด๊ฑฐ๋‚˜ ๋ฏธ์„ค์ •์ด๋ฉด ''. + * bridgeFetch ๊ฐ€ ์ด ๊ฐ’์„ ์š”์ฒญ ํ—ค๋”์— ์‹ค์–ด ๋ณด๋‚ธ๋‹ค(๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ํ—ค๋” ๋ฏธ๋ถ€์ฐฉ). + */ +export function getBridgeAuthToken(): string { + const cfg = vscode.workspace.getConfiguration('g1nation'); + if (cfg.get('datacollectBridgeTarget', 'local') !== 'nas') return ''; + return (cfg.get('datacollectBridgeNasToken')?.trim()) || ''; } /** @@ -26,19 +43,13 @@ export function getBridgeBaseUrl(): string { * ๋ฐ”๋€Œ๋ฉด 5+ ๊ณณ์„ ๋™์‹œ ์ˆ˜์ •. ์ด ๊ฐ์ฒด๋กœ ๋ชจ์•„ ํ•œ ๊ณณ๋งŒ ๋ฐ”๊พธ๋ฉด ๋จ. * * ์นดํ…Œ๊ณ ๋ฆฌ: - * - research: NotebookLM Deep Research ์›Œํฌํ”Œ๋กœ * - youtube: yt-dlp + youtube-transcript-api ๊ธฐ๋ฐ˜ ์˜์ƒ ๋ถ„์„ * - web: Playwright ๊ธฐ๋ฐ˜ ์›น ํŽ˜์ด์ง€ ์ถ”์ถœยท๋ฒค์น˜๋งˆํฌ * - wiki: ์ƒ์„ฑ๋œ ์œ„ํ‚ค ๋ฌธ์„œ ๋””์Šคํฌ ์ €์žฅ * - lm: ์‚ฌ์šฉ์ž์˜ LM Studio / Ollama ๋กœ LLM ํ˜ธ์ถœ ํ”„๋ก์‹œ */ export const BRIDGE_API = { - research: { - start: '/api/research/start', - status: '/api/research/status', - import: '/api/research/import', - synthesize: '/api/research/synthesize', - }, + // research(NotebookLM)๋Š” ASTRA ์—์„œ ์ œ๊ฑฐ๋จ(v2.2.205) โ€” ๋กœ์ปฌ Datacollect ์•ฑ ์ „์šฉ. youtube: { extract: '/api/youtube/extract', }, @@ -104,11 +115,13 @@ export async function bridgeFetch( } try { + const token = getBridgeAuthToken(); const res = await fetch(url, { ...init, signal: controller.signal, headers: { 'Content-Type': 'application/json', + ...(token ? { 'x-bridge-token': token } : {}), ...(init?.headers || {}), }, }); diff --git a/src/features/datacollect/handlers.ts b/src/features/datacollect/handlers.ts index 60ad61c..cf41592 100644 --- a/src/features/datacollect/handlers.ts +++ b/src/features/datacollect/handlers.ts @@ -1,5 +1,6 @@ /** - * Datacollect handlers โ€” /research ยท /benchmark ยท /youtube ยท /blog ยท /wikify ยท /meet. + * Datacollect handlers โ€” /benchmark ยท /youtube ยท /blog ยท /wikify ยท /meet. + * (/research(NotebookLM)๋Š” v2.2.205 ์—์„œ ์ œ๊ฑฐ โ€” ๋กœ์ปฌ Datacollect ์•ฑ ์ „์šฉ์œผ๋กœ ๋ถ„๋ฆฌ) * * v2.2.201 ์—์„œ slashRouter.ts ์—์„œ ๋ถ„๋ฆฌ. Datacollect bridge (port 3002) ํ†ตํ•ฉ * ์Šฌ๋ž˜์‹œ ๋ช…๋ น ํด๋Ÿฌ์Šคํ„ฐ โ€” NotebookLM Deep Research ยท Playwright ์›น ์Šค์บ” ยท @@ -33,100 +34,6 @@ import { parseActionItems, } from './scheduling/calendarHelpers'; -// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /research โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -async function runResearch(topic: string, view: Webview | undefined): Promise { - if (!topic) { - chunk(view, `์‚ฌ์šฉ๋ฒ•: \`/research <์ฃผ์ œ>\`\n์ฃผ์ œ๋ฅผ ์ž…๋ ฅํ•ด NotebookLM Deep Research๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.\n`); - return true; - } - - chunk(view, `๐Ÿš€ **Research ์‹œ์ž‘**: \`${topic}\`\n\n`); - const start = await bridgeFetch<{ success: boolean; notebookId: string; taskId: string }>( - BRIDGE_API.research.start, - { method: 'POST', body: JSON.stringify({ topic }) }, - { timeoutMs: 60_000 }, - ); - chunk(view, `- notebookId: \`${start.notebookId}\`\n- taskId: \`${start.taskId}\`\n\nโณ ์ƒํƒœ polling (5์ดˆ ๊ฐ„๊ฒฉ, ์ตœ๋Œ€ 10๋ถ„)โ€ฆ\n`); - - const deadline = Date.now() + 10 * 60_000; - const HEARTBEAT_MS = 30_000; - const MAX_CONSECUTIVE_FAILS = 5; - const COMPLETED_SET = new Set(['completed', 'done', 'success', 'finished']); - const FAILED_SET = new Set(['failed', 'error', 'cancelled', 'canceled', 'aborted']); - - let lastStatus = ''; - let lastChangeAt = Date.now(); - let consecutiveFails = 0; - let pollCount = 0; - let researchOk = false; - while (Date.now() < deadline) { - await new Promise(r => setTimeout(r, 5_000)); - pollCount++; - let st: { success: boolean; result: any } | undefined; - try { - st = await bridgeFetch<{ success: boolean; result: any }>( - `${BRIDGE_API.research.status}?notebookId=${encodeURIComponent(start.notebookId)}&taskId=${encodeURIComponent(start.taskId)}`, - { method: 'GET' }, - { timeoutMs: 60_000 }, - ); - consecutiveFails = 0; - } catch (e: any) { - consecutiveFails++; - if (consecutiveFails >= MAX_CONSECUTIVE_FAILS) { - chunk(view, `\nโŒ Status polling ์—ฐ์† ์‹คํŒจ ${consecutiveFails}ํšŒ โ€” bridge ๊ฐ€ ์‘๋‹ตํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.\n(์›์ธ: ${e?.message || String(e)})\n`); - return true; - } - chunk(view, `\n ยท status ํ˜ธ์ถœ ์‹คํŒจ ${consecutiveFails}/${MAX_CONSECUTIVE_FAILS} (${e?.message || 'unknown'})\n`); - continue; - } - const status = String(st.result?.status || st.result || '').trim().toLowerCase(); - if (status && status !== lastStatus) { - chunk(view, ` ยท ${status}\n`); - lastStatus = status; - lastChangeAt = Date.now(); - } else if (Date.now() - lastChangeAt > HEARTBEAT_MS) { - chunk(view, ` ยท โณ ๋Œ€๊ธฐ ์ค‘ (${Math.round((Date.now() - lastChangeAt) / 1000)}s, ํด๋ง ${pollCount}ํšŒ)\n`); - lastChangeAt = Date.now(); - } - if (COMPLETED_SET.has(status)) { researchOk = true; break; } - if (FAILED_SET.has(status)) { - chunk(view, `\nโŒ Research ์‹คํŒจ: ${JSON.stringify(st.result).slice(0, 400)}\n`); - return true; - } - } - - if (!researchOk) { - chunk(view, `\nโŒ 10๋ถ„ polling ํ›„์—๋„ ์™„๋ฃŒ ์‹ ํ˜ธ๊ฐ€ ์˜ค์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค (๋งˆ์ง€๋ง‰ status: \`${lastStatus || '(์—†์Œ)'}\`). ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.\n`); - return true; - } - - chunk(view, `\n๐Ÿ“ฅ importโ€ฆ\n`); - await bridgeFetch(BRIDGE_API.research.import, { - method: 'POST', - body: JSON.stringify({ notebookId: start.notebookId, taskId: start.taskId }), - }, { - timeoutMs: 300_000, - onHeartbeat: (elapsedMs) => chunk(view, ` ยท import ์ง„ํ–‰ ์ค‘ (${Math.round(elapsedMs / 1000)}s)\n`), - }); - - chunk(view, `๐Ÿงช synthesizeโ€ฆ\n\n`); - const synth = await bridgeFetch<{ success: boolean; markdown?: string; result?: string }>( - BRIDGE_API.research.synthesize, - { - method: 'POST', - body: JSON.stringify({ notebookId: start.notebookId, topic, rootTopic: topic, includeKnowledgeConnections: true }), - }, - { - timeoutMs: 600_000, - onHeartbeat: (elapsedMs) => chunk(view, ` ยท synthesize LLM ์ž‘์—… ์ค‘ (${Math.round(elapsedMs / 1000)}s)\n`), - }, - ); - const md = synth.markdown || synth.result || '(๋นˆ ์‘๋‹ต)'; - chunk(view, `---\n\n${md}\n`); - return true; -} - // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ /benchmark โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async function runBenchmark(arg: string, view: Webview | undefined): Promise { @@ -749,7 +656,9 @@ async function runMeet(arg: string, view: Webview | undefined, context?: vscode. // โ”€โ”€โ”€ ๋“ฑ๋ก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -registerSlashCommand({ name: '/research', description: 'NotebookLM Deep Research ํ˜ธ์ถœ ํ›„ ์œ„ํ‚ค ํ•ฉ์„ฑ', handler: runResearch }); +// /research(NotebookLM Deep Research)๋Š” v2.2.205 ์—์„œ ์ œ๊ฑฐ โ€” NotebookLM ์€ ๋กœ์ปฌ +// Datacollect ์•ฑ ์ „์šฉ์œผ๋กœ ๋ถ„๋ฆฌ(Chrome/Google ๋กœ๊ทธ์ธ ์˜์กด). ASTRA ๋ฐฑ์—”๋“œ๋Š” NAS ๊ฒฝ๋Ÿ‰ +// Bridge ๋กœ ์šด์˜ ๊ฐ€๋Šฅํ•ด์•ผ ํ•˜๋ฏ€๋กœ brower-auth ๊ฐ€ ํ•„์š”ํ•œ ๋ช…๋ น์€ ๋‘์ง€ ์•Š๋Š”๋‹ค. registerSlashCommand({ name: '/benchmark', description: 'Playwright ์›น ๋ฒค์น˜๋งˆํฌ + 4-๋ Œ์ฆˆ LLM ๋ถ„์„', handler: runBenchmark }); registerSlashCommand({ name: '/youtube', description: 'YouTube ๋‹จ์ผ ์˜์ƒ ๋˜๋Š” ์ฑ„๋„/ํ”Œ๋ ˆ์ด๋ฆฌ์ŠคํŠธ ๋ถ„์„', handler: runYoutube }); registerSlashCommand({ name: '/blog', description: 'Blog Pipeline ์•ˆ๋‚ด (Datacollect ๋ณ„๋„ ํ๋ฆ„)', handler: runBlog }); diff --git a/src/features/datacollect/prompts/meetPrompt.ts b/src/features/datacollect/prompts/meetPrompt.ts index 0bc280f..3465f88 100644 --- a/src/features/datacollect/prompts/meetPrompt.ts +++ b/src/features/datacollect/prompts/meetPrompt.ts @@ -6,8 +6,9 @@ export function buildMeetPrompt(transcript: string, metadata: string): string { const metaBlock = metadata.trim() || '(๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฏธ์ž…๋ ฅ โ€” ๋…น์ทจ๋ก ๋‚ด์šฉ์—์„œ ์ถ”๋ก ํ•˜๊ฑฐ๋‚˜ "ํ™•์ธ ๋ถˆ๊ฐ€"๋กœ ํ‘œ๊ธฐ)'; return `# ์ž„๋ฌด (Objective) -์ œ๊ณต๋œ ํšŒ์˜ ๋…น์ทจ ํ…์ŠคํŠธ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ, ์™ธ๋ถ€ ์ง€์‹ ์—†์ด ์‚ฌ์‹ค ๊ธฐ๋ฐ˜์˜ -๊ตฌ์กฐํ™”๋œ ํšŒ์˜๋ก(Actionable Minutes)์„ ์ƒ์„ฑํ•œ๋‹ค. +์ œ๊ณต๋œ ํšŒ์˜ ๋…น์ทจ ํ…์ŠคํŠธ์™€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ, ์‚ฌ์‹ค ๊ธฐ๋ฐ˜์˜ ๊ตฌ์กฐํ™”๋œ +ํšŒ์˜๋ก(Actionable Minutes)์„ ์ƒ์„ฑํ•œ๋‹ค. ์™ธ๋ถ€/๋„๋ฉ”์ธ ์ง€์‹์€ *STT ์˜คํƒ€ ๋ณด์ •๊ณผ ์šฉ์–ด +ํ•ด์„*์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , *๋…น์ทจ๋ก์— ์—†๋Š” ์ƒˆ๋กœ์šด ์‚ฌ์‹ค์„ ์ถ”๊ฐ€*ํ•˜๋Š” ๋ฐ๋Š” ์ ˆ๋Œ€ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. # ์—ญํ•  (Role) - Fact Extractor: ๋…น์ทจ๋ก์— ๋ช…์‹œ์ ์œผ๋กœ ์กด์žฌํ•˜๋Š” ์‚ฌ์‹ค๋งŒ ์ถ”์ถœ @@ -19,6 +20,14 @@ export function buildMeetPrompt(transcript: string, metadata: string): string { # ๋ฐ์ดํ„ฐ ์šฐ์„ ์ˆœ์œ„ (Data Priority) 1์ˆœ์œ„: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ / 2์ˆœ์œ„: ๋…น์ทจ๋ก ๋‚ด์šฉ. ์ถฉ๋Œ ์‹œ ๋ฐ˜๋“œ์‹œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. +# STT ์˜คํƒ€ ๋ณด์ • (Transcription Noise Handling โ€” ์ด ๋…น์ทจ๋ก์€ ์Œ์„ฑโ†’ํ…์ŠคํŠธ ๋ณ€ํ™˜๋ฌผ์ด๋ผ ์˜คํƒ€๊ฐ€ ๋งŽ๋‹ค) +- ๋ฐœ์Œ์ด ์œ ์‚ฌํ•œ ๋‹จ์–ด๊ฐ€ ์ž˜๋ชป ํ‘œ๊ธฐ๋ผ ์žˆ๋‹ค(์˜ˆ: "Dovrunner"โ†’"Doverunner", "ํŽ˜์–ดํ”Œ๋ ˆ์ด"โ†’"ํŽ˜์–ดํ”Œ๋ž˜์ด"). **ํ•œ ๋‹จ์–ด์˜ ์ฒ ์ž์— ์ง‘์ฐฉํ•˜์ง€ ๋ง๊ณ  ์ฃผ๋ณ€ ๋ฌธ๋งฅ(์•ž๋’ค ํ‚ค์›Œ๋“œ)์œผ๋กœ ์˜๋ฏธ๋ฅผ ๋ณต์›ํ•˜๋ผ.** +- ๋ฐœ์Œ์ด ์œ ์‚ฌํ•œ ๋ช…๋ฐฑํ•œ ์˜คํƒ€๋Š” ๋ฌธ๋งฅ์ƒ ๋งž๋Š” ๊ธฐ์ˆ  ์šฉ์–ดยท๊ณ ์œ ๋ช…์‚ฌ๋กœ **์ •๊ทœํ™”**ํ•˜๋ผ. ํ”ํ•œ ๊ธฐ์ˆ  ์šฉ์–ด(DRM, SDK, ๋”ฅ๋งํฌ, API, ๋ Œ๋”๋ง, ํŽ˜์–ดํ”Œ๋ ˆ์ด, ์•”ํ˜ธํ™” ๋“ฑ)๋Š” ๋„๋ฉ”์ธ ์ง€์‹์œผ๋กœ ๋ณด์ •ํ•ด๋„ ๋œ๋‹ค. +- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ธ๋ช…ยท๊ธฐ์—…๋ช…ยท์ œํ’ˆ๋ช…ยท์šฉ์–ด๊ฐ€ ์ฃผ์–ด์กŒ์œผ๋ฉด ๊ทธ๊ฒƒ์„ **์ •๋‹ต ํ‘œ๊ธฐ**๋กœ ๋ณด๊ณ , ๋…น์ทจ๋ก์˜ ์œ ์‚ฌ ์˜คํƒ€๋ฅผ ๊ทธ ํ‘œ๊ธฐ๋กœ ๋งž์ถ˜๋‹ค(๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์‚ฌ์‹ค์ƒ ์šฉ์–ด์ง‘ ์—ญํ• ). +- **ํ•ต์‹ฌ ๊ตฌ๋ถ„ (์ ˆ๋Œ€ ํ˜ผ๋™ ๊ธˆ์ง€)**: '๋…น์ทจ๋ก์— *์žˆ๋Š”* ๋‹จ์–ด์˜ ์ฒ ์ž๋ฅผ ๋ฌธ๋งฅ์œผ๋กœ ๋ฐ”๋กœ์žก๋Š” ๊ฒƒ'์€ ํ—ˆ์šฉยท๊ถŒ์žฅ๋œ๋‹ค. '๋…น์ทจ๋ก์— *์—†๋Š”* ์‚ฌ์‹ค(์ˆ˜์น˜ยท๊ฒฐ์ •ยท์—†๋˜ ํ•ญ๋ชฉ)์„ ์ง€์–ด๋‚ด๋Š” ๊ฒƒ'์€ ๊ธˆ์ง€๋‹ค. **์ฒ ์ž ๋ณด์ • โ‰  ์‚ฌ์‹ค ๋‚ ์กฐ.** +- ์ฒ ์ž๊ฐ€ ํ‹€๋ ค๋„ ๋ฌธ๋งฅ์ƒ ์˜๋ฏธ๊ฐ€ ๋ถ„๋ช…ํ•˜๋ฉด ๊ทธ ์˜๋ฏธ๋ฅผ ํ™•์ •๋œ ๊ฒƒ์œผ๋กœ ๋‹ค๋ค„๋ผ โ€” **์˜คํƒ€ ํ•˜๋‚˜ ๋•Œ๋ฌธ์— ๋ฉ€์ฉกํ•œ ๋‚ด์šฉ ์ „์ฒด๋ฅผ "ํ™•์ธ ๋ถˆ๊ฐ€"๋กœ ๋ง‰์ง€ ๋ง ๊ฒƒ.** +- ์ •๊ทœํ™”๋Š” ํ–ˆ์ง€๋งŒ ๋ฌธ๋งฅ์œผ๋กœ๋„ ์ •์ฒด๊ฐ€ ๋๋‚ด ๋ชจํ˜ธํ•œ ์šฉ์–ด์— ํ•œํ•ด, ์ •๊ทœํ™” ํ‘œ๊ธฐ ์˜†์— ์›๋ฌธ์„ ํ•จ๊ป˜ ๋‚จ๊ธด๋‹ค: ์˜ˆ) \`Doverunner(์›๋ฌธ: "Dovrunner", ํ‘œ๊ธฐ ๋ถˆํ™•์‹ค)\`. + # ์ฒ˜๋ฆฌ ์ ˆ์ฐจ (Processing Flow) 1. Speaker Tracking โ€” ๋ฐœ์–ธ์ž ID/์ด๋ฆ„์„ ๋๊นŒ์ง€ ์œ ์ง€ํ•œ๋‹ค. **๋ˆ„๊ฐ€ ํ•œ ๋ง์ธ์ง€๋ฅผ ์ ˆ๋Œ€ ์ž„์˜๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ํ•ฉ์น˜์ง€ ๋ง ๊ฒƒ.** 2. Topic Reclustering โ€” ์ด ๋…น์ทจ๋ก์€ ๋น„์„ ํ˜•์ด๋‹ค(Aโ†’Bโ†’Zโ†’๋‹ค์‹œ A ์‹์œผ๋กœ ์ฃผ์ œ๊ฐ€ ํŠ„๋‹ค). ๋…น์ทจ๋ก ์ „์ฒด๋ฅผ ํ›‘์–ด **ํฉ์–ด์ง„ ๋ฐœ์–ธ์„ ์ฃผ์ œ๋ณ„๋กœ ๋‹ค์‹œ ๋ฌถ์€ ๋’ค** ์ •๋ฆฌํ•œ๋‹ค. ๋…น์ทจ๋ก์ƒ ์•ž๋’ค๋กœ ๋ถ™์–ด ์žˆ๋‹ค๋Š” ์ด์œ ๋งŒ์œผ๋กœ ๋‘ ๋ฐœ์–ธ์„ ์ธ๊ณผยท์—ฐ๊ฒฐ ๊ด€๊ณ„๋กœ ์—ฎ์ง€ ๋ง ๊ฒƒ(**์ธ์ ‘ โ‰  ์—ฐ๊ฒฐ**). @@ -34,8 +43,8 @@ export function buildMeetPrompt(transcript: string, metadata: string): string { # ๊ทผ๊ฑฐยท์ •ํ™•์„ฑ ๊ทœ์น™ (Grounding Rules โ€” ๋ฐ˜๋“œ์‹œ ์ค€์ˆ˜, ํ• ๋ฃจ์‹œ๋„ค์ด์…˜ ๋ฐฉ์ง€) - **๋…น์ทจ๋ก์— ๋ช…์‹œ๋œ ๋‚ด์šฉ๋งŒ ์ ๋Š”๋‹ค.** ์ถ”๋ก ์œผ๋กœ ๋นˆ์นธ์„ ๋ฉ”์šฐ๊ฑฐ๋‚˜, ๋ณ„๊ฐœ์˜ ๋ฐœ์–ธ์„ ํ•˜๋‚˜์˜ ์ธ๊ณผ ์‚ฌ์Šฌ๋กœ ํ•ฉ์„ฑํ•˜์ง€ ๋ง ๊ฒƒ. - **๋ฐœ์–ธ ์ฃผ์ฒด๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•˜๋ฉด ์ถ”์ธกํ•˜์ง€ ๋ง ๊ฒƒ.** ๋ˆ„๊ฐ€ ๋งํ–ˆ๋Š”์ง€ ํ™•์‹คํ•˜์ง€ ์•Š์œผ๋ฉด ์ด๋ฆ„์„ ๋ถ™์ด์ง€ ๋ง๊ณ  "(๋ฐœ์–ธ ์ฃผ์ฒด ๋ถˆ๋ช…ํ™•)"์œผ๋กœ ํ‘œ๊ธฐํ•˜๊ฑฐ๋‚˜ "~๋ผ๋Š” ์˜๊ฒฌ์ด ์ œ์‹œ๋จ"์ฒ˜๋Ÿผ ์ฃผ์ฒด ์—†์ด ์ค‘๋ฆฝ์ ์œผ๋กœ ์„œ์ˆ ํ•œ๋‹ค. ์–ด๋–ค ๋ฐœ์–ธ์ž์˜ ๋ง์„ ๋‹ค๋ฅธ ๋ฐœ์–ธ์ž์˜ ๊ฒฐ๋ก ์œผ๋กœ ์˜ฎ๊ธฐ๋Š” ๊ฒƒ์€ ๊ฐ€์žฅ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜๋‹ค. -- **๋…น์ทจ๋ก์— ์—†๋Š” ์ˆซ์žยท๋‚ ์งœยท๊ธˆ์•กยท๊ณ ์œ ๋ช…์‚ฌยท์ œํ’ˆ๋ช…์„ ๋งŒ๋“ค์–ด๋‚ด์ง€ ๋ง ๊ฒƒ.** ๋ถˆํ™•์‹คํ•˜๋ฉด "ํ™•์ธ ํ•„์š”"๋กœ ๋‘”๋‹ค. -- ์–ด๋–ค ํ•ญ๋ชฉ์˜ ๊ทผ๊ฑฐ๊ฐ€ ๋…น์ทจ๋ก์—์„œ ์•ฝํ•˜๊ฑฐ๋‚˜ ๋ชจํ˜ธํ•˜๋ฉด, ์ง€์–ด๋‚ด์ง€ ๋ง๊ณ  ํ•ด๋‹น ํ•ญ๋ชฉ ๋์— "(ํ™•์ธ ํ•„์š”)"๋ฅผ ๋ถ™์ธ๋‹ค. +- **๋…น์ทจ๋ก์— ์—†๋Š” ์ˆซ์žยท๋‚ ์งœยท๊ธˆ์•กยท๊ฒฐ์ •ยท์—†๋˜ ํ•ญ๋ชฉ์„ ๋งŒ๋“ค์–ด๋‚ด์ง€ ๋ง ๊ฒƒ.** (๋‹จ, ๋…น์ทจ๋ก์— *์žˆ๋Š”๋ฐ ์ฒ ์ž๋งŒ ํ‹€๋ฆฐ* ์šฉ์–ดยท๊ณ ์œ ๋ช…์‚ฌ๋ฅผ ๋ฌธ๋งฅ์œผ๋กœ ์ •๊ทœํ™”ํ•˜๋Š” ๊ฒƒ์€ ํ—ˆ์šฉ โ€” ์œ„ 'STT ์˜คํƒ€ ๋ณด์ •' ์ฐธ์กฐ.) ์ •๋ง ๊ทผ๊ฑฐ ์—†๋Š” *์‚ฌ์‹ค*๋งŒ "ํ™•์ธ ํ•„์š”"๋กœ ๋‘”๋‹ค. +- ์–ด๋–ค ํ•ญ๋ชฉ์˜ *๋‚ด์šฉ ์ž์ฒด*๊ฐ€ ๋…น์ทจ๋ก์—์„œ ์•ฝํ•˜๊ฑฐ๋‚˜ ๋ชจํ˜ธํ•˜๋ฉด(=์ฒ ์ž ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ์‚ฌ์‹ค์ด ๋ถˆํ™•์‹ค) ์ง€์–ด๋‚ด์ง€ ๋ง๊ณ  ํ•ด๋‹น ํ•ญ๋ชฉ ๋์— "(ํ™•์ธ ํ•„์š”)"๋ฅผ ๋ถ™์ธ๋‹ค. ๋‹จ์ˆœ ํ‘œ๊ธฐ ์˜คํƒ€๋Š” ์—ฌ๊ธฐ ํ•ด๋‹นํ•˜์ง€ ์•Š๋Š”๋‹ค. - Decision์€ ๋ช…์‹œ์  ํ•ฉ์˜ ํ‘œํ˜„์ด ์žˆ์„ ๋•Œ๋งŒ '๊ฒฐ์ •๋จ'์ด๋‹ค. ํ•ฉ์˜๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•˜๋ฉด '๋…ผ์˜ ์ค‘' ๋˜๋Š” ์˜คํ”ˆ ์ด์Šˆ๋กœ ๋‘”๋‹ค. # ์ถœ๋ ฅ ๊ฒ€์ฆ (Validation) diff --git a/src/features/settings/settingsPanelProvider.ts b/src/features/settings/settingsPanelProvider.ts index 7fb80a9..8d402d4 100644 --- a/src/features/settings/settingsPanelProvider.ts +++ b/src/features/settings/settingsPanelProvider.ts @@ -88,7 +88,13 @@ interface SettingsState { polishPersonaOverride: string; }; datacollect: { + /** 'local' | 'nas' โ€” ์–ด๋А Bridge ์ธ์Šคํ„ด์Šค๋ฅผ ํ˜ธ์ถœํ• ์ง€. */ + bridgeTarget: string; bridgeUrl: string; + /** NAS ๊ฒฝ๋Ÿ‰ Bridge URL (nas ํƒ€๊นƒ์ผ ๋•Œ). */ + bridgeNasUrl: string; + /** NAS Bridge ์˜ x-bridge-token (nas ํƒ€๊นƒ์ผ ๋•Œ ํ—ค๋”๋กœ ์ „์†ก). */ + bridgeNasToken: string; /** Empty โ†’ results saved to the Bridge's WIKI_RAW_PATH default. */ savePath: string; crawlDepth: number; @@ -605,9 +611,19 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { // savePath ๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด Bridge ์˜ WIKI_RAW_PATH ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์ €์žฅ ์œ„์น˜๋ฅผ ๊ฒฐ์ •ํ•œ๋‹ค. private async _handleDatacollectUpdate(msg: any): Promise { + if (typeof msg.bridgeTarget === 'string') { + const t = msg.bridgeTarget.trim() === 'nas' ? 'nas' : 'local'; + await this._safeConfigUpdate('datacollectBridgeTarget', t); + } if (typeof msg.bridgeUrl === 'string') { await this._safeConfigUpdate('datacollectBridgeUrl', msg.bridgeUrl.trim()); } + if (typeof msg.bridgeNasUrl === 'string') { + await this._safeConfigUpdate('datacollectBridgeNasUrl', msg.bridgeNasUrl.trim()); + } + if (typeof msg.bridgeNasToken === 'string') { + await this._safeConfigUpdate('datacollectBridgeNasToken', msg.bridgeNasToken.trim()); + } if (typeof msg.savePath === 'string') { await this._safeConfigUpdate('datacollectSavePath', msg.savePath.trim()); } @@ -675,7 +691,10 @@ export class SettingsPanelProvider implements vscode.WebviewViewProvider { polishPersonaOverride: cfg.get('polishPersonaOverride', '') ?? '', }, datacollect: { + bridgeTarget: cfg.get('datacollectBridgeTarget', 'local') || 'local', bridgeUrl: cfg.get('datacollectBridgeUrl', '') || '', + bridgeNasUrl: cfg.get('datacollectBridgeNasUrl', '') || '', + bridgeNasToken: cfg.get('datacollectBridgeNasToken', '') || '', savePath: cfg.get('datacollectSavePath', '') || '', crawlDepth: cfg.get('datacollectCrawlDepth', 1) ?? 1, maxPages: cfg.get('datacollectMaxPages', 8) ?? 8, diff --git a/src/features/setup/datacollectSetup.ts b/src/features/setup/datacollectSetup.ts index ee08b87..06f28eb 100644 --- a/src/features/setup/datacollectSetup.ts +++ b/src/features/setup/datacollectSetup.ts @@ -165,7 +165,7 @@ export async function runDatacollectSetup(): Promise { if (after.missingPackages.length === 0) { output.appendLine('\nโœ… ์„ค์น˜ ํ›„ import ๊ฒ€์ฆ ํ†ต๊ณผ. Datacollect ์Šฌ๋ž˜์‹œ ๋ช…๋ น์„ ๋ฐ”๋กœ ์“ธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.'); vscode.window.showInformationMessage( - `Astra Setup ์™„๋ฃŒ: ${probe.missingPackages.join(', ')} ์„ค์น˜๋จ. /youtube /research ๋“ฑ ๋‹ค์‹œ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”.`, + `Astra Setup ์™„๋ฃŒ: ${probe.missingPackages.join(', ')} ์„ค์น˜๋จ. /youtube ๋“ฑ ๋‹ค์‹œ ์‹œ๋„ํ•ด ๋ณด์„ธ์š”.`, ); } else { output.appendLine(`\nโš ๏ธ pip ์€ ์„ฑ๊ณต์œผ๋กœ ๋๋‚ฌ์ง€๋งŒ import ๊ฒ€์ฆ์—์„œ ์—ฌ์ „ํžˆ ${after.missingPackages.join(', ')} ๊ฐ€ ์•ˆ ๋ณด์ž…๋‹ˆ๋‹ค.`); diff --git a/src/features/system/handlers.ts b/src/features/system/handlers.ts index b99a217..1f14299 100644 --- a/src/features/system/handlers.ts +++ b/src/features/system/handlers.ts @@ -311,8 +311,8 @@ const HELP_CATEGORIES: HelpCategory[] = [ { title: '๋ฆฌ์„œ์น˜ยท๋ถ„์„', emoji: '๐Ÿ”ฌ', - match: (n) => ['/research', '/benchmark', '/youtube', '/blog', '/wikify', '/meet'].includes(n), - blurb: 'Datacollect bridge ํ†ตํ•ฉ โ€” Deep Research / ์›น ๋ฒค์น˜๋งˆํฌ / YouTube 4-๋ Œ์ฆˆ / Blog ํŒŒ์ดํ”„๋ผ์ธ / Wikify / ํšŒ์˜๋ก', + match: (n) => ['/benchmark', '/youtube', '/blog', '/wikify', '/meet'].includes(n), + blurb: 'Datacollect bridge ํ†ตํ•ฉ โ€” ์›น ๋ฒค์น˜๋งˆํฌ / YouTube 4-๋ Œ์ฆˆ / Blog ํŒŒ์ดํ”„๋ผ์ธ / Wikify / ํšŒ์˜๋ก (NotebookLM Deep Research ๋Š” ๋กœ์ปฌ Datacollect ์•ฑ์œผ๋กœ ๋ถ„๋ฆฌ)', }, { title: '์‹œ์Šคํ…œยท๋ฉ”๋ชจ๋ฆฌ', diff --git a/src/memory/LongTermMemory.ts b/src/memory/LongTermMemory.ts index 77e8f21..083a77b 100644 --- a/src/memory/LongTermMemory.ts +++ b/src/memory/LongTermMemory.ts @@ -169,6 +169,12 @@ export class LongTermMemory { .slice(0, 5); if (alwaysInclude.length === 0) return null; + // ํ‘œ์‹œ๋˜๋Š”(=์‚ฌ์šฉ๋˜๋Š”) ์ž๋™ ์ถ”์ถœ ํ•ญ๋ชฉ์˜ ๋งŒ๋ฃŒ๋ฅผ ์—ฐ์žฅ. + const refreshAt = Date.now() + LongTermMemory.AUTO_EXTRACT_TTL_MS; + for (const e of alwaysInclude) { + if (e.expiresAt) { e.expiresAt = refreshAt; this.dirty = true; } + } + const content = alwaysInclude .map((e) => `- [${e.category}] ${e.content}`) .join('\n'); @@ -181,10 +187,13 @@ export class LongTermMemory { }; } - // Mark as referenced + // Mark as referenced โ€” ์ž๋™ ์ถ”์ถœ(๋งŒ๋ฃŒ ์žˆ์Œ) ํ•ญ๋ชฉ์€ ์ฐธ์กฐ ์‹œ ๋งŒ๋ฃŒ๋ฅผ ์Šฌ๋ผ์ด๋”ฉ ์—ฐ์žฅํ•ด + // '์“ฐ๋ฉด ์‚ด์•„๋‚จ๊ณ , ์•ˆ ์“ฐ๋ฉด TTL ๋’ค ์†Œ๋ฉธ'. ์˜์†(์ˆ˜๋™) ํ•ญ๋ชฉ์€ expiresAt ์ด ์—†์–ด ๋ฌด์˜ํ–ฅ. + const refreshAt = Date.now() + LongTermMemory.AUTO_EXTRACT_TTL_MS; for (const { entry } of relevant) { entry.lastReferencedAt = Date.now(); entry.referenceCount++; + if (entry.expiresAt) entry.expiresAt = refreshAt; } this.dirty = true; @@ -202,6 +211,34 @@ export class LongTermMemory { // โ”€โ”€โ”€ Extraction Helpers โ”€โ”€โ”€ + /** ์ž๋™ ์ถ”์ถœ ์žฅ๊ธฐ๊ธฐ์–ต ๊ธฐ๋ณธ TTL (14์ผ). ์ฐธ์กฐ๋  ๋•Œ๋งˆ๋‹ค ์Šฌ๋ผ์ด๋”ฉ ์—ฐ์žฅ๋œ๋‹ค. */ + public static readonly AUTO_EXTRACT_TTL_MS = 14 * 24 * 60 * 60 * 1000; + + /** ์งง์€ ํ›„๋ณด ๋ฌธ์ž์—ด์— ๋ฐ•ํžŒ ๊ตฌ์ฒด์  ์—๋Ÿฌ ์‹œ๊ทธ๋‹ˆ์ฒ˜(์˜ˆ์™ธ๋ช…/์—๋Ÿฌ์ฝ”๋“œ/์Šคํƒ ์กฐ๊ฐ) ํƒ์ง€. */ + private static readonly ERROR_NOISE = /\b(?:TypeError|ReferenceError|SyntaxError|RangeError|KeyError|ValueError|Exception|Traceback|ECONNREFUSED|ETIMEDOUT|ENOENT|EACCES|ECONNRESET|errno|npm ERR!)\b|error\s+TS\d|:\d+:\d+\)|File ".+", line \d/i; + + /** + * ๋ถ™์—ฌ๋„ฃ์€ ์—๋Ÿฌ ๋กœ๊ทธยท์Šคํƒ ํŠธ๋ ˆ์ด์Šคยท์‹คํŒจ ์ถœ๋ ฅ์ฒ˜๋Ÿผ ๋ณด์ด๋Š” ํ…์ŠคํŠธ์ธ์ง€ *๋ณด์ˆ˜์ ์œผ๋กœ* ์ถ”์ •. + * ์ด๋Ÿฐ ์ž…๋ ฅ์€ '๋ถ„์„ ๋Œ€์ƒ'(ํœ˜๋ฐœ)์ด์ง€ '์ง€์‹'(์˜์†)์ด ์•„๋‹ˆ๋ฏ€๋กœ ์žฅ๊ธฐ ๊ธฐ์–ต ์ฑ„๊ตด์—์„œ ์ œ์™ธํ•œ๋‹ค. + * ์ผ๋ฐ˜ ์‚ฐ๋ฌธ์ด 'error' ๋ฅผ ํ•œ ๋ฒˆ ์–ธ๊ธ‰ํ•œ ์ •๋„๋กœ๋Š” ๊ฑธ๋ฆฌ์ง€ ์•Š๊ฒŒ ๊ฐ•ํ•œ/์•ฝํ•œ ์‹ ํ˜ธ๋ฅผ ๊ตฌ๋ถ„ํ•œ๋‹ค. + */ + public static looksLikeErrorLog(text: string): boolean { + if (!text) return false; + const strong = [ + /Traceback \(most recent call last\)/, + /^\s*at\s+.+\(.+:\d+:\d+\)/m, // JS ์Šคํƒ ํ”„๋ ˆ์ž„ + /\bFile ".+", line \d+/, // Python ํ”„๋ ˆ์ž„ + /npm ERR!/, + /\b(?:TypeError|ReferenceError|SyntaxError|RangeError|KeyError|ValueError)\b/, + /\b(?:ECONNREFUSED|ETIMEDOUT|ENOENT|EACCES|ECONNRESET)\b/, + /error\s+TS\d{3,}/i, // tsc ์—๋Ÿฌ + ]; + if (strong.some((re) => re.test(text))) return true; + const weak = (text.match(/\b(?:error|errors|exception|failed|failure|stacktrace|errno|fatal|panic|assertion)\b/gi) || []).length + + (text.match(/\[(?:error|warn|fatal)\]/gi) || []).length; + return weak >= 3 && text.split('\n').length >= 3; + } + /** * ๋Œ€ํ™” ๋ฉ”์‹œ์ง€์—์„œ ์žฅ๊ธฐ ๊ธฐ์–ต ํ›„๋ณด๋ฅผ ํŒจํ„ด ๋งค์นญ์œผ๋กœ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. * LLM ํ˜ธ์ถœ ์—†์ด ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. @@ -235,6 +272,8 @@ export class LongTermMemory { for (const msg of messages) { if (msg.role !== 'user') continue; const text = msg.content; + // ์—๋Ÿฌ ๋กœ๊ทธ/์Šคํƒ ํŠธ๋ ˆ์ด์Šค ๋คํ”„๋Š” '๋ถ„์„ ๋Œ€์ƒ'(ํœ˜๋ฐœ)์ด๋ฏ€๋กœ ํ†ต์งธ๋กœ ์ฑ„๊ตด ์ œ์™ธ. + if (LongTermMemory.looksLikeErrorLog(text)) continue; for (const pattern of rulePatterns) { pattern.lastIndex = 0; @@ -269,9 +308,11 @@ export class LongTermMemory { } } - // Deduplicate by content + // Deduplicate by content + ์—๋Ÿฌ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๋ฐ•ํžŒ ํ›„๋ณด ์ œ๊ฑฐ + // ('goal: fix ECONNREFUSED ...' ๊ฐ™์€ ์—๋Ÿฌ ๋‚ด์šฉ์ด ์ง€์‹์œผ๋กœ ํก์ˆ˜๋˜๋Š” ์˜ค์—ผ ๋ฐฉ์ง€). const seen = new Set(); return candidates.filter((c) => { + if (LongTermMemory.ERROR_NOISE.test(c.content)) return false; const key = c.content.toLowerCase(); if (seen.has(key)) return false; seen.add(key); diff --git a/src/memory/MemoryExtractor.ts b/src/memory/MemoryExtractor.ts index f0e7fa6..a2d14a0 100644 --- a/src/memory/MemoryExtractor.ts +++ b/src/memory/MemoryExtractor.ts @@ -38,13 +38,18 @@ export class MemoryExtractor { }; // 1. Long-Term Memory ์ถ”์ถœ + // ์ž๋™ ์ถ”์ถœ ํ•ญ๋ชฉ์—” TTL(14์ผ)์„ ๋ถ€์—ฌ โ€” ์ฐธ์กฐ๋  ๋•Œ๋งˆ๋‹ค ์Šฌ๋ผ์ด๋”ฉ ์—ฐ์žฅ๋˜๋ฏ€๋กœ ์‹ค์ œ๋กœ + // ์“ฐ์ด๋Š” ์ง€์‹์€ ์‚ด์•„๋‚จ๊ณ , ํ•œ ๋ฒˆ ๋“ค์–ด์˜จ ์ผํšŒ์„ฑยท์žก์Œ ๋‚ด์šฉ์€ 14์ผ ๋’ค ์ž์—ฐ ์†Œ๋ฉธํ•œ๋‹ค. + // (์—๋Ÿฌ ๋กœ๊ทธ/์‹คํŒจ ๋ฐ์ดํ„ฐ๋Š” extractCandidates ๋‹จ๊ณ„์—์„œ ์ด๋ฏธ ๊ฑธ๋Ÿฌ์ง.) const candidates = LongTermMemory.extractCandidates(messages); + const expiresAt = Date.now() + LongTermMemory.AUTO_EXTRACT_TTL_MS; for (const candidate of candidates) { longTermMemory.addEntry( candidate.category, candidate.content, `session:${sessionId}`, - 0.7 // ์ž๋™ ์ถ”์ถœ์ด๋ฏ€๋กœ ๊ธฐ๋ณธ ์‹ ๋ขฐ๋„ 0.7 + 0.7, // ์ž๋™ ์ถ”์ถœ์ด๋ฏ€๋กœ ๊ธฐ๋ณธ ์‹ ๋ขฐ๋„ 0.7 + { expiresAt }, ); } result.longTermCandidates = candidates.length; diff --git a/src/retrieval/contextBudget.ts b/src/retrieval/contextBudget.ts index 26ce0a7..bf14bd9 100644 --- a/src/retrieval/contextBudget.ts +++ b/src/retrieval/contextBudget.ts @@ -90,6 +90,19 @@ export function selectWithinBudget( return { selected, dropped, tokensUsed }; } +/** + * ์ฒญํฌ์˜ '์ฃผ์ œ(Subject)' ํƒœ๊ทธ๋ฅผ ๋„์ถœํ•œ๋‹ค โ€” ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ/์ฃผ์ œ์˜ ์ •๋ณด๊ฐ€ ํ•œ + * ์ปจํ…์ŠคํŠธ์— ์„ž์ผ ๋•Œ ๋ชจ๋ธ์ด ๊ฒฝ๊ณ„๋ฅผ ์ธ์ง€ํ•˜๋„๋ก(๋ฌด์„ฑ ๊ต์ฐจ์˜ค์—ผ ๋ฐฉ์ง€). category ๊ฐ€ ์žˆ์œผ๋ฉด + * ๊ทธ๊ฑธ, ์—†์œผ๋ฉด title/filePath ์˜ ์ตœ์ƒ์œ„ ํด๋” ์„ธ๊ทธ๋จผํŠธ๋ฅผ ์ฃผ์ œ๋กœ ๋ณธ๋‹ค. ํŒŒ์ผ๋ช…๋งŒ ์žˆ์œผ๋ฉด ''. + */ +function deriveSubject(chunk: RetrievalChunk): string { + const cat = (chunk.metadata.category || '').trim(); + if (cat) return cat; + const ref = (chunk.title || chunk.metadata.filePath || '').replace(/\\/g, '/'); + const seg = ref.split('/').filter(Boolean); + return seg.length >= 2 ? seg[0] : ''; +} + /** * ์„ ํƒ๋œ ์ฒญํฌ๋“ค์„ ํ•˜๋‚˜์˜ ์ปจํ…์ŠคํŠธ ๋ฌธ์ž์—ด๋กœ ์กฐ๋ฆฝํ•ฉ๋‹ˆ๋‹ค. * ์†Œ์Šค๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๊ฐ€๋…์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค. @@ -123,9 +136,11 @@ export function assembleContext(chunks: RetrievalChunk[]): string { const items = groupChunks .map((c) => { const metadata = c.metadata; + const subject = deriveSubject(c); + const subjectTag = subject ? `[${subject}] ` : ''; const conflictTag = metadata.conflictDetected ? ` [โš ๏ธ CONFLICT: ${metadata.conflictSeverity}]` : ''; const coverageTag = metadata.queryCoverage !== undefined ? ` (Coverage: ${metadata.queryCoverage.toFixed(2)})` : ''; - return `- ${c.title}${conflictTag}${coverageTag}: ${c.content}`; + return `- ${subjectTag}${c.title}${conflictTag}${coverageTag}: ${c.content}`; }) .join('\n'); sections.push(`### ${label}\n${items}`); @@ -134,6 +149,7 @@ export function assembleContext(chunks: RetrievalChunk[]): string { return [ '[MEMORY CONTEXT]', 'Review this layered memory before preparing the answer. Use it only when relevant, and prefer the current user request when there is conflict.', + '๊ฐ ํ•ญ๋ชฉ ์•ž์˜ [์ฃผ์ œ] ํƒœ๊ทธ์™€ ์„น์…˜ ์ถœ์ฒ˜๋ฅผ ํ™•์ธํ•˜๋ผ. **ํ˜„์žฌ ์š”์ฒญ๊ณผ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธยท์ฃผ์ œ์˜ ํ•ญ๋ชฉ์€ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ๋ผ** โ€” ์„œ๋กœ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์˜ ๊ทœ์น™ยท๊ฒฐ์ •ยท์ˆ˜์น˜ยท๊ณ ์œ ๋ช…์‚ฌ๋ฅผ ์„ž์ง€ ๋ง ๊ฒƒ. ์–ด๋А ํ•ญ๋ชฉ์ด ํ˜„์žฌ ์ž‘์—…๊ณผ ๊ด€๋ จ ์žˆ๋Š”์ง€ ๋ถˆํ™•์‹คํ•˜๋ฉด ๊ทธ ํ•ญ๋ชฉ์— ์˜์กดํ•˜์ง€ ๋งˆ๋ผ.', '', sections.join('\n\n') ].join('\n'); diff --git a/src/utils.ts b/src/utils.ts index 88f20ce..3c0572e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -234,6 +234,7 @@ Then reply with one short line stating what was started and where. 2. [NO MARKDOWN MARKERS] PLAIN TEXT ONLY. Do NOT emit "#", "##", "###", "**", "__", "> ", "* " as formatting. Section labels are bare Korean words on their own line (e.g. a line that says just "ํ•ต์‹ฌ ์š”์•ฝ" โ€” no "#", no "**"). Bullets use "- " only. Inline code with backticks (e.g. \`src/agent.ts\`) and triple-backtick code blocks for actual code are fine. 3. [NO INTERNAL LOGS] Never output
, "2nd Brain Trace", or "Debug JSON" blocks. 4. [NO SECTION LEAKAGE] Never output sections named "์š”์ฒญ ์š”์•ฝ", "์‚ฌ์šฉ์ž ์˜๋„ ์ถ”๋ก ", "ํ”„๋กœ์ ํŠธ ๊ธฐ๋ก ๋Œ€์ƒ ํ™•์ธ", "ํ•ต์‹ฌ ํ™•์ธ ์งˆ๋ฌธ", or "๊ทผ๊ฑฐ ํŒŒ์ผ ๊ฒฝ๋กœ". +5. [ํ™•์ธ ๋ถˆ๊ฐ€ โ€” ์‚ฌ์‹ค ๋‚ ์กฐ ๊ธˆ์ง€] ์ง€์‹ ๋ฒ ์ด์Šคยท์ œ๊ณต๋œ ์ปจํ…์ŠคํŠธยท์ด๋ฒˆ ์„ธ์…˜์— ์ฝ์€ ํŒŒ์ผ์— ๊ทผ๊ฑฐ๊ฐ€ ์—†๋Š” ์‚ฌ์‹ค(์ˆ˜์น˜, ๋‚ ์งœ, ๊ธˆ์•ก, ๊ณ ์œ ๋ช…์‚ฌ, ํŒŒ์ผ/ํ•จ์ˆ˜/ํฌํŠธ๋ช…, ๊ฒฐ์ • ์‚ฌํ•ญ, "์ด๋ฏธ ~๋‹ค/~๋กœ ์ •ํ•ด์กŒ๋‹ค" ๋ฅ˜ ๋‹จ์ •)์€ ์ง€์–ด๋‚ด์ง€ ๋งˆ๋ผ. ๊ทผ๊ฑฐ๊ฐ€ ์—†์œผ๋ฉด ์ถ”์ธก์œผ๋กœ ๋ฉ”์šฐ์ง€ ๋ง๊ณ  "ํ™•์ธ ๋ถˆ๊ฐ€" ๋˜๋Š” "๊ทผ๊ฑฐ ์—†์Œ โ€” ํ™•์ธ ํ•„์š”"๋ผ๊ณ  ๋ช…์‹œํ•˜๋ผ. ๋ถˆํ™•์‹คํ•˜๋ฉด ๋‹จ์ • ํ†ค์„ ๋‚ฎ์ถฐ๋ผ("~๋กœ ๋ณด์ธ๋‹ค", "ํ™•์ธ ํ•„์š”"). ๋‹จ, ์ด ๊ทœ์น™์€ *์‚ฌ์‹ค ์ฃผ์žฅ*์—๋งŒ ์ ์šฉ๋œ๋‹ค โ€” R7 ์˜ 'ํ•ฉ๋ฆฌ์  ๊ฐ€์ • ํ›„ ์ง„ํ–‰'์€ *์ž‘์—… ์ˆ˜ํ–‰*์˜ ๊ธฐ๋ณธ๊ฐ’ ์„ ํƒ์—๋Š” ๊ทธ๋Œ€๋กœ ์œ ํšจํ•˜๋‹ค(๊ฐ€์ •์€ "๊ฐ€์ •:" ํ•œ ์ค„๋กœ ๋ฐํžŒ๋‹ค). [OUTPUT FORMAT โ€” 7 hard rules] These rules override any other formatting habit. Apply them to EVERY answer.