swarm-backend / web_app.py
lk080424's picture
Upload web_app.py with huggingface_hub
a952464 verified
"""
虫群Swarm Web界面 — 纯FastAPI+HTML,零额外依赖
功能:
- 聊天对话(本地记忆+API推理+MOA聚合)
- 系统状态面板
- 记忆管理
"""
import os, sys, time, json, asyncio
from typing import Optional
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
app = FastAPI(title="虫群Swarm", version="8.0")
# ============================================================
# 全局状态
# ============================================================
class SwarmAppState:
def __init__(self):
self.node = None
self.initialized = False
self.chat_history = []
def initialize(self):
if self.initialized:
return True
try:
from core.swarm_node import SwarmNode
from core.aggregation_protocol.types import PermissionLevel
self.node = SwarmNode(
node_id="web_user",
name="Web用户",
permission_level=PermissionLevel.QUEEN,
model_config="tiny",
initial_balance=100.0,
)
self.node.start()
self.initialized = True
return True
except Exception as e:
print(f"[WebApp] 初始化失败: {e}")
import traceback; traceback.print_exc()
return False
state = SwarmAppState()
# ============================================================
# HTML页面
# ============================================================
HTML_PAGE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>虫群 Swarm</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; }
.sidebar { width: 240px; background: #111; border-right: 1px solid #222;
display: flex; flex-direction: column; }
.sidebar-header { padding: 16px; border-bottom: 1px solid #222; }
.sidebar-header h2 { font-size: 18px; color: #4ecdc4; }
.sidebar-header p { font-size: 12px; color: #666; margin-top: 4px; }
.stat-item { padding: 10px 16px; border-bottom: 1px solid #1a1a1a; font-size: 13px; }
.stat-label { color: #666; font-size: 11px; }
.stat-value { color: #4ecdc4; font-weight: bold; }
.main { flex:1; display: flex; flex-direction: column; }
.chat-header { padding: 12px 20px; border-bottom: 1px solid #222;
background: #111; display: flex; justify-content: space-between; align-items: center; }
.chat-header h3 { font-size: 15px; }
.mode-select { background: #1a1a1a; color: #e0e0e0; border: 1px solid #333;
padding: 6px 12px; border-radius: 6px; font-size: 12px; }
.messages { flex:1; overflow-y: auto; padding: 20px; }
.msg { margin-bottom: 16px; max-width: 80%; }
.msg.user { margin-left: auto; }
.msg-bubble { padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; }
.msg.user .msg-bubble { background: #1a3a4a; color: #e0e0e0; border-bottom-right-radius: 4px; }
.msg.assistant .msg-bubble { background: #1a1a2e; color: #e0e0e0; border-bottom-left-radius: 4px; }
.msg-meta { font-size: 11px; color: #555; margin-top: 4px; }
.msg-source { color: #4ecdc4; }
.input-area { padding: 16px; border-top: 1px solid #222; background: #111;
display: flex; gap: 10px; }
.input-area input { flex:1; background: #1a1a1a; border: 1px solid #333; color: #e0e0e0;
padding: 10px 14px; border-radius: 8px; font-size: 14px; outline: none; }
.input-area input:focus { border-color: #4ecdc4; }
.input-area button { background: #4ecdc4; color: #000; border: none; padding: 10px 20px;
border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 14px; }
.input-area button:hover { background: #3dbdb5; }
.input-area button:disabled { background: #333; color: #666; cursor: not-allowed; }
.loading { display: none; color: #4ecdc4; font-size: 13px; padding: 4px 0; }
.loading.active { display: block; }
.tag { display: inline-block; background: #1a2a2a; color: #4ecdc4; font-size: 11px;
padding: 2px 8px; border-radius: 4px; margin-right: 4px; }
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<h2>🦠 虫群 Swarm</h2>
<p>v8.0 分布式智能体</p>
</div>
<div id="stats">
<div class="stat-item"><div class="stat-label">状态</div><div class="stat-value" id="st-status">初始化中...</div></div>
<div class="stat-item"><div class="stat-label">本地模型</div><div class="stat-value" id="st-model">-</div></div>
<div class="stat-item"><div class="stat-label">API模型</div><div class="stat-value" id="st-api">-</div></div>
<div class="stat-item"><div class="stat-label">记忆数</div><div class="stat-value" id="st-mem">0</div></div>
<div class="stat-item"><div class="stat-label">余额</div><div class="stat-value" id="st-bal">0 CC</div></div>
<div class="stat-item"><div class="stat-label">查询次数</div><div class="stat-value" id="st-queries">0</div></div>
</div>
</div>
<div class="main">
<div class="chat-header">
<h3>💬 对话</h3>
<div>
<select class="mode-select" id="mode">
<option value="smart">智能模式(记忆+API+MOA)</option>
<option value="local">仅本地记忆</option>
<option value="api">仅API推理</option>
</select>
</div>
</div>
<div class="messages" id="messages"></div>
<div class="loading" id="loading">🦠 虫群思考中...</div>
<div class="input-area">
<input id="input" placeholder="输入消息..." autofocus />
<button id="send" onclick="sendMessage()">发送</button>
</div>
</div>
<script>
const msgs = document.getElementById('messages');
const inp = document.getElementById('input');
const loading = document.getElementById('loading');
const sendBtn = document.getElementById('send');
let busy = false;
inp.addEventListener('keydown', e => { if(e.key==='Enter') sendMessage(); });
async function sendMessage() {
const text = inp.value.trim();
if(!text || busy) return;
busy = true;
inp.value = '';
sendBtn.disabled = true;
loading.classList.add('active');
addMsg('user', text);
const mode = document.getElementById('mode').value;
const useApi = mode !== 'local';
const useMoa = mode === 'smart';
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 60000);
try {
const resp = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({message: text, use_api: useApi, use_moa: useMoa}),
signal: controller.signal
});
clearTimeout(timeout);
const data = await resp.json();
let sourceTag = '';
if(data.source) sourceTag = '<span class="tag">' + data.source + '</span>';
if(data.contributors && data.contributors.length)
sourceTag += data.contributors.map(c => '<span class="tag">' + c + '</span>').join('');
const meta = '<div class="msg-meta">' + sourceTag +
' 置信度:' + (data.confidence||0).toFixed(2) +
' 延迟:' + Math.round(data.latency_ms||0) + 'ms</div>';
addMsg('assistant', data.response || '(无回答)', meta);
// 更新统计
if(data.detail) {
document.getElementById('st-queries').textContent =
(parseInt(document.getElementById('st-queries').textContent) + 1);
}
} catch(e) {
addMsg('assistant', '❌ 请求失败: ' + e.message);
}
busy = false;
sendBtn.disabled = false;
loading.classList.remove('active');
}
function addMsg(role, text, meta='') {
const div = document.createElement('div');
div.className = 'msg ' + role;
div.innerHTML = '<div class="msg-bubble">' + escapeHtml(text) + '</div>' + meta;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\\n/g,'<br>');
}
// 初始化状态
async function loadStatus() {
try {
const r = await fetch('/api/status');
const d = await r.json();
document.getElementById('st-status').textContent = d.ready ? '✅ 就绪' : '❌ 未就绪';
document.getElementById('st-model').textContent = d.model || '-';
document.getElementById('st-api').textContent = (d.api_models||0) + '个';
document.getElementById('st-mem').textContent = d.memories || 0;
document.getElementById('st-bal').textContent = (d.balance||0).toFixed(1) + ' CC';
document.getElementById('st-queries').textContent = d.queries || 0;
} catch(e) {
document.getElementById('st-status').textContent = '❌ 连接失败';
}
}
loadStatus();
setInterval(loadStatus, 10000);
</script>
</body>
</html>
"""
# ============================================================
# API路由
# ============================================================
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML_PAGE
@app.get("/api/status")
async def get_status():
if not state.initialized:
state.initialize()
if not state.initialized or not state.node:
return {"ready": False}
node = state.node
mem_status = node.memory.get_status()
api_count = len(node.api_manager.models) if node._api_models_available else 0
balance = node.get_balance()
return {
"ready": True,
"model": mem_status.get("model_config", "tiny"),
"api_models": api_count,
"memories": mem_status.get("total_memories", 0),
"balance": balance,
"queries": node.stats.get("queries_processed", 0),
}
@app.post("/api/chat")
async def chat(req: dict):
if not state.initialized:
state.initialize()
if not state.initialized or not state.node:
return {"response": "系统初始化失败", "source": "error", "confidence": 0}
message = req.get("message", "")
use_api = req.get("use_api", True)
use_moa = req.get("use_moa", True)
# 存储用户消息
state.chat_history.append({"role": "user", "content": message})
# 智能查询
result = state.node.smart_query(
message, use_api=use_api, use_moa=use_moa
)
# 存储AI回答到记忆
if result.get("response"):
state.node.store_memory(message, result["response"], importance=0.5)
state.chat_history.append({"role": "assistant", "content": result["response"]})
return result
@app.post("/api/memory")
async def store_memory(req: dict):
"""手动存储记忆"""
if not state.initialized or not state.node:
return {"ok": False}
mid = state.node.store_memory(
req.get("question", ""),
req.get("answer", ""),
importance=req.get("importance", 0.5)
)
return {"ok": True, "memory_id": mid}
@app.get("/api/memory/stats")
async def memory_stats():
if not state.initialized or not state.node:
return {"total": 0}
return state.node.memory.get_status()
# 启动
if __name__ == "__main__":
import uvicorn
print("=" * 50)
print("🦠 虫群 Swarm Web界面")
print("=" * 50)
uvicorn.run(app, host="0.0.0.0", port=7860)