neuro-simulator 0.1.3__py3-none-any.whl → 0.2.0__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 +111 -397
- 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 +53 -142
- 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.0.dist-info}/METADATA +176 -176
- neuro_simulator-0.2.0.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.0.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.0.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# neuro_simulator/core/agent_interface.py
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import List, Dict, Any, Optional
|
4
|
+
|
5
|
+
class BaseAgent(ABC):
|
6
|
+
"""Abstract base class for all agents, defining a common interface for the server."""
|
7
|
+
|
8
|
+
@abstractmethod
|
9
|
+
async def initialize(self):
|
10
|
+
"""Initialize the agent."""
|
11
|
+
pass
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
async def reset_memory(self):
|
15
|
+
"""Reset all types of agent memory."""
|
16
|
+
pass
|
17
|
+
|
18
|
+
@abstractmethod
|
19
|
+
async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
20
|
+
"""Process messages and generate a response."""
|
21
|
+
pass
|
22
|
+
|
23
|
+
# Memory Block Management
|
24
|
+
@abstractmethod
|
25
|
+
async def get_memory_blocks(self) -> List[Dict[str, Any]]:
|
26
|
+
"""Get all memory blocks."""
|
27
|
+
pass
|
28
|
+
|
29
|
+
@abstractmethod
|
30
|
+
async def get_memory_block(self, block_id: str) -> Optional[Dict[str, Any]]:
|
31
|
+
"""Get a specific memory block by its ID."""
|
32
|
+
pass
|
33
|
+
|
34
|
+
@abstractmethod
|
35
|
+
async def create_memory_block(self, title: str, description: str, content: List[str]) -> Dict[str, str]:
|
36
|
+
"""Create a new memory block."""
|
37
|
+
pass
|
38
|
+
|
39
|
+
@abstractmethod
|
40
|
+
async def update_memory_block(self, block_id: str, title: Optional[str], description: Optional[str], content: Optional[List[str]]):
|
41
|
+
"""Update an existing memory block."""
|
42
|
+
pass
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
async def delete_memory_block(self, block_id: str):
|
46
|
+
"""Delete a memory block."""
|
47
|
+
pass
|
48
|
+
|
49
|
+
# Init Memory Management
|
50
|
+
@abstractmethod
|
51
|
+
async def get_init_memory(self) -> Dict[str, Any]:
|
52
|
+
"""Get the agent's initialization memory."""
|
53
|
+
pass
|
54
|
+
|
55
|
+
@abstractmethod
|
56
|
+
async def update_init_memory(self, memory: Dict[str, Any]):
|
57
|
+
"""Update the agent's initialization memory."""
|
58
|
+
pass
|
59
|
+
|
60
|
+
# Temp Memory Management
|
61
|
+
@abstractmethod
|
62
|
+
async def get_temp_memory(self) -> List[Dict[str, Any]]:
|
63
|
+
"""Get the agent's temporary memory."""
|
64
|
+
pass
|
65
|
+
|
66
|
+
@abstractmethod
|
67
|
+
async def add_temp_memory(self, content: str, role: str):
|
68
|
+
"""Add an item to the agent's temporary memory."""
|
69
|
+
pass
|
70
|
+
|
71
|
+
@abstractmethod
|
72
|
+
async def clear_temp_memory(self):
|
73
|
+
"""Clear the agent's temporary memory."""
|
74
|
+
pass
|
75
|
+
|
76
|
+
# Tool Management
|
77
|
+
@abstractmethod
|
78
|
+
async def get_available_tools(self) -> List[Dict[str, Any]]:
|
79
|
+
"""Get a list of available tools."""
|
80
|
+
pass
|
81
|
+
|
82
|
+
@abstractmethod
|
83
|
+
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
84
|
+
"""Execute a tool with given parameters."""
|
85
|
+
pass
|
86
|
+
|
87
|
+
# Context/Message History
|
88
|
+
@abstractmethod
|
89
|
+
async def get_message_history(self, limit: int = 20) -> List[Dict[str, Any]]:
|
90
|
+
"""Get the recent message history."""
|
91
|
+
pass
|
@@ -0,0 +1,278 @@
|
|
1
|
+
# neuro_simulator/core/application.py
|
2
|
+
"""Main application file: FastAPI app instance, events, and websockets."""
|
3
|
+
|
4
|
+
import asyncio
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import random
|
8
|
+
import re
|
9
|
+
import time
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import List
|
12
|
+
|
13
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
14
|
+
from fastapi.middleware.cors import CORSMiddleware
|
15
|
+
from starlette.websockets import WebSocketState
|
16
|
+
|
17
|
+
# --- Core Imports ---
|
18
|
+
from .config import config_manager, AppSettings
|
19
|
+
from ..core.agent_factory import create_agent
|
20
|
+
|
21
|
+
# --- API Routers ---
|
22
|
+
from ..api.agent import router as agent_router
|
23
|
+
from ..api.stream import router as stream_router
|
24
|
+
from ..api.system import router as system_router
|
25
|
+
|
26
|
+
# --- Services and Utilities ---
|
27
|
+
from ..services.audience import AudienceChatbotManager, get_dynamic_audience_prompt
|
28
|
+
from ..services.audio import synthesize_audio_segment
|
29
|
+
from ..services.stream import live_stream_manager
|
30
|
+
from ..utils.logging import configure_server_logging, server_log_queue, agent_log_queue
|
31
|
+
from ..utils.process import process_manager
|
32
|
+
from ..utils.queue import (
|
33
|
+
add_to_audience_buffer,
|
34
|
+
add_to_neuro_input_queue,
|
35
|
+
get_recent_audience_chats,
|
36
|
+
is_neuro_input_queue_empty,
|
37
|
+
get_all_neuro_input_chats
|
38
|
+
)
|
39
|
+
from ..utils.state import app_state
|
40
|
+
from ..utils.websocket import connection_manager
|
41
|
+
|
42
|
+
# --- Logger Setup ---
|
43
|
+
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
44
|
+
|
45
|
+
# --- FastAPI App Initialization ---
|
46
|
+
app = FastAPI(
|
47
|
+
title="Neuro-Sama Simulator API",
|
48
|
+
version="2.0.0",
|
49
|
+
description="Backend for the Neuro-Sama digital being simulator."
|
50
|
+
)
|
51
|
+
|
52
|
+
app.add_middleware(
|
53
|
+
CORSMiddleware,
|
54
|
+
allow_origins=config_manager.settings.server.client_origins + ["http://localhost:8080", "https://dashboard.live.jiahui.cafe"],
|
55
|
+
allow_credentials=True,
|
56
|
+
allow_methods=["*"],
|
57
|
+
allow_headers=["*"],
|
58
|
+
expose_headers=["X-API-Token"],
|
59
|
+
)
|
60
|
+
|
61
|
+
app.include_router(agent_router)
|
62
|
+
app.include_router(stream_router)
|
63
|
+
app.include_router(system_router)
|
64
|
+
|
65
|
+
# --- Background Task Definitions ---
|
66
|
+
|
67
|
+
chatbot_manager: AudienceChatbotManager = None
|
68
|
+
|
69
|
+
async def broadcast_events_task():
|
70
|
+
"""Broadcasts events from the live_stream_manager's queue to all clients."""
|
71
|
+
while True:
|
72
|
+
try:
|
73
|
+
event = await live_stream_manager.event_queue.get()
|
74
|
+
await connection_manager.broadcast(event)
|
75
|
+
live_stream_manager.event_queue.task_done()
|
76
|
+
except asyncio.CancelledError:
|
77
|
+
break
|
78
|
+
except Exception as e:
|
79
|
+
logger.error(f"Error in broadcast_events_task: {e}", exc_info=True)
|
80
|
+
|
81
|
+
async def fetch_and_process_audience_chats():
|
82
|
+
"""Generates a batch of audience chat messages."""
|
83
|
+
if not chatbot_manager or not chatbot_manager.client:
|
84
|
+
return
|
85
|
+
try:
|
86
|
+
dynamic_prompt = await get_dynamic_audience_prompt()
|
87
|
+
raw_chat_text = await chatbot_manager.client.generate_chat_messages(
|
88
|
+
prompt=dynamic_prompt,
|
89
|
+
max_tokens=config_manager.settings.audience_simulation.max_output_tokens
|
90
|
+
)
|
91
|
+
|
92
|
+
parsed_chats = []
|
93
|
+
for line in raw_chat_text.split('\n'):
|
94
|
+
line = line.strip()
|
95
|
+
if ':' in line:
|
96
|
+
username_raw, text = line.split(':', 1)
|
97
|
+
username = username_raw.strip()
|
98
|
+
if username in config_manager.settings.audience_simulation.username_blocklist:
|
99
|
+
username = random.choice(config_manager.settings.audience_simulation.username_pool)
|
100
|
+
if username and text.strip():
|
101
|
+
parsed_chats.append({"username": username, "text": text.strip()})
|
102
|
+
elif line:
|
103
|
+
parsed_chats.append({"username": random.choice(config_manager.settings.audience_simulation.username_pool), "text": line})
|
104
|
+
|
105
|
+
chats_to_broadcast = parsed_chats[:config_manager.settings.audience_simulation.chats_per_batch]
|
106
|
+
|
107
|
+
for chat in chats_to_broadcast:
|
108
|
+
add_to_audience_buffer(chat)
|
109
|
+
add_to_neuro_input_queue(chat)
|
110
|
+
broadcast_message = {"type": "chat_message", **chat, "is_user_message": False}
|
111
|
+
await connection_manager.broadcast(broadcast_message)
|
112
|
+
await asyncio.sleep(random.uniform(0.1, 0.4))
|
113
|
+
except Exception as e:
|
114
|
+
logger.error(f"Error in fetch_and_process_audience_chats: {e}", exc_info=True)
|
115
|
+
|
116
|
+
async def generate_audience_chat_task():
|
117
|
+
"""Periodically triggers the audience chat generation task."""
|
118
|
+
while True:
|
119
|
+
try:
|
120
|
+
asyncio.create_task(fetch_and_process_audience_chats())
|
121
|
+
await asyncio.sleep(config_manager.settings.audience_simulation.chat_generation_interval_sec)
|
122
|
+
except asyncio.CancelledError:
|
123
|
+
break
|
124
|
+
|
125
|
+
async def neuro_response_cycle():
|
126
|
+
"""The core response loop for the agent."""
|
127
|
+
await app_state.live_phase_started_event.wait()
|
128
|
+
agent = await create_agent()
|
129
|
+
is_first_response = True
|
130
|
+
|
131
|
+
while True:
|
132
|
+
try:
|
133
|
+
if is_first_response:
|
134
|
+
add_to_neuro_input_queue({"username": "System", "text": config_manager.settings.neuro_behavior.initial_greeting})
|
135
|
+
is_first_response = False
|
136
|
+
elif is_neuro_input_queue_empty():
|
137
|
+
await asyncio.sleep(1)
|
138
|
+
continue
|
139
|
+
|
140
|
+
current_queue_snapshot = get_all_neuro_input_chats()
|
141
|
+
sample_size = min(config_manager.settings.neuro_behavior.input_chat_sample_size, len(current_queue_snapshot))
|
142
|
+
selected_chats = random.sample(current_queue_snapshot, sample_size)
|
143
|
+
|
144
|
+
response_result = await asyncio.wait_for(agent.process_messages(selected_chats), timeout=20.0)
|
145
|
+
|
146
|
+
response_text = response_result.get("final_response", "").strip()
|
147
|
+
if not response_text:
|
148
|
+
continue
|
149
|
+
|
150
|
+
async with app_state.neuro_last_speech_lock:
|
151
|
+
app_state.neuro_last_speech = response_text
|
152
|
+
|
153
|
+
sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+', response_text.replace('\n', ' ')) if s.strip()]
|
154
|
+
if not sentences: continue
|
155
|
+
|
156
|
+
synthesis_tasks = [synthesize_audio_segment(s) for s in sentences]
|
157
|
+
synthesis_results = await asyncio.gather(*synthesis_tasks, return_exceptions=True)
|
158
|
+
|
159
|
+
speech_packages = [
|
160
|
+
{"segment_id": i, "text": sentences[i], "audio_base64": res[0], "duration": res[1]}
|
161
|
+
for i, res in enumerate(synthesis_results) if not isinstance(res, Exception)
|
162
|
+
]
|
163
|
+
|
164
|
+
if not speech_packages: continue
|
165
|
+
|
166
|
+
live_stream_manager.set_neuro_speaking_status(True)
|
167
|
+
for package in speech_packages:
|
168
|
+
await connection_manager.broadcast({"type": "neuro_speech_segment", **package, "is_end": False})
|
169
|
+
await asyncio.sleep(package['duration'])
|
170
|
+
|
171
|
+
await connection_manager.broadcast({"type": "neuro_speech_segment", "is_end": True})
|
172
|
+
live_stream_manager.set_neuro_speaking_status(False)
|
173
|
+
await asyncio.sleep(config_manager.settings.neuro_behavior.post_speech_cooldown_sec)
|
174
|
+
|
175
|
+
except asyncio.TimeoutError:
|
176
|
+
logger.warning("Agent response timed out, skipping this cycle.")
|
177
|
+
await asyncio.sleep(5)
|
178
|
+
except asyncio.CancelledError:
|
179
|
+
live_stream_manager.set_neuro_speaking_status(False)
|
180
|
+
break
|
181
|
+
except Exception as e:
|
182
|
+
logger.error(f"Critical error in neuro_response_cycle: {e}", exc_info=True)
|
183
|
+
live_stream_manager.set_neuro_speaking_status(False)
|
184
|
+
await asyncio.sleep(10)
|
185
|
+
|
186
|
+
# --- Application Lifecycle Events ---
|
187
|
+
|
188
|
+
@app.on_event("startup")
|
189
|
+
async def startup_event():
|
190
|
+
"""Actions to perform on application startup."""
|
191
|
+
global chatbot_manager
|
192
|
+
configure_server_logging()
|
193
|
+
|
194
|
+
chatbot_manager = AudienceChatbotManager()
|
195
|
+
|
196
|
+
async def metadata_callback(settings: AppSettings):
|
197
|
+
await live_stream_manager.broadcast_stream_metadata()
|
198
|
+
|
199
|
+
config_manager.register_update_callback(metadata_callback)
|
200
|
+
config_manager.register_update_callback(chatbot_manager.handle_config_update)
|
201
|
+
|
202
|
+
try:
|
203
|
+
await create_agent()
|
204
|
+
logger.info(f"Successfully initialized agent type: {config_manager.settings.agent_type}")
|
205
|
+
except Exception as e:
|
206
|
+
logger.critical(f"Agent initialization failed on startup: {e}", exc_info=True)
|
207
|
+
|
208
|
+
logger.info("FastAPI application has started.")
|
209
|
+
|
210
|
+
@app.on_event("shutdown")
|
211
|
+
async def shutdown_event():
|
212
|
+
"""Actions to perform on application shutdown."""
|
213
|
+
if process_manager.is_running:
|
214
|
+
process_manager.stop_live_processes()
|
215
|
+
logger.info("FastAPI application has shut down.")
|
216
|
+
|
217
|
+
# --- WebSocket Endpoints ---
|
218
|
+
|
219
|
+
@app.websocket("/ws/stream")
|
220
|
+
async def websocket_stream_endpoint(websocket: WebSocket):
|
221
|
+
await connection_manager.connect(websocket)
|
222
|
+
try:
|
223
|
+
await connection_manager.send_personal_message(live_stream_manager.get_initial_state_for_client(), websocket)
|
224
|
+
await connection_manager.send_personal_message({"type": "update_stream_metadata", **config_manager.settings.stream_metadata.model_dump()}, websocket)
|
225
|
+
|
226
|
+
initial_chats = get_recent_audience_chats(config_manager.settings.performance.initial_chat_backlog_limit)
|
227
|
+
for chat in initial_chats:
|
228
|
+
await connection_manager.send_personal_message({"type": "chat_message", **chat, "is_user_message": False}, websocket)
|
229
|
+
await asyncio.sleep(0.01)
|
230
|
+
|
231
|
+
while True:
|
232
|
+
raw_data = await websocket.receive_text()
|
233
|
+
data = json.loads(raw_data)
|
234
|
+
if data.get("type") == "user_message":
|
235
|
+
user_message = {"username": data.get("username", "User"), "text": data.get("message", "").strip()}
|
236
|
+
if user_message["text"]:
|
237
|
+
add_to_audience_buffer(user_message)
|
238
|
+
add_to_neuro_input_queue(user_message)
|
239
|
+
await connection_manager.broadcast({"type": "chat_message", **user_message, "is_user_message": True})
|
240
|
+
except WebSocketDisconnect:
|
241
|
+
pass
|
242
|
+
finally:
|
243
|
+
connection_manager.disconnect(websocket)
|
244
|
+
|
245
|
+
@app.websocket("/ws/admin")
|
246
|
+
async def websocket_admin_endpoint(websocket: WebSocket):
|
247
|
+
await websocket.accept()
|
248
|
+
try:
|
249
|
+
for log_entry in list(server_log_queue): await websocket.send_json({"type": "server_log", "data": log_entry})
|
250
|
+
for log_entry in list(agent_log_queue): await websocket.send_json({"type": "agent_log", "data": log_entry})
|
251
|
+
|
252
|
+
agent = await create_agent()
|
253
|
+
initial_context = await agent.get_message_history()
|
254
|
+
await websocket.send_json({"type": "agent_context", "action": "update", "messages": initial_context})
|
255
|
+
|
256
|
+
while websocket.client_state == WebSocketState.CONNECTED:
|
257
|
+
if server_log_queue: await websocket.send_json({"type": "server_log", "data": server_log_queue.popleft()})
|
258
|
+
if agent_log_queue: await websocket.send_json({"type": "agent_log", "data": agent_log_queue.popleft()})
|
259
|
+
await asyncio.sleep(0.1)
|
260
|
+
except WebSocketDisconnect:
|
261
|
+
pass
|
262
|
+
finally:
|
263
|
+
logger.info("Admin WebSocket client disconnected.")
|
264
|
+
|
265
|
+
# --- Server Entrypoint ---
|
266
|
+
|
267
|
+
def run_server(host: str = None, port: int = None):
|
268
|
+
"""Runs the FastAPI server with Uvicorn."""
|
269
|
+
import uvicorn
|
270
|
+
server_host = host or config_manager.settings.server.host
|
271
|
+
server_port = port or config_manager.settings.server.port
|
272
|
+
|
273
|
+
uvicorn.run(
|
274
|
+
"neuro_simulator.core.application:app",
|
275
|
+
host=server_host,
|
276
|
+
port=server_port,
|
277
|
+
reload=False
|
278
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
# This file makes the 'services' directory a Python package.
|
@@ -1,11 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# neuro_simulator/services/audience.py
|
2
|
+
import asyncio
|
3
|
+
import logging
|
4
|
+
import random
|
5
|
+
|
2
6
|
from google import genai
|
3
7
|
from google.genai import types
|
4
8
|
from openai import AsyncOpenAI
|
5
|
-
|
6
|
-
import
|
7
|
-
from .
|
8
|
-
|
9
|
+
|
10
|
+
from ..core.config import config_manager, AppSettings
|
11
|
+
from ..utils.state import app_state
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
9
14
|
|
10
15
|
class AudienceLLMClient:
|
11
16
|
async def generate_chat_messages(self, prompt: str, max_tokens: int) -> str:
|
@@ -15,13 +20,11 @@ class GeminiAudienceLLM(AudienceLLMClient):
|
|
15
20
|
def __init__(self, api_key: str, model_name: str):
|
16
21
|
if not api_key:
|
17
22
|
raise ValueError("Gemini API Key is not provided for GeminiAudienceLLM.")
|
18
|
-
# 根据新文档,正确初始化客户端
|
19
23
|
self.client = genai.Client(api_key=api_key)
|
20
24
|
self.model_name = model_name
|
21
|
-
|
25
|
+
logger.info(f"Initialized GeminiAudienceLLM (new SDK), model: {self.model_name}")
|
22
26
|
|
23
27
|
async def generate_chat_messages(self, prompt: str, max_tokens: int) -> str:
|
24
|
-
# 根据新文档,使用正确的异步方法和参数
|
25
28
|
response = await self.client.aio.models.generate_content(
|
26
29
|
model=self.model_name,
|
27
30
|
contents=prompt,
|
@@ -45,7 +48,7 @@ class OpenAIAudienceLLM(AudienceLLMClient):
|
|
45
48
|
raise ValueError("OpenAI API Key is not provided for OpenAIAudienceLLM.")
|
46
49
|
self.client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
47
50
|
self.model_name = model_name
|
48
|
-
|
51
|
+
logger.info(f"Initialized OpenAIAudienceLLM, model: {self.model_name}, API Base: {base_url}")
|
49
52
|
|
50
53
|
async def generate_chat_messages(self, prompt: str, max_tokens: int) -> str:
|
51
54
|
response = await self.client.chat.completions.create(
|
@@ -60,45 +63,42 @@ class OpenAIAudienceLLM(AudienceLLMClient):
|
|
60
63
|
|
61
64
|
async def get_dynamic_audience_prompt() -> str:
|
62
65
|
current_neuro_speech = ""
|
63
|
-
async with
|
64
|
-
current_neuro_speech =
|
65
|
-
|
66
|
-
# 使用 settings 对象中的模板和变量
|
66
|
+
async with app_state.neuro_last_speech_lock:
|
67
|
+
current_neuro_speech = app_state.neuro_last_speech
|
68
|
+
|
67
69
|
prompt = config_manager.settings.audience_simulation.prompt_template.format(
|
68
70
|
neuro_speech=current_neuro_speech,
|
69
71
|
num_chats_to_generate=config_manager.settings.audience_simulation.chats_per_batch
|
70
72
|
)
|
71
73
|
return prompt
|
72
74
|
|
73
|
-
class
|
75
|
+
class AudienceChatbotManager:
|
74
76
|
def __init__(self):
|
75
77
|
self.client: AudienceLLMClient = self._create_client(config_manager.settings)
|
76
78
|
self._last_checked_settings: dict = config_manager.settings.audience_simulation.model_dump()
|
77
|
-
|
79
|
+
logger.info("AudienceChatbotManager initialized.")
|
78
80
|
|
79
81
|
def _create_client(self, settings: AppSettings) -> AudienceLLMClient:
|
80
82
|
provider = settings.audience_simulation.llm_provider
|
81
|
-
|
83
|
+
logger.info(f"Creating new audience LLM client for provider: {provider}")
|
82
84
|
if provider.lower() == "gemini":
|
83
85
|
if not settings.api_keys.gemini_api_key:
|
84
|
-
raise ValueError("GEMINI_API_KEY
|
86
|
+
raise ValueError("GEMINI_API_KEY not set in config")
|
85
87
|
return GeminiAudienceLLM(api_key=settings.api_keys.gemini_api_key, model_name=settings.audience_simulation.gemini_model)
|
86
88
|
elif provider.lower() == "openai":
|
87
89
|
if not settings.api_keys.openai_api_key:
|
88
|
-
raise ValueError("OPENAI_API_KEY
|
90
|
+
raise ValueError("OPENAI_API_KEY not set in config")
|
89
91
|
return OpenAIAudienceLLM(api_key=settings.api_keys.openai_api_key, model_name=settings.audience_simulation.openai_model, base_url=settings.api_keys.openai_api_base_url)
|
90
92
|
else:
|
91
|
-
raise ValueError(f"
|
93
|
+
raise ValueError(f"Unsupported AUDIENCE_LLM_PROVIDER: {provider}")
|
92
94
|
|
93
95
|
def handle_config_update(self, new_settings: AppSettings):
|
94
96
|
new_audience_settings = new_settings.audience_simulation.model_dump()
|
95
97
|
if new_audience_settings != self._last_checked_settings:
|
96
|
-
|
98
|
+
logger.info("Audience simulation settings changed, re-initializing LLM client...")
|
97
99
|
try:
|
98
100
|
self.client = self._create_client(new_settings)
|
99
101
|
self._last_checked_settings = new_audience_settings
|
100
|
-
|
102
|
+
logger.info("LLM client hot-reloaded successfully.")
|
101
103
|
except Exception as e:
|
102
|
-
|
103
|
-
else:
|
104
|
-
print("观众模拟设置未更改,跳过 LLM client 重载。")
|
104
|
+
logger.error(f"Error hot-reloading LLM client: {e}", exc_info=True)
|
@@ -1,24 +1,27 @@
|
|
1
|
-
#
|
2
|
-
import
|
1
|
+
# neuro_simulator/services/audio.py
|
2
|
+
import asyncio
|
3
3
|
import base64
|
4
|
+
import html
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
|
4
8
|
import azure.cognitiveservices.speech as speechsdk
|
5
|
-
|
6
|
-
from .config import config_manager
|
9
|
+
|
10
|
+
from ..core.config import config_manager
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
7
13
|
|
8
14
|
async def synthesize_audio_segment(text: str, voice_name: str = None, pitch: float = None) -> tuple[str, float]:
|
9
15
|
"""
|
10
|
-
|
11
|
-
|
12
|
-
返回 Base64 编码的音频字符串和音频时长(秒)。
|
16
|
+
Synthesizes audio using Azure TTS.
|
17
|
+
Returns a Base64 encoded audio string and the audio duration in seconds.
|
13
18
|
"""
|
14
|
-
# 使用 config_manager.settings 中的值
|
15
19
|
azure_key = config_manager.settings.api_keys.azure_speech_key
|
16
20
|
azure_region = config_manager.settings.api_keys.azure_speech_region
|
17
21
|
|
18
22
|
if not azure_key or not azure_region:
|
19
|
-
raise ValueError("Azure Speech Key
|
23
|
+
raise ValueError("Azure Speech Key or Region is not set in the configuration.")
|
20
24
|
|
21
|
-
# 如果未传入参数,则使用配置的默认值
|
22
25
|
final_voice_name = voice_name if voice_name is not None else config_manager.settings.tts.voice_name
|
23
26
|
final_pitch = pitch if pitch is not None else config_manager.settings.tts.voice_pitch
|
24
27
|
|
@@ -52,15 +55,15 @@ async def synthesize_audio_segment(text: str, voice_name: str = None, pitch: flo
|
|
52
55
|
audio_data = result.audio_data
|
53
56
|
encoded_audio = base64.b64encode(audio_data).decode('utf-8')
|
54
57
|
audio_duration_sec = result.audio_duration.total_seconds()
|
55
|
-
|
58
|
+
logger.info(f"TTS synthesis completed: '{text[:30]}...' (Duration: {audio_duration_sec:.2f}s)")
|
56
59
|
return encoded_audio, audio_duration_sec
|
57
60
|
else:
|
58
61
|
cancellation_details = result.cancellation_details
|
59
|
-
error_message = f"TTS
|
62
|
+
error_message = f"TTS synthesis failed (Reason: {cancellation_details.reason}). Text: '{text}'"
|
60
63
|
if cancellation_details.error_details:
|
61
|
-
error_message += f" |
|
62
|
-
|
64
|
+
error_message += f" | Details: {cancellation_details.error_details}"
|
65
|
+
logger.error(error_message)
|
63
66
|
raise Exception(error_message)
|
64
67
|
except Exception as e:
|
65
|
-
|
68
|
+
logger.error(f"An exception occurred during the Azure TTS SDK call: {e}", exc_info=True)
|
66
69
|
raise
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# neuro_simulator/services/builtin.py
|
2
|
+
"""Builtin agent module for Neuro Simulator"""
|
3
|
+
|
4
|
+
import asyncio
|
5
|
+
import re
|
6
|
+
import logging
|
7
|
+
from typing import List, Dict, Any, Optional
|
8
|
+
|
9
|
+
from ..core.agent_interface import BaseAgent
|
10
|
+
from ..agent.core import Agent as LocalAgent
|
11
|
+
from ..services.stream import live_stream_manager
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
14
|
+
|
15
|
+
async def initialize_builtin_agent() -> Optional[LocalAgent]:
|
16
|
+
"""Initializes the builtin agent instance and returns it."""
|
17
|
+
try:
|
18
|
+
working_dir = live_stream_manager._working_dir
|
19
|
+
agent_instance = LocalAgent(working_dir=working_dir)
|
20
|
+
await agent_instance.initialize()
|
21
|
+
logger.info("Builtin agent implementation initialized successfully.")
|
22
|
+
return agent_instance
|
23
|
+
except Exception as e:
|
24
|
+
logger.error(f"Failed to initialize local agent implementation: {e}", exc_info=True)
|
25
|
+
return None
|
26
|
+
|
27
|
+
class BuiltinAgentWrapper(BaseAgent):
|
28
|
+
"""Wrapper for the builtin agent to implement the BaseAgent interface."""
|
29
|
+
def __init__(self, agent_instance: LocalAgent):
|
30
|
+
self.agent_instance = agent_instance
|
31
|
+
|
32
|
+
async def initialize(self):
|
33
|
+
if self.agent_instance is None:
|
34
|
+
raise RuntimeError("Builtin agent not initialized")
|
35
|
+
await self.agent_instance.initialize()
|
36
|
+
|
37
|
+
async def reset_memory(self):
|
38
|
+
await self.agent_instance.reset_all_memory()
|
39
|
+
|
40
|
+
async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
41
|
+
return await self.agent_instance.process_messages(messages)
|
42
|
+
|
43
|
+
# Memory Block Management
|
44
|
+
async def get_memory_blocks(self) -> List[Dict[str, Any]]:
|
45
|
+
blocks_dict = await self.agent_instance.memory_manager.get_core_memory_blocks()
|
46
|
+
return list(blocks_dict.values())
|
47
|
+
|
48
|
+
async def get_memory_block(self, block_id: str) -> Optional[Dict[str, Any]]:
|
49
|
+
return await self.agent_instance.memory_manager.get_core_memory_block(block_id)
|
50
|
+
|
51
|
+
async def create_memory_block(self, title: str, description: str, content: List[str]) -> Dict[str, str]:
|
52
|
+
block_id = await self.agent_instance.memory_manager.create_core_memory_block(title, description, content)
|
53
|
+
return {"block_id": block_id}
|
54
|
+
|
55
|
+
async def update_memory_block(self, block_id: str, title: Optional[str], description: Optional[str], content: Optional[List[str]]):
|
56
|
+
await self.agent_instance.memory_manager.update_core_memory_block(block_id, title, description, content)
|
57
|
+
|
58
|
+
async def delete_memory_block(self, block_id: str):
|
59
|
+
await self.agent_instance.memory_manager.delete_core_memory_block(block_id)
|
60
|
+
|
61
|
+
# Init Memory Management
|
62
|
+
async def get_init_memory(self) -> Dict[str, Any]:
|
63
|
+
return self.agent_instance.memory_manager.init_memory
|
64
|
+
|
65
|
+
async def update_init_memory(self, memory: Dict[str, Any]):
|
66
|
+
await self.agent_instance.memory_manager.update_init_memory(memory)
|
67
|
+
|
68
|
+
# Temp Memory Management
|
69
|
+
async def get_temp_memory(self) -> List[Dict[str, Any]]:
|
70
|
+
return self.agent_instance.memory_manager.temp_memory
|
71
|
+
|
72
|
+
async def add_temp_memory(self, content: str, role: str):
|
73
|
+
await self.agent_instance.memory_manager.add_temp_memory(content, role)
|
74
|
+
|
75
|
+
async def clear_temp_memory(self):
|
76
|
+
await self.agent_instance.memory_manager.reset_temp_memory()
|
77
|
+
|
78
|
+
# Tool Management
|
79
|
+
async def get_available_tools(self) -> str:
|
80
|
+
return self.agent_instance.tool_manager.get_tool_descriptions()
|
81
|
+
|
82
|
+
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
83
|
+
return await self.agent_instance.execute_tool(tool_name, params)
|
84
|
+
|
85
|
+
# Context/Message History
|
86
|
+
async def get_message_history(self, limit: int = 20) -> List[Dict[str, Any]]:
|
87
|
+
return await self.agent_instance.memory_manager.get_recent_context(limit)
|