diff --git a/deepsearcher/backend/templates/index.html b/deepsearcher/backend/templates/index.html deleted file mode 100644 index b81719f..0000000 --- a/deepsearcher/backend/templates/index.html +++ /dev/null @@ -1,948 +0,0 @@ - - - - - - DeepSearcher - 智能搜索系统 - - - - - -
-
-

DeepSearcher 智能搜索系统

-

- 基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。 -

-
- -
-
-

文件加载

-
- - -
-
- - -
-
- - -
- -
-
- -
-

网站内容加载

-
- - -
-
- - -
-
- - -
- -
-
- -
-

智能查询

-
- - -
-
- - -
- - -
- -
-

查询结果:

-
-
- -
-

处理过程:

-
-
-
-
-
-
- - -
- - - - diff --git a/deepsearcher/templates/html/index.html b/deepsearcher/templates/html/index.html new file mode 100644 index 0000000..f9591d0 --- /dev/null +++ b/deepsearcher/templates/html/index.html @@ -0,0 +1,88 @@ + + + + + + DeepSearcher - 智能搜索系统 + + + +
+
+

DeepSearcher 智能搜索系统

+

基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。

+
+ +
+
+

文件加载

+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

网站内容加载

+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

智能查询

+
+ + +
+
+ + +
+ + +
+ +
+

查询结果:

+
+
+ +
+

处理过程:

+
+
+
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/deepsearcher/templates/index.html b/deepsearcher/templates/index.html new file mode 100644 index 0000000..20af32f --- /dev/null +++ b/deepsearcher/templates/index.html @@ -0,0 +1,88 @@ + + + + + + DeepSearcher - 智能搜索系统 + + + +
+
+

DeepSearcher 智能搜索系统

+

基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。

+
+ +
+
+

文件加载

+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

网站加载

+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

智能查询

+
+ + +
+
+ + +
+ + +
+ +
+

查询结果:

+
+
+ +
+

处理过程:

