Update Astra: v2.80.19 - Refactoring Sidebar, LM Studio integration, and new tests

This commit is contained in:
g1nation
2026-05-08 23:14:47 +09:00
parent d083177d95
commit 5ffb472d22
28 changed files with 3125 additions and 1797 deletions
+39 -24
View File
@@ -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,