feat(wiki): /wikify 포맷 정본 통일 + 채팅 URL 실데이터 주입 (v2.2.229)
[포맷 통일 — Datacollect 가 정본] /wikify 와 Datacollect research 가 각자 포맷 사본을 들고 어긋났던 문제. 더 최신인 Datacollect 포맷을 wiki_format.mjs 정본으로 추출(브리지 측)하고, /wikify 는 GET /api/wiki/template 로 받아 소비. 구버전 브리지면 내장 사본 fallback (정본 v3.1과 동일 내용). 포맷 수정은 이제 wiki_format.mjs 한 곳. /wikify 가 정본을 따르며 고쳐진 것: - category "10_Wiki/Topics"(물리 경로 버그) → 논리 도메인 규칙 - 고정 신뢰도 B/0.8 → 소스 평가 동적 부여 (충돌 신뢰도 권고의 입력 품질) - aliases 빈 배열 → 동의어 3-8개 강제 (어휘갭 검색 보완) - "## 🔗 관련 문서 링크" → "## 🔗 지식 그래프" + 고아 방지 up-link - 인라인 [S#] 출처 인용 + 📚 출처 섹션, 비교표·코드 패턴 조건 섹션 [채팅 URL 접근 — 강제 주입 패턴 4번째 적용] 일반 채팅에 URL 을 주면 "접근 불가"라고 답하던 공백: urlContext 가 URL 감지 시 브리지 /api/web-extract(기존 /wikify 인프라 재사용)로 본문 추출 → 컨텍스트 주입 (8K 캡, 잘림 시 /wikify 안내). 실패 시 정직 블록 (브리지 확인 안내 + 추측 금지). 슬래시 명령은 제외 (자체 처리). 주입 성공 로그 포함. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "astra",
|
"name": "astra",
|
||||||
"version": "2.2.228",
|
"version": "2.2.229",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "astra",
|
"name": "astra",
|
||||||
"version": "2.2.228",
|
"version": "2.2.229",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lmstudio/sdk": "^1.5.0",
|
"@lmstudio/sdk": "^1.5.0",
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "astra",
|
"name": "astra",
|
||||||
"displayName": "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.",
|
"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.228",
|
"version": "2.2.229",
|
||||||
"publisher": "g1nation",
|
"publisher": "g1nation",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { AgentWorkflowManager } from './agents/AgentWorkflowManager';
|
|||||||
import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraModeArchitecture';
|
import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraModeArchitecture';
|
||||||
import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext';
|
import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext';
|
||||||
import { isSelfAssessRequest, buildSelfAssessContext } from './lib/contextBuilders/selfAssessContext';
|
import { isSelfAssessRequest, buildSelfAssessContext } from './lib/contextBuilders/selfAssessContext';
|
||||||
|
import { extractUrlFromPrompt, buildUrlContext } from './lib/contextBuilders/urlContext';
|
||||||
import { looksLikeCorrection, captureCorrection } from './intelligence/correctionLoop';
|
import { looksLikeCorrection, captureCorrection } from './intelligence/correctionLoop';
|
||||||
import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting';
|
import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting';
|
||||||
import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract';
|
import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract';
|
||||||
@@ -553,6 +554,20 @@ export class AgentExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [URL 실데이터] 채팅 프롬프트에 URL 이 있으면 브리지로 본문을 추출해 주입.
|
||||||
|
// /wikify 만 URL 접근이 가능하고 일반 채팅은 "접근 불가"라고 답하던 공백 수정.
|
||||||
|
if (prompt && loopDepth === 0 && !isCasualConversation) {
|
||||||
|
const url = extractUrlFromPrompt(prompt);
|
||||||
|
if (url) {
|
||||||
|
try {
|
||||||
|
contextBlock += `\n\n${await buildUrlContext(url)}`;
|
||||||
|
logInfo('URL 컨텍스트 주입 시도.', { url });
|
||||||
|
} catch (e: any) {
|
||||||
|
logError('URL 컨텍스트 주입 실패 (계속 진행).', { error: e?.message ?? String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// [Correction Loop ①] 이 발화가 직전 답변에 대한 *정정*이면 fire-and-forget
|
// [Correction Loop ①] 이 발화가 직전 답변에 대한 *정정*이면 fire-and-forget
|
||||||
// 캡처 — 오류 분류 → 태깅 레슨 + 회귀 케이스(.astra/eval/corrections.jsonl).
|
// 캡처 — 오류 분류 → 태깅 레슨 + 회귀 케이스(.astra/eval/corrections.jsonl).
|
||||||
// 정정 자체가 Ground Truth 가 되어 주간 회귀 테스트·약점 프로필의 원료가 된다.
|
// 정정 자체가 Ground Truth 가 되어 주간 회귀 테스트·약점 프로필의 원료가 된다.
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const BRIDGE_API = {
|
|||||||
},
|
},
|
||||||
wiki: {
|
wiki: {
|
||||||
save: '/api/wiki/save',
|
save: '/api/wiki/save',
|
||||||
|
template: '/api/wiki/template',
|
||||||
},
|
},
|
||||||
lm: {
|
lm: {
|
||||||
proxy: '/api/lm',
|
proxy: '/api/lm',
|
||||||
|
|||||||
@@ -428,12 +428,21 @@ async function wikifyOne(url: string, userContent: string, view: Webview | undef
|
|||||||
}
|
}
|
||||||
|
|
||||||
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
|
const model = (cfg.get<string>('defaultModel', '') || 'gemma4:e2b').trim();
|
||||||
const wikiSystem = '당신은 지식 큐레이터입니다. 제공된 웹사이트 본문을 P-Reinforce v3.0 규격의 고밀도 위키 문서로 정리합니다. 본문에 없는 내용은 절대 지어내지 않으며, 모든 문서는 한국어로 작성합니다.';
|
const wikiSystem = '당신은 지식 큐레이터입니다. 제공된 웹사이트 본문을 P-Reinforce 규격의 고밀도 위키 문서로 정리합니다. 본문에 없는 내용은 절대 지어내지 않으며, 모든 문서는 한국어로 작성합니다.';
|
||||||
|
// 포맷 정본을 브리지에서 가져온다 (wiki_format.mjs — research 와 동일 포맷 보장).
|
||||||
|
// 구버전 브리지(엔드포인트 없음)면 wikifyPrompt 의 내장 사본 fallback.
|
||||||
|
let canonicalFormat: import('./prompts/wikifyPrompt').CanonicalWikiFormat | null = null;
|
||||||
|
try {
|
||||||
|
canonicalFormat = await bridgeFetch<any>(BRIDGE_API.wiki.template, { method: 'GET' }, { timeoutMs: 5000 });
|
||||||
|
chunk(view, `📐 포맷 정본 v${canonicalFormat?.version ?? '?'} (브리지)\n`);
|
||||||
|
} catch {
|
||||||
|
chunk(view, `📐 포맷 내장 사본 사용 (브리지 템플릿 미제공 — 브리지 갱신 시 정본 적용)\n`);
|
||||||
|
}
|
||||||
chunk(view, `🧪 P-Reinforce 위키 합성 (모델 \`${model}\`)…`);
|
chunk(view, `🧪 P-Reinforce 위키 합성 (모델 \`${model}\`)…`);
|
||||||
let report: string;
|
let report: string;
|
||||||
try {
|
try {
|
||||||
const synthT0 = Date.now();
|
const synthT0 = Date.now();
|
||||||
report = await callLmSynthesis(buildWikifyPrompt(data, userContent), wikiSystem);
|
report = await callLmSynthesis(buildWikifyPrompt(data, userContent, canonicalFormat), wikiSystem);
|
||||||
if (!report) throw new Error('LLM 응답이 비어 있습니다.');
|
if (!report) throw new Error('LLM 응답이 비어 있습니다.');
|
||||||
report = report.replace(/\[\[([^\[\]]+?)\](?!\])/g, '[[$1]]');
|
report = report.replace(/\[\[([^\[\]]+?)\](?!\])/g, '[[$1]]');
|
||||||
chunk(view, ` ✓ (${Math.round((Date.now() - synthT0) / 1000)}s)\n\n`);
|
chunk(view, ` ✓ (${Math.round((Date.now() - synthT0) / 1000)}s)\n\n`);
|
||||||
|
|||||||
@@ -1,8 +1,119 @@
|
|||||||
/**
|
/**
|
||||||
* 추출된 웹사이트 본문 → Datacollect Research(P-Reinforce v3.0)와 동일한 위키 문서
|
* 추출된 웹사이트 본문 → P-Reinforce 위키 문서 프롬프트.
|
||||||
* 프롬프트. Bridge의 /api/research/synthesize 템플릿을 웹 본문 소스용으로 이식.
|
*
|
||||||
|
* 포맷 정본은 Datacollect 브리지의 wiki_format.mjs — 핸들러가
|
||||||
|
* GET /api/wiki/template 로 받아 `canonical` 로 주입한다 (양쪽 포맷 통일).
|
||||||
|
* 브리지가 구버전(엔드포인트 없음)일 때만 아래 내장 사본(fallback)을 쓴다.
|
||||||
|
* 내장 사본은 정본을 따라 갱신하되, 차이가 생기면 정본이 항상 옳다.
|
||||||
*/
|
*/
|
||||||
export function buildWikifyPrompt(extracted: any, userContent: string): string {
|
|
||||||
|
export interface CanonicalWikiFormat {
|
||||||
|
version: string;
|
||||||
|
/** 규칙 7-15 (별칭·출처·동적 신뢰도·그래프·논리 도메인 분류…), {{ROOT}} placeholder. */
|
||||||
|
commonRules: string;
|
||||||
|
/** Frontmatter 골격 — {{ID}} {{TITLE}} {{TODAY}} {{TAGS}} {{SOURCES}}. */
|
||||||
|
frontmatter: string;
|
||||||
|
/** 본문 섹션 골격 — {{TOPIC}} {{ROOT}} {{TODAY}} {{ORIGIN}}. */
|
||||||
|
sections: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 정본 골격의 placeholder 치환. */
|
||||||
|
function fill(tpl: string, vars: Record<string, string>): string {
|
||||||
|
return tpl.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 내장 fallback (정본 wiki_format.mjs v3.1 사본 — 브리지 구버전 대비) ────────
|
||||||
|
|
||||||
|
const FALLBACK_VERSION = '3.1-embedded';
|
||||||
|
|
||||||
|
const FALLBACK_COMMON_RULES = `
|
||||||
|
7. [별칭] Frontmatter 'aliases'에 동의어·약어·영문/국문 표기 3-8개를 YAML 리스트로 채우시오.
|
||||||
|
8. [출처] '세부 내용' 주장 끝에 [S1],[S2] 인라인 표기 + 문서 끝 '## 📚 출처'에 번호별 제목/URL 나열, Frontmatter 'raw_sources'에도 동일 기입(placeholder 금지).
|
||||||
|
9. [신뢰도] 소스 평가로 source_trust_level(S/A/B/C/D)·confidence_score(0.00-1.00) 부여(문서마다 다르게). 이 값은 지식 충돌 시 우선순위 판단에 사용되므로 정직하게 평가하시오.
|
||||||
|
10. [그래프] '## 🔗 지식 그래프' 항상 작성: 루트 [[{{ROOT}}]] + 관련 개념 2개+ 링크.
|
||||||
|
11. [분류] category는 폴더경로 말고 논리 도메인(AI_and_ML/Frontend/Architecture 등, 없으면 "Topic").
|
||||||
|
12. [비교] 대안·경쟁 기술이 있으면 '## ⚖️ 비교 및 선택 기준'에 표(|항목|장점|단점|언제 선택|), 없으면 생략.
|
||||||
|
13. [코드] 소스에 코드가 있으면 '## 💻 코드 패턴'에 최소 스니펫(언어/버전), 없으면 "소스에 코드 예시 없음".
|
||||||
|
14. [완결성] 빈 섹션·(TODO)·플레이스홀더 금지. 근거 없으면 "소스에서 확인되지 않음".
|
||||||
|
15. [인용정렬] 인라인 [S#]는 '## 📚 출처' 번호와 1부터 순차 일치.`;
|
||||||
|
|
||||||
|
const FALLBACK_FRONTMATTER = `---
|
||||||
|
id: {{ID}}
|
||||||
|
title: "{{TITLE}}"
|
||||||
|
category: "Topic"
|
||||||
|
status: "draft"
|
||||||
|
verification_status: "conceptual"
|
||||||
|
canonical_id: ""
|
||||||
|
aliases: []
|
||||||
|
duplicate_of: ""
|
||||||
|
source_trust_level: "B"
|
||||||
|
confidence_score: 0.85
|
||||||
|
created_at: {{TODAY}}
|
||||||
|
updated_at: {{TODAY}}
|
||||||
|
review_reason: ""
|
||||||
|
merge_history: []
|
||||||
|
tags: [{{TAGS}}]
|
||||||
|
raw_sources: {{SOURCES}}
|
||||||
|
applied_in: []
|
||||||
|
github_commit: ""
|
||||||
|
---`;
|
||||||
|
|
||||||
|
const FALLBACK_SECTIONS = `# [[{{TOPIC}}]]
|
||||||
|
|
||||||
|
## 🎯 한 줄 통찰 (One-line insight)
|
||||||
|
(이 주제의 핵심 가치를 관통하는 강력한 한 줄 정의)
|
||||||
|
|
||||||
|
## 🧠 핵심 개념 (Core concepts)
|
||||||
|
(이 주제를 구성하는 가장 중요한 3-5가지 핵심 기둥/개념)
|
||||||
|
|
||||||
|
## 🧩 추출된 패턴 (Extracted patterns)
|
||||||
|
(소스에서 발견된 반복되는 구조, 전략, 설계 패턴 또는 휴리스틱)
|
||||||
|
|
||||||
|
## ⚖️ 비교 및 선택 기준 (Comparison & decision criteria)
|
||||||
|
(이 주제에 대안·경쟁 기술이 있을 때만 작성. 대안이 없으면 이 섹션 전체를 생략.)
|
||||||
|
|
||||||
|
| 항목 (Option) | 장점 | 단점 | 언제 선택 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
## 📖 세부 내용 (Details)
|
||||||
|
(소스에서 합성된 상세하고 전문적인 설명. 논리적 단락이나 글머리 기호로 구분하시오.)
|
||||||
|
|
||||||
|
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
|
||||||
|
(소스 내에서 상충되는 정보가 있거나, 기존 상식과 다른 최신 정보가 있다면 서술하시오.)
|
||||||
|
|
||||||
|
## 🛠️ 적용 사례 (Applied in summary)
|
||||||
|
(소스에서 실제 적용된 코드·커밋·프로젝트·결정이 발견되면 요약. 없으면 "현재 발견된 실제 적용 사례가 없습니다.")
|
||||||
|
|
||||||
|
## 💻 코드 패턴 (Code patterns)
|
||||||
|
(소스에 코드·구현·API 사용법이 있으면 최소 스니펫을 언어/버전과 함께. 없으면 "소스에 코드 예시 없음".)
|
||||||
|
|
||||||
|
## ✅ 검증 상태 및 신뢰도
|
||||||
|
- **상태:** draft
|
||||||
|
- **검증 단계:** conceptual (실제 적용 사례 발견 시 applied/validated로 승격 가능)
|
||||||
|
- **출처 신뢰도:** (소스 평가에 따라 S/A/B/C/D 중 부여 — Frontmatter source_trust_level과 일치)
|
||||||
|
- **신뢰 점수:** (0.00-1.00 평가값 — Frontmatter confidence_score와 일치)
|
||||||
|
- **중복 검사 결과:** 신규 생성 (New discovery)
|
||||||
|
|
||||||
|
## 🔗 지식 그래프 (Knowledge Graph)
|
||||||
|
(항상 작성 — 이 문서를 위키 그래프에 연결한다. 고아 문서 금지.)
|
||||||
|
- **상위/루트:** [[{{ROOT}}]]
|
||||||
|
- **관련 개념:** (직접 관련된 핵심 개념 2개 이상을 [[대괄호 두 개]]로.)
|
||||||
|
- **참조 맥락:** (이 지식이 어떤 작업·주제·결정에서 참조될지 한 줄로.)
|
||||||
|
|
||||||
|
## 📚 출처 (Sources)
|
||||||
|
('세부 내용'의 [S1], [S2] 인라인 표기와 번호를 일치시키시오.)
|
||||||
|
- [S1]
|
||||||
|
|
||||||
|
## 📝 변경 이력 (Change history)
|
||||||
|
- {{TODAY}}: {{ORIGIN}}`;
|
||||||
|
|
||||||
|
// ── 프롬프트 빌드 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function buildWikifyPrompt(extracted: any, userContent: string, canonical?: CanonicalWikiFormat | null): string {
|
||||||
|
const fmt: CanonicalWikiFormat = canonical && canonical.commonRules && canonical.frontmatter && canonical.sections
|
||||||
|
? canonical
|
||||||
|
: { version: FALLBACK_VERSION, commonRules: FALLBACK_COMMON_RULES, frontmatter: FALLBACK_FRONTMATTER, sections: FALLBACK_SECTIONS };
|
||||||
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
const topic = (userContent.trim() || extracted?.title || extracted?.url || '웹사이트 지식').trim();
|
const topic = (userContent.trim() || extracted?.title || extracted?.url || '웹사이트 지식').trim();
|
||||||
const url = extracted?.url || '';
|
const url = extracted?.url || '';
|
||||||
@@ -10,15 +121,23 @@ export function buildWikifyPrompt(extracted: any, userContent: string): string {
|
|||||||
const headings = Array.isArray(extracted?.headings) ? extracted.headings.slice(0, 40) : [];
|
const headings = Array.isArray(extracted?.headings) ? extracted.headings.slice(0, 40) : [];
|
||||||
const body = String(extracted?.text || '').slice(0, 30000);
|
const body = String(extracted?.text || '').slice(0, 30000);
|
||||||
|
|
||||||
return `임무: 아래 웹사이트에서 추출한 본문을 근거로 P-Reinforce v3.0 규격에 맞춰 고밀도 지식 문서를 작성하시오.
|
const vars = {
|
||||||
|
ID: idSlug, TITLE: topic, TOPIC: topic, ROOT: topic, TODAY: today,
|
||||||
|
TAGS: '"web", "wikify"', SOURCES: `["${url}"]`,
|
||||||
|
ORIGIN: `Astra /wikify 로 ${url} 본문에서 초안 생성.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return `임무: 아래 웹사이트에서 추출한 본문을 근거로 P-Reinforce v${fmt.version.replace('-embedded', '')} 규격에 맞춰 고밀도 지식 문서를 작성하시오.
|
||||||
주제: '${topic}'
|
주제: '${topic}'
|
||||||
|
|
||||||
[필수 규칙]
|
[필수 규칙]
|
||||||
1. 반드시 아래 [웹사이트 본문]의 내용만을 근거로 작성하시오. 외부 지식을 절대 섞지 마시오.
|
1. 반드시 아래 [웹사이트 본문]의 내용만을 근거로 작성하시오. 외부 지식을 절대 섞지 마시오.
|
||||||
2. 반드시 아래 Markdown 템플릿과 Frontmatter 형식을 정확히 따르시오. 섹션 이름과 구조를 변경하지 마시오.
|
2. 반드시 아래 Markdown 템플릿과 Frontmatter 형식을 정확히 따르시오. 섹션 이름과 구조를 변경하지 마시오.
|
||||||
3. 본문에 없는 정보는 지어내지 말고, 해당 섹션에 "본문에서 확인되지 않음"이라고 명시하시오.
|
3. 본문에 없는 정보는 지어내지 말고, 해당 섹션에 "소스에서 확인되지 않음"이라고 명시하시오.
|
||||||
4. 한국어로 작성하시오. 관련 개념·고유명사는 [[대괄호 두 개]]로 감싸 위키 링크로 만드시오. 위키 링크는 반드시 \`[[\` 로 열고 \`]]\` 로 닫으시오 (닫는 대괄호를 빠뜨리지 마시오).
|
4. 한국어로 작성하시오. 관련 개념·고유명사는 [[대괄호 두 개]]로 감싸 위키 링크로 만드시오. 위키 링크는 반드시 \`[[\` 로 열고 \`]]\` 로 닫으시오.
|
||||||
5. 원문이 JSON Schema·API 명세·기술 스펙·설정 레퍼런스인 경우, '📖 세부 내용'에 등장하는 모든 필드·속성·파라미터를 **누락 없이** 마크다운 표로 정리하시오. 표 컬럼은 [필드 | 타입 | 필수/선택 | 제약·설명]. 원문의 \`required\` 배열에 있는 항목만 '필수'로 표기하고, 그 목록을 임의로 바꾸거나 추가하지 마시오. \`additionalProperties\`·\`const\`·\`enum\` 같은 제약과 중첩 객체 구조도 원문 그대로 반영하시오. 최상위 필드를 하나라도 빠뜨리지 마시오.
|
5. 원문이 JSON Schema·API 명세·기술 스펙·설정 레퍼런스인 경우, '📖 세부 내용'에 등장하는 모든 필드·속성·파라미터를 **누락 없이** 마크다운 표로 정리하시오. 표 컬럼은 [필드 | 타입 | 필수/선택 | 제약·설명]. 원문의 \`required\` 배열에 있는 항목만 '필수'로 표기하고, 그 목록을 임의로 바꾸거나 추가하지 마시오.
|
||||||
|
6. 이 문서의 출처는 웹 본문 1건이므로 '## 📚 출처'의 [S1]은 위 URL을 가리키며, source_trust_level 은 사이트 성격(공식 문서/학술 S~A, 기술 블로그 B, 커뮤니티/익명 C~D)으로 평가하시오.
|
||||||
|
${fill(fmt.commonRules, vars)}
|
||||||
|
|
||||||
[웹사이트 메타]
|
[웹사이트 메타]
|
||||||
- URL: ${url}
|
- URL: ${url}
|
||||||
@@ -33,56 +152,7 @@ ${body}
|
|||||||
|
|
||||||
[출력 템플릿 - 이 형식을 정확히 따르시오]
|
[출력 템플릿 - 이 형식을 정확히 따르시오]
|
||||||
|
|
||||||
---
|
${fill(fmt.frontmatter, vars)}
|
||||||
id: ${idSlug}
|
|
||||||
title: "${topic}"
|
|
||||||
category: "10_Wiki/Topics"
|
|
||||||
status: "draft"
|
|
||||||
verification_status: "conceptual"
|
|
||||||
canonical_id: ""
|
|
||||||
aliases: []
|
|
||||||
duplicate_of: ""
|
|
||||||
source_trust_level: "B"
|
|
||||||
confidence_score: 0.8
|
|
||||||
created_at: ${today}
|
|
||||||
updated_at: ${today}
|
|
||||||
review_reason: ""
|
|
||||||
merge_history: []
|
|
||||||
tags: ["web", "wikify"]
|
|
||||||
raw_sources: ["${url}"]
|
|
||||||
applied_in: []
|
|
||||||
github_commit: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
# [[${topic}]]
|
${fill(fmt.sections, vars)}`;
|
||||||
|
|
||||||
## 🎯 한 줄 통찰 (One-line insight)
|
|
||||||
(이 웹사이트/주제의 핵심 가치를 관통하는 강력한 한 줄 정의)
|
|
||||||
|
|
||||||
## 🧠 핵심 개념 (Core concepts)
|
|
||||||
(본문을 구성하는 가장 중요한 3-5가지 핵심 개념/기둥)
|
|
||||||
|
|
||||||
## 🧩 추출된 패턴 (Extracted patterns)
|
|
||||||
(본문에서 발견된 반복되는 구조, 전략, 주장 또는 접근법)
|
|
||||||
|
|
||||||
## 📖 세부 내용 (Details)
|
|
||||||
(본문에서 합성된 상세하고 전문적인 설명. 논리적 단락이나 글머리 기호로 구분하시오. 원문이 명세·스키마·API 레퍼런스라면 위 규칙 5에 따라 모든 필드를 표로 빠짐없이 정리하시오.)
|
|
||||||
|
|
||||||
## ⚖️ 모순 및 업데이트 (Contradictions & updates)
|
|
||||||
(본문 내에서 상충되는 정보나 주목할 최신 정보가 있다면 서술. 없으면 "본문에서 확인되지 않음".)
|
|
||||||
|
|
||||||
## 🛠️ 적용 사례 (Applied in summary)
|
|
||||||
(본문에 구체적 사례·수치·제품·프로젝트·의사결정이 있으면 요약하여 기술. 없으면 "본문에서 확인되지 않음".)
|
|
||||||
|
|
||||||
## ✅ 검증 상태 및 신뢰도
|
|
||||||
- **상태:** draft
|
|
||||||
- **검증 단계:** conceptual
|
|
||||||
- **출처 신뢰도:** B (Primary Source — 웹사이트 본문 직접 추출)
|
|
||||||
- **중복 검사 결과:** 신규 생성 (New discovery)
|
|
||||||
|
|
||||||
## 🔗 관련 문서 링크 (Related document links)
|
|
||||||
(이 주제와 직접 연결되는 핵심 개념 3-7개를 [[위키링크]]로 제시하고, 각 링크마다 연결 이유를 한 줄로 적으시오. 본문에 등장한 개념을 우선 사용.)
|
|
||||||
|
|
||||||
## 📝 변경 이력 (Change history)
|
|
||||||
- ${today}: Astra /wikify 로 ${url} 본문에서 초안 생성.`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* URL 컨텍스트 빌더 — 채팅 프롬프트에 URL 이 있으면 본문을 추출해 실데이터로 주입.
|
||||||
|
*
|
||||||
|
* 문제: /wikify 는 URL 에 접근하지만(브리지 /api/web-extract), 일반 채팅에 URL 을
|
||||||
|
* 주면 추출 경로가 없어 모델이 "접근할 수 없습니다"라고 답하거나 내용을 추측했다.
|
||||||
|
*
|
||||||
|
* 수정: 강제 주입 패턴의 4번째 적용 (일정→캘린더, 자기평가→인벤토리, 정정→캡처와
|
||||||
|
* 동일 설계). 이미 검증된 브리지 추출 인프라를 재사용 — 새 크롤러 없음.
|
||||||
|
* 실패 시(브리지 다운/추출 실패) 정직한 안내 블록 — 모델이 내용을 지어내지 않게.
|
||||||
|
*/
|
||||||
|
import { logInfo } from '../../utils';
|
||||||
|
import { bridgeFetch, BRIDGE_API } from '../../features/datacollect/bridgeClient';
|
||||||
|
|
||||||
|
const URL_RE = /https?:\/\/[^\s<>"'`)\]]+/i;
|
||||||
|
const MAX_BODY_CHARS = 8000;
|
||||||
|
const EXTRACT_TIMEOUT_MS = 45_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 프롬프트에서 추출 대상 URL 을 찾는다. 슬래시 명령(/wikify 등)은 자체 처리하므로
|
||||||
|
* 제외. 없으면 null.
|
||||||
|
*/
|
||||||
|
export function extractUrlFromPrompt(prompt: string): string | null {
|
||||||
|
const p = (prompt || '').trim();
|
||||||
|
if (!p || p.startsWith('/')) return null;
|
||||||
|
const m = URL_RE.exec(p);
|
||||||
|
if (!m) return null;
|
||||||
|
// 문장 끝 구두점이 URL 에 붙는 흔한 오염 제거.
|
||||||
|
return m[0].replace(/[.,;:!?…」』)]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** URL 본문을 추출해 컨텍스트 블록 생성. 실패해도 throw 하지 않는다 (정직 블록 반환). */
|
||||||
|
export async function buildUrlContext(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const data = await bridgeFetch<{ success: boolean; title?: string; description?: string; text?: string; textLength?: number; truncated?: boolean }>(
|
||||||
|
BRIDGE_API.web.extract,
|
||||||
|
{ method: 'POST', body: JSON.stringify({ url }) },
|
||||||
|
{ timeoutMs: EXTRACT_TIMEOUT_MS },
|
||||||
|
);
|
||||||
|
const body = String(data?.text || '').slice(0, MAX_BODY_CHARS);
|
||||||
|
if (!body.trim() || body.trim().length < 50) {
|
||||||
|
return [
|
||||||
|
`[URL CONTENT — ${url}]`,
|
||||||
|
'상태: 본문 추출 실패 (콘텐츠 없음 또는 JS 전용 렌더링).',
|
||||||
|
'→ 사용자에게 이 URL 의 본문을 가져오지 못했다고 정직하게 알리고, 내용을 추측해 답하지 마라.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
logInfo('URL 컨텍스트 주입.', { url, chars: body.length, truncated: !!data?.truncated });
|
||||||
|
return [
|
||||||
|
`[URL CONTENT — 실데이터 · ${url}]`,
|
||||||
|
`제목: ${data?.title || '(없음)'}`,
|
||||||
|
data?.description ? `설명: ${data.description}` : '',
|
||||||
|
'아래 본문만 근거로 답하라. 본문에 없는 내용은 "본문에서 확인되지 않음"이라고 답하고 지어내지 마라.' +
|
||||||
|
(body.length >= MAX_BODY_CHARS || data?.truncated ? ' (본문 일부 잘림 — 전체가 필요하면 /wikify 사용을 안내하라.)' : ''),
|
||||||
|
'',
|
||||||
|
body,
|
||||||
|
].filter(Boolean).join('\n');
|
||||||
|
} catch (e: any) {
|
||||||
|
return [
|
||||||
|
`[URL CONTENT — ${url}]`,
|
||||||
|
`상태: 접근 실패 — ${String(e?.message ?? e).slice(0, 120)}`,
|
||||||
|
'→ Datacollect 브리지(:3002)가 실행 중인지 확인하라고 사용자에게 안내하고, URL 내용을 추측해 답하지 마라.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* URL 컨텍스트 + wikify 정본 포맷 — 순수 로직 테스트.
|
||||||
|
*/
|
||||||
|
import { extractUrlFromPrompt } from '../src/lib/contextBuilders/urlContext';
|
||||||
|
import { buildWikifyPrompt, type CanonicalWikiFormat } from '../src/features/datacollect/prompts/wikifyPrompt';
|
||||||
|
|
||||||
|
describe('extractUrlFromPrompt', () => {
|
||||||
|
test('URL 추출 + 끝 구두점 제거', () => {
|
||||||
|
expect(extractUrlFromPrompt('이 글 요약해줘 https://example.com/post.')).toBe('https://example.com/post');
|
||||||
|
expect(extractUrlFromPrompt('https://a.io/x?q=1 내용 확인')).toBe('https://a.io/x?q=1');
|
||||||
|
});
|
||||||
|
test('슬래시 명령은 제외 (/wikify 가 자체 처리)', () => {
|
||||||
|
expect(extractUrlFromPrompt('/wikify https://example.com')).toBeNull();
|
||||||
|
});
|
||||||
|
test('URL 없으면 null', () => {
|
||||||
|
expect(extractUrlFromPrompt('오늘 일정 알려줘')).toBeNull();
|
||||||
|
expect(extractUrlFromPrompt('')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildWikifyPrompt — 정본/fallback 포맷', () => {
|
||||||
|
const extracted = { url: 'https://ex.com/doc', title: '문서', text: '본문 내용입니다. '.repeat(10), headings: ['h1'] };
|
||||||
|
|
||||||
|
test('정본 주입 시 placeholder 가 채워진다', () => {
|
||||||
|
const canonical: CanonicalWikiFormat = {
|
||||||
|
version: '9.9',
|
||||||
|
commonRules: '\n7. 규칙 루트={{ROOT}}',
|
||||||
|
frontmatter: '---\nid: {{ID}}\ntitle: "{{TITLE}}"\ntags: [{{TAGS}}]\nraw_sources: {{SOURCES}}\n---',
|
||||||
|
sections: '# [[{{TOPIC}}]]\n- {{TODAY}}: {{ORIGIN}}',
|
||||||
|
};
|
||||||
|
const p = buildWikifyPrompt(extracted, '테스트 주제', canonical);
|
||||||
|
expect(p).toContain('P-Reinforce v9.9');
|
||||||
|
expect(p).toContain('규칙 루트=테스트 주제');
|
||||||
|
expect(p).toContain('title: "테스트 주제"');
|
||||||
|
expect(p).toContain('raw_sources: ["https://ex.com/doc"]');
|
||||||
|
expect(p).not.toContain('{{'); // placeholder 잔존 금지
|
||||||
|
});
|
||||||
|
|
||||||
|
test('정본 없으면 내장 사본 — 현대화 항목 포함', () => {
|
||||||
|
const p = buildWikifyPrompt(extracted, '주제', null);
|
||||||
|
expect(p).toContain('category는 폴더경로 말고 논리 도메인'); // 물리 경로 버그 수정
|
||||||
|
expect(p).toContain("aliases'에 동의어"); // 별칭 강제
|
||||||
|
expect(p).toContain('지식 충돌 시 우선순위 판단'); // 동적 신뢰도 — 충돌 권고 입력
|
||||||
|
expect(p).toContain('## 🔗 지식 그래프'); // 섹션명 통일 (구 "관련 문서 링크" 대체)
|
||||||
|
expect(p).not.toContain('category: "10_Wiki/Topics"');
|
||||||
|
expect(p).not.toContain('{{');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user