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.
Files changed (49) hide show
  1. neuro_simulator/__init__.py +1 -10
  2. neuro_simulator/agent/__init__.py +1 -8
  3. neuro_simulator/agent/base.py +43 -0
  4. neuro_simulator/agent/core.py +105 -398
  5. neuro_simulator/agent/factory.py +30 -0
  6. neuro_simulator/agent/llm.py +34 -31
  7. neuro_simulator/agent/memory/__init__.py +1 -4
  8. neuro_simulator/agent/memory/manager.py +61 -203
  9. neuro_simulator/agent/tools/__init__.py +1 -4
  10. neuro_simulator/agent/tools/core.py +8 -18
  11. neuro_simulator/api/__init__.py +1 -0
  12. neuro_simulator/api/agent.py +163 -0
  13. neuro_simulator/api/stream.py +55 -0
  14. neuro_simulator/api/system.py +90 -0
  15. neuro_simulator/cli.py +60 -143
  16. neuro_simulator/core/__init__.py +1 -0
  17. neuro_simulator/core/agent_factory.py +52 -0
  18. neuro_simulator/core/agent_interface.py +91 -0
  19. neuro_simulator/core/application.py +278 -0
  20. neuro_simulator/services/__init__.py +1 -0
  21. neuro_simulator/{chatbot.py → services/audience.py} +24 -24
  22. neuro_simulator/{audio_synthesis.py → services/audio.py} +18 -15
  23. neuro_simulator/services/builtin.py +87 -0
  24. neuro_simulator/services/letta.py +206 -0
  25. neuro_simulator/{stream_manager.py → services/stream.py} +39 -47
  26. neuro_simulator/utils/__init__.py +1 -0
  27. neuro_simulator/utils/logging.py +90 -0
  28. neuro_simulator/utils/process.py +67 -0
  29. neuro_simulator/{stream_chat.py → utils/queue.py} +17 -4
  30. neuro_simulator/utils/state.py +14 -0
  31. neuro_simulator/{websocket_manager.py → utils/websocket.py} +18 -14
  32. {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/METADATA +83 -33
  33. neuro_simulator-0.2.1.dist-info/RECORD +37 -0
  34. neuro_simulator/agent/api.py +0 -737
  35. neuro_simulator/agent/memory.py +0 -137
  36. neuro_simulator/agent/tools.py +0 -69
  37. neuro_simulator/builtin_agent.py +0 -83
  38. neuro_simulator/config.yaml.example +0 -157
  39. neuro_simulator/letta.py +0 -164
  40. neuro_simulator/log_handler.py +0 -43
  41. neuro_simulator/main.py +0 -673
  42. neuro_simulator/media/neuro_start.mp4 +0 -0
  43. neuro_simulator/process_manager.py +0 -70
  44. neuro_simulator/shared_state.py +0 -11
  45. neuro_simulator-0.1.3.dist-info/RECORD +0 -31
  46. /neuro_simulator/{config.py → core/config.py} +0 -0
  47. {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/WHEEL +0 -0
  48. {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/entry_points.txt +0 -0
  49. {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