Update Astra: v2.80.19 - Refactoring Sidebar, LM Studio integration, and new tests
This commit is contained in:
@@ -85,7 +85,12 @@ export function buildSecondBrainTrace(userQuery: string, brainRoot: string, opti
|
||||
const terms = tokenize(retrievalQuery);
|
||||
const knowledgeSlots = buildKnowledgeSlots(query, queryIntent);
|
||||
const targetProject = inferTargetProject(query);
|
||||
const scored = files.map((file) => scoreFile(file, brainRoot, terms, queryIntent, targetProject))
|
||||
|
||||
// Read each file from disk only once per request and reuse the parsed scan
|
||||
// for every (query terms, slot terms…) re-scoring pass below.
|
||||
const scans = files.map((file) => scanFile(file, brainRoot));
|
||||
|
||||
const scored = scans.map((scan) => scoreScan(scan, terms, queryIntent, targetProject))
|
||||
.filter((doc) => doc.score >= 0.25)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, options.limit || (knowledgeSlots.length > 0 ? 8 : 5));
|
||||
@@ -94,9 +99,9 @@ export function buildSecondBrainTrace(userQuery: string, brainRoot: string, opti
|
||||
const slotDocByPath = new Map<string, SecondBrainTraceDocument>();
|
||||
const slotSelections = knowledgeSlots.map((slot) => {
|
||||
const slotTerms = tokenize(slot.retrievalQuery);
|
||||
const slotCandidates = files
|
||||
.map((file) => {
|
||||
const doc = scoreFile(file, brainRoot, slotTerms, queryIntent, targetProject);
|
||||
const slotCandidates = scans
|
||||
.map((scan) => {
|
||||
const doc = scoreScan(scan, slotTerms, queryIntent, targetProject);
|
||||
// 슬롯 ID와 문서 디렉토리명 매칭 보너스 (e.g. ontology 슬롯 → Ontology/ 디렉토리)
|
||||
const dirName = path.dirname(doc.path).toLowerCase();
|
||||
if (dirName.includes(slot.id.toLowerCase())) {
|
||||
@@ -567,10 +572,21 @@ function inferTargetProject(query: string): string | undefined {
|
||||
return namedProject?.[1]?.toLowerCase();
|
||||
}
|
||||
|
||||
function scoreFile(file: string, brainRoot: string, terms: string[], intent: SecondBrainQueryIntent, targetProject?: string): SecondBrainTraceDocument {
|
||||
interface FileScan {
|
||||
file: string;
|
||||
relative: string;
|
||||
title: string;
|
||||
titleWithPath: string;
|
||||
content: string;
|
||||
lower: string;
|
||||
sourceType: SecondBrainSourceType;
|
||||
knowledgeRole: SecondBrainKnowledgeRole;
|
||||
documentProject: string | undefined;
|
||||
}
|
||||
|
||||
function scanFile(file: string, brainRoot: string): FileScan {
|
||||
const relative = path.relative(brainRoot, file);
|
||||
const title = path.basename(file, path.extname(file));
|
||||
const basename = relative.toLowerCase();
|
||||
let content = '';
|
||||
try {
|
||||
content = fs.readFileSync(file, 'utf8');
|
||||
@@ -579,37 +595,36 @@ function scoreFile(file: string, brainRoot: string, terms: string[], intent: Sec
|
||||
}
|
||||
const sourceType = classifySourceType(relative, content);
|
||||
const knowledgeRole = classifyKnowledgeRole(relative, content, sourceType);
|
||||
|
||||
const lower = content.toLowerCase();
|
||||
const documentProject = inferDocumentProject(relative, lower);
|
||||
const projectMatchesTarget = !targetProject || !documentProject || documentProject === targetProject;
|
||||
const canSupportProjectClaim = projectMatchesTarget && (sourceType === 'Project Evidence' || sourceType === 'User Decision');
|
||||
let score = pathPriority(relative, intent);
|
||||
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
|
||||
return { file, relative, title, titleWithPath, content, lower, sourceType, knowledgeRole, documentProject };
|
||||
}
|
||||
|
||||
function scoreScan(scan: FileScan, terms: string[], intent: SecondBrainQueryIntent, targetProject?: string): SecondBrainTraceDocument {
|
||||
const projectMatchesTarget = !targetProject || !scan.documentProject || scan.documentProject === targetProject;
|
||||
const canSupportProjectClaim = projectMatchesTarget && (scan.sourceType === 'Project Evidence' || scan.sourceType === 'User Decision');
|
||||
let score = pathPriority(scan.relative, intent);
|
||||
if (targetProject) {
|
||||
score += projectRelevanceScore(relative, lower, targetProject, documentProject);
|
||||
score += projectRelevanceScore(scan.relative, scan.lower, targetProject, scan.documentProject);
|
||||
}
|
||||
const expandedTerms = expandQuery(terms);
|
||||
// 디렉토리 경로를 title에 포함하여 카테고리 키워드 매칭 향상 (e.g. Ontology/ → 'ontology' 토큰)
|
||||
const titleWithPath = `${relative.replace(/[\\/]/g, ' ')} ${title}`;
|
||||
const scoredTfIdf = scoreTfIdf(expandedTerms, [{ title: titleWithPath, content, lastModified: Date.now() }])[0];
|
||||
|
||||
const scoredTfIdf = scoreTfIdf(expandedTerms, [{ title: scan.titleWithPath, content: scan.content, lastModified: Date.now() }])[0];
|
||||
score += scoredTfIdf.score;
|
||||
|
||||
if (knowledgeRole === 'routing-hint') {
|
||||
if (scan.knowledgeRole === 'routing-hint') {
|
||||
score -= 8;
|
||||
}
|
||||
|
||||
const finalExcerpt = extractBestExcerpt(content, expandedTerms, 420);
|
||||
const finalExcerpt = extractBestExcerpt(scan.content, expandedTerms, 420);
|
||||
|
||||
return {
|
||||
title,
|
||||
path: relative,
|
||||
absolutePath: file,
|
||||
title: scan.title,
|
||||
path: scan.relative,
|
||||
absolutePath: scan.file,
|
||||
// sqrt 정규화: 동의어 확장으로 분모가 과도하게 커지는 것을 방지
|
||||
score: Number((Math.max(score, 0) / Math.max(Math.sqrt(expandedTerms.length), 1)).toFixed(2)),
|
||||
excerpt: summarizeText(finalExcerpt, 420),
|
||||
sourceType,
|
||||
knowledgeRole,
|
||||
sourceType: scan.sourceType,
|
||||
knowledgeRole: scan.knowledgeRole,
|
||||
canSupportProjectClaim,
|
||||
warning: canSupportProjectClaim ? undefined : '이 문서는 현재 프로젝트의 실제 구현 근거가 아닙니다.',
|
||||
usedInAnswer: false,
|
||||
|
||||
Reference in New Issue
Block a user