revert: ASTRA 이메일 기능 제거 — Datacollect wiki화로 피벗

Revert "feat(astra): 이메일 Settings 패널 섹션" (eb4bef0)
Revert "feat(astra): Project Astra 이메일 자산화 Phase 1+2" (7e96e56)

방향 전환: 이메일은 ASTRA에 전용 소스로 넣는 대신 Datacollect가 수집·wiki화해
brain(제2뇌)에 저장하고, ASTRA는 기존 brain 검색으로 그대로 활용한다.
Gmail 인증은 Datacollect 소유. /email-status(라이브 현황)는 폐기.
gmailApi 파싱 로직은 Datacollect 이전 시 재사용 예정.

타입체크·빌드 통과.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 19:15:19 +09:00
parent eb4bef0744
commit 681cfd2393
15 changed files with 6 additions and 892 deletions
+1 -2
View File
@@ -119,8 +119,7 @@ export function assembleContext(chunks: RetrievalChunk[]): string {
'procedural-memory': '📋 Procedural Memory (반복 절차)',
'episodic-memory': '📖 Episodic Memory (과거 대화 흐름)',
'project-scan': '🔍 Project Scan',
'recent-knowledge': '📄 Recent Project Knowledge',
'email': '📧 이메일 (수집된 메일 근거 — 원문 링크 포함)'
'recent-knowledge': '📄 Recent Project Knowledge'
};
// Group by source
+1 -96
View File
@@ -26,7 +26,6 @@ import { extractLessonEssence } from './lessonHelpers';
import { cosineSimilarity } from './embeddings';
import { applyActionabilityBoost, WorkStateSignals, ActionabilityWeights } from './actionabilityScoring';
import { applyHierarchicalReweight, classifyQueryLevel, AbstractionLevel, HierarchicalWeights } from './hierarchicalLevel';
import { loadEmailRecords } from '../features/email/emailStore';
export { tokenize, expandQuery, scoreTfIdf, scoreTfIdfPreTokenized, extractBestExcerpt } from './scoring';
export { selectWithinBudget, assembleContext, estimateTokens } from './contextBudget';
@@ -84,8 +83,6 @@ interface RetrievalOptions {
embeddingModel?: string;
/** Blend weight: 0 = TF-IDF only, 1 = cosine only. Default 0.5. */
embeddingBlendAlpha?: number;
/** Project Astra — email 소스에서 가져올 최대 메일 수. 기본 6. 0 이면 스킵. */
emailLimit?: number;
/**
* Actionability — "현재 작업 상태" 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과 재가중.
* undefined 면 actionability re-rank 안 함 (legacy 동작).
@@ -154,19 +151,6 @@ export class RetrievalOrchestrator {
allChunks.push(...memoryChunks);
fusionLog.push(`Memory search: ${memoryChunks.length} chunks found`);
// ── ①-b Email Search (Project Astra) — 수집된 메일에서 근거 검색 ──
// 인덱스가 비어 있으면(미수집) 즉시 [] 반환하므로 비용 0.
const emailChunks = this.searchEmailIndex(
expandedTokens,
options.brain,
options.emailLimit ?? 6,
options.queryEmbedding,
options.embeddingModel,
options.embeddingBlendAlpha,
);
allChunks.push(...emailChunks);
if (emailChunks.length > 0) fusionLog.push(`Email search: ${emailChunks.length} chunks found`);
// ── ②-b Medium-Term Memory (recent sessions) ──
const mediumChunks = this.scoreRecentSessions(
expandedTokens,
@@ -359,84 +343,6 @@ export class RetrievalOrchestrator {
}
}
// ─── Email Search (Project Astra) ───
/**
* 수집된 이메일 인덱스({brainPath}/memory/email_index.json)에서 TF-IDF 로 근거 메일을
* 찾는다. 브레인 파일 검색과 *동일한* scoreTfIdfPreTokenized 를 써서 일관성 유지.
* 각 청크는 messageId/permalink 메타를 실어 답변이 원문 메일로 점프할 수 있게 한다.
* Phase 1 은 키워드(TF-IDF)만 — 임베딩 블렌드는 Phase 2.
*/
private searchEmailIndex(
expandedTokens: string[],
brain: BrainProfile,
limit: number,
queryEmbedding?: number[],
embeddingModel?: string,
embeddingBlendAlpha?: number,
): RetrievalChunk[] {
if (limit <= 0) return [];
try {
const records = loadEmailRecords(brain.localBrainPath);
if (records.length === 0) return [];
const scored = scoreTfIdfPreTokenized(
expandedTokens,
records.map((r) => ({
tokens: r.tokens,
titleTokens: r.subjectTokens,
lastModified: r.date,
conflictCount: 0,
})),
);
// (Phase 2) 하이브리드 블렌드 — 브레인 검색과 동일 공식. 같은 모델로 임베딩된
// 레코드만 코사인 가산, 없으면 순수 TF-IDF 유지(graceful fallback).
if (queryEmbedding && embeddingModel && (embeddingBlendAlpha ?? 0) > 0) {
const alpha = Math.max(0, Math.min(1, embeddingBlendAlpha!));
const maxTfidf = scored.reduce((m, s) => s.score > m ? s.score : m, 0) || 1;
for (const s of scored) {
const rec = records[s.index];
if (!rec.embedding || rec.embeddingModel !== embeddingModel) continue;
const cos = cosineSimilarity(queryEmbedding, rec.embedding);
const tfidfNorm = s.score / maxTfidf;
s.score = (1 - alpha) * tfidfNorm + alpha * Math.max(0, cos);
}
}
const ranked = scored.filter((x) => x.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
const out: RetrievalChunk[] = [];
for (const s of ranked) {
const r = records[s.index];
const dateStr = new Date(r.date).toISOString().slice(0, 10);
const title = `메일: "${r.subject || '(제목 없음)'}" (${dateStr}, from ${r.from})`;
const body = summarizeText(r.bodyText || r.snippet || '', 700);
const content = `${body}${r.permalink ? `\n[원문 링크] ${r.permalink}` : ''}`;
out.push({
id: `email-${r.messageId}`,
source: 'email' as const,
title,
content,
score: s.score,
tokenEstimate: estimateTokens(content),
metadata: {
category: 'email',
lastUpdated: r.date,
queryCoverage: s.queryCoverage,
emailMessageId: r.messageId,
emailThreadId: r.threadId,
emailFrom: r.from,
emailSubject: r.subject,
emailDate: r.date,
emailPermalink: r.permalink,
},
});
}
return out;
} catch {
return [];
}
}
// ─── Memory Layer Search ───
private searchMemoryLayers(
@@ -607,8 +513,7 @@ export class RetrievalOrchestrator {
'medium-term-memory': 0.78, // recent sessions: useful when the user references "last time / yesterday"
'episodic-memory': 0.7,
'project-scan': 0.6,
'recent-knowledge': 0.75,
'email': 0.9 // 메일은 직접 근거 — 브레인 노트와 동급으로 취급
'recent-knowledge': 0.75
};
for (const chunk of chunks) {
+1 -11
View File
@@ -16,8 +16,7 @@ export type RetrievalSource =
| 'procedural-memory' // Procedural Memory
| 'episodic-memory' // Episodic Memory
| 'project-scan' // Local Project Path scan
| 'recent-knowledge' // Recent Project Knowledge record
| 'email'; // Project Astra — 수집된 Gmail/이메일
| 'recent-knowledge'; // Recent Project Knowledge record
export type ConflictSeverity = 'NONE' | 'LOW' | 'MEDIUM' | 'HIGH';
@@ -44,15 +43,6 @@ export interface RetrievalChunk {
isLesson?: boolean;
/** 'lesson' | 'playbook' | 'qa-finding' when isLesson is true. */
lessonKind?: string;
// --- Email (Project Astra) — 출처 추적: 원문 메일로 점프 ---
emailMessageId?: string;
emailThreadId?: string;
emailFrom?: string;
emailSubject?: string;
emailDate?: number;
/** 원문 메일 딥링크. */
emailPermalink?: string;
};
}