neuro-simulator 0.0.4__py3-none-any.whl → 0.1.2__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 +10 -1
- neuro_simulator/agent/__init__.py +8 -0
- neuro_simulator/agent/api.py +737 -0
- neuro_simulator/agent/core.py +471 -0
- neuro_simulator/agent/llm.py +104 -0
- neuro_simulator/agent/memory/__init__.py +4 -0
- neuro_simulator/agent/memory/manager.py +370 -0
- neuro_simulator/agent/memory.py +137 -0
- neuro_simulator/agent/tools/__init__.py +4 -0
- neuro_simulator/agent/tools/core.py +112 -0
- neuro_simulator/agent/tools.py +69 -0
- neuro_simulator/builtin_agent.py +83 -0
- neuro_simulator/cli.py +45 -0
- neuro_simulator/config.py +217 -79
- neuro_simulator/config.yaml.example +16 -2
- neuro_simulator/letta.py +71 -45
- neuro_simulator/log_handler.py +30 -16
- neuro_simulator/main.py +167 -30
- neuro_simulator/process_manager.py +5 -2
- neuro_simulator/stream_manager.py +6 -0
- {neuro_simulator-0.0.4.dist-info → neuro_simulator-0.1.2.dist-info}/METADATA +1 -1
- neuro_simulator-0.1.2.dist-info/RECORD +31 -0
- neuro_simulator-0.0.4.dist-info/RECORD +0 -20
- {neuro_simulator-0.0.4.dist-info → neuro_simulator-0.1.2.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.0.4.dist-info → neuro_simulator-0.1.2.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.0.4.dist-info → neuro_simulator-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# agent/tools.py
|
2
|
+
"""
|
3
|
+
Tools that the Neuro Simulator Agent can use
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import asyncio
|
8
|
+
from typing import Dict, Any
|
9
|
+
from .memory import MemoryManager
|
10
|
+
|
11
|
+
class ToolManager:
|
12
|
+
"""Manages tools that the agent can use to interact with its memory"""
|
13
|
+
|
14
|
+
def __init__(self, memory_manager: MemoryManager):
|
15
|
+
self.memory_manager = memory_manager
|
16
|
+
self.tools = {}
|
17
|
+
self._register_default_tools()
|
18
|
+
|
19
|
+
def _register_default_tools(self):
|
20
|
+
"""Register default tools for memory management"""
|
21
|
+
self.tools["update_mood"] = self._update_mood
|
22
|
+
self.tools["update_topic"] = self._update_topic
|
23
|
+
self.tools["update_viewer_count"] = self._update_viewer_count
|
24
|
+
self.tools["get_memory_state"] = self._get_memory_state
|
25
|
+
|
26
|
+
async def _update_mood(self, mood: str) -> str:
|
27
|
+
"""Update the agent's mood"""
|
28
|
+
await self.memory_manager.update_mutable_memory({"mood": mood})
|
29
|
+
return f"Mood updated to: {mood}"
|
30
|
+
|
31
|
+
async def _update_topic(self, topic: str) -> str:
|
32
|
+
"""Update the current topic"""
|
33
|
+
await self.memory_manager.update_mutable_memory({"current_topic": topic})
|
34
|
+
return f"Topic updated to: {topic}"
|
35
|
+
|
36
|
+
async def _update_viewer_count(self, count: int) -> str:
|
37
|
+
"""Update the viewer count"""
|
38
|
+
await self.memory_manager.update_mutable_memory({"viewer_count": count})
|
39
|
+
return f"Viewer count updated to: {count}"
|
40
|
+
|
41
|
+
async def _get_memory_state(self) -> Dict[str, Any]:
|
42
|
+
"""Get the current state of all memory"""
|
43
|
+
return {
|
44
|
+
"immutable": self.memory_manager.immutable_memory,
|
45
|
+
"mutable": self.memory_manager.mutable_memory,
|
46
|
+
"conversation_history_length": len(self.memory_manager.conversation_history)
|
47
|
+
}
|
48
|
+
|
49
|
+
def get_tool_descriptions(self) -> str:
|
50
|
+
"""Get descriptions of all available tools"""
|
51
|
+
descriptions = [
|
52
|
+
"Available tools:",
|
53
|
+
"1. update_mood(mood: string) - Update the agent's mood",
|
54
|
+
"2. update_topic(topic: string) - Update the current topic of conversation",
|
55
|
+
"3. update_viewer_count(count: integer) - Update the viewer count",
|
56
|
+
"4. get_memory_state() - Get the current state of all memory"
|
57
|
+
]
|
58
|
+
return "\n".join(descriptions)
|
59
|
+
|
60
|
+
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
61
|
+
"""Execute a tool by name with given parameters"""
|
62
|
+
if tool_name in self.tools:
|
63
|
+
try:
|
64
|
+
result = await self.tools[tool_name](**params)
|
65
|
+
return result
|
66
|
+
except Exception as e:
|
67
|
+
return f"Error executing tool '{tool_name}': {str(e)}"
|
68
|
+
else:
|
69
|
+
return f"Tool '{tool_name}' not found"
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# backend/builtin_agent.py
|
2
|
+
"""Builtin agent module for Neuro Simulator"""
|
3
|
+
|
4
|
+
import asyncio
|
5
|
+
from typing import List, Dict, Union
|
6
|
+
from .config import config_manager
|
7
|
+
import time
|
8
|
+
from datetime import datetime
|
9
|
+
|
10
|
+
# Global variables
|
11
|
+
local_agent = None
|
12
|
+
|
13
|
+
async def initialize_builtin_agent():
|
14
|
+
"""Initialize the builtin agent"""
|
15
|
+
global local_agent
|
16
|
+
|
17
|
+
try:
|
18
|
+
from .agent.core import Agent as LocalAgentImport
|
19
|
+
from .stream_manager import live_stream_manager
|
20
|
+
|
21
|
+
local_agent = LocalAgentImport(working_dir=live_stream_manager._working_dir)
|
22
|
+
await local_agent.initialize()
|
23
|
+
except Exception as e:
|
24
|
+
print(f"初始化本地 Agent 失败: {e}")
|
25
|
+
import traceback
|
26
|
+
traceback.print_exc()
|
27
|
+
local_agent = None
|
28
|
+
|
29
|
+
async def reset_builtin_agent_memory():
|
30
|
+
"""Reset the builtin agent's memory"""
|
31
|
+
global local_agent
|
32
|
+
|
33
|
+
if local_agent is not None:
|
34
|
+
await local_agent.reset_all_memory()
|
35
|
+
else:
|
36
|
+
print("错误: 本地 Agent 未初始化,无法重置记忆。")
|
37
|
+
|
38
|
+
async def clear_builtin_agent_temp_memory():
|
39
|
+
"""Clear the builtin agent's temp memory"""
|
40
|
+
global local_agent
|
41
|
+
|
42
|
+
if local_agent is not None:
|
43
|
+
# Reset only temp memory
|
44
|
+
await local_agent.memory_manager.reset_temp_memory()
|
45
|
+
else:
|
46
|
+
print("错误: 本地 Agent 未初始化,无法清空临时记忆。")
|
47
|
+
|
48
|
+
async def clear_builtin_agent_context():
|
49
|
+
"""Clear the builtin agent's context (dialog history)"""
|
50
|
+
global local_agent
|
51
|
+
|
52
|
+
if local_agent is not None:
|
53
|
+
# Reset only context
|
54
|
+
await local_agent.memory_manager.reset_context()
|
55
|
+
|
56
|
+
# Send context update via WebSocket to notify frontend
|
57
|
+
from .websocket_manager import connection_manager
|
58
|
+
await connection_manager.broadcast({
|
59
|
+
"type": "agent_context",
|
60
|
+
"action": "update",
|
61
|
+
"messages": []
|
62
|
+
})
|
63
|
+
else:
|
64
|
+
print("错误: 本地 Agent 未初始化,无法清空上下文。")
|
65
|
+
|
66
|
+
async def get_builtin_response(chat_messages: list[dict]) -> dict:
|
67
|
+
"""Get response from the builtin agent with detailed processing information"""
|
68
|
+
global local_agent
|
69
|
+
|
70
|
+
if local_agent is not None:
|
71
|
+
response = await local_agent.process_messages(chat_messages)
|
72
|
+
|
73
|
+
# Return the response directly without adding processing details to temp memory
|
74
|
+
return response
|
75
|
+
else:
|
76
|
+
print("错误: 本地 Agent 未初始化,无法获取响应。")
|
77
|
+
return {
|
78
|
+
"input_messages": chat_messages,
|
79
|
+
"llm_response": "",
|
80
|
+
"tool_executions": [],
|
81
|
+
"final_response": "Someone tell Vedal there is a problem with my AI.",
|
82
|
+
"error": "Agent not initialized"
|
83
|
+
}
|
neuro_simulator/cli.py
CHANGED
@@ -6,6 +6,7 @@ import sys
|
|
6
6
|
import shutil
|
7
7
|
from pathlib import Path
|
8
8
|
|
9
|
+
|
9
10
|
def main():
|
10
11
|
parser = argparse.ArgumentParser(description="Neuro-Simulator Server")
|
11
12
|
parser.add_argument("-D", "--dir", help="Working directory containing config.yaml")
|
@@ -98,6 +99,49 @@ def main():
|
|
98
99
|
except Exception as e:
|
99
100
|
print(f"Warning: Could not copy media folder from package: {e}")
|
100
101
|
|
102
|
+
# Handle agent/memory directory and example JSON files
|
103
|
+
agent_memory_dir = work_dir / "agent" / "memory"
|
104
|
+
agent_memory_dir.mkdir(parents=True, exist_ok=True)
|
105
|
+
|
106
|
+
# List of example JSON files to copy
|
107
|
+
example_memory_files = [
|
108
|
+
"context.json",
|
109
|
+
"core_memory.json",
|
110
|
+
"dialog_history.json",
|
111
|
+
"init_memory.json"
|
112
|
+
]
|
113
|
+
|
114
|
+
# Copy each example memory file if it doesn't exist
|
115
|
+
for filename in example_memory_files:
|
116
|
+
target_path = agent_memory_dir / filename
|
117
|
+
if not target_path.exists():
|
118
|
+
try:
|
119
|
+
# Try pkg_resources first (for installed packages)
|
120
|
+
try:
|
121
|
+
import pkg_resources
|
122
|
+
package_example_path = pkg_resources.resource_filename('neuro_simulator', f'agent/memory/{filename}')
|
123
|
+
if os.path.exists(package_example_path):
|
124
|
+
shutil.copy(package_example_path, target_path)
|
125
|
+
print(f"Created {target_path} from package example")
|
126
|
+
else:
|
127
|
+
# Fallback to relative path (for development mode)
|
128
|
+
dev_example_path = Path(__file__).parent / "agent" / "memory" / filename
|
129
|
+
if dev_example_path.exists():
|
130
|
+
shutil.copy(dev_example_path, target_path)
|
131
|
+
print(f"Created {target_path} from development example")
|
132
|
+
else:
|
133
|
+
print(f"Warning: {filename} not found in package or development folder")
|
134
|
+
except Exception:
|
135
|
+
# Fallback to relative path (for development mode)
|
136
|
+
dev_example_path = Path(__file__).parent / "agent" / "memory" / filename
|
137
|
+
if dev_example_path.exists():
|
138
|
+
shutil.copy(dev_example_path, target_path)
|
139
|
+
print(f"Created {target_path} from development example")
|
140
|
+
else:
|
141
|
+
print(f"Warning: {filename} not found in package or development folder")
|
142
|
+
except Exception as e:
|
143
|
+
print(f"Warning: Could not copy {filename} from package: {e}")
|
144
|
+
|
101
145
|
# Now check for required files and handle errors appropriately
|
102
146
|
errors = []
|
103
147
|
|
@@ -128,5 +172,6 @@ def main():
|
|
128
172
|
from neuro_simulator.main import run_server
|
129
173
|
run_server(args.host, args.port)
|
130
174
|
|
175
|
+
|
131
176
|
if __name__ == "__main__":
|
132
177
|
main()
|
neuro_simulator/config.py
CHANGED
@@ -23,73 +23,61 @@ class ApiKeysSettings(BaseModel):
|
|
23
23
|
azure_speech_region: Optional[str] = None
|
24
24
|
|
25
25
|
class StreamMetadataSettings(BaseModel):
|
26
|
-
streamer_nickname: str
|
27
|
-
stream_title: str
|
28
|
-
stream_category: str
|
29
|
-
stream_tags: List[str] = Field(default_factory=
|
26
|
+
streamer_nickname: str
|
27
|
+
stream_title: str
|
28
|
+
stream_category: str
|
29
|
+
stream_tags: List[str] = Field(default_factory=list)
|
30
|
+
|
31
|
+
class AgentSettings(BaseModel):
|
32
|
+
"""Settings for the built-in agent"""
|
33
|
+
agent_provider: str
|
34
|
+
agent_model: str
|
30
35
|
|
31
36
|
class NeuroBehaviorSettings(BaseModel):
|
32
|
-
input_chat_sample_size: int
|
33
|
-
post_speech_cooldown_sec: float
|
34
|
-
initial_greeting: str
|
37
|
+
input_chat_sample_size: int
|
38
|
+
post_speech_cooldown_sec: float
|
39
|
+
initial_greeting: str
|
35
40
|
|
36
41
|
class AudienceSimSettings(BaseModel):
|
37
|
-
llm_provider: str
|
38
|
-
gemini_model: str
|
39
|
-
openai_model: str
|
40
|
-
llm_temperature: float
|
41
|
-
chat_generation_interval_sec: int
|
42
|
-
chats_per_batch: int
|
43
|
-
max_output_tokens: int
|
44
|
-
prompt_template: str = Field(default=""
|
45
|
-
|
46
|
-
|
47
|
-
---
|
48
|
-
{neuro_speech}
|
49
|
-
---
|
50
|
-
Based on what Neuro-Sama said, generate a variety of chat messages. Your messages should be:
|
51
|
-
- Directly reacting to her words.
|
52
|
-
- Asking follow-up questions.
|
53
|
-
- Using relevant Twitch emotes (like LUL, Pog, Kappa, etc.).
|
54
|
-
- General banter related to the topic.
|
55
|
-
- Short and punchy, like real chat messages.
|
56
|
-
Do NOT act as the streamer. Do NOT generate full conversations.
|
57
|
-
Generate exactly {num_chats_to_generate} distinct chat messages. Each message must be prefixed with a DIFFERENT fictional username, like 'ChatterBoy: message text', 'EmoteFan: message text'.
|
58
|
-
""")
|
59
|
-
username_blocklist: List[str] = Field(default_factory=lambda: ["ChatterBoy", "EmoteFan", "Username", "User"])
|
60
|
-
username_pool: List[str] = Field(default_factory=lambda: [
|
61
|
-
"ChatterBox", "EmoteLord", "QuestionMark", "StreamFan", "PixelPundit",
|
62
|
-
"CodeSage", "DataDiver", "ByteBard", "LogicLover", "AI_Enthusiast"
|
63
|
-
])
|
42
|
+
llm_provider: str
|
43
|
+
gemini_model: str
|
44
|
+
openai_model: str
|
45
|
+
llm_temperature: float
|
46
|
+
chat_generation_interval_sec: int
|
47
|
+
chats_per_batch: int
|
48
|
+
max_output_tokens: int
|
49
|
+
prompt_template: str = Field(default="")
|
50
|
+
username_blocklist: List[str] = Field(default_factory=list)
|
51
|
+
username_pool: List[str] = Field(default_factory=list)
|
64
52
|
|
65
53
|
class TTSSettings(BaseModel):
|
66
|
-
voice_name: str
|
67
|
-
voice_pitch: float
|
54
|
+
voice_name: str
|
55
|
+
voice_pitch: float
|
68
56
|
|
69
57
|
class PerformanceSettings(BaseModel):
|
70
|
-
neuro_input_queue_max_size: int
|
71
|
-
audience_chat_buffer_max_size: int
|
72
|
-
initial_chat_backlog_limit: int
|
58
|
+
neuro_input_queue_max_size: int
|
59
|
+
audience_chat_buffer_max_size: int
|
60
|
+
initial_chat_backlog_limit: int
|
73
61
|
|
74
62
|
class ServerSettings(BaseModel):
|
75
|
-
host: str
|
76
|
-
port: int
|
77
|
-
client_origins: List[str] = Field(default_factory=
|
63
|
+
host: str
|
64
|
+
port: int
|
65
|
+
client_origins: List[str] = Field(default_factory=list)
|
78
66
|
panel_password: Optional[str] = None
|
79
67
|
|
80
68
|
class AppSettings(BaseModel):
|
81
69
|
api_keys: ApiKeysSettings = Field(default_factory=ApiKeysSettings)
|
82
|
-
stream_metadata: StreamMetadataSettings
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
70
|
+
stream_metadata: StreamMetadataSettings
|
71
|
+
agent_type: str # 可选 "letta" 或 "builtin"
|
72
|
+
agent: AgentSettings
|
73
|
+
neuro_behavior: NeuroBehaviorSettings
|
74
|
+
audience_simulation: AudienceSimSettings
|
75
|
+
tts: TTSSettings
|
76
|
+
performance: PerformanceSettings
|
77
|
+
server: ServerSettings
|
88
78
|
|
89
79
|
# --- 2. 加载和管理配置的逻辑 ---
|
90
80
|
|
91
|
-
CONFIG_FILE_PATH = "config.yaml"
|
92
|
-
|
93
81
|
def _deep_update(source: dict, overrides: dict) -> dict:
|
94
82
|
"""
|
95
83
|
Recursively update a dictionary.
|
@@ -118,59 +106,209 @@ class ConfigManager:
|
|
118
106
|
self._update_callbacks = []
|
119
107
|
self._initialized = True
|
120
108
|
|
109
|
+
def _get_config_file_path(self) -> str:
|
110
|
+
"""获取配置文件路径"""
|
111
|
+
import sys
|
112
|
+
import argparse
|
113
|
+
|
114
|
+
# 解析命令行参数以获取工作目录
|
115
|
+
parser = argparse.ArgumentParser()
|
116
|
+
parser.add_argument('--dir', '-D', type=str, help='Working directory')
|
117
|
+
# 只解析已知参数,避免干扰其他模块的参数解析
|
118
|
+
args, _ = parser.parse_known_args()
|
119
|
+
|
120
|
+
if args.dir:
|
121
|
+
# 如果指定了工作目录,使用该目录下的配置文件
|
122
|
+
config_path = os.path.join(args.dir, "config.yaml")
|
123
|
+
else:
|
124
|
+
# 默认使用 ~/.config/neuro-simulator 目录
|
125
|
+
config_path = os.path.join(os.path.expanduser("~"), ".config", "neuro-simulator", "config.yaml")
|
126
|
+
|
127
|
+
return config_path
|
128
|
+
|
121
129
|
def _load_config_from_yaml(self) -> dict:
|
122
|
-
|
123
|
-
|
124
|
-
|
130
|
+
# 获取配置文件路径
|
131
|
+
config_path = self._get_config_file_path()
|
132
|
+
|
133
|
+
# 检查配置文件是否存在
|
134
|
+
if not os.path.exists(config_path):
|
135
|
+
raise FileNotFoundError(f"Configuration file '{config_path}' not found. "
|
136
|
+
"Please create it from config.yaml.example.")
|
137
|
+
|
125
138
|
try:
|
126
|
-
with open(
|
127
|
-
|
139
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
140
|
+
content = yaml.safe_load(f)
|
141
|
+
if content is None:
|
142
|
+
raise ValueError(f"Configuration file '{config_path}' is empty.")
|
143
|
+
return content
|
128
144
|
except Exception as e:
|
129
|
-
logging.error(f"Error loading or parsing {
|
130
|
-
|
145
|
+
logging.error(f"Error loading or parsing {config_path}: {e}")
|
146
|
+
raise
|
131
147
|
|
132
148
|
def _load_settings(self) -> AppSettings:
|
133
149
|
yaml_config = self._load_config_from_yaml()
|
150
|
+
|
134
151
|
base_settings = AppSettings.model_validate(yaml_config)
|
135
152
|
|
136
153
|
# 检查关键配置项
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
154
|
+
if base_settings.agent_type == "letta":
|
155
|
+
missing_keys = []
|
156
|
+
if not base_settings.api_keys.letta_token:
|
157
|
+
missing_keys.append("api_keys.letta_token")
|
158
|
+
if not base_settings.api_keys.neuro_agent_id:
|
159
|
+
missing_keys.append("api_keys.neuro_agent_id")
|
142
160
|
|
143
|
-
|
144
|
-
|
145
|
-
|
161
|
+
if missing_keys:
|
162
|
+
raise ValueError(f"Critical config missing in config.yaml for letta agent: {', '.join(missing_keys)}. "
|
163
|
+
f"Please check your config.yaml file against config.yaml.example.")
|
146
164
|
|
147
165
|
logging.info("Configuration loaded successfully.")
|
148
166
|
return base_settings
|
149
167
|
|
150
168
|
def save_settings(self):
|
151
|
-
"""Saves the current configuration to config.yaml."""
|
169
|
+
"""Saves the current configuration to config.yaml while preserving comments and formatting."""
|
152
170
|
try:
|
153
|
-
#
|
171
|
+
# 获取配置文件路径
|
172
|
+
config_file_path = self._get_config_file_path()
|
173
|
+
|
174
|
+
# 检查配置文件目录是否存在,如果不存在则创建
|
175
|
+
config_dir = os.path.dirname(config_file_path)
|
176
|
+
if config_dir and not os.path.exists(config_dir):
|
177
|
+
os.makedirs(config_dir, exist_ok=True)
|
178
|
+
|
179
|
+
# 1. Read the existing config file as text to preserve comments and formatting
|
180
|
+
with open(config_file_path, 'r', encoding='utf-8') as f:
|
181
|
+
config_lines = f.readlines()
|
182
|
+
|
183
|
+
# 2. Get the current settings from memory
|
154
184
|
config_to_save = self.settings.model_dump(mode='json', exclude={'api_keys'})
|
155
185
|
|
156
|
-
#
|
186
|
+
# 3. Read the existing config on disk to get the api_keys that should be preserved.
|
157
187
|
existing_config = self._load_config_from_yaml()
|
158
188
|
if 'api_keys' in existing_config:
|
159
|
-
#
|
189
|
+
# 4. Add the preserved api_keys block back to the data to be saved.
|
160
190
|
config_to_save['api_keys'] = existing_config['api_keys']
|
161
191
|
|
162
|
-
#
|
163
|
-
|
164
|
-
for field_name in AppSettings.model_fields:
|
165
|
-
if field_name in config_to_save:
|
166
|
-
final_config[field_name] = config_to_save[field_name]
|
192
|
+
# 5. Update the config lines while preserving comments and formatting
|
193
|
+
updated_lines = self._update_config_lines(config_lines, config_to_save)
|
167
194
|
|
168
|
-
#
|
169
|
-
with open(
|
170
|
-
|
171
|
-
|
195
|
+
# 6. Write the updated lines back to the file
|
196
|
+
with open(config_file_path, 'w', encoding='utf-8') as f:
|
197
|
+
f.writelines(updated_lines)
|
198
|
+
|
199
|
+
logging.info(f"Configuration saved to {config_file_path}")
|
172
200
|
except Exception as e:
|
173
|
-
logging.error(f"Failed to save configuration to {
|
201
|
+
logging.error(f"Failed to save configuration to {config_file_path}: {e}")
|
202
|
+
|
203
|
+
def _update_config_lines(self, lines, config_data):
|
204
|
+
"""Updates config lines with new values while preserving comments and formatting."""
|
205
|
+
updated_lines = []
|
206
|
+
i = 0
|
207
|
+
while i < len(lines):
|
208
|
+
line = lines[i]
|
209
|
+
stripped_line = line.strip()
|
210
|
+
|
211
|
+
# Skip empty lines and comments
|
212
|
+
if not stripped_line or stripped_line.startswith('#'):
|
213
|
+
updated_lines.append(line)
|
214
|
+
i += 1
|
215
|
+
continue
|
216
|
+
|
217
|
+
# Check if this line is a top-level key
|
218
|
+
if ':' in stripped_line and not stripped_line.startswith(' ') and not stripped_line.startswith('\t'):
|
219
|
+
key = stripped_line.split(':')[0].strip()
|
220
|
+
if key in config_data:
|
221
|
+
value = config_data[key]
|
222
|
+
if isinstance(value, dict):
|
223
|
+
# Handle nested dictionaries
|
224
|
+
updated_lines.append(line)
|
225
|
+
i += 1
|
226
|
+
# Process nested items
|
227
|
+
i = self._update_nested_config_lines(lines, updated_lines, i, value, 1)
|
228
|
+
else:
|
229
|
+
# Handle simple values
|
230
|
+
indent = len(line) - len(line.lstrip())
|
231
|
+
if isinstance(value, str) and '\n' in value:
|
232
|
+
# Handle multiline strings
|
233
|
+
updated_lines.append(' ' * indent + f"{key}: |\n")
|
234
|
+
for subline in value.split('\n'):
|
235
|
+
updated_lines.append(' ' * (indent + 2) + subline + '\n')
|
236
|
+
elif isinstance(value, list):
|
237
|
+
# Handle lists
|
238
|
+
updated_lines.append(' ' * indent + f"{key}:\n")
|
239
|
+
for item in value:
|
240
|
+
updated_lines.append(' ' * (indent + 2) + f"- {item}\n")
|
241
|
+
else:
|
242
|
+
# Handle simple values
|
243
|
+
updated_lines.append(' ' * indent + f"{key}: {value}\n")
|
244
|
+
i += 1
|
245
|
+
else:
|
246
|
+
updated_lines.append(line)
|
247
|
+
i += 1
|
248
|
+
else:
|
249
|
+
updated_lines.append(line)
|
250
|
+
i += 1
|
251
|
+
|
252
|
+
return updated_lines
|
253
|
+
|
254
|
+
def _update_nested_config_lines(self, lines, updated_lines, start_index, config_data, depth):
|
255
|
+
"""Recursively updates nested config lines."""
|
256
|
+
i = start_index
|
257
|
+
indent_size = depth * 2
|
258
|
+
|
259
|
+
while i < len(lines):
|
260
|
+
line = lines[i]
|
261
|
+
stripped_line = line.strip()
|
262
|
+
|
263
|
+
# Check indentation level
|
264
|
+
current_indent = len(line) - len(line.lstrip())
|
265
|
+
|
266
|
+
# If we've moved to a less indented section, we're done with this nested block
|
267
|
+
if current_indent < indent_size:
|
268
|
+
break
|
269
|
+
|
270
|
+
# Skip empty lines and comments
|
271
|
+
if not stripped_line or stripped_line.startswith('#'):
|
272
|
+
updated_lines.append(line)
|
273
|
+
i += 1
|
274
|
+
continue
|
275
|
+
|
276
|
+
# Check if this line is a key at the current nesting level
|
277
|
+
if current_indent == indent_size and ':' in stripped_line:
|
278
|
+
key = stripped_line.split(':')[0].strip()
|
279
|
+
if key in config_data:
|
280
|
+
value = config_data[key]
|
281
|
+
if isinstance(value, dict):
|
282
|
+
# Handle nested dictionaries
|
283
|
+
updated_lines.append(line)
|
284
|
+
i += 1
|
285
|
+
i = self._update_nested_config_lines(lines, updated_lines, i, value, depth + 1)
|
286
|
+
else:
|
287
|
+
# Handle simple values
|
288
|
+
if isinstance(value, str) and '\n' in value:
|
289
|
+
# Handle multiline strings
|
290
|
+
updated_lines.append(' ' * indent_size + f"{key}: |\n")
|
291
|
+
for subline in value.split('\n'):
|
292
|
+
updated_lines.append(' ' * (indent_size + 2) + subline + '\n')
|
293
|
+
i += 1
|
294
|
+
elif isinstance(value, list):
|
295
|
+
# Handle lists
|
296
|
+
updated_lines.append(' ' * indent_size + f"{key}:\n")
|
297
|
+
for item in value:
|
298
|
+
updated_lines.append(' ' * (indent_size + 2) + f"- {item}\n")
|
299
|
+
i += 1
|
300
|
+
else:
|
301
|
+
# Handle simple values
|
302
|
+
updated_lines.append(' ' * indent_size + f"{key}: {value}\n")
|
303
|
+
i += 1
|
304
|
+
else:
|
305
|
+
updated_lines.append(line)
|
306
|
+
i += 1
|
307
|
+
else:
|
308
|
+
updated_lines.append(line)
|
309
|
+
i += 1
|
310
|
+
|
311
|
+
return i
|
174
312
|
|
175
313
|
def register_update_callback(self, callback):
|
176
314
|
"""Registers a callback function to be called on settings update."""
|
@@ -39,6 +39,20 @@ stream_metadata:
|
|
39
39
|
# 直播标签 - 用于描述直播内容的标签列表
|
40
40
|
stream_tags: ["Vtuber", "AI", "Cute", "English", "Gremlin", "catgirl"]
|
41
41
|
|
42
|
+
# --- Agent 类型设置 ---
|
43
|
+
# 选择一个用来模拟Neuro的Agent提供方
|
44
|
+
# - "letta": 使用Letta作为Agent,需要在上方配置Letta API相关信息
|
45
|
+
# - "builtin": 使用内建Agent,请在下方填写配置
|
46
|
+
agent_type: "builtin"
|
47
|
+
|
48
|
+
# --- 内建Agent设置 ---
|
49
|
+
# 仅当agent_type设置为"builtin"时生效
|
50
|
+
agent:
|
51
|
+
# Agent的API服务商,支持"gemini"和"openai",API Key配置使用顶部填写的值
|
52
|
+
agent_provider: "gemini"
|
53
|
+
# Agent使用的模型,切换gemini/openai时记得更改
|
54
|
+
agent_model: "gemini-2.5-flash-lite"
|
55
|
+
|
42
56
|
# --- Neuro 行为与节奏控制 ---
|
43
57
|
neuro_behavior:
|
44
58
|
# 输入聊天采样数量 - 每次生成 Neuro 回复时从观众聊天中采样的消息数量,不建议太长
|
@@ -60,7 +74,7 @@ audience_simulation:
|
|
60
74
|
gemini_model: "gemma-3-27b-it"
|
61
75
|
|
62
76
|
# OpenAI 模型 - 使用 OpenAI 服务时的具体模型名称
|
63
|
-
#
|
77
|
+
# 推荐使用SiliconFlow,9B以下模型免费不限量调用(注意TPM限制)
|
64
78
|
openai_model: "THUDM/GLM-4-9B-0414"
|
65
79
|
|
66
80
|
# LLM 温度 - 控制 AI 生成内容的随机性,值越高越随机(0-2之间)
|
@@ -140,4 +154,4 @@ server:
|
|
140
154
|
# 客户端来源 - 允许跨域访问的客户端地址列表,非本机访问时记得添加一下
|
141
155
|
client_origins:
|
142
156
|
- "http://localhost:5173"
|
143
|
-
- "http://127.0.0.1:5173"
|
157
|
+
- "http://127.0.0.1:5173"
|