Browse Source

添加简单前端页面

main
tanxing 2 weeks ago
parent
commit
16455a144c
  1. 28
      README.md
  2. 469
      deepsearcher/backend/templates/index.html
  3. 67
      main.py

28
README.md

@ -22,6 +22,7 @@ DeepSearcher combines cutting-edge LLMs (OpenAI o3, Qwen3, DeepSeek, Grok 4, Cla
- **Flexible Embedding Options**: Compatible with multiple embedding models for optimal selection.
- **Multiple LLM Support**: Supports DeepSeek, OpenAI, and other large models for intelligent Q&A and content generation.
- **Document Loader**: Supports local file loading, with web crawling capabilities under development.
- **Web Interface**: Provides a user-friendly web interface for loading documents and performing queries.
---
@ -92,6 +93,33 @@ load_from_website(urls=website_url)
# Query
result = query("Write a report about xxx.") # Your question here
```
### Web Interface
DeepSearcher now includes a web interface for easier interaction with the system. To use it:
1. Start the service:
```shell
python main.py
```
2. Open your browser and navigate to http://localhost:8000
3. Use the web interface to:
- Load local files by specifying their paths
- Load website content by providing URLs
- Perform queries against the loaded data
You can also enable CORS support for development:
```shell
python main.py --enable-cors
```
To customize the host and port:
```shell
python main.py --host 127.0.0.1 --port 8080
```
### Configuration Details:
#### LLM Configuration

469
deepsearcher/backend/templates/index.html

@ -0,0 +1,469 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSearcher - 智能搜索系统</title>
<style>
: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;
}
* {
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);
}
.loading-spinner {
display: none;
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
vertical-align: middle;
}
@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;
}
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;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>DeepSearcher 智能搜索系统</h1>
<p class="app-description">基于大型语言模型和向量数据库的企业知识管理系统,支持私有数据搜索和在线内容整合,提供准确答案和综合报告。</p>
</header>
<main>
<div class="card">
<h2 class="card-title">文件加载</h2>
<div class="form-group">
<label for="filePaths">文件路径(多个路径用逗号分隔)</label>
<input type="text" id="filePaths" placeholder="例如: /path/to/file1.pdf,/path/to/file2.txt">
</div>
<div class="form-group">
<label for="collectionName">集合名称(可选)</label>
<input type="text" id="collectionName" placeholder="例如: my_collection">
</div>
<div class="form-group">
<label for="collectionDesc">集合描述(可选)</label>
<textarea id="collectionDesc" rows="2" placeholder="例如: 这是一个测试集合"></textarea>
</div>
<button id="loadFilesBtn">加载文件</button>
<div id="loadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">网站内容加载</h2>
<div class="form-group">
<label for="websiteUrls">网站URL(多个URL用逗号分隔)</label>
<input type="text" id="websiteUrls" placeholder="例如: https://example.com/page1,https://example.com/page2">
</div>
<div class="form-group">
<label for="webCollectionName">集合名称(可选)</label>
<input type="text" id="webCollectionName" placeholder="例如: web_collection">
</div>
<div class="form-group">
<label for="webCollectionDesc">集合描述(可选)</label>
<textarea id="webCollectionDesc" rows="2" placeholder="例如: 来自网站的内容"></textarea>
</div>
<button id="loadWebsiteBtn">加载网站内容</button>
<div id="webLoadStatus" class="status"></div>
</div>
<div class="card">
<h2 class="card-title">智能查询</h2>
<div class="form-group">
<label for="queryText">请输入您的问题</label>
<textarea id="queryText" rows="3" placeholder="例如: 请生成一份关于人工智能发展趋势的报告"></textarea>
</div>
<div class="form-group">
<label for="maxIter">最大迭代次数 (1-10)</label>
<input type="number" id="maxIter" min="1" max="10" value="3">
</div>
<button id="queryBtn">执行查询</button>
<div id="queryStatus" class="status"></div>
<div id="queryResult" class="result-container">
<h3>查询结果:</h3>
<div class="query-result" id="resultText"></div>
<div id="tokenInfo"></div>
</div>
</div>
</main>
<footer>
<p>DeepSearcher © 2025 | 企业知识管理与智能问答系统</p>
</footer>
</div>
<script>
// 工具函数:显示状态信息
function showStatus(elementId, message, type) {
const statusElement = document.getElementById(elementId);
statusElement.textContent = message;
statusElement.className = 'status visible';
// 清除之前的类型类
statusElement.classList.remove('status-success', 'status-error', 'status-loading');
// 添加新的类型类
if (type === 'success') {
statusElement.classList.add('status-success');
} else if (type === 'error') {
statusElement.classList.add('status-error');
} else if (type === 'loading') {
statusElement.classList.add('status-loading');
}
}
// 工具函数:隐藏状态信息
function hideStatus(elementId) {
const statusElement = document.getElementById(elementId);
statusElement.classList.remove('visible');
}
// 工具函数:显示结果
function showResult(message) {
const resultElement = document.getElementById('queryResult');
const resultTextElement = document.getElementById('resultText');
resultTextElement.textContent = message;
resultElement.classList.add('visible');
}
// 工具函数:隐藏结果
function hideResult() {
const resultElement = document.getElementById('queryResult');
resultElement.classList.remove('visible');
}
// 工具函数:设置按钮加载状态
function setButtonLoading(button, loading) {
if (loading) {
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
}
}
// 加载文件功能
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();
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('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();
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;
}
setButtonLoading(button, true);
showStatus('queryStatus', '正在处理查询...', 'loading');
hideResult();
try {
const response = await fetch(`/query/?original_query=${encodeURIComponent(queryText)}&max_iter=${maxIter}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
if (response.ok) {
showStatus('queryStatus', '查询完成', 'success');
document.getElementById('resultText').textContent = data.result;
document.getElementById('tokenInfo').textContent = `消耗Token数: ${data.consume_token}`;
showResult();
} else {
showStatus('queryStatus', `查询失败: ${data.detail}`, 'error');
}
} catch (error) {
showStatus('queryStatus', `请求失败: ${error.message}`, 'error');
} finally {
setButtonLoading(button, false);
}
});
</script>
</body>
</html>

