mcp-magma-handbook
Version:
Enhanced MCP server with multi-query search, hybrid search, and collections for MAGMA computational algebra system
295 lines • 11.9 kB
JavaScript
// Polyfill fetch for Node.js
import fetch from 'node-fetch';
// @ts-ignore
global.fetch = fetch;
import { createClient } from '@supabase/supabase-js';
import { OpenAIEmbeddings } from '@langchain/openai';
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
export class SupabaseMagmaKnowledgeBase {
supabase;
embeddings;
textSplitter;
isInitialized = false;
constructor() {
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
if (!supabaseUrl || !supabaseKey) {
throw new Error('SUPABASE_URL and SUPABASE_KEY must be set');
}
this.supabase = createClient(supabaseUrl, supabaseKey);
this.embeddings = new OpenAIEmbeddings({
modelName: 'text-embedding-3-small', // Advanced와 동일한 모델
dimensions: 1536, // Advanced와 동일한 차원
});
// MAGMA 코드에 최적화된 텍스트 분할기
this.textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 2000, // 더 큰 청크로 맥락 보존
chunkOverlap: 400, // 더 많은 오버랩
separators: [
'\n\n\n\n', // 챕터 구분
'\n\n\n', // 섹션 구분
'\n\n', // 단락 구분
'\n>', // MAGMA 명령 시작
';', // MAGMA 문장 종료
'\n', // 줄 구분
'.', // 일반 문장
' ', // 공백
''
],
});
}
async initialize() {
if (this.isInitialized)
return;
try {
// 테이블 생성 (pgvector 확장 필요)
await this.createTables();
// 문서 수 확인
const { count } = await this.supabase
.from('magma_documents')
.select('*', { count: 'exact', head: true });
if (count === 0) {
console.error('No documents in database. Please run indexing first.');
console.error('Place MAGMA_HANDBOOK.pdf in data/pdfs/ and run: npm run index');
}
else {
console.error(`Loaded database with ${count} documents`);
}
this.isInitialized = true;
}
catch (error) {
console.error('Failed to initialize knowledge base:', error);
throw error;
}
}
async createTables() {
// 테이블 존재 확인 (RPC 호출 대신 직접 접근)
const { error } = await this.supabase
.from('magma_documents')
.select('id')
.limit(1);
if (error) {
console.error('Tables not found. Please run the SQL schema in Supabase Dashboard first.');
console.error('Error:', error.message);
throw new Error('Database tables not initialized. Please run supabase/schema.sql in Supabase Dashboard.');
}
}
async indexPDF(pdfPath) {
console.log(`Indexing PDF: ${pdfPath}`);
// Load PDF
const loader = new PDFLoader(pdfPath);
const docs = await loader.load();
// Split documents
const splitDocs = await this.textSplitter.splitDocuments(docs);
// 임베딩 생성
const contents = splitDocs.map(doc => doc.pageContent);
const embeddings = await this.embeddings.embedDocuments(contents);
// 문서 처리
const processedDocs = splitDocs.map((doc, index) => ({
content: doc.pageContent,
embedding: embeddings[index],
metadata: {
...doc.metadata,
category: this.categorizeContent(doc.pageContent),
indexed_at: new Date().toISOString(),
}
}));
// 배치 삽입
const batchSize = 50;
for (let i = 0; i < processedDocs.length; i += batchSize) {
const batch = processedDocs.slice(i, i + batchSize);
const { error } = await this.supabase
.from('magma_documents')
.insert(batch);
if (error) {
console.error('Error inserting batch:', error);
throw error;
}
console.log(`Inserted batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(processedDocs.length / batchSize)}`);
}
console.log(`Indexed ${processedDocs.length} documents from ${pdfPath}`);
}
categorizeContent(content) {
const lowerContent = content.toLowerCase();
// MAGMA 함수 패턴 감지
const functionPatterns = [
/^[A-Z][a-zA-Z]*\s*\(/m, // 함수 시그니처
/intrinsic\s+[A-Z]/i, // intrinsic 선언
/procedure\s+[A-Z]/i, // procedure 선언
/function\s+[A-Z]/i // function 선언
];
// 예제 패턴 감지
const examplePatterns = [
/example\s+h\d+e\d+/i, // Example H120E1 패턴
/^>\s+/m, // MAGMA 명령 프롬프트
/^> [A-Z]/m // MAGMA 명령
];
// 함수 검사
if (functionPatterns.some(pattern => pattern.test(content))) {
return 'function';
}
// 예제 검사
if (examplePatterns.some(pattern => pattern.test(content))) {
return 'example';
}
// 키워드 기반 분류
if (lowerContent.includes('syntax') || lowerContent.includes('::=') ||
lowerContent.includes('grammar')) {
return 'syntax';
}
else if (lowerContent.includes('algorithm') ||
lowerContent.includes('procedure') ||
lowerContent.includes('method')) {
return 'algorithm';
}
else if (lowerContent.includes('theorem') ||
lowerContent.includes('lemma') ||
lowerContent.includes('proposition')) {
return 'theory';
}
return 'general';
}
async search(query, limit = 5, category = 'all') {
// 쿼리 임베딩 생성
const queryEmbedding = await this.embeddings.embedQuery(query);
// Supabase 벡터 검색 (더 높은 품질 임계값)
let rpcQuery = this.supabase.rpc('search_magma_documents', {
query_embedding: queryEmbedding,
similarity_threshold: 0.4, // 더 엄격한 임계값
match_count: limit * 2 // 더 많이 가져와서 필터링
});
// 카테고리 필터링
if (category !== 'all') {
rpcQuery = rpcQuery.eq('metadata->>category', category);
}
const { data, error } = await rpcQuery;
if (error) {
console.error('Search error:', error);
throw error;
}
return data.map((doc) => ({
content: doc.content,
metadata: doc.metadata,
score: doc.similarity,
}));
}
async getExamples(topic, complexity = 'basic') {
// Search for examples related to the topic
const searchResults = await this.search(`${topic} example code`, 10, 'example');
// Extract and format MAGMA code examples
const examples = searchResults
.map(result => {
const codeMatches = result.content.match(/```magma([\s\S]*?)```|>(.*?)$/gm);
if (codeMatches) {
return {
title: this.extractTitle(result.content),
code: this.cleanMagmaCode(codeMatches[0]),
explanation: this.extractExplanation(result.content),
complexity: this.assessComplexity(codeMatches[0]),
source: result.metadata.source,
page: result.metadata.page,
};
}
return null;
})
.filter(Boolean)
.filter(ex => complexity === 'all' || ex?.complexity === complexity);
return examples;
}
async explainCode(code, context) {
// Search for relevant documentation about the functions used in the code
const functions = this.extractFunctions(code);
const searchPromises = functions.map(func => this.search(`${func} function syntax usage`, 3, 'function'));
const searchResults = await Promise.all(searchPromises);
const relevantDocs = searchResults.flat();
// Build explanation
let explanation = '# MAGMA Code Explanation\n\n';
explanation += '## Code:\n```magma\n' + code + '\n```\n\n';
if (context) {
explanation += `## Context:\n${context}\n\n`;
}
explanation += '## Analysis:\n';
// Parse and explain the code structure
const lines = code.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.trim().startsWith('//'))
continue;
const analysis = this.analyzeLine(line, relevantDocs);
if (analysis) {
explanation += `- ${analysis}\n`;
}
}
// Add relevant function documentation
if (functions.length > 0) {
explanation += '\n## Related Functions:\n';
const uniqueFunctions = [...new Set(functions)];
for (const func of uniqueFunctions) {
const funcDocs = relevantDocs.filter(doc => doc.content.toLowerCase().includes(func.toLowerCase()));
if (funcDocs.length > 0) {
explanation += `\n### ${func}\n`;
explanation += funcDocs[0].content.substring(0, 300) + '...\n';
}
}
}
return explanation;
}
extractFunctions(code) {
const functionPattern = /([A-Z][a-zA-Z0-9]*)\s*\(/g;
const matches = code.match(functionPattern) || [];
return matches.map(m => m.replace('(', '').trim());
}
analyzeLine(line, docs) {
const trimmed = line.trim();
if (trimmed.includes(':=')) {
const [varName, value] = trimmed.split(':=').map(s => s.trim());
return `\`${varName}\` is assigned the value/result of \`${value}\``;
}
const funcMatch = trimmed.match(/([A-Z][a-zA-Z0-9]*)\s*\(/);
if (funcMatch) {
return `Calls function \`${funcMatch[1]}\``;
}
if (trimmed.startsWith('if') || trimmed.startsWith('while') || trimmed.startsWith('for')) {
return `Control structure: ${trimmed.split(' ')[0]}`;
}
return null;
}
extractTitle(content) {
const lines = content.split('\n');
for (const line of lines) {
if (line.trim() && !line.startsWith('>') && line.length < 100) {
return line.trim();
}
}
return 'MAGMA Example';
}
cleanMagmaCode(code) {
return code
.replace(/```magma/g, '')
.replace(/```/g, '')
.replace(/^>\s*/gm, '')
.trim();
}
extractExplanation(content) {
const parts = content.split(/```magma[\s\S]*?```|>.*$/gm);
return parts
.map(p => p.trim())
.filter(p => p.length > 20)
.join(' ')
.substring(0, 200);
}
assessComplexity(code) {
const lines = code.split('\n').length;
const hasLoops = /for|while/.test(code);
const hasFunctions = /function|procedure/.test(code);
const complexFunctions = /Factorization|IsIrreducible|GaloisGroup/.test(code);
if (complexFunctions || (hasLoops && hasFunctions) || lines > 20) {
return 'advanced';
}
else if (hasLoops || hasFunctions || lines > 10) {
return 'intermediate';
}
return 'basic';
}
}
//# sourceMappingURL=supabase-knowledge-base.js.map