Feat: Enhance query intent search and answer format readability

This commit is contained in:
g1nation
2026-05-02 18:20:22 +09:00
parent 26bbb22c7e
commit da4ebe3942
9 changed files with 173 additions and 12 deletions
+58 -8
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import { findBrainFiles, summarizeText } from '../utils';
export type SecondBrainSourceType = 'Project Evidence' | 'User Decision' | 'General Knowledge' | 'Reference Only';
export type SecondBrainQueryIntent = 'technical' | 'ux-business' | 'governance' | 'general';
export interface SecondBrainTraceDocument {
title: string;
@@ -21,6 +22,7 @@ export interface SecondBrainTraceDocument {
export interface SecondBrainTrace {
userQuery: string;
queryIntent: SecondBrainQueryIntent;
shouldUseSecondBrain: boolean;
secondBrainUsed: boolean;
reason: string;
@@ -38,9 +40,11 @@ export function buildSecondBrainTrace(userQuery: string, brainRoot: string, opti
} = {}): SecondBrainTrace {
const query = userQuery.trim();
const shouldUseSecondBrain = !!options.force || shouldUseBrain(query);
const retrievalQuery = buildRetrievalQuery(query);
const queryIntent = classifyQueryIntent(query);
const retrievalQuery = buildRetrievalQuery(query, queryIntent);
const baseTrace: SecondBrainTrace = {
userQuery: query,
queryIntent,
shouldUseSecondBrain,
secondBrainUsed: false,
reason: shouldUseSecondBrain
@@ -66,7 +70,7 @@ export function buildSecondBrainTrace(userQuery: string, brainRoot: string, opti
const files = findBrainFiles(brainRoot)
.filter((file) => includeRaw || !isRawConversationPath(path.relative(brainRoot, file)));
const terms = tokenize(retrievalQuery);
const scored = files.map((file) => scoreFile(file, brainRoot, terms))
const scored = files.map((file) => scoreFile(file, brainRoot, terms, queryIntent))
.filter((doc) => doc.score >= 0.25)
.sort((a, b) => b.score - a.score)
.slice(0, options.limit || 5);
@@ -134,6 +138,7 @@ export function renderSecondBrainTraceContext(trace: SecondBrainTrace): string {
return [
'[SECOND BRAIN TRACE]',
`Second Brain used: ${trace.secondBrainUsed ? 'yes' : 'no'}`,
`Query intent: ${trace.queryIntent}`,
`Retrieval query: ${trace.retrievalQuery}`,
`Reason: ${trace.reason}`,
docs ? `Selected notes:\n${docs}` : 'Selected notes: none',
@@ -143,6 +148,12 @@ export function renderSecondBrainTraceContext(trace: SecondBrainTrace): string {
'No Evidence, No Project Claim: do not state that the current project has a technical structure unless it is supported by user-provided facts, source code, design docs, project docs, or project records.',
'General Knowledge notes can explain concepts, but cannot prove the current project actually implements those concepts.',
'Classify major claims as Confirmed, Inference, General Knowledge, or Needs Verification when the answer discusses a project.',
trace.queryIntent === 'ux-business'
? 'This is a UX/business/approval-fit question. Prefer customer journey, product discovery, requirement fit, business value, stakeholder approval, acceptance criteria, and conversion-flow reasoning over technical architecture reasoning.'
: '',
trace.queryIntent === 'ux-business'
? 'Approval likelihood is an inference unless explicit approval criteria are provided. Use wording such as "보완 요청 가능성이 높습니다", "승인 가능성을 높일 수 있습니다", and "확인 필요".'
: '',
hasProjectEvidence
? 'At least one selected note can support project-specific claims.'
: 'No selected note can support project-specific implementation claims.',
@@ -189,6 +200,9 @@ export function renderSecondBrainTraceMarkdown(trace: SecondBrainTrace, debug: b
'## 2nd Brain 사용 여부',
status,
'',
'## 질문 의도',
trace.queryIntent,
'',
'## 이유',
trace.reason,
'',
@@ -214,6 +228,7 @@ export function renderSecondBrainTraceMarkdown(trace: SecondBrainTrace, debug: b
JSON.stringify({
secondBrainUsed: trace.secondBrainUsed,
shouldUseSecondBrain: trace.shouldUseSecondBrain,
queryIntent: trace.queryIntent,
retrievalQuery: trace.retrievalQuery,
searchedCollections: trace.searchedCollections,
retrievedDocuments: trace.retrievedDocuments.map((doc) => ({
@@ -311,11 +326,35 @@ function deriveProjectClaimPolicy(
function shouldUseBrain(query: string): boolean {
const normalized = query.toLowerCase();
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|문서|노트|내가|우리|프로젝트|결정|adr|chronicle|가드|설계 원칙|mvp|제외|왜|dependency|schema|documentation|drift|integration|overhead|의존성|스키마|문서화)/i.test(normalized);
return /(second brain|2nd brain|제2뇌|브레인|brain|기억|기록|문서|노트|내가|우리|프로젝트|결정|adr|chronicle|가드|설계 원칙|mvp|제외|왜|dependency|schema|documentation|drift|integration|overhead|의존성|스키마|문서화|고객|사용자|ux|경험|구매|전환|상품|공간|요구사항|승인|평가|비즈니스|가치|stakeholder|approval|customer|journey|conversion|requirement)/i.test(normalized);
}
function buildRetrievalQuery(query: string): string {
return tokenize(query).slice(0, 16).join(' ');
function classifyQueryIntent(query: string): SecondBrainQueryIntent {
const normalized = query.toLowerCase();
if (/(고객|사용자|ux|경험|구매|전환|상품|공간|요구사항|승인|평가|비즈니스|가치|웹스토어|virtual store|stakeholder|approval|customer|journey|conversion|requirement|acceptance|product discovery|business value)/i.test(normalized)) {
return 'ux-business';
}
if (/(dependency|schema|documentation|drift|integration|overhead|의존성|스키마|문서화|보안|검증|리스크|governance|reliability)/i.test(normalized)) {
return 'governance';
}
if (/(api|gateway|architecture|microservice|monolith|database|backend|frontend|routing|아키텍처|기술|라우팅|데이터|서버|클라이언트)/i.test(normalized)) {
return 'technical';
}
return 'general';
}
function buildRetrievalQuery(query: string, intent: SecondBrainQueryIntent): string {
const intentTerms: Record<SecondBrainQueryIntent, string[]> = {
'ux-business': [
'ux', 'customer journey', 'product discovery', 'virtual store', 'stakeholder approval',
'requirement fit', 'business value proposition', 'acceptance criteria', 'conversion flow',
'고객 경험', '상품 탐색', '구매 전환', '요구사항 적합성', '승인 기준', '비즈니스 가치'
],
technical: ['architecture', 'routing', 'api', 'implementation', 'source code', 'design document'],
governance: ['dependency', 'schema drift', 'documentation', 'validation', 'risk', 'decision'],
general: []
};
return [...tokenize(query), ...intentTerms[intent]].slice(0, 28).join(' ');
}
function tokenize(value: string): string[] {
@@ -331,7 +370,7 @@ function tokenize(value: string): string[] {
.filter((term) => term.length >= 2 && !stopWords.has(term));
}
function scoreFile(file: string, brainRoot: string, terms: string[]): SecondBrainTraceDocument {
function scoreFile(file: string, brainRoot: string, terms: string[], intent: SecondBrainQueryIntent): SecondBrainTraceDocument {
const relative = path.relative(brainRoot, file);
const title = path.basename(file, path.extname(file));
const basename = relative.toLowerCase();
@@ -345,7 +384,7 @@ function scoreFile(file: string, brainRoot: string, terms: string[]): SecondBrai
const canSupportProjectClaim = sourceType === 'Project Evidence' || sourceType === 'User Decision';
const lower = content.toLowerCase();
let score = pathPriority(relative);
let score = pathPriority(relative, intent);
for (const term of terms) {
if (basename.includes(term)) score += 4;
const matches = lower.split(term).length - 1;
@@ -388,7 +427,7 @@ function isRawConversationPath(relativePath: string): boolean {
return /(^|[\\/])(00_Raw|raw-data|conversations?|transcripts?)([\\/]|$)/i.test(relativePath);
}
function pathPriority(relativePath: string): number {
function pathPriority(relativePath: string, intent: SecondBrainQueryIntent): number {
const normalized = relativePath.toLowerCase();
let score = 0;
if (/(^|[\\/])(decisions?|adr|planning|development|bugs|retrospectives|records)([\\/]|$)/i.test(normalized)) {
@@ -403,6 +442,17 @@ function pathPriority(relativePath: string): number {
if (/(^|[\\/])index(_\d+)?\.md$/i.test(normalized) || /[\\/]index\.md$/i.test(normalized)) {
score -= 2;
}
if (intent === 'ux-business') {
if (/(ux|customer|journey|product|discovery|virtual|store|stakeholder|approval|requirement|business|value|acceptance|conversion|commerce|webstore|고객|사용자|경험|상품|구매|전환|공간|요구사항|승인|평가|비즈니스|가치)/i.test(normalized)) {
score += 5;
}
if (/(api|gateway|microservice|monolithic|backend|database|routing|architecture_principles|programming)/i.test(normalized)) {
score -= 3;
}
}
if (intent === 'technical' && /(api|gateway|microservice|architecture|routing|backend|database)/i.test(normalized)) {
score += 2;
}
return score;
}