Skip to main content
Glama
http_server.py71.4 kB
"""HTTP server for MCP-RAG configuration and document management.""" import logging from pathlib import Path from typing import Dict, Any, List from fastapi import FastAPI, HTTPException, UploadFile, File, Form from fastapi.staticfiles import StaticFiles from fastapi.responses import HTMLResponse, RedirectResponse from pydantic import BaseModel import tempfile import shutil from .config import config_manager, settings from .database import get_vector_database from .embedding import get_embedding_model from .document_processor import get_document_processor logger = logging.getLogger(__name__) app = FastAPI(title="MCP-RAG HTTP API", description="API for configuring MCP-RAG and adding documents") # Mount static files static_path = Path(__file__).parent / "static" static_path.mkdir(exist_ok=True) app.mount("/static", StaticFiles(directory=str(static_path)), name="static") class ConfigUpdate(BaseModel): """Configuration update model.""" key: str value: Any class BulkConfigUpdate(BaseModel): """Bulk configuration update model.""" updates: Dict[str, Any] class DocumentAdd(BaseModel): """Document addition model.""" content: str collection: str = "default" metadata: Dict[str, Any] = {} class FileUploadResponse(BaseModel): """File upload response model.""" filename: str file_type: str content_length: int processed: bool error: str = "" preview: str = "" class BatchUploadResponse(BaseModel): """Batch file upload response model.""" total_files: int successful: int failed: int results: List[FileUploadResponse] class DeleteDocumentRequest(BaseModel): """Delete document request model.""" document_id: str collection: str = "default" class DeleteFileRequest(BaseModel): """Delete file request model.""" filename: str collection: str = "default" @app.get("/") async def root(): """Root endpoint - redirect to documents page.""" return RedirectResponse(url="/documents-page") @app.get("/documents-page", response_class=HTMLResponse) async def documents_page(): """Serve the documents management page.""" html_content = """ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP-RAG 资料管理</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1400px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #333; text-align: center; margin-bottom: 30px; } .tabs { display: flex; margin-bottom: 30px; border-bottom: 1px solid #ddd; } .tab { padding: 10px 20px; cursor: pointer; background: #f8f9fa; border: 1px solid #ddd; border-bottom: none; margin-right: 5px; border-radius: 5px 5px 0 0; } .tab.active { background: white; border-bottom: 1px solid white; margin-bottom: -1px; } .tab-content { display: none; } .tab-content.active { display: block; } .section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; } .section h2 { color: #555; margin-top: 0; border-bottom: 2px solid #007acc; padding-bottom: 10px; } .upload-area { border: 2px dashed #007acc; border-radius: 10px; padding: 40px; text-align: center; background: #f8f9fa; transition: all 0.3s; } .upload-area:hover { background: #e9ecef; border-color: #005aa3; } .upload-area.dragover { background: #e3f2fd; border-color: #2196f3; } .file-input { display: none; } .upload-btn { background-color: #007acc; color: white; padding: 12px 24px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; margin: 10px; } .upload-btn:hover { background-color: #005aa3; } .file-list { margin-top: 20px; } .file-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 10px; background: #f8f9fa; } .file-info { flex: 1; } .file-name { font-weight: bold; } .file-meta { color: #666; font-size: 14px; } .file-status { padding: 5px 10px; border-radius: 3px; font-size: 12px; font-weight: bold; } .status-success { background: #d4edda; color: #155724; } .status-error { background: #f8d7da; color: #721c24; } .status-processing { background: #fff3cd; color: #856404; } .preview-content { background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; padding: 15px; margin-top: 10px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; font-family: monospace; font-size: 14px; } .btn { background-color: #007acc; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-right: 5px; } .btn:hover { background-color: #005aa3; } .btn-danger { background-color: #dc3545; } .btn-danger:hover { background-color: #c82333; } .btn-success { background-color: #28a745; } .btn-success:hover { background-color: #218838; border-radius: 10px; overflow: hidden; margin: 10px 0; } .progress-fill { height: 100%; background: #007acc; width: 0%; transition: width 0.3s; } .status-message { margin-top: 20px; padding: 10px; border-radius: 4px; display: none; } .status-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .chat-message { margin-bottom: 10px; } .chat-message.user { text-align: right; } .chat-message.assistant { text-align: left; } .view-toggle { margin-bottom: 15px; } .view-toggle .btn { margin-right: 5px; background-color: #6c757d; } .view-toggle .btn.active { background-color: #007bff; } </style> </head> <body> <div class="container"> <div style="text-align: center; margin-bottom: 20px;"> <a href="/config-page" style="margin: 0 10px; color: #007acc; text-decoration: none;">配置管理</a> | <a href="/documents-page" style="margin: 0 10px; color: #007acc; text-decoration: none;">资料管理</a> </div> <h1>MCP-RAG 资料管理</h1> <div class="tabs"> <div class="tab active" onclick="switchTab('upload')">资料上传</div> <div class="tab" onclick="switchTab('search')">资料查询</div> <div class="tab" onclick="switchTab('chat')">知识库对话</div> <div class="tab" onclick="switchTab('manage')">内容管理</div> </div> <div id="upload" class="tab-content active"> <div class="section"> <h2>文件上传</h2> <div class="collection-select"> <label>选择集合: </label> <select id="collectionSelect"> <option value="default">默认集合</option> </select> </div> <div class="upload-area" id="uploadArea"> <div> <p>拖拽文件到此处或点击选择文件</p> <p style="color: #666; font-size: 14px;">支持格式: TXT, MD, PDF, DOCX</p> <input type="file" id="fileInput" class="file-input" multiple accept=".txt,.md,.pdf,.docx"> <br> <button class="upload-btn" onclick="document.getElementById('fileInput').click()">选择文件</button> </div> </div> <div class="progress-bar" id="progressBar" style="display: none;"> <div class="progress-fill" id="progressFill"></div> </div> <div class="file-list" id="fileList"></div> <div class="status-message" id="statusMessage"></div> </div> <div class="section"> <h2>文本输入</h2> <div class="collection-select"> <label>选择集合: </label> <select id="textCollectionSelect"> <option value="default">默认集合</option> </select> </div> <div style="margin-bottom: 15px;"> <label for="documentTitle" style="display: block; margin-bottom: 5px; font-weight: bold;">文档标题 (可选):</label> <input type="text" id="documentTitle" placeholder="输入文档标题..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 10px;"> </div> <div style="margin-bottom: 15px;"> <textarea id="documentContent" placeholder="输入文档内容..." style="width: 100%; height: 200px; padding: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical;"></textarea> </div> <button class="btn btn-success" onclick="addTextDocument()">添加文档</button> <div class="status-message" id="textStatusMessage"></div> </div> </div> <div id="search" class="tab-content"> <div class="section"> <h2>资料查询</h2> <div style="margin-bottom: 15px;"> <input type="text" id="searchQuery" placeholder="输入搜索关键词..." style="width: 60%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"> <select id="searchCollection" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-left: 10px;"> <option value="default">默认集合</option> </select> <button class="btn" onclick="searchDocuments()" style="margin-left: 10px;">搜索</button> </div> <div id="searchResults"></div> </div> </div> <div id="chat" class="tab-content"> <div class="section"> <h2>知识库对话测试</h2> <div style="margin-bottom: 15px;"> <select id="chatCollection" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px;"> <option value="default">默认集合</option> </select> <span style="color: #666; font-size: 14px;">选择要对话的知识库集合</span> </div> <div id="chatHistory" style="border: 1px solid #ddd; border-radius: 5px; padding: 15px; height: 400px; overflow-y: auto; background: #f8f9fa; margin-bottom: 15px;"> <div style="text-align: center; color: #666; margin-top: 150px;"> 开始与知识库对话吧! </div> </div> <div style="display: flex; gap: 10px;"> <input type="text" id="chatQuery" placeholder="输入您的问题..." style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px;" onkeypress="handleChatKeyPress(event)"> <button class="btn btn-success" onclick="sendChatMessage()">发送</button> <button class="btn btn-danger" onclick="clearChatHistory()">清空</button> </div> <div class="status-message" id="chatStatusMessage"></div> </div> </div> <div id="manage" class="tab-content"> <div class="section"> <h2>内容管理</h2> <div style="margin-bottom: 15px;"> <select id="manageCollection" style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px;" onchange="loadDocuments()"> <option value="default">默认集合</option> </select> <button class="btn" onclick="loadDocuments()">刷新列表</button> </div> <div class="view-toggle"> <button class="btn active" id="btn-view-files" onclick="switchView('files')">文件视图</button> <button class="btn" id="btn-view-docs" onclick="switchView('docs')">片段视图</button> </div> <div id="fileListContainer"></div> <div id="documentList" style="display: none;"></div> <div style="margin-top: 20px; text-align: center;" id="pagination"> <button class="btn" onclick="prevPage()" id="prevPageBtn" disabled>上一页</button> <span id="pageInfo" style="margin: 0 10px;">第 1 页</span> <button class="btn" onclick="nextPage()" id="nextPageBtn" disabled>下一页</button> </div> <div class="status-message" id="manageStatusMessage"></div> </div> </div> </div> <script> const API_BASE = ''; let uploadedFiles = []; function switchTab(tabName) { // Hide all tabs document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); // Show selected tab document.querySelector(`[onclick="switchTab('${tabName}')"]`).classList.add('active'); document.getElementById(tabName).classList.add('active'); // Save current tab to localStorage localStorage.setItem('currentTab', tabName); } function showStatus(message, isSuccess = true) { const statusDiv = document.getElementById('statusMessage'); statusDiv.textContent = message; statusDiv.className = `status-message ${isSuccess ? 'status-success' : 'status-error'}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } async function loadCollections() { try { const response = await fetch(`${API_BASE}/collections`); const data = await response.json(); const selects = ['collectionSelect', 'searchCollection', 'textCollectionSelect', 'chatCollection', 'manageCollection']; selects.forEach(selectId => { const select = document.getElementById(selectId); if (select) { select.innerHTML = ''; // Always add default collection first const defaultOption = document.createElement('option'); defaultOption.value = 'default'; defaultOption.textContent = '默认集合'; select.appendChild(defaultOption); // Add other collections if (data.collections) { data.collections.forEach(collection => { if (collection !== 'default') { // Avoid duplicate default const option = document.createElement('option'); option.value = collection; option.textContent = collection; select.appendChild(option); } }); } } }); } catch (error) { console.error('Failed to load collections:', error); // Ensure default collection is always available const selects = ['collectionSelect', 'searchCollection', 'textCollectionSelect', 'chatCollection', 'manageCollection']; selects.forEach(selectId => { const select = document.getElementById(selectId); if (select && select.children.length === 0) { const defaultOption = document.createElement('option'); defaultOption.value = 'default'; defaultOption.textContent = '默认集合'; select.appendChild(defaultOption); } }); } } function updateFileList() { const fileList = document.getElementById('fileList'); fileList.innerHTML = ''; if (uploadedFiles.length === 0) { fileList.innerHTML = '<div style="text-align: center; color: #666;">暂无文件</div>'; return; } uploadedFiles.forEach((file, index) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` <div class="file-info"> <div class="file-name">${file.name}</div> <div class="file-meta">${(file.size / 1024).toFixed(1)} KB</div> </div> <div> <button class="btn btn-danger" onclick="removeFile(${index})">删除</button> </div> `; fileList.appendChild(fileItem); }); } function removeFile(index) { uploadedFiles.splice(index, 1); updateFileList(); } function handleFileSelect(files) { if (!files || files.length === 0) return; Array.from(files).forEach(file => { // Check if file already exists if (!uploadedFiles.some(f => f.name === file.name)) { uploadedFiles.push(file); } }); updateFileList(); } async function uploadFiles() { if (uploadedFiles.length === 0) { showStatus('请先选择文件', false); return; } const collection = document.getElementById('collectionSelect').value; const progressBar = document.getElementById('progressBar'); const progressFill = document.getElementById('progressFill'); progressBar.style.display = 'block'; progressFill.style.width = '0%'; const formData = new FormData(); uploadedFiles.forEach(file => { formData.append('files', file); }); formData.append('collection', collection); try { const response = await fetch(`${API_BASE}/upload-files`, { method: 'POST', body: formData }); const result = await response.json(); if (response.ok) { progressFill.style.width = '100%'; const successCount = typeof result.successful === 'number' ? result.successful : 0; const totalFiles = typeof result.total_files === 'number' ? result.total_files : uploadedFiles.length; if (successCount === totalFiles && totalFiles > 0) { showStatus(`上传完成: ${successCount}/${totalFiles} 个文件成功`, true); uploadedFiles = []; updateFileList(); } else if (successCount > 0) { showStatus(`上传部分成功: ${successCount}/${totalFiles} 个文件成功`, false); } else { showStatus('上传失败: 所有文件处理失败', false); } // Show results result.results.forEach(fileResult => { updateFileStatus(fileResult); }); } else { showStatus('上传失败: ' + result.detail, false); } } catch (error) { showStatus('上传失败: ' + error.message, false); } finally { setTimeout(() => { progressBar.style.display = 'none'; }, 2000); } } function updateFileStatus(fileResult) { const fileList = document.getElementById('fileList'); const fileItems = fileList.querySelectorAll('.file-item'); fileItems.forEach(item => { const fileName = item.querySelector('.file-name').textContent; if (fileName === fileResult.filename) { let statusClass = 'status-processing'; if (fileResult.processed) { statusClass = 'status-success'; } else if (fileResult.error) { statusClass = 'status-error'; } const statusDiv = document.createElement('div'); statusDiv.className = `file-status ${statusClass}`; statusDiv.textContent = fileResult.processed ? '处理成功' : (fileResult.error || '处理中'); item.appendChild(statusDiv); if (fileResult.preview) { const previewDiv = document.createElement('div'); previewDiv.className = 'preview-content'; previewDiv.textContent = fileResult.preview.length > 500 ? fileResult.preview.substring(0, 500) + '...' : fileResult.preview; item.appendChild(previewDiv); } } }); } async function searchDocuments() { const query = document.getElementById('searchQuery').value.trim(); const collection = document.getElementById('searchCollection').value; if (!query) { showStatus('请输入搜索关键词', false); return; } try { const response = await fetch(`${API_BASE}/search?query=${encodeURIComponent(query)}&collection=${collection}&limit=10`); const data = await response.json(); const resultsDiv = document.getElementById('searchResults'); resultsDiv.innerHTML = `<h3>搜索结果 (${data.results.length} 个)</h3>`; data.results.forEach(result => { const resultDiv = document.createElement('div'); resultDiv.className = 'file-item'; resultDiv.innerHTML = ` <div class="file-info"> <div class="file-name">相似度: ${(result.score * 100).toFixed(1)}%</div> <div class="file-meta">${result.metadata ? JSON.stringify(result.metadata) : ''}</div> </div> <div class="preview-content" style="margin-top: 10px;"> ${result.content.length > 300 ? result.content.substring(0, 300) + '...' : result.content} </div> `; resultsDiv.appendChild(resultDiv); }); // Display LLM summary if available // Display LLM summary if available if (data.summary) { const summaryDiv = document.createElement('div'); summaryDiv.className = 'file-item'; summaryDiv.style.border = '2px solid #007acc'; summaryDiv.innerHTML = ` <div class="file-info"> <div class="file-name" style="color: #007acc;">🤖 LLM 总结</div> <div class="file-meta">基于查询生成的智能总结</div> </div> <div class="preview-content" style="margin-top: 10px; background: #e3f2fd;"> ${data.summary} </div> `; resultsDiv.insertBefore(summaryDiv, resultsDiv.firstChild); } } catch (error) { showStatus('搜索失败: ' + error.message, false); } } async function addTextDocument() { const title = document.getElementById('documentTitle').value.trim(); const content = document.getElementById('documentContent').value.trim(); const collection = document.getElementById('textCollectionSelect').value; if (!content) { showTextStatus('请输入文档内容', false); return; } try { const metadata = {}; if (title) { metadata.title = title; } metadata.source = 'manual_input'; metadata.timestamp = new Date().toISOString(); const response = await fetch(`${API_BASE}/add-document`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: content, collection: collection, metadata: metadata }) }); if (response.ok) { showTextStatus('文档添加成功', true); document.getElementById('documentContent').value = ''; document.getElementById('documentTitle').value = ''; } else { const error = await response.json(); showTextStatus('添加失败: ' + (error.detail || '未知错误'), false); } } catch (error) { showTextStatus('添加失败: ' + error.message, false); } } function showTextStatus(message, isSuccess = true) { const statusDiv = document.getElementById('textStatusMessage'); statusDiv.textContent = message; statusDiv.className = `status-message ${isSuccess ? 'status-success' : 'status-error'}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } async function sendChatMessage() { const query = document.getElementById('chatQuery').value.trim(); const collection = document.getElementById('chatCollection').value; if (!query) { showChatStatus('请输入问题', false); return; } // Add user message to chat addMessageToChat('user', query); document.getElementById('chatQuery').value = ''; try { const response = await fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: query, collection: collection }) }); if (response.ok) { const data = await response.json(); addMessageToChat('assistant', data.response, data.sources); } else { const error = await response.json(); addMessageToChat('assistant', '抱歉,处理您的请求时出现错误: ' + (error.detail || '未知错误')); } } catch (error) { addMessageToChat('assistant', '网络错误,请稍后重试: ' + error.message); } } function addMessageToChat(role, content, sources = null) { const chatHistory = document.getElementById('chatHistory'); const messageDiv = document.createElement('div'); messageDiv.className = `chat-message ${role}`; messageDiv.style.marginBottom = '15px'; messageDiv.style.padding = '10px'; messageDiv.style.borderRadius = '8px'; if (role === 'user') { messageDiv.style.background = '#007acc'; messageDiv.style.color = 'white'; messageDiv.style.textAlign = 'right'; messageDiv.innerHTML = `<strong>您:</strong> ${content}`; } else { messageDiv.style.background = '#f0f0f0'; messageDiv.style.border = '1px solid #ddd'; messageDiv.innerHTML = `<strong>助手:</strong> ${content}`; if (sources && sources.length > 0) { const sourcesDiv = document.createElement('div'); sourcesDiv.style.marginTop = '10px'; sourcesDiv.style.fontSize = '12px'; sourcesDiv.style.color = '#666'; sourcesDiv.innerHTML = '<strong>参考来源:</strong>'; sources.forEach((source, index) => { const sourceDiv = document.createElement('div'); sourceDiv.style.marginTop = '5px'; sourceDiv.style.padding = '5px'; sourceDiv.style.background = '#f8f9fa'; sourceDiv.style.borderRadius = '4px'; sourceDiv.innerHTML = `<strong>来源 ${index + 1}:</strong> ${source.content}`; sourcesDiv.appendChild(sourceDiv); }); messageDiv.appendChild(sourcesDiv); } } chatHistory.appendChild(messageDiv); chatHistory.scrollTop = chatHistory.scrollHeight; } function clearChatHistory() { document.getElementById('chatHistory').innerHTML = '<div style="text-align: center; color: #666; margin-top: 150px;">开始与知识库对话吧!</div>'; } function handleChatKeyPress(event) { if (event.key === 'Enter') { sendChatMessage(); } } function showChatStatus(message, isSuccess = true) { const statusDiv = document.getElementById('chatStatusMessage'); statusDiv.textContent = message; statusDiv.className = `status-message ${isSuccess ? 'status-success' : 'status-error'}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } // Content Management Functions let currentPage = 0; const pageSize = 10; let currentView = 'files'; let currentFileFilter = null; async function switchView(view) { currentView = view; document.getElementById('btn-view-files').className = view === 'files' ? 'btn active' : 'btn'; document.getElementById('btn-view-docs').className = view === 'docs' ? 'btn active' : 'btn'; document.getElementById('fileListContainer').style.display = view === 'files' ? 'block' : 'none'; document.getElementById('documentList').style.display = view === 'docs' ? 'block' : 'none'; // Hide pagination in file view for now document.getElementById('pagination').style.display = view === 'docs' ? 'block' : 'none'; // Save current view to localStorage localStorage.setItem('currentView', view); if (view === 'files') { // Clear file filter when switching to file view currentFileFilter = null; const filterInfo = document.getElementById('fileFilterInfo'); if (filterInfo) { filterInfo.remove(); } await loadFiles(); } else { currentPage = 0; await loadDocuments(); } } async function loadFiles() { const collection = document.getElementById('manageCollection').value || 'default'; const listDiv = document.getElementById('fileListContainer'); listDiv.innerHTML = '<div style="text-align: center; padding: 20px;">加载中...</div>'; try { const response = await fetch(`${API_BASE}/list-files?collection=${collection}`); const data = await response.json(); listDiv.innerHTML = ''; if (!data.files || data.files.length === 0) { listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">暂无文件</div>'; return; } data.files.forEach(file => { const itemDiv = document.createElement('div'); itemDiv.className = 'file-item'; const fileInfo = document.createElement('div'); fileInfo.className = 'file-info'; fileInfo.innerHTML = ` <div class="file-name">文件: ${file.filename}</div> <div class="file-meta"> 类型: ${file.file_type} | 片段数: ${file.chunk_count} | 总大小: ${(file.total_size / 1024).toFixed(1)} KB </div> `; const buttonContainer = document.createElement('div'); const viewChunksBtn = document.createElement('button'); viewChunksBtn.className = 'btn'; viewChunksBtn.textContent = '查看片段'; viewChunksBtn.onclick = () => viewFileChunks(file.filename); const deleteBtn = document.createElement('button'); deleteBtn.className = 'btn btn-danger'; deleteBtn.textContent = '删除'; deleteBtn.onclick = () => deleteFile(file.filename); buttonContainer.appendChild(viewChunksBtn); buttonContainer.appendChild(deleteBtn); itemDiv.appendChild(fileInfo); itemDiv.appendChild(buttonContainer); listDiv.appendChild(itemDiv); }); } catch (error) { listDiv.innerHTML = `<div style="color: red; text-align: center;">加载失败: ${error.message}</div>`; } } async function deleteFile(filename) { if (!confirm(`确定要删除文件 "${filename}" 及其所有片段吗?`)) { return; } const collection = document.getElementById('manageCollection').value || 'default'; try { const response = await fetch(`${API_BASE}/delete-file`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ filename: filename, collection: collection }) }); if (response.ok) { showStatus('文件删除成功', true); loadFiles(); } else { const error = await response.json(); showStatus('删除失败: ' + (error.detail || '未知错误'), false); } } catch (error) { showStatus('删除请求失败: ' + error.message, false); } } function viewFileChunks(filename) { currentFileFilter = filename; switchView('docs'); } async function loadDocuments(page = 0) { if (currentView === 'files') { await loadFiles(); return; } currentPage = page; const collection = document.getElementById('manageCollection').value; const listDiv = document.getElementById('documentList'); // Remove any existing filter info first const existingFilterInfo = document.getElementById('fileFilterInfo'); if (existingFilterInfo) { existingFilterInfo.remove(); } // Show filter info if active if (currentFileFilter) { const filterInfo = document.createElement('div'); filterInfo.id = 'fileFilterInfo'; filterInfo.style.marginBottom = '15px'; filterInfo.style.padding = '10px'; filterInfo.style.background = '#e3f2fd'; filterInfo.style.border = '1px solid #007acc'; filterInfo.style.borderRadius = '5px'; filterInfo.innerHTML = ` 正在显示文件的片段: <strong>${currentFileFilter}</strong> <button class="btn" onclick="clearFileFilter()" style="margin-left: 10px;">显示全部</button> `; listDiv.parentElement.insertBefore(filterInfo, listDiv); } listDiv.innerHTML = '<div style="text-align: center; padding: 20px;">加载中...</div>'; try { // Add filename parameter if filtering by file const filenameParam = currentFileFilter ? `&filename=${encodeURIComponent(currentFileFilter)}` : ''; const response = await fetch(`${API_BASE}/list-documents?collection=${collection}&limit=${pageSize}&offset=${page * pageSize}${filenameParam}`); if (!response.ok) throw new Error('Failed to load documents'); const data = await response.json(); listDiv.innerHTML = ''; // No need to filter on frontend anymore - backend handles it const filteredDocs = data.documents; if (filteredDocs.length === 0) { listDiv.innerHTML = '<div style="text-align: center; padding: 20px; color: #666;">暂无文档</div>'; } else { filteredDocs.forEach(doc => { const item = document.createElement('div'); item.className = 'file-item'; item.innerHTML = ` <div class="file-info"> <div class="file-name">ID: ${doc.id}</div> <div class="file-meta"> ${doc.metadata.filename ? `文件: ${doc.metadata.filename}` : ''} ${doc.metadata.timestamp ? ` | 时间: ${new Date(doc.metadata.timestamp).toLocaleString()}` : ''} </div> <div class="preview-content" style="margin-top: 5px; max-height: 100px;"> ${doc.content.substring(0, 200)}${doc.content.length > 200 ? '...' : ''} </div> </div> <div> <button class="btn btn-danger" onclick="deleteDocument('${doc.id}')">删除</button> </div> `; listDiv.appendChild(item); }); } // Update pagination document.getElementById('pageInfo').textContent = `第 ${currentPage + 1} 页`; document.getElementById('prevPageBtn').disabled = currentPage === 0; document.getElementById('nextPageBtn').disabled = filteredDocs.length < pageSize; } catch (error) { listDiv.innerHTML = `<div style="color: red; text-align: center;">加载失败: ${error.message}</div>`; } } async function deleteDocument(docId) { if (!confirm('确定要删除这个文档吗?')) return; const collection = document.getElementById('manageCollection').value; try { const response = await fetch(`${API_BASE}/delete-document`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ document_id: docId, collection: collection }) }); if (response.ok) { showManageStatus('删除成功', true); loadDocuments(currentPage); } else { const error = await response.json(); showManageStatus('删除失败: ' + (error.detail || '未知错误'), false); } } catch (error) { showManageStatus('删除失败: ' + error.message, false); } } function prevPage() { if (currentPage > 0) { loadDocuments(currentPage - 1); } } function nextPage() { loadDocuments(currentPage + 1); } function showManageStatus(message, isSuccess = true) { const statusDiv = document.getElementById('manageStatusMessage'); statusDiv.textContent = message; statusDiv.className = `status-message ${isSuccess ? 'status-success' : 'status-error'}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } function clearFileFilter() { currentFileFilter = null; // Remove filter info element by ID const filterInfo = document.getElementById('fileFilterInfo'); if (filterInfo) { filterInfo.remove(); } loadDocuments(0); } // Initialize document.addEventListener('DOMContentLoaded', async function() { await loadCollections(); // Restore tab state from localStorage const savedTab = localStorage.getItem('currentTab'); if (savedTab && ['upload', 'search', 'chat', 'manage'].includes(savedTab)) { switchTab(savedTab); } // Restore view state from localStorage const savedView = localStorage.getItem('currentView'); if (savedView && ['files', 'docs'].includes(savedView)) { switchView(savedView); } else { switchView('files'); } // File input handling document.getElementById('fileInput').addEventListener('change', function(e) { handleFileSelect(e.target.files); }); // Drag and drop handling const uploadArea = document.getElementById('uploadArea'); uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', function(e) { e.preventDefault(); uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', function(e) { e.preventDefault(); uploadArea.classList.remove('dragover'); handleFileSelect(e.dataTransfer.files); }); // Auto upload when files are selected document.getElementById('fileInput').addEventListener('change', function() { if (uploadedFiles.length > 0) { uploadFiles(); } }); }); </script> </body> </html> """ return HTMLResponse(content=html_content) @app.get("/config-page", response_class=HTMLResponse) async def config_page(): """Serve the configuration page.""" html_content = """ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MCP-RAG 配置管理</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { color: #333; text-align: center; margin-bottom: 30px; } .section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; } .section h2 { color: #555; margin-top: 0; border-bottom: 2px solid #007acc; padding-bottom: 10px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; color: #333; } input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .checkbox-group { display: flex; align-items: center; } .checkbox-group input { width: auto; margin-right: 10px; } .btn { background-color: #007acc; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin-right: 10px; } .btn:hover { background-color: #005aa3; } .btn-danger { background-color: #dc3545; } .btn-danger:hover { background-color: #c82333; } .btn-success { background-color: #28a745; } .btn-success:hover { background-color: #218838; } .status { margin-top: 20px; padding: 10px; border-radius: 4px; display: none; } .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .current-config { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .config-item { display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #eee; } .config-key { font-weight: bold; } .config-value { color: #007acc; } </style> </head> <body> <div class="container"> <div style="text-align: center; margin-bottom: 20px;"> <a href="/config-page" style="margin: 0 10px; color: #007acc; text-decoration: none;">配置管理</a> | <a href="/documents-page" style="margin: 0 10px; color: #007acc; text-decoration: none;">资料管理</a> </div> <h1>MCP-RAG 配置管理</h1> <div id="currentConfig" class="current-config"> <h3>当前配置</h3> <div id="configDisplay"></div> </div> <div class="section"> <h2>服务器设置</h2> <div class="form-group"> <label for="host">主机地址:</label> <input type="text" id="host" placeholder="0.0.0.0"> </div> <div class="form-group"> <label for="http_port">HTTP端口:</label> <input type="number" id="http_port" placeholder="8000"> </div> <div class="form-group"> <div class="checkbox-group"> <input type="checkbox" id="debug"> <label for="debug">调试模式</label> </div> </div> </div> <div class="section"> <h2>向量数据库设置</h2> <div class="form-group"> <label for="vector_db_type">数据库类型:</label> <select id="vector_db_type"> <option value="chroma">ChromaDB</option> <option value="qdrant">Qdrant</option> </select> </div> <div class="form-group"> <label for="chroma_persist_directory">ChromaDB 数据目录:</label> <input type="text" id="chroma_persist_directory" placeholder="./data/chroma"> </div> <div class="form-group"> <label for="qdrant_url">Qdrant 服务器地址:</label> <input type="text" id="qdrant_url" placeholder="http://localhost:6333"> </div> </div> <div class="section"> <h2>嵌入模型设置</h2> <div class="form-group"> <label for="embedding_provider">嵌入提供商:</label> <select id="embedding_provider"> <option value="doubao">豆包 (Doubao)</option> <option value="local">本地模型</option> </select> </div> <div class="form-group"> <label for="embedding_model">嵌入模型:</label> <input type="text" id="embedding_model" placeholder="doubao-embedding-text-240715"> </div> <div class="form-group"> <label for="embedding_api_key">豆包API密钥:</label> <input type="text" id="embedding_api_key" placeholder="您的豆包API密钥"> </div> <div class="form-group"> <label for="embedding_base_url">豆包API基础地址:</label> <input type="text" id="embedding_base_url" placeholder="https://ark.cn-beijing.volces.com/api/v3"> </div> <div class="form-group"> <label for="embedding_device">设备 (仅本地模型):</label> <select id="embedding_device"> <option value="cpu">CPU</option> <option value="cuda">CUDA (GPU)</option> </select> </div> <div class="form-group"> <label for="embedding_cache_dir">缓存目录 (仅本地模型):</label> <div class="form-group"> <label for="llm_api_key">API 密钥:</label> <input type="text" id="llm_api_key" placeholder="可选"> </div> <div class="form-group"> <div class="checkbox-group"> <input type="checkbox" id="enable_llm_summary"> <label for="enable_llm_summary">启用LLM总结</label> </div> </div> <div class="form-group"> <div class="checkbox-group"> <input type="checkbox" id="enable_thinking"> <label for="enable_thinking">启用深度思考</label> </div> </div> </div> <div class="section"> <h2>RAG 设置</h2> <div class="form-group"> <label for="max_retrieval_results">最大检索结果数:</label> <input type="number" id="max_retrieval_results" min="1" max="20" placeholder="5"> </div> <div class="form-group"> <label for="similarity_threshold">相似度阈值:</label> <input type="number" id="similarity_threshold" min="0" max="1" step="0.1" placeholder="0.7"> </div> <div class="form-group"> <div class="checkbox-group"> <input type="checkbox" id="enable_reranker"> <label for="enable_reranker">启用重排序</label> </div> </div> <div class="form-group"> <div class="checkbox-group"> <input type="checkbox" id="enable_cache"> <label for="enable_cache">启用缓存</label> </div> </div> </div> <div class="section"> <button class="btn btn-success" onclick="loadConfig()">加载配置</button> <button class="btn" onclick="saveAllConfig()">保存所有配置</button> <button class="btn btn-danger" onclick="resetConfig()">重置为默认</button> </div> <div id="status" class="status"></div> </div> <script> const API_BASE = ''; async function showStatus(message, isSuccess = true) { const statusDiv = document.getElementById('status'); statusDiv.textContent = message; statusDiv.className = `status ${isSuccess ? 'success' : 'error'}`; statusDiv.style.display = 'block'; setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } async function loadConfig() { try { const response = await fetch(`${API_BASE}/config`); const config = await response.json(); // Fill form fields Object.keys(config).forEach(key => { const element = document.getElementById(key); if (element) { if (element.type === 'checkbox') { element.checked = config[key]; } else { element.value = config[key] || ''; } } }); // Display current config displayCurrentConfig(config); showStatus('配置加载成功', true); } catch (error) { showStatus('加载配置失败: ' + error.message, false); } } function displayCurrentConfig(config) { const displayDiv = document.getElementById('configDisplay'); displayDiv.innerHTML = ''; Object.entries(config).forEach(([key, value]) => { const itemDiv = document.createElement('div'); itemDiv.className = 'config-item'; itemDiv.innerHTML = ` <span class="config-key">${key}:</span> <span class="config-value">${value}</span> `; displayDiv.appendChild(itemDiv); }); } async function saveAllConfig() { const updates = {}; // Collect all form values const inputs = document.querySelectorAll('input, select'); inputs.forEach(input => { if (input.id) { if (input.type === 'checkbox') { updates[input.id] = input.checked; } else if (input.type === 'number') { updates[input.id] = parseFloat(input.value) || 0; } else { updates[input.id] = input.value || null; } } }); try { const response = await fetch(`${API_BASE}/config/bulk`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ updates }) }); if (response.ok) { showStatus('配置保存成功', true); loadConfig(); // Reload to show updated config } else { const error = await response.json(); showStatus('保存失败: ' + (error.detail || '未知错误'), false); } } catch (error) { showStatus('保存配置失败: ' + error.message, false); } } async function resetConfig() { if (!confirm('确定要重置所有配置为默认值吗?')) { return; } try { const response = await fetch(`${API_BASE}/config/reset`, { method: 'POST' }); if (response.ok) { showStatus('配置已重置为默认值', true); loadConfig(); } else { const error = await response.json(); showStatus('重置失败: ' + (error.detail || '未知错误'), false); } } catch (error) { showStatus('重置配置失败: ' + error.message, false); } } // Load config on page load window.onload = loadConfig; </script> </body> </html> """ return HTMLResponse(content=html_content) @app.get("/config") async def get_config(): """Get current configuration.""" return config_manager.get_all_settings() @app.post("/config") async def update_config(config: ConfigUpdate): """Update a single configuration setting.""" success = config_manager.update_setting(config.key, config.value) if not success: raise HTTPException(status_code=400, detail=f"Failed to update config {config.key}") return {"message": f"Config {config.key} updated successfully"} @app.post("/config/bulk") async def update_config_bulk(config: BulkConfigUpdate): """Update multiple configuration settings.""" success = config_manager.update_settings(config.updates) if not success: raise HTTPException(status_code=400, detail="Failed to update config") return {"message": "Config updated successfully"} @app.post("/config/reset") async def reset_config(): """Reset configuration to defaults.""" success = config_manager.reset_to_defaults() if not success: raise HTTPException(status_code=400, detail="Failed to reset config") return {"message": "Config reset to defaults successfully"} @app.post("/add-document") async def add_document(doc: DocumentAdd): """Add a single document.""" try: vector_db = await get_vector_database() await vector_db.add_document( content=doc.content, collection_name=doc.collection, metadata=doc.metadata ) return {"message": "Document added successfully"} except Exception as e: logger.error(f"Failed to add document: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/upload-files") async def upload_files( files: List[UploadFile] = File(...), collection: str = Form("default") ): """Upload and process multiple files.""" if not files: raise HTTPException(status_code=400, detail="No files provided") processor = get_document_processor() results = [] for file in files: try: # Save uploaded file to temporary location with tempfile.NamedTemporaryFile(delete=False, suffix=file.filename) as temp_file: shutil.copyfileobj(file.file, temp_file) temp_path = Path(temp_file.name) # Process the file processed_doc = processor.process_file(temp_path, file.filename) # Clean up temp file temp_path.unlink() # Add to vector database if processing was successful if not processed_doc.error and processed_doc.content.strip(): try: vector_db = await get_vector_database() await vector_db.add_document( content=processed_doc.content, collection_name=collection, metadata={ **processed_doc.metadata, "filename": processed_doc.filename, "file_type": processed_doc.file_type, "source": "upload" } ) processed = True error = "" except Exception as e: processed = False error = f"Failed to add to database: {str(e)}" else: processed = False error = processed_doc.error or "No content extracted" # Create preview (first 500 characters) preview = processed_doc.content[:500] + "..." if len(processed_doc.content) > 500 else processed_doc.content result = FileUploadResponse( filename=file.filename, file_type=processed_doc.file_type, content_length=len(processed_doc.content), processed=processed, error=error, preview=preview if processed else "" ) results.append(result) except Exception as e: result = FileUploadResponse( filename=file.filename, file_type="unknown", content_length=0, processed=False, error=str(e), preview="" ) results.append(result) return BatchUploadResponse( total_files=len(files), successful=len([r for r in results if r.processed]), failed=len([r for r in results if not r.processed]), results=results ) @app.get("/collections") async def list_collections(): """List all collections.""" try: vector_db = await get_vector_database() collections = await vector_db.list_collections() return {"collections": collections} except Exception as e: logger.error(f"Failed to list collections: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.post("/chat") async def chat_with_knowledge_base(chat_request: dict): """Chat with knowledge base using LLM.""" try: query = chat_request.get("query", "") collection = chat_request.get("collection", "default") if not query: raise HTTPException(status_code=400, detail="Query is required") # Get components vector_db = await get_vector_database() embedding_model = await get_embedding_model() # Encode query and search query_embedding = await embedding_model.encode_single(query) search_results = await vector_db.search( query_embedding=query_embedding, collection_name=collection, limit=5 ) # Combine retrieved content context = "\n\n".join([ f"文档 {i+1}: {r.document.content}" for i, r in enumerate(search_results) ]) # Generate response using LLM from .llm import get_llm_model llm_model = await get_llm_model() prompt = f"""基于以下知识库内容回答用户的问题。如果知识库内容不足以回答问题,请说明无法找到相关信息。 知识库内容: {context} 用户问题: {query} 请提供准确、简洁的回答:""" response = await llm_model.generate(prompt) return { "query": query, "response": response, "sources": [ { "content": r.document.content[:200] + "..." if len(r.document.content) > 200 else r.document.content, "score": r.score, "metadata": r.document.metadata } for r in search_results ] } except Exception as e: logger.error(f"Failed to chat: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/search") async def search_documents(query: str, collection: str = "default", limit: int = 5): """Search documents.""" try: logger.info(f"Searching for '{query}' in collection '{collection}'") # Get components vector_db = await get_vector_database() embedding_model = await get_embedding_model() # Encode query query_embedding = await embedding_model.encode_single(query) # Search results = await vector_db.search( query_embedding=query_embedding, collection_name=collection, limit=limit ) # Check if LLM summary is enabled if settings.enable_llm_summary: try: from .llm import get_llm_model llm_model = await get_llm_model() # Combine all retrieved content combined_content = "\n\n".join([ f"文档 {i+1} (相似度: {r.score:.3f}):\n{r.document.content}" for i, r in enumerate(results) ]) # Generate summary using LLM summary = await llm_model.summarize(combined_content, query) return { "query": query, "collection": collection, "summary": summary, "results": [ { "content": r.document.content, "score": r.score, "metadata": r.document.metadata } for r in results ] } except Exception as llm_error: logger.warning(f"LLM summary failed, falling back to direct results: {llm_error}") # Fall back to direct results if LLM fails # Return direct search results return { "query": query, "collection": collection, "results": [ { "content": r.document.content, "score": r.score, "metadata": r.document.metadata } for r in results ] } except Exception as e: logger.error(f"Failed to search: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/list-documents") async def list_documents(collection: str = "default", limit: int = 100, offset: int = 0, filename: str = None): """List documents in a collection.""" try: db = await get_vector_database() result = await db.list_documents(collection_name=collection, limit=limit, offset=offset, filename=filename) return result except Exception as e: logger.error(f"Error listing documents: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.delete("/delete-document") async def delete_document(request: DeleteDocumentRequest): """Delete a document.""" try: db = await get_vector_database() success = await db.delete_document(document_id=request.document_id, collection_name=request.collection) if success: return {"message": "Document deleted successfully"} else: raise HTTPException(status_code=404, detail="Document not found or failed to delete") except Exception as e: logger.error(f"Error deleting document: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.get("/list-files") async def list_files(collection: str = "default"): """List files in a collection.""" try: db = await get_vector_database() result = await db.list_files(collection_name=collection) return {"files": result} except Exception as e: logger.error(f"Error listing files: {e}") raise HTTPException(status_code=500, detail=str(e)) @app.delete("/delete-file") async def delete_file(request: DeleteFileRequest): """Delete a file.""" try: db = await get_vector_database() success = await db.delete_file(filename=request.filename, collection_name=request.collection) if success: return {"message": "File deleted successfully"} else: raise HTTPException(status_code=404, detail="File not found or failed to delete") except Exception as e: logger.error(f"Error deleting file: {e}") raise HTTPException(status_code=500, detail=str(e))

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kalicyh/mcp-rag'

If you have feedback or need assistance with the MCP directory API, please join our Discord server