+
+
+
+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/deepsearcher/templates/static/css/styles.css b/deepsearcher/templates/static/css/styles.css new file mode 100644 index 0000000..29175ab --- /dev/null +++ b/deepsearcher/templates/static/css/styles.css @@ -0,0 +1,280 @@ +:root { + --primary-color: #4f46e5; + --secondary-color: #f9fafb; + --border-color: #e5e7eb; + --text-primary: #1f2937; + --text-secondary: #6b7280; + --success-color: #10b981; + --error-color: #ef4444; + --warning-color: #f59e0b; + --info-color: #3b82f6; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: var(--text-primary); + background-color: #f3f4f6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +h1 { + color: var(--primary-color); + margin-bottom: 10px; +} + +.app-description { + color: var(--text-secondary); + max-width: 800px; + margin: 0 auto; +} + +.card { + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 24px; + margin-bottom: 24px; +} + +.card-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 16px; + color: var(--text-primary); +} + +.form-group { + margin-bottom: 16px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 500; +} + +input, textarea, select { + width: 100%; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 16px; + transition: border-color 0.15s; +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +button { + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 6px; + padding: 12px 20px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.15s; +} + +button:hover { + background-color: #4338ca; +} + +button:disabled { + background-color: var(--border-color); + cursor: not-allowed; +} + +.btn-secondary { + background-color: var(--text-secondary); +} + +.btn-secondary:hover { + background-color: var(--text-primary); +} + +.result-container { + margin-top: 20px; + padding: 16px; + border-radius: 6px; + background-color: var(--secondary-color); + display: none; +} + +.result-container.visible { + display: block; +} + +.status { + padding: 12px; + border-radius: 6px; + margin: 10px 0; + display: none; +} + +.status.visible { + display: block; +} + +.status-success { + background-color: #d1fae5; + color: var(--success-color); + border: 1px solid var(--success-color); +} + +.status-error { + background-color: #fee2e2; + color: var(--error-color); + border: 1px solid var(--error-color); +} + +.status-loading { + background-color: #fffbeb; + color: var(--warning-color); + border: 1px solid var(--warning-color); + display: flex; + align-items: center; +} + +.loading-spinner { + display: none; + width: 12px; + height: 12px; + border: 2px solid #f3f3f3; + border-top: 2px solid var(--warning-color); + border-radius: 50%; + animation: spin 1s linear infinite; + flex-shrink: 0; + align-items: center; +} + +.status-loading .loading-spinner { + display: inline-block; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading .loading-spinner { + display: inline-block; +} + +.query-result { + white-space: pre-wrap; + line-height: 1.6; + background-color: white; + padding: 16px; + border-radius: 6px; + border: 1px solid var(--border-color); + margin-top: 8px; +} + +.message-stream { + margin-top: 16px; +} + +#processResult { + margin-top: 16px; +} + +#processResult h3 { + color: var(--text-secondary); + font-size: 1rem; +} + +#queryResult h3 { + color: var(--text-secondary); + font-size: 1rem; +} + +.message-container { + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: white; + padding: 12px; +} + +.message { + margin-bottom: 12px; + padding: 8px 12px; + border-radius: 4px; + border-left: 4px solid; + font-size: 14px; + line-height: 1.4; +} + +.message-start { + background-color: #f0f9ff; + border-left-color: var(--info-color); +} + +.message-info { + background-color: #fef3c7; + border-left-color: var(--warning-color); +} + +.message-answer { + background-color: #d1fae5; + border-left-color: var(--success-color); +} + +.message-complete { + background-color: #dbeafe; + border-left-color: var(--primary-color); +} + +.message-error { + background-color: #fee2e2; + border-left-color: var(--error-color); +} + +.message-timestamp { + font-size: 12px; + color: var(--text-secondary); + margin-top: 4px; +} + +footer { + text-align: center; + margin-top: 30px; + padding: 20px; + color: var(--text-secondary); + font-size: 0.875rem; +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .card { + padding: 16px; + } +} diff --git a/deepsearcher/templates/static/js/app.js b/deepsearcher/templates/static/js/app.js new file mode 100644 index 0000000..d13cbd2 --- /dev/null +++ b/deepsearcher/templates/static/js/app.js @@ -0,0 +1,446 @@ +// 全局变量 +let eventSource = null; +let isStreaming = false; + +// 工具函数:显示状态信息 +function showStatus(elementId, message, type) { + const statusElement = document.getElementById(elementId); + + // 清除之前的类型类 + statusElement.classList.remove('status-success', 'status-error', 'status-loading'); + + // 添加新的类型类 + if (type === 'success') { + statusElement.classList.add('status-success'); + statusElement.innerHTML = message; + } else if (type === 'error') { + statusElement.classList.add('status-error'); + statusElement.innerHTML = message; + } else if (type === 'loading') { + statusElement.classList.add('status-loading'); + statusElement.innerHTML = `
${message}`; + } + + statusElement.classList.add('visible'); +} + +// 工具函数:显示消息流 +function displayMessages(messages) { + const container = document.getElementById('messageContainer'); + container.innerHTML = ''; + + messages.forEach(message => { + addMessageToContainer(message); + }); + + // 滚动到底部 + container.scrollTop = container.scrollHeight; +} + +// 工具函数:添加单个消息到容器 +function addMessageToContainer(message) { + console.log('Adding message to container:', message); + + const container = document.getElementById('messageContainer'); + if (!container) { + console.error('Message container not found!'); + return; + } + + const messageElement = document.createElement('div'); + messageElement.className = `message message-${message.type}`; + + const contentElement = document.createElement('div'); + contentElement.textContent = message.content; + + messageElement.appendChild(contentElement); + + // 只有在有有效时间戳时才显示时间 + if (message.timestamp && !isNaN(message.timestamp)) { + const date = new Date(message.timestamp * 1000); + if (!isNaN(date.getTime())) { + const timestampElement = document.createElement('div'); + timestampElement.className = 'message-timestamp'; + timestampElement.textContent = date.toLocaleTimeString(); + messageElement.appendChild(timestampElement); + } + } + container.appendChild(messageElement); + + // 确保处理过程容器是可见的 + const processContainer = document.getElementById('processResult'); + if (processContainer && !processContainer.classList.contains('visible')) { + processContainer.classList.add('visible'); + } + + // 滚动到底部 + container.scrollTop = container.scrollHeight; + + console.log('Message added successfully, container now has', container.children.length, 'messages'); +} + +// 工具函数:隐藏状态信息 +function hideStatus(elementId) { + const statusElement = document.getElementById(elementId); + statusElement.classList.remove('visible'); +} + +// 工具函数:显示结果 +function showResult() { + const resultElement = document.getElementById('queryResult'); + resultElement.classList.add('visible'); +} + +// 工具函数:隐藏结果 +function hideResult() { + const resultElement = document.getElementById('queryResult'); + resultElement.classList.remove('visible'); +} + +// 工具函数:显示处理过程 +function showProcessResult() { + const processElement = document.getElementById('processResult'); + processElement.classList.add('visible'); +} + +// 工具函数:隐藏处理过程 +function hideProcessResult() { + const processElement = document.getElementById('processResult'); + processElement.classList.remove('visible'); +} + +// 工具函数:转义HTML特殊字符 +function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +// 工具函数:设置按钮加载状态 +function setButtonLoading(button, loading) { + if (loading) { + button.classList.add('loading'); + button.disabled = true; + } else { + button.classList.remove('loading'); + button.disabled = false; + } +} + +// 工具函数:关闭EventSource连接 +function closeEventSource() { + if (eventSource) { + console.log('Closing eventSource in closeEventSource function'); + eventSource.close(); + eventSource = null; + } + if (window.currentEventSource) { + console.log('Closing currentEventSource in closeEventSource function'); + window.currentEventSource.close(); + window.currentEventSource = null; + } + isStreaming = false; +} + +// 工具函数:处理实时消息流 +function handleStreamMessage(data) { + try { + const message = JSON.parse(data); + + switch (message.type) { + case 'connection': + console.log('Connected to message stream:', message.message); + break; + case 'heartbeat': + // 心跳消息,不需要处理 + break; + case 'start': + console.log('Query started:', message.content); + showStatus('queryStatus', ' 正在处理...', 'loading'); + addMessageToContainer(message); + break; + case 'complete': + console.log('Query completed - closing connection'); + showStatus('queryStatus', '查询完成', 'success'); + addMessageToContainer(message); + // 关闭EventSource连接 + if (window.currentEventSource) { + console.log('Closing currentEventSource'); + window.currentEventSource.close(); + window.currentEventSource = null; + } + isStreaming = false; + setButtonLoading(document.getElementById('queryBtn'), false); + console.log('Query completed - connection closed, isStreaming set to false'); + break; + case 'error': + console.error('Error:', message.content); + showStatus('queryStatus', message.content, 'error'); + addMessageToContainer(message); + // 关闭EventSource连接 + if (window.currentEventSource) { + window.currentEventSource.close(); + window.currentEventSource = null; + } + isStreaming = false; + setButtonLoading(document.getElementById('queryBtn'), false); + break; + case 'info': + // 处理信息消息 + console.log('Processing info message:', message.content.substring(0, 100) + '...'); + addMessageToContainer(message); + break; + case 'answer': + // 处理answer类型,显示查询结果 + console.log('Processing answer message:', message.content.substring(0, 100) + '...'); + // 将结果内容显示在结果区域 + if (message.content && message.content !== "==== FINAL ANSWER====") { + document.getElementById('resultText').textContent = message.content; + showResult(); + } + // 不将answer消息添加到处理过程容器中,只显示在查询结果框中 + break; + default: + console.log('Unknown message type:', message.type); + } + } catch (error) { + console.error('Error parsing message:', error); + } +} + +// 工具函数:开始实时消息流 +function startMessageStream() { + closeEventSource(); // 关闭之前的连接 + + eventSource = new EventSource('/stream-messages/'); + + eventSource.onopen = function(event) { + console.log('EventSource connection opened'); + }; + + eventSource.onmessage = function(event) { + handleStreamMessage(event.data); + }; + + eventSource.onerror = function(event) { + console.error('EventSource error:', event); + if (eventSource.readyState === EventSource.CLOSED) { + console.log('EventSource connection closed'); + } + }; +} + +// 加载文件功能 +document.getElementById('loadFilesBtn').addEventListener('click', async function() { + const button = this; + const filePathsInput = document.getElementById('filePaths').value; + const collectionName = document.getElementById('collectionName').value; + const collectionDesc = document.getElementById('collectionDesc').value; + + if (!filePathsInput) { + showStatus('loadStatus', '请提供至少一个文件路径', 'error'); + return; + } + + const filePaths = filePathsInput.split(',').map(path => path.trim()).filter(path => path); + + setButtonLoading(button, true); + showStatus('loadStatus', '正在加载文件...', 'loading'); + hideResult(); + hideProcessResult(); + + try { + const response = await fetch('/load-files/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + paths: filePaths, + collection_name: collectionName || undefined, + collection_description: collectionDesc || undefined + }) + }); + + const data = await response.json(); + + if (response.ok) { + showStatus('loadStatus', data.message, 'success'); + } else { + showStatus('loadStatus', `加载失败: ${data.detail}`, 'error'); + } + } catch (error) { + showStatus('loadStatus', `请求失败: ${error.message}`, 'error'); + } finally { + setButtonLoading(button, false); + } +}); + +// 清空消息功能 +document.getElementById('clearMessagesBtn').addEventListener('click', async function() { + try { + const response = await fetch('/clear-messages/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + // 清空消息容器 + const container = document.getElementById('messageContainer'); + container.innerHTML = ''; + + // 清空查询结果 + const resultText = document.getElementById('resultText'); + resultText.textContent = ''; + + // 隐藏处理过程容器 + hideProcessResult(); + + // 隐藏查询结果容器 + hideResult(); + + showStatus('queryStatus', '消息已清空', 'success'); + } else { + showStatus('queryStatus', '清空消息失败', 'error'); + } + } catch (error) { + showStatus('queryStatus', `请求失败: ${error.message}`, 'error'); + } +}); + +// 加载网站内容功能 +document.getElementById('loadWebsiteBtn').addEventListener('click', async function() { + const button = this; + const urlsInput = document.getElementById('websiteUrls').value; + const collectionName = document.getElementById('webCollectionName').value; + const collectionDesc = document.getElementById('webCollectionDesc').value; + + if (!urlsInput) { + showStatus('webLoadStatus', '请提供至少一个网站URL', 'error'); + return; + } + + const urls = urlsInput.split(',').map(url => url.trim()).filter(url => url); + + setButtonLoading(button, true); + showStatus('webLoadStatus', '正在加载网站内容...', 'loading'); + hideResult(); + hideProcessResult(); + + try { + const response = await fetch('/load-website/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + urls: urls, + collection_name: collectionName || undefined, + collection_description: collectionDesc || undefined + }) + }); + + const data = await response.json(); + + if (response.ok) { + showStatus('webLoadStatus', data.message, 'success'); + } else { + showStatus('webLoadStatus', `加载失败: ${data.detail}`, 'error'); + } + } catch (error) { + showStatus('webLoadStatus', `请求失败: ${error.message}`, 'error'); + } finally { + setButtonLoading(button, false); + } +}); + +// 查询功能 - 使用实时流 +document.getElementById('queryBtn').addEventListener('click', async function() { + const button = this; + const queryText = document.getElementById('queryText').value; + const maxIter = parseInt(document.getElementById('maxIter').value); + + if (!queryText) { + showStatus('queryStatus', '请输入查询问题', 'error'); + return; + } + + if (isNaN(maxIter) || maxIter < 1 || maxIter > 10) { + showStatus('queryStatus', '迭代次数必须是1到10之间的整数', 'error'); + return; + } + + if (isStreaming) { + console.log('Query already in progress, isStreaming:', isStreaming); + showStatus('queryStatus', '查询正在进行中,请等待完成', 'error'); + return; + } + + setButtonLoading(button, true); + showStatus('queryStatus', '正在启动查询...', 'loading'); + hideResult(); + hideProcessResult(); + + // 清空消息容器 + const container = document.getElementById('messageContainer'); + container.innerHTML = ''; + + try { + console.log('Starting new query, setting isStreaming to true'); + isStreaming = true; + + // 确保没有其他连接存在 + if (window.currentEventSource) { + console.log('Closing existing EventSource connection'); + window.currentEventSource.close(); + window.currentEventSource = null; + } + + // 使用EventSource直接连接到查询流 + const eventSource = new EventSource(`/query-stream/?original_query=${encodeURIComponent(queryText)}&max_iter=${maxIter}`); + + // 保存EventSource引用以便后续关闭 + window.currentEventSource = eventSource; + + eventSource.onopen = function(event) { + console.log('EventSource connection opened for query'); + showStatus('queryStatus', ' 正在处理...', 'loading'); + }; + + eventSource.onmessage = function(event) { + console.log('Received message:', event.data); + handleStreamMessage(event.data); + }; + + eventSource.onerror = function(event) { + console.error('EventSource error:', event); + if (eventSource.readyState === EventSource.CLOSED) { + console.log('EventSource connection closed due to error'); + isStreaming = false; + setButtonLoading(button, false); + window.currentEventSource = null; + } + }; + + } catch (error) { + console.error('Query error:', error); + showStatus('queryStatus', `请求失败: ${error.message}`, 'error'); + isStreaming = false; + setButtonLoading(button, false); + } +}); + +// 页面卸载时清理连接 +window.addEventListener('beforeunload', function() { + if (window.currentEventSource) { + window.currentEventSource.close(); + window.currentEventSource = null; + } +}); diff --git a/main.py b/main.py index 7e268e2..e9e5eda 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import uvicorn from fastapi import Body, FastAPI, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles from pydantic import BaseModel import os import asyncio @@ -18,6 +19,11 @@ from deepsearcher.utils.message_stream import get_message_stream app = FastAPI() +# 配置静态文件服务 +current_dir = os.path.dirname(os.path.abspath(__file__)) +static_dir = os.path.join(current_dir, "deepsearcher", "templates", "static") +app.mount("/static", StaticFiles(directory=static_dir), name="static") + config = Configuration() init_config(config) @@ -47,7 +53,7 @@ async def read_root(): Serve the main HTML page. """ current_dir = os.path.dirname(os.path.abspath(__file__)) - template_path = os.path.join(current_dir, "deepsearcher", "backend", "templates", "index.html") + template_path = os.path.join(current_dir, "deepsearcher", "templates", "html", "index.html") try: with open(template_path, encoding="utf-8") as file: diff --git a/test_fix.py b/test_fix.py deleted file mode 100644 index 5fb3044..0000000 --- a/test_fix.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -""" -测试修复后的查询功能 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from deepsearcher.configuration import Configuration, init_config -from deepsearcher.online_query import query - -def test_query_fix(): - """测试修复后的查询功能""" - print("=== 测试修复后的查询功能 ===") - - # 初始化配置 - config = Configuration() - init_config(config) - - try: - print("开始查询...") - result_text, retrieval_results = query("什么是Milvus?", max_iter=1) - - print(f"查询完成!") - print(f"结果长度: {len(result_text) if result_text else 0}") - print(f"检索结果数量: {len(retrieval_results) if retrieval_results else 0}") - - if result_text: - print(f"结果预览: {result_text[:200]}...") - - except Exception as e: - import traceback - print(f"查询失败: {e}") - print(f"错误详情: {traceback.format_exc()}") - -if __name__ == "__main__": - test_query_fix()