neuro-simulator 0.1.3__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neuro_simulator/__init__.py +1 -10
- neuro_simulator/agent/__init__.py +1 -8
- neuro_simulator/agent/base.py +43 -0
- neuro_simulator/agent/core.py +105 -398
- neuro_simulator/agent/factory.py +30 -0
- neuro_simulator/agent/llm.py +34 -31
- neuro_simulator/agent/memory/__init__.py +1 -4
- neuro_simulator/agent/memory/manager.py +61 -203
- neuro_simulator/agent/tools/__init__.py +1 -4
- neuro_simulator/agent/tools/core.py +8 -18
- neuro_simulator/api/__init__.py +1 -0
- neuro_simulator/api/agent.py +163 -0
- neuro_simulator/api/stream.py +55 -0
- neuro_simulator/api/system.py +90 -0
- neuro_simulator/cli.py +60 -143
- neuro_simulator/core/__init__.py +1 -0
- neuro_simulator/core/agent_factory.py +52 -0
- neuro_simulator/core/agent_interface.py +91 -0
- neuro_simulator/core/application.py +278 -0
- neuro_simulator/services/__init__.py +1 -0
- neuro_simulator/{chatbot.py → services/audience.py} +24 -24
- neuro_simulator/{audio_synthesis.py → services/audio.py} +18 -15
- neuro_simulator/services/builtin.py +87 -0
- neuro_simulator/services/letta.py +206 -0
- neuro_simulator/{stream_manager.py → services/stream.py} +39 -47
- neuro_simulator/utils/__init__.py +1 -0
- neuro_simulator/utils/logging.py +90 -0
- neuro_simulator/utils/process.py +67 -0
- neuro_simulator/{stream_chat.py → utils/queue.py} +17 -4
- neuro_simulator/utils/state.py +14 -0
- neuro_simulator/{websocket_manager.py → utils/websocket.py} +18 -14
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/METADATA +83 -33
- neuro_simulator-0.2.1.dist-info/RECORD +37 -0
- neuro_simulator/agent/api.py +0 -737
- neuro_simulator/agent/memory.py +0 -137
- neuro_simulator/agent/tools.py +0 -69
- neuro_simulator/builtin_agent.py +0 -83
- neuro_simulator/config.yaml.example +0 -157
- neuro_simulator/letta.py +0 -164
- neuro_simulator/log_handler.py +0 -43
- neuro_simulator/main.py +0 -673
- neuro_simulator/media/neuro_start.mp4 +0 -0
- neuro_simulator/process_manager.py +0 -70
- neuro_simulator/shared_state.py +0 -11
- neuro_simulator-0.1.3.dist-info/RECORD +0 -31
- /neuro_simulator/{config.py → core/config.py} +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/top_level.txt +0 -0
neuro_simulator/main.py
DELETED
@@ -1,673 +0,0 @@
|
|
1
|
-
# backend/main.py
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
import json
|
5
|
-
import traceback
|
6
|
-
import random
|
7
|
-
import re
|
8
|
-
import time
|
9
|
-
import os
|
10
|
-
import sys
|
11
|
-
from typing import Optional
|
12
|
-
|
13
|
-
from fastapi import (
|
14
|
-
FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Request, Form, Depends, status
|
15
|
-
)
|
16
|
-
from pydantic import BaseModel
|
17
|
-
from fastapi.middleware.cors import CORSMiddleware
|
18
|
-
from fastapi.templating import Jinja2Templates
|
19
|
-
from fastapi.responses import RedirectResponse, HTMLResponse
|
20
|
-
from starlette.websockets import WebSocketState
|
21
|
-
from starlette.status import HTTP_303_SEE_OTHER
|
22
|
-
from fastapi.security import APIKeyCookie
|
23
|
-
|
24
|
-
# --- 核心模块导入 ---
|
25
|
-
from .config import config_manager, AppSettings
|
26
|
-
from .process_manager import process_manager
|
27
|
-
from .log_handler import configure_server_logging, server_log_queue, agent_log_queue
|
28
|
-
|
29
|
-
# --- 功能模块导入 ---
|
30
|
-
from .chatbot import ChatbotManager, get_dynamic_audience_prompt
|
31
|
-
# from .letta import get_neuro_response, reset_neuro_agent_memory, initialize_agent # This will be imported dynamically
|
32
|
-
from .audio_synthesis import synthesize_audio_segment
|
33
|
-
from .stream_chat import (
|
34
|
-
add_to_audience_buffer, add_to_neuro_input_queue,
|
35
|
-
get_recent_audience_chats, is_neuro_input_queue_empty, get_all_neuro_input_chats
|
36
|
-
)
|
37
|
-
from .websocket_manager import connection_manager
|
38
|
-
from .stream_manager import live_stream_manager
|
39
|
-
import neuro_simulator.shared_state as shared_state
|
40
|
-
|
41
|
-
# --- FastAPI 应用和模板设置 ---
|
42
|
-
from .agent.api import router as agent_router
|
43
|
-
|
44
|
-
app = FastAPI(title="vedal987 Simulator API", version="1.0.0")
|
45
|
-
|
46
|
-
# 注册API路由
|
47
|
-
app.include_router(agent_router)
|
48
|
-
app.include_router(agent_router) # Include the agent management API router
|
49
|
-
app.add_middleware(
|
50
|
-
CORSMiddleware,
|
51
|
-
allow_origins=config_manager.settings.server.client_origins + ["http://localhost:8080", "https://dashboard.live.jiahui.cafe"], # 添加dashboard_web的地址
|
52
|
-
allow_credentials=True,
|
53
|
-
allow_methods=["*"],
|
54
|
-
allow_headers=["*"],
|
55
|
-
expose_headers=["X-API-Token"], # 暴露API Token头
|
56
|
-
)
|
57
|
-
|
58
|
-
# --- 安全和认证 ---
|
59
|
-
API_TOKEN_HEADER = "X-API-Token"
|
60
|
-
|
61
|
-
async def get_api_token(request: Request):
|
62
|
-
"""检查API token是否有效"""
|
63
|
-
password = config_manager.settings.server.panel_password
|
64
|
-
if not password:
|
65
|
-
# No password set, allow access
|
66
|
-
return True
|
67
|
-
|
68
|
-
# 检查header中的token
|
69
|
-
header_token = request.headers.get(API_TOKEN_HEADER)
|
70
|
-
if header_token and header_token == password:
|
71
|
-
return True
|
72
|
-
|
73
|
-
raise HTTPException(
|
74
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
75
|
-
detail="Invalid API token",
|
76
|
-
headers={"WWW-Authenticate": "Bearer"},
|
77
|
-
)
|
78
|
-
|
79
|
-
# -------------------------------------------------------------
|
80
|
-
# --- 后台任务函数定义 ---
|
81
|
-
# -------------------------------------------------------------
|
82
|
-
|
83
|
-
async def broadcast_events_task():
|
84
|
-
"""从 live_stream_manager 的队列中获取事件并广播给所有客户端。"""
|
85
|
-
while True:
|
86
|
-
try:
|
87
|
-
event = await live_stream_manager.event_queue.get()
|
88
|
-
print(f"广播事件: {event}")
|
89
|
-
await connection_manager.broadcast(event)
|
90
|
-
live_stream_manager.event_queue.task_done()
|
91
|
-
except asyncio.CancelledError:
|
92
|
-
print("广播任务被取消。")
|
93
|
-
break
|
94
|
-
except Exception as e:
|
95
|
-
print(f"广播事件时出错: {e}")
|
96
|
-
|
97
|
-
async def fetch_and_process_audience_chats():
|
98
|
-
"""单个聊天生成任务的执行体。"""
|
99
|
-
if not chatbot_manager or not chatbot_manager.client:
|
100
|
-
print("错误: Chatbot manager 未初始化,跳过聊天生成。")
|
101
|
-
return
|
102
|
-
try:
|
103
|
-
dynamic_prompt = await get_dynamic_audience_prompt()
|
104
|
-
raw_chat_text = await chatbot_manager.client.generate_chat_messages(
|
105
|
-
prompt=dynamic_prompt,
|
106
|
-
max_tokens=config_manager.settings.audience_simulation.max_output_tokens
|
107
|
-
)
|
108
|
-
|
109
|
-
parsed_chats = []
|
110
|
-
for line in raw_chat_text.split('\n'):
|
111
|
-
line = line.strip()
|
112
|
-
if ':' in line:
|
113
|
-
username_raw, text = line.split(':', 1)
|
114
|
-
username = username_raw.strip()
|
115
|
-
if username in config_manager.settings.audience_simulation.username_blocklist:
|
116
|
-
username = random.choice(config_manager.settings.audience_simulation.username_pool)
|
117
|
-
if username and text.strip():
|
118
|
-
parsed_chats.append({"username": username, "text": text.strip()})
|
119
|
-
elif line:
|
120
|
-
parsed_chats.append({"username": random.choice(config_manager.settings.audience_simulation.username_pool), "text": line})
|
121
|
-
|
122
|
-
chats_to_broadcast = parsed_chats[:config_manager.settings.audience_simulation.chats_per_batch]
|
123
|
-
|
124
|
-
for chat in chats_to_broadcast:
|
125
|
-
add_to_audience_buffer(chat)
|
126
|
-
add_to_neuro_input_queue(chat)
|
127
|
-
broadcast_message = {"type": "chat_message", **chat, "is_user_message": False}
|
128
|
-
await connection_manager.broadcast(broadcast_message)
|
129
|
-
await asyncio.sleep(random.uniform(0.1, 0.4))
|
130
|
-
except Exception:
|
131
|
-
print("错误: 单个聊天生成任务失败。详情见 traceback。")
|
132
|
-
traceback.print_exc()
|
133
|
-
|
134
|
-
async def generate_audience_chat_task():
|
135
|
-
"""周期性地调度聊天生成任务。"""
|
136
|
-
print("观众聊天调度器: 任务启动。")
|
137
|
-
while True:
|
138
|
-
try:
|
139
|
-
asyncio.create_task(fetch_and_process_audience_chats())
|
140
|
-
await asyncio.sleep(config_manager.settings.audience_simulation.chat_generation_interval_sec)
|
141
|
-
except asyncio.CancelledError:
|
142
|
-
print("观众聊天调度器任务被取消。")
|
143
|
-
break
|
144
|
-
|
145
|
-
async def neuro_response_cycle():
|
146
|
-
"""Neuro 的核心响应循环。"""
|
147
|
-
await shared_state.live_phase_started_event.wait()
|
148
|
-
print("Neuro响应周期: 任务启动。")
|
149
|
-
is_first_response = True
|
150
|
-
|
151
|
-
# Dynamically import get_neuro_response to respect agent_type
|
152
|
-
agent_type = config_manager.settings.agent_type
|
153
|
-
if agent_type == "builtin":
|
154
|
-
from .builtin_agent import get_builtin_response as get_neuro_response
|
155
|
-
else:
|
156
|
-
from .letta import get_neuro_response
|
157
|
-
|
158
|
-
while True:
|
159
|
-
try:
|
160
|
-
if is_first_response:
|
161
|
-
print("首次响应: 注入开场白。")
|
162
|
-
add_to_neuro_input_queue({"username": "System", "text": config_manager.settings.neuro_behavior.initial_greeting})
|
163
|
-
is_first_response = False
|
164
|
-
elif is_neuro_input_queue_empty():
|
165
|
-
await asyncio.sleep(1)
|
166
|
-
continue
|
167
|
-
|
168
|
-
current_queue_snapshot = get_all_neuro_input_chats()
|
169
|
-
sample_size = min(config_manager.settings.neuro_behavior.input_chat_sample_size, len(current_queue_snapshot))
|
170
|
-
selected_chats = random.sample(current_queue_snapshot, sample_size)
|
171
|
-
|
172
|
-
# 使用 asyncio.wait_for 添加超时机制,避免长时间阻塞
|
173
|
-
try:
|
174
|
-
ai_full_response_text = await asyncio.wait_for(
|
175
|
-
get_neuro_response(selected_chats),
|
176
|
-
timeout=10.0 # 默认10秒超时
|
177
|
-
)
|
178
|
-
except asyncio.TimeoutError:
|
179
|
-
print(f"警告: {agent_type} 响应超时,跳过本轮。")
|
180
|
-
await asyncio.sleep(5)
|
181
|
-
continue
|
182
|
-
|
183
|
-
async with shared_state.neuro_last_speech_lock:
|
184
|
-
# Handle both string and dict responses
|
185
|
-
response_text = ""
|
186
|
-
if isinstance(ai_full_response_text, dict):
|
187
|
-
# Extract the final response from the dict
|
188
|
-
response_text = ai_full_response_text.get("final_response", "")
|
189
|
-
else:
|
190
|
-
response_text = ai_full_response_text if ai_full_response_text else ""
|
191
|
-
|
192
|
-
if response_text and response_text.strip():
|
193
|
-
shared_state.neuro_last_speech = response_text
|
194
|
-
else:
|
195
|
-
shared_state.neuro_last_speech = "(Neuro-Sama is currently silent...)"
|
196
|
-
print(f"警告: 从 {agent_type} 获取的响应为空,跳过本轮。")
|
197
|
-
continue
|
198
|
-
|
199
|
-
# Handle both string and dict responses for sentence splitting
|
200
|
-
response_text = ""
|
201
|
-
if isinstance(ai_full_response_text, dict):
|
202
|
-
response_text = ai_full_response_text.get("final_response", "")
|
203
|
-
else:
|
204
|
-
response_text = ai_full_response_text if ai_full_response_text else ""
|
205
|
-
|
206
|
-
sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', response_text.replace('\n', ' ').strip()) if s.strip()]
|
207
|
-
if not sentences:
|
208
|
-
continue
|
209
|
-
|
210
|
-
synthesis_tasks = [synthesize_audio_segment(s) for s in sentences]
|
211
|
-
synthesis_results = await asyncio.gather(*synthesis_tasks, return_exceptions=True)
|
212
|
-
|
213
|
-
speech_packages = [
|
214
|
-
{"segment_id": i, "text": sentences[i], "audio_base64": res[0], "duration": res[1]}
|
215
|
-
for i, res in enumerate(synthesis_results) if not isinstance(res, Exception)
|
216
|
-
]
|
217
|
-
|
218
|
-
if not speech_packages:
|
219
|
-
print("错误: 所有句子的 TTS 合成都失败了。")
|
220
|
-
await connection_manager.broadcast({"type": "neuro_error_signal"})
|
221
|
-
await asyncio.sleep(15)
|
222
|
-
continue
|
223
|
-
|
224
|
-
live_stream_manager.set_neuro_speaking_status(True)
|
225
|
-
for package in speech_packages:
|
226
|
-
broadcast_package = {"type": "neuro_speech_segment", **package, "is_end": False}
|
227
|
-
await connection_manager.broadcast(broadcast_package)
|
228
|
-
await asyncio.sleep(package['duration'])
|
229
|
-
|
230
|
-
await connection_manager.broadcast({"type": "neuro_speech_segment", "is_end": True})
|
231
|
-
live_stream_manager.set_neuro_speaking_status(False)
|
232
|
-
|
233
|
-
await asyncio.sleep(config_manager.settings.neuro_behavior.post_speech_cooldown_sec)
|
234
|
-
except asyncio.CancelledError:
|
235
|
-
print("Neuro 响应周期任务被取消。")
|
236
|
-
live_stream_manager.set_neuro_speaking_status(False)
|
237
|
-
break
|
238
|
-
except Exception:
|
239
|
-
print("Neuro响应周期发生严重错误,将在10秒后恢复。详情见 traceback。")
|
240
|
-
traceback.print_exc()
|
241
|
-
live_stream_manager.set_neuro_speaking_status(False)
|
242
|
-
await asyncio.sleep(10)
|
243
|
-
|
244
|
-
|
245
|
-
# -------------------------------------------------------------
|
246
|
-
# --- 应用生命周期事件 ---
|
247
|
-
# -------------------------------------------------------------
|
248
|
-
|
249
|
-
@app.on_event("startup")
|
250
|
-
async def startup_event():
|
251
|
-
"""应用启动时执行。"""
|
252
|
-
global chatbot_manager
|
253
|
-
configure_server_logging()
|
254
|
-
|
255
|
-
# 实例化管理器
|
256
|
-
chatbot_manager = ChatbotManager()
|
257
|
-
|
258
|
-
# 定义并注册回调
|
259
|
-
async def metadata_callback(updated_settings: AppSettings):
|
260
|
-
await live_stream_manager.broadcast_stream_metadata()
|
261
|
-
|
262
|
-
config_manager.register_update_callback(metadata_callback)
|
263
|
-
config_manager.register_update_callback(chatbot_manager.handle_config_update)
|
264
|
-
|
265
|
-
# Initialize the appropriate agent
|
266
|
-
from .letta import initialize_agent
|
267
|
-
from .builtin_agent import initialize_builtin_agent
|
268
|
-
|
269
|
-
agent_type = config_manager.settings.agent_type
|
270
|
-
if agent_type == "builtin":
|
271
|
-
await initialize_builtin_agent()
|
272
|
-
else:
|
273
|
-
await initialize_agent()
|
274
|
-
|
275
|
-
print("FastAPI 应用已启动。请通过外部控制面板控制直播进程。")
|
276
|
-
|
277
|
-
@app.on_event("shutdown")
|
278
|
-
async def shutdown_event():
|
279
|
-
"""应用关闭时执行。"""
|
280
|
-
if process_manager.is_running:
|
281
|
-
process_manager.stop_live_processes()
|
282
|
-
print("FastAPI 应用已关闭。")
|
283
|
-
|
284
|
-
|
285
|
-
# -------------------------------------------------------------
|
286
|
-
# --- 直播控制 API 端点 ---
|
287
|
-
# -------------------------------------------------------------
|
288
|
-
|
289
|
-
@app.post("/api/stream/start", tags=["Stream Control"], dependencies=[Depends(get_api_token)])
|
290
|
-
async def api_start_stream():
|
291
|
-
"""启动直播"""
|
292
|
-
# If using builtin agent, clear temp memory and context when starting stream
|
293
|
-
agent_type = config_manager.settings.agent_type
|
294
|
-
if agent_type == "builtin":
|
295
|
-
from .builtin_agent import clear_builtin_agent_temp_memory, clear_builtin_agent_context
|
296
|
-
await clear_builtin_agent_temp_memory()
|
297
|
-
await clear_builtin_agent_context()
|
298
|
-
|
299
|
-
if not process_manager.is_running:
|
300
|
-
process_manager.start_live_processes()
|
301
|
-
return {"status": "success", "message": "直播已启动"}
|
302
|
-
else:
|
303
|
-
return {"status": "info", "message": "直播已在运行"}
|
304
|
-
|
305
|
-
@app.post("/api/stream/stop", tags=["Stream Control"], dependencies=[Depends(get_api_token)])
|
306
|
-
async def api_stop_stream():
|
307
|
-
"""停止直播"""
|
308
|
-
if process_manager.is_running:
|
309
|
-
process_manager.stop_live_processes()
|
310
|
-
return {"status": "success", "message": "直播已停止"}
|
311
|
-
else:
|
312
|
-
return {"status": "info", "message": "直播未在运行"}
|
313
|
-
|
314
|
-
@app.post("/api/stream/restart", tags=["Stream Control"], dependencies=[Depends(get_api_token)])
|
315
|
-
async def api_restart_stream():
|
316
|
-
"""重启直播"""
|
317
|
-
process_manager.stop_live_processes()
|
318
|
-
await asyncio.sleep(1)
|
319
|
-
process_manager.start_live_processes()
|
320
|
-
return {"status": "success", "message": "直播已重启"}
|
321
|
-
|
322
|
-
@app.post("/api/agent/reset_memory", tags=["Agent"], dependencies=[Depends(get_api_token)])
|
323
|
-
async def api_reset_agent_memory():
|
324
|
-
"""重置Agent记忆"""
|
325
|
-
agent_type = config_manager.settings.agent_type
|
326
|
-
|
327
|
-
if agent_type == "builtin":
|
328
|
-
from .builtin_agent import reset_builtin_agent_memory
|
329
|
-
await reset_builtin_agent_memory()
|
330
|
-
return {"status": "success", "message": "内置Agent记忆已重置"}
|
331
|
-
else:
|
332
|
-
from .letta import reset_neuro_agent_memory
|
333
|
-
await reset_neuro_agent_memory()
|
334
|
-
return {"status": "success", "message": "Letta Agent记忆已重置"}
|
335
|
-
|
336
|
-
@app.get("/api/stream/status", tags=["Stream Control"], dependencies=[Depends(get_api_token)])
|
337
|
-
async def api_get_stream_status():
|
338
|
-
"""获取直播状态"""
|
339
|
-
return {
|
340
|
-
"is_running": process_manager.is_running,
|
341
|
-
"backend_status": "running" if process_manager.is_running else "stopped"
|
342
|
-
}
|
343
|
-
|
344
|
-
# -------------------------------------------------------------
|
345
|
-
# --- WebSocket 端点 ---
|
346
|
-
# -------------------------------------------------------------
|
347
|
-
|
348
|
-
@app.websocket("/ws/stream")
|
349
|
-
async def websocket_stream_endpoint(websocket: WebSocket):
|
350
|
-
await connection_manager.connect(websocket)
|
351
|
-
try:
|
352
|
-
initial_event = live_stream_manager.get_initial_state_for_client()
|
353
|
-
await connection_manager.send_personal_message(initial_event, websocket)
|
354
|
-
|
355
|
-
metadata_event = {"type": "update_stream_metadata", **config_manager.settings.stream_metadata.model_dump()}
|
356
|
-
await connection_manager.send_personal_message(metadata_event, websocket)
|
357
|
-
|
358
|
-
initial_chats = get_recent_audience_chats(config_manager.settings.performance.initial_chat_backlog_limit)
|
359
|
-
for chat in initial_chats:
|
360
|
-
await connection_manager.send_personal_message({"type": "chat_message", **chat, "is_user_message": False}, websocket)
|
361
|
-
await asyncio.sleep(0.01)
|
362
|
-
|
363
|
-
while True:
|
364
|
-
raw_data = await websocket.receive_text()
|
365
|
-
data = json.loads(raw_data)
|
366
|
-
if data.get("type") == "user_message":
|
367
|
-
user_message = {"username": data.get("username", "User"), "text": data.get("message", "").strip()}
|
368
|
-
if user_message["text"]:
|
369
|
-
add_to_audience_buffer(user_message)
|
370
|
-
add_to_neuro_input_queue(user_message)
|
371
|
-
broadcast_message = {"type": "chat_message", **user_message, "is_user_message": True}
|
372
|
-
await connection_manager.broadcast(broadcast_message)
|
373
|
-
except WebSocketDisconnect:
|
374
|
-
print(f"客户端 {websocket.client} 已断开连接。")
|
375
|
-
finally:
|
376
|
-
connection_manager.disconnect(websocket)
|
377
|
-
|
378
|
-
@app.websocket("/ws/admin")
|
379
|
-
async def websocket_admin_endpoint(websocket: WebSocket):
|
380
|
-
await websocket.accept()
|
381
|
-
try:
|
382
|
-
# Send initial server logs
|
383
|
-
for log_entry in list(server_log_queue):
|
384
|
-
await websocket.send_json({"type": "server_log", "data": log_entry})
|
385
|
-
|
386
|
-
# Send initial agent logs
|
387
|
-
for log_entry in list(agent_log_queue):
|
388
|
-
await websocket.send_json({"type": "agent_log", "data": log_entry})
|
389
|
-
|
390
|
-
# Send initial context
|
391
|
-
# Import the appropriate agent based on config
|
392
|
-
from .config import config_manager
|
393
|
-
agent_type = config_manager.settings.agent_type
|
394
|
-
if agent_type == "builtin":
|
395
|
-
from .builtin_agent import local_agent
|
396
|
-
if local_agent is not None:
|
397
|
-
context_messages = await local_agent.memory_manager.get_recent_context()
|
398
|
-
await websocket.send_json({
|
399
|
-
"type": "agent_context",
|
400
|
-
"action": "update",
|
401
|
-
"messages": context_messages
|
402
|
-
})
|
403
|
-
|
404
|
-
# Keep track of last context messages to detect changes
|
405
|
-
last_context_messages = []
|
406
|
-
|
407
|
-
# Start heartbeat task
|
408
|
-
heartbeat_task = asyncio.create_task(send_heartbeat(websocket))
|
409
|
-
|
410
|
-
while websocket.client_state == WebSocketState.CONNECTED:
|
411
|
-
# Check for new server logs
|
412
|
-
if server_log_queue:
|
413
|
-
log_entry = server_log_queue.popleft()
|
414
|
-
await websocket.send_json({"type": "server_log", "data": log_entry})
|
415
|
-
|
416
|
-
# Check for new agent logs
|
417
|
-
if agent_log_queue:
|
418
|
-
log_entry = agent_log_queue.popleft()
|
419
|
-
await websocket.send_json({"type": "agent_log", "data": log_entry})
|
420
|
-
|
421
|
-
# Check for context updates (for builtin agent)
|
422
|
-
if agent_type == "builtin" and local_agent is not None:
|
423
|
-
context_messages = await local_agent.memory_manager.get_recent_context()
|
424
|
-
# Compare with last context to detect changes
|
425
|
-
if context_messages != last_context_messages:
|
426
|
-
# Send only new messages
|
427
|
-
if len(context_messages) > len(last_context_messages):
|
428
|
-
new_messages = context_messages[len(last_context_messages):]
|
429
|
-
await websocket.send_json({
|
430
|
-
"type": "agent_context",
|
431
|
-
"action": "append",
|
432
|
-
"messages": new_messages
|
433
|
-
})
|
434
|
-
else:
|
435
|
-
# Only send full update if messages were actually removed (e.g., context reset)
|
436
|
-
# Don't send update if it's just a reordering or modification
|
437
|
-
if len(context_messages) < len(last_context_messages):
|
438
|
-
await websocket.send_json({
|
439
|
-
"type": "agent_context",
|
440
|
-
"action": "update",
|
441
|
-
"messages": context_messages
|
442
|
-
})
|
443
|
-
else:
|
444
|
-
# Send as append if same length but different content
|
445
|
-
await websocket.send_json({
|
446
|
-
"type": "agent_context",
|
447
|
-
"action": "append",
|
448
|
-
"messages": context_messages
|
449
|
-
})
|
450
|
-
last_context_messages = context_messages
|
451
|
-
|
452
|
-
# Small delay to prevent busy waiting
|
453
|
-
await asyncio.sleep(0.1)
|
454
|
-
except WebSocketDisconnect:
|
455
|
-
print("管理面板WebSocket客户端已断开连接。")
|
456
|
-
finally:
|
457
|
-
# Cancel heartbeat task
|
458
|
-
if 'heartbeat_task' in locals():
|
459
|
-
heartbeat_task.cancel()
|
460
|
-
print("管理面板WebSocket连接关闭。")
|
461
|
-
|
462
|
-
|
463
|
-
# 心跳任务,定期发送心跳消息以保持连接活跃
|
464
|
-
async def send_heartbeat(websocket: WebSocket):
|
465
|
-
while websocket.client_state == WebSocketState.CONNECTED:
|
466
|
-
try:
|
467
|
-
# 发送心跳消息
|
468
|
-
await websocket.send_json({"type": "heartbeat", "timestamp": time.time()})
|
469
|
-
# 每5秒发送一次心跳
|
470
|
-
await asyncio.sleep(5)
|
471
|
-
except Exception as e:
|
472
|
-
print(f"发送心跳消息时出错: {e}")
|
473
|
-
break
|
474
|
-
|
475
|
-
|
476
|
-
# -------------------------------------------------------------
|
477
|
-
# --- 其他 API 端点 ---
|
478
|
-
# -------------------------------------------------------------
|
479
|
-
|
480
|
-
class ErrorSpeechRequest(BaseModel):
|
481
|
-
text: str
|
482
|
-
voice_name: str | None = None
|
483
|
-
pitch: float | None = None
|
484
|
-
|
485
|
-
@app.post("/api/tts/synthesize", tags=["TTS"], dependencies=[Depends(get_api_token)])
|
486
|
-
async def synthesize_speech_endpoint(request: ErrorSpeechRequest):
|
487
|
-
"""TTS语音合成端点"""
|
488
|
-
try:
|
489
|
-
audio_base64, _ = await synthesize_audio_segment(
|
490
|
-
text=request.text, voice_name=request.voice_name, pitch=request.pitch
|
491
|
-
)
|
492
|
-
return {"audio_base64": audio_base64}
|
493
|
-
except Exception as e:
|
494
|
-
raise HTTPException(status_code=500, detail=str(e))
|
495
|
-
|
496
|
-
# -------------------------------------------------------------
|
497
|
-
# --- 配置管理 API 端点 ---
|
498
|
-
# -------------------------------------------------------------
|
499
|
-
|
500
|
-
def filter_config_for_frontend(settings):
|
501
|
-
"""过滤配置,只返回前端需要的配置项"""
|
502
|
-
# 创建一个新的字典,只包含前端需要的配置项
|
503
|
-
filtered_settings = {}
|
504
|
-
|
505
|
-
# Stream metadata (除了streamer_nickname)
|
506
|
-
if hasattr(settings, 'stream_metadata'):
|
507
|
-
filtered_settings['stream_metadata'] = {
|
508
|
-
'stream_title': settings.stream_metadata.stream_title,
|
509
|
-
'stream_category': settings.stream_metadata.stream_category,
|
510
|
-
'stream_tags': settings.stream_metadata.stream_tags
|
511
|
-
}
|
512
|
-
|
513
|
-
# Agent settings (不包含 agent_type)
|
514
|
-
if hasattr(settings, 'agent'):
|
515
|
-
filtered_settings['agent'] = {
|
516
|
-
'agent_provider': settings.agent.agent_provider,
|
517
|
-
'agent_model': settings.agent.agent_model
|
518
|
-
}
|
519
|
-
|
520
|
-
# Neuro behavior settings
|
521
|
-
if hasattr(settings, 'neuro_behavior'):
|
522
|
-
filtered_settings['neuro_behavior'] = {
|
523
|
-
'input_chat_sample_size': settings.neuro_behavior.input_chat_sample_size,
|
524
|
-
'post_speech_cooldown_sec': settings.neuro_behavior.post_speech_cooldown_sec,
|
525
|
-
'initial_greeting': settings.neuro_behavior.initial_greeting
|
526
|
-
}
|
527
|
-
|
528
|
-
# Audience simulation settings
|
529
|
-
if hasattr(settings, 'audience_simulation'):
|
530
|
-
filtered_settings['audience_simulation'] = {
|
531
|
-
'llm_provider': settings.audience_simulation.llm_provider,
|
532
|
-
'gemini_model': settings.audience_simulation.gemini_model,
|
533
|
-
'openai_model': settings.audience_simulation.openai_model,
|
534
|
-
'llm_temperature': settings.audience_simulation.llm_temperature,
|
535
|
-
'chat_generation_interval_sec': settings.audience_simulation.chat_generation_interval_sec,
|
536
|
-
'chats_per_batch': settings.audience_simulation.chats_per_batch,
|
537
|
-
'max_output_tokens': settings.audience_simulation.max_output_tokens,
|
538
|
-
'username_blocklist': settings.audience_simulation.username_blocklist,
|
539
|
-
'username_pool': settings.audience_simulation.username_pool
|
540
|
-
}
|
541
|
-
|
542
|
-
# Performance settings
|
543
|
-
if hasattr(settings, 'performance'):
|
544
|
-
filtered_settings['performance'] = {
|
545
|
-
'neuro_input_queue_max_size': settings.performance.neuro_input_queue_max_size,
|
546
|
-
'audience_chat_buffer_max_size': settings.performance.audience_chat_buffer_max_size,
|
547
|
-
'initial_chat_backlog_limit': settings.performance.initial_chat_backlog_limit
|
548
|
-
}
|
549
|
-
|
550
|
-
return filtered_settings
|
551
|
-
|
552
|
-
@app.get("/api/configs", tags=["Config Management"], dependencies=[Depends(get_api_token)])
|
553
|
-
async def get_configs():
|
554
|
-
"""获取当前配置(已过滤,不包含敏感信息)"""
|
555
|
-
return filter_config_for_frontend(config_manager.settings)
|
556
|
-
|
557
|
-
@app.patch("/api/configs", tags=["Config Management"], dependencies=[Depends(get_api_token)])
|
558
|
-
async def update_configs(new_settings: dict):
|
559
|
-
"""更新配置(已过滤,不包含敏感信息)"""
|
560
|
-
try:
|
561
|
-
# 过滤掉不应该被修改的配置项
|
562
|
-
filtered_settings = {}
|
563
|
-
|
564
|
-
# 定义允许修改的配置路径
|
565
|
-
allowed_paths = {
|
566
|
-
'stream_metadata.stream_title',
|
567
|
-
'stream_metadata.stream_category',
|
568
|
-
'stream_metadata.stream_tags',
|
569
|
-
'agent.agent_provider', # 添加 agent 配置项(不包含 agent_type)
|
570
|
-
'agent.agent_model',
|
571
|
-
'neuro_behavior.input_chat_sample_size',
|
572
|
-
'neuro_behavior.post_speech_cooldown_sec',
|
573
|
-
'neuro_behavior.initial_greeting',
|
574
|
-
'audience_simulation.llm_provider',
|
575
|
-
'audience_simulation.gemini_model',
|
576
|
-
'audience_simulation.openai_model',
|
577
|
-
'audience_simulation.llm_temperature',
|
578
|
-
'audience_simulation.chat_generation_interval_sec',
|
579
|
-
'audience_simulation.chats_per_batch',
|
580
|
-
'audience_simulation.max_output_tokens',
|
581
|
-
'audience_simulation.username_blocklist',
|
582
|
-
'audience_simulation.username_pool',
|
583
|
-
'performance.neuro_input_queue_max_size',
|
584
|
-
'performance.audience_chat_buffer_max_size',
|
585
|
-
'performance.initial_chat_backlog_limit'
|
586
|
-
}
|
587
|
-
|
588
|
-
# 递归函数来检查和过滤配置项
|
589
|
-
def filter_nested_dict(obj, prefix=''):
|
590
|
-
filtered = {}
|
591
|
-
for key, value in obj.items():
|
592
|
-
full_path = f"{prefix}.{key}" if prefix else key
|
593
|
-
if full_path in allowed_paths:
|
594
|
-
filtered[key] = value
|
595
|
-
elif isinstance(value, dict):
|
596
|
-
nested_filtered = filter_nested_dict(value, full_path)
|
597
|
-
if nested_filtered: # 只有当过滤后还有内容时才添加
|
598
|
-
filtered[key] = nested_filtered
|
599
|
-
return filtered
|
600
|
-
|
601
|
-
# 应用过滤
|
602
|
-
filtered_settings = filter_nested_dict(new_settings)
|
603
|
-
|
604
|
-
# 更新配置
|
605
|
-
await config_manager.update_settings(filtered_settings)
|
606
|
-
return filter_config_for_frontend(config_manager.settings)
|
607
|
-
except Exception as e:
|
608
|
-
raise HTTPException(status_code=500, detail=f"更新配置失败: {str(e)}")
|
609
|
-
|
610
|
-
@app.post("/api/configs/reload", tags=["Config Management"], dependencies=[Depends(get_api_token)])
|
611
|
-
async def reload_configs():
|
612
|
-
"""重载配置文件"""
|
613
|
-
try:
|
614
|
-
await config_manager.update_settings({}) # 传入空字典,强制重载并触发回调
|
615
|
-
return {"status": "success", "message": "配置已重载"}
|
616
|
-
except Exception as e:
|
617
|
-
raise HTTPException(status_code=500, detail=f"重载配置失败: {str(e)}")
|
618
|
-
|
619
|
-
@app.get("/api/system/health", tags=["System"])
|
620
|
-
async def health_check():
|
621
|
-
"""健康检查端点,用于监控系统状态"""
|
622
|
-
return {
|
623
|
-
"status": "healthy",
|
624
|
-
"backend_running": True,
|
625
|
-
"process_manager_running": process_manager.is_running,
|
626
|
-
"timestamp": time.time()
|
627
|
-
}
|
628
|
-
|
629
|
-
@app.get("/", tags=["Root"])
|
630
|
-
async def root():
|
631
|
-
return {
|
632
|
-
"message": "Neuro-Sama Simulator Backend",
|
633
|
-
"version": "2.0",
|
634
|
-
"api_docs": "/docs",
|
635
|
-
"api_structure": {
|
636
|
-
"stream": "/api/stream",
|
637
|
-
"configs": "/api/configs",
|
638
|
-
"logs": "/api/logs",
|
639
|
-
"tts": "/api/tts",
|
640
|
-
"system": "/api/system",
|
641
|
-
"websocket": "/ws/stream"
|
642
|
-
}
|
643
|
-
}
|
644
|
-
|
645
|
-
# -------------------------------------------------------------
|
646
|
-
# --- Uvicorn 启动 ---
|
647
|
-
# -------------------------------------------------------------
|
648
|
-
|
649
|
-
def run_server(host: str = None, port: int = None):
|
650
|
-
"""Run the server with optional host and port overrides"""
|
651
|
-
import uvicorn
|
652
|
-
|
653
|
-
# Use provided host/port or fall back to config values
|
654
|
-
server_host = host or config_manager.settings.server.host
|
655
|
-
server_port = port or config_manager.settings.server.port
|
656
|
-
|
657
|
-
# When running as a package, we need to specify the full module path
|
658
|
-
uvicorn.run(
|
659
|
-
"neuro_simulator.main:app",
|
660
|
-
host=server_host,
|
661
|
-
port=server_port,
|
662
|
-
reload=False # 生产环境中建议关闭reload
|
663
|
-
)
|
664
|
-
|
665
|
-
if __name__ == "__main__":
|
666
|
-
import uvicorn
|
667
|
-
# 从配置文件中读取host和port设置
|
668
|
-
uvicorn.run(
|
669
|
-
"neuro_simulator.main:app",
|
670
|
-
host=config_manager.settings.server.host,
|
671
|
-
port=config_manager.settings.server.port,
|
672
|
-
reload=False # 生产环境中建议关闭reload
|
673
|
-
)
|
Binary file
|