67
main.py

@ -4,7 +4,9 @@ from typing import Dict, List, Union
import uvicorn
from fastapi import Body, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
import os
from deepsearcher.configuration import Configuration, init_config
from deepsearcher.offline_loading import load_from_local_files, load_from_website
@ -32,6 +34,62 @@ class ProviderConfigRequest(BaseModel):
config: Dict
@app.get("/", response_class=HTMLResponse)
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")
# 读取HTML文件内容
try:
with open(template_path, "r", encoding="utf-8") as file:
html_content = file.read()
return HTMLResponse(content=html_content, status_code=200)
except FileNotFoundError:
# 如果找不到文件,提供一个简单的默认页面
default_html = """
<!DOCTYPE html>
<html>
<head>
<title>DeepSearcher</title>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; }}
.container {{ max-width: 800px; margin: 0 auto; }}
h1 {{ color: #333; }}
.info {{ background: #f0f8ff; padding: 15px; border-radius: 5px; }}
.error {{ background: #ffe4e1; padding: 15px; border-radius: 5px; color: #d00; }}
</style>
</head>
<body>
<div class="container">
<h1>DeepSearcher</h1>
<div class="info">
<p>欢迎使用 DeepSearcher 智能搜索系统!</p>
<p>系统正在运行但未找到前端模板文件</p>
<p>请确认文件是否存在: {}</p>
</div>
<div class="info">
<h2>API 接口</h2>
<p>您仍然可以通过以下 API 接口使用系统:</p>
<ul>
<li><code>POST /load-files/</code> - 加载本地文件</li>
<li><code>POST /load-website/</code> - 加载网站内容</li>
<li><code>GET /query/</code> - 执行查询</li>
</ul>
<p>有关 API 使用详情请查看 <a href="/docs">API 文档</a></p>
</div>
</div>
</body>
</html>
""".format(template_path)
return HTMLResponse(content=default_html, status_code=200)
@app.post("/set-provider-config/")
def set_provider_config(request: ProviderConfigRequest):
"""
@ -194,8 +252,11 @@ def perform_query(
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="FastAPI Server")
parser.add_argument("--enable-cors", type=bool, default=False, help="Enable CORS support")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind the server to")
parser.add_argument("--port", type=int, default=8000, help="Port to bind the server to")
parser.add_argument("--enable-cors", action="store_true", default=False, help="Enable CORS support")
args = parser.parse_args()
if args.enable_cors:
app.add_middleware(
CORSMiddleware,
@ -207,4 +268,6 @@ if __name__ == "__main__":
print("CORS is enabled.")
else:
print("CORS is disabled.")
uvicorn.run(app, host="0.0.0.0", port=8000)
print(f"Starting server on {args.host}:{args.port}")
uvicorn.run(app, host=args.host, port=args.port)
Loading…
Cancel
Save