neuro-simulator 0.3.3__py3-none-any.whl → 0.4.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/agent/core.py +73 -138
- neuro_simulator/agent/llm.py +1 -1
- neuro_simulator/agent/memory/manager.py +24 -103
- neuro_simulator/agent/tools/add_temp_memory.py +3 -3
- neuro_simulator/agent/tools/add_to_core_memory_block.py +2 -2
- neuro_simulator/agent/tools/create_core_memory_block.py +2 -2
- neuro_simulator/agent/tools/delete_core_memory_block.py +2 -2
- neuro_simulator/agent/tools/get_core_memory_block.py +2 -2
- neuro_simulator/agent/tools/get_core_memory_blocks.py +2 -2
- neuro_simulator/agent/tools/manager.py +86 -63
- neuro_simulator/agent/tools/remove_from_core_memory_block.py +2 -2
- neuro_simulator/agent/tools/speak.py +2 -2
- neuro_simulator/agent/tools/update_core_memory_block.py +2 -2
- neuro_simulator/api/system.py +5 -2
- neuro_simulator/cli.py +83 -53
- neuro_simulator/core/agent_factory.py +0 -1
- neuro_simulator/core/application.py +46 -32
- neuro_simulator/core/config.py +66 -63
- neuro_simulator/core/path_manager.py +69 -0
- neuro_simulator/services/audience.py +0 -2
- neuro_simulator/services/audio.py +0 -1
- neuro_simulator/services/builtin.py +10 -25
- neuro_simulator/services/letta.py +19 -1
- neuro_simulator/services/stream.py +24 -21
- neuro_simulator/utils/logging.py +9 -0
- neuro_simulator/utils/queue.py +27 -4
- neuro_simulator/utils/websocket.py +1 -3
- {neuro_simulator-0.3.3.dist-info → neuro_simulator-0.4.0.dist-info}/METADATA +1 -1
- neuro_simulator-0.4.0.dist-info/RECORD +46 -0
- neuro_simulator/agent/base.py +0 -43
- neuro_simulator/agent/factory.py +0 -30
- neuro_simulator/api/stream.py +0 -1
- neuro_simulator-0.3.3.dist-info/RECORD +0 -48
- {neuro_simulator-0.3.3.dist-info → neuro_simulator-0.4.0.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.3.3.dist-info → neuro_simulator-0.4.0.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.3.3.dist-info → neuro_simulator-0.4.0.dist-info}/top_level.txt +0 -0
neuro_simulator/core/config.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# backend/config.py
|
2
|
-
import
|
2
|
+
import shutil
|
3
|
+
from pathlib import Path
|
3
4
|
import yaml
|
4
5
|
from pydantic import BaseModel, Field
|
5
|
-
from typing import
|
6
|
+
from typing import Dict, Optional, List
|
6
7
|
import logging
|
7
8
|
import asyncio
|
8
9
|
from collections.abc import Mapping
|
@@ -106,76 +107,77 @@ class ConfigManager:
|
|
106
107
|
self._update_callbacks = []
|
107
108
|
self._initialized = True
|
108
109
|
|
109
|
-
|
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
|
110
|
+
import sys
|
128
111
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
112
|
+
class ConfigManager:
|
113
|
+
_instance = None
|
114
|
+
|
115
|
+
def __new__(cls):
|
116
|
+
if cls._instance is None:
|
117
|
+
cls._instance = super(ConfigManager, cls).__new__(cls)
|
118
|
+
cls._instance._initialized = False
|
119
|
+
return cls._instance
|
120
|
+
|
121
|
+
def __init__(self):
|
122
|
+
if self._initialized:
|
123
|
+
return
|
124
|
+
self.settings: Optional[AppSettings] = None
|
125
|
+
self._update_callbacks = []
|
126
|
+
self._initialized = True
|
127
|
+
|
128
|
+
def load_and_validate(self, config_path_str: str, example_path_str: str):
|
129
|
+
"""Loads the main config file, handling first-run scenarios, and validates it."""
|
130
|
+
config_path = Path(config_path_str)
|
131
|
+
example_path = Path(example_path_str)
|
132
|
+
|
133
|
+
# Scenario 1: Both config and example are missing in the working directory.
|
134
|
+
if not config_path.exists() and not example_path.exists():
|
135
|
+
try:
|
136
|
+
import pkg_resources
|
137
|
+
package_example_path_str = pkg_resources.resource_filename('neuro_simulator', 'core/config.yaml.example')
|
138
|
+
shutil.copy(package_example_path_str, example_path)
|
139
|
+
logging.info(f"Created '{example_path}' from package resource.")
|
140
|
+
logging.error(f"Configuration file '{config_path.name}' not found. A new '{example_path.name}' has been created. Please configure it and rename it to '{config_path.name}'.")
|
141
|
+
sys.exit(1)
|
142
|
+
except Exception as e:
|
143
|
+
logging.error(f"FATAL: Could not create config from package resources: {e}")
|
144
|
+
sys.exit(1)
|
145
|
+
|
146
|
+
# Scenario 2: Config is missing, but example exists.
|
147
|
+
elif not config_path.exists() and example_path.exists():
|
148
|
+
logging.error(f"Configuration file '{config_path.name}' not found, but '{example_path.name}' exists. Please rename it to '{config_path.name}' after configuration.")
|
149
|
+
sys.exit(1)
|
150
|
+
|
151
|
+
# Scenario 3: Config exists, but example is missing.
|
152
|
+
elif config_path.exists() and not example_path.exists():
|
153
|
+
try:
|
154
|
+
import pkg_resources
|
155
|
+
package_example_path_str = pkg_resources.resource_filename('neuro_simulator', 'core/config.yaml.example')
|
156
|
+
shutil.copy(package_example_path_str, example_path)
|
157
|
+
logging.info(f"Created missing '{example_path.name}' from package resource.")
|
158
|
+
except Exception as e:
|
159
|
+
logging.warning(f"Could not create missing '{example_path.name}': {e}")
|
160
|
+
|
161
|
+
# Proceed with loading the config if it exists.
|
138
162
|
try:
|
139
163
|
with open(config_path, 'r', encoding='utf-8') as f:
|
140
|
-
|
141
|
-
if
|
164
|
+
yaml_config = yaml.safe_load(f)
|
165
|
+
if yaml_config is None:
|
142
166
|
raise ValueError(f"Configuration file '{config_path}' is empty.")
|
143
|
-
return content
|
144
|
-
except Exception as e:
|
145
|
-
logging.error(f"Error loading or parsing {config_path}: {e}")
|
146
|
-
raise
|
147
|
-
|
148
|
-
def _load_settings(self) -> AppSettings:
|
149
|
-
yaml_config = self._load_config_from_yaml()
|
150
|
-
|
151
|
-
base_settings = AppSettings.model_validate(yaml_config)
|
152
|
-
|
153
|
-
# 检查关键配置项
|
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")
|
160
167
|
|
161
|
-
|
162
|
-
|
163
|
-
f"Please check your config.yaml file against config.yaml.example.")
|
168
|
+
self.settings = AppSettings.model_validate(yaml_config)
|
169
|
+
logging.info("Main configuration loaded successfully.")
|
164
170
|
|
165
|
-
|
166
|
-
|
171
|
+
except Exception as e:
|
172
|
+
logging.error(f"Error loading or parsing {config_path}: {e}")
|
173
|
+
sys.exit(1) # Exit if the main config is invalid
|
167
174
|
|
168
175
|
def save_settings(self):
|
169
176
|
"""Saves the current configuration to config.yaml while preserving comments and formatting."""
|
177
|
+
from .path_manager import path_manager
|
178
|
+
config_file_path = str(path_manager.working_dir / "config.yaml")
|
179
|
+
|
170
180
|
try:
|
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
181
|
# 1. Read the existing config file as text to preserve comments and formatting
|
180
182
|
with open(config_file_path, 'r', encoding='utf-8') as f:
|
181
183
|
config_lines = f.readlines()
|
@@ -184,7 +186,8 @@ class ConfigManager:
|
|
184
186
|
config_to_save = self.settings.model_dump(mode='json', exclude={'api_keys'})
|
185
187
|
|
186
188
|
# 3. Read the existing config on disk to get the api_keys that should be preserved.
|
187
|
-
|
189
|
+
with open(config_file_path, 'r', encoding='utf-8') as f:
|
190
|
+
existing_config = yaml.safe_load(f)
|
188
191
|
if 'api_keys' in existing_config:
|
189
192
|
# 4. Add the preserved api_keys block back to the data to be saved.
|
190
193
|
config_to_save['api_keys'] = existing_config['api_keys']
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# neuro_simulator/core/path_manager.py
|
2
|
+
"""Manages all file and directory paths for the application's working directory."""
|
3
|
+
|
4
|
+
import os
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
class PathManager:
|
8
|
+
"""A centralized manager for all dynamic paths within the working directory."""
|
9
|
+
|
10
|
+
def __init__(self, working_dir: str):
|
11
|
+
"""Initializes the PathManager and defines the directory structure."""
|
12
|
+
self.working_dir = Path(working_dir).resolve()
|
13
|
+
|
14
|
+
# Top-level directories
|
15
|
+
self.agents_dir = self.working_dir / "agents"
|
16
|
+
self.assets_dir = self.working_dir / "assets"
|
17
|
+
|
18
|
+
# Agents subdirectories
|
19
|
+
self.neuro_agent_dir = self.agents_dir / "neuro"
|
20
|
+
self.memory_agent_dir = self.agents_dir / "memory_manager"
|
21
|
+
self.shared_memories_dir = self.agents_dir / "memories"
|
22
|
+
self.user_tools_dir = self.agents_dir / "tools"
|
23
|
+
self.builtin_tools_dir = self.user_tools_dir / "builtin_tools"
|
24
|
+
|
25
|
+
# Agent-specific config files
|
26
|
+
self.neuro_config_path = self.neuro_agent_dir / "config.yaml"
|
27
|
+
self.neuro_tools_path = self.neuro_agent_dir / "tools.json"
|
28
|
+
self.neuro_history_path = self.neuro_agent_dir / "history.jsonl"
|
29
|
+
self.neuro_prompt_path = self.neuro_agent_dir / "neuro_prompt.txt"
|
30
|
+
|
31
|
+
self.memory_agent_config_path = self.memory_agent_dir / "config.yaml"
|
32
|
+
self.memory_agent_tools_path = self.memory_agent_dir / "tools.json"
|
33
|
+
self.memory_agent_history_path = self.memory_agent_dir / "history.jsonl"
|
34
|
+
self.memory_agent_prompt_path = self.memory_agent_dir / "memory_prompt.txt"
|
35
|
+
|
36
|
+
# Shared memory files
|
37
|
+
self.init_memory_path = self.shared_memories_dir / "init_memory.json"
|
38
|
+
self.core_memory_path = self.shared_memories_dir / "core_memory.json"
|
39
|
+
self.temp_memory_path = self.shared_memories_dir / "temp_memory.json"
|
40
|
+
|
41
|
+
def initialize_directories(self):
|
42
|
+
"""Creates all necessary directories if they don't exist."""
|
43
|
+
dirs_to_create = [
|
44
|
+
self.agents_dir,
|
45
|
+
self.assets_dir,
|
46
|
+
self.neuro_agent_dir,
|
47
|
+
self.memory_agent_dir,
|
48
|
+
self.shared_memories_dir,
|
49
|
+
self.user_tools_dir,
|
50
|
+
self.builtin_tools_dir
|
51
|
+
]
|
52
|
+
for dir_path in dirs_to_create:
|
53
|
+
os.makedirs(dir_path, exist_ok=True)
|
54
|
+
|
55
|
+
# Create the warning file in the builtin_tools directory
|
56
|
+
warning_file_path = self.builtin_tools_dir / "!!!NO-CHANGE-WILL-BE-SAVED-AFTER-RESTART!!!"
|
57
|
+
if not warning_file_path.exists():
|
58
|
+
warning_file_path.touch()
|
59
|
+
|
60
|
+
# A global instance that can be imported and used by other modules.
|
61
|
+
# It will be initialized on application startup.
|
62
|
+
path_manager: PathManager = None
|
63
|
+
|
64
|
+
def initialize_path_manager(working_dir: str):
|
65
|
+
"""Initializes the global path_manager instance."""
|
66
|
+
global path_manager
|
67
|
+
if path_manager is None:
|
68
|
+
path_manager = PathManager(working_dir)
|
69
|
+
path_manager.initialize_directories()
|
@@ -1,27 +1,24 @@
|
|
1
1
|
# neuro_simulator/services/builtin.py
|
2
2
|
"""Builtin agent module for Neuro Simulator"""
|
3
3
|
|
4
|
-
import asyncio
|
5
|
-
import re
|
6
4
|
import logging
|
7
5
|
from typing import List, Dict, Any, Optional
|
8
6
|
|
9
7
|
from ..core.agent_interface import BaseAgent
|
10
8
|
from ..agent.core import Agent as LocalAgent
|
11
|
-
from ..
|
9
|
+
from ..utils.websocket import connection_manager
|
12
10
|
|
13
11
|
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
14
12
|
|
15
13
|
async def initialize_builtin_agent() -> Optional[LocalAgent]:
|
16
|
-
"""Initializes
|
14
|
+
"""Initializes a new builtin agent instance and returns it."""
|
17
15
|
try:
|
18
|
-
|
19
|
-
agent_instance = LocalAgent(working_dir=working_dir)
|
16
|
+
agent_instance = LocalAgent()
|
20
17
|
await agent_instance.initialize()
|
21
|
-
logger.info("
|
18
|
+
logger.info("New builtin agent instance initialized successfully.")
|
22
19
|
return agent_instance
|
23
20
|
except Exception as e:
|
24
|
-
logger.error(f"Failed to initialize local agent
|
21
|
+
logger.error(f"Failed to initialize local agent instance: {e}", exc_info=True)
|
25
22
|
return None
|
26
23
|
|
27
24
|
class BuiltinAgentWrapper(BaseAgent):
|
@@ -50,24 +47,18 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
50
47
|
|
51
48
|
async def create_memory_block(self, title: str, description: str, content: List[str]) -> Dict[str, str]:
|
52
49
|
block_id = await self.agent_instance.memory_manager.create_core_memory_block(title, description, content)
|
53
|
-
# Broadcast core_memory_updated event
|
54
50
|
updated_blocks = await self.get_memory_blocks()
|
55
|
-
from ..utils.websocket import connection_manager
|
56
51
|
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
57
52
|
return {"block_id": block_id}
|
58
53
|
|
59
54
|
async def update_memory_block(self, block_id: str, title: Optional[str], description: Optional[str], content: Optional[List[str]]):
|
60
55
|
await self.agent_instance.memory_manager.update_core_memory_block(block_id, title, description, content)
|
61
|
-
# Broadcast core_memory_updated event
|
62
56
|
updated_blocks = await self.get_memory_blocks()
|
63
|
-
from ..utils.websocket import connection_manager
|
64
57
|
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
65
58
|
|
66
59
|
async def delete_memory_block(self, block_id: str):
|
67
60
|
await self.agent_instance.memory_manager.delete_core_memory_block(block_id)
|
68
|
-
# Broadcast core_memory_updated event
|
69
61
|
updated_blocks = await self.get_memory_blocks()
|
70
|
-
from ..utils.websocket import connection_manager
|
71
62
|
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
72
63
|
|
73
64
|
# Init Memory Management
|
@@ -76,9 +67,7 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
76
67
|
|
77
68
|
async def update_init_memory(self, memory: Dict[str, Any]):
|
78
69
|
await self.agent_instance.memory_manager.update_init_memory(memory)
|
79
|
-
# Broadcast init_memory_updated event
|
80
70
|
updated_init_mem = await self.get_init_memory()
|
81
|
-
from ..utils.websocket import connection_manager
|
82
71
|
await connection_manager.broadcast_to_admins({"type": "init_memory_updated", "payload": updated_init_mem})
|
83
72
|
|
84
73
|
# Temp Memory Management
|
@@ -87,31 +76,27 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
87
76
|
|
88
77
|
async def add_temp_memory(self, content: str, role: str):
|
89
78
|
await self.agent_instance.memory_manager.add_temp_memory(content, role)
|
90
|
-
# Broadcast temp_memory_updated event
|
91
79
|
updated_temp_mem = await self.get_temp_memory()
|
92
|
-
from ..utils.websocket import connection_manager
|
93
80
|
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
94
81
|
|
95
82
|
async def clear_temp_memory(self):
|
96
83
|
await self.agent_instance.memory_manager.reset_temp_memory()
|
97
|
-
# Broadcast temp_memory_updated event
|
98
84
|
updated_temp_mem = await self.get_temp_memory()
|
99
|
-
from ..utils.websocket import connection_manager
|
100
85
|
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
101
86
|
|
102
87
|
# Tool Management
|
103
88
|
async def get_available_tools(self) -> str:
|
104
|
-
|
89
|
+
# This method is now for internal use, the dashboard uses the new API
|
90
|
+
schemas = self.agent_instance.tool_manager.get_tool_schemas_for_agent('neuro_agent')
|
91
|
+
return self.agent_instance._format_tool_schemas_for_prompt(schemas)
|
105
92
|
|
106
93
|
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
107
|
-
result = await self.agent_instance.execute_tool(tool_name, params)
|
108
|
-
# If the tool was add_temp_memory, broadcast temp_memory_updated event
|
94
|
+
result = await self.agent_instance.tool_manager.execute_tool(tool_name, **params)
|
109
95
|
if tool_name == "add_temp_memory":
|
110
96
|
updated_temp_mem = await self.get_temp_memory()
|
111
|
-
from ..utils.websocket import connection_manager
|
112
97
|
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
113
98
|
return result
|
114
99
|
|
115
100
|
# Context/Message History
|
116
101
|
async def get_message_history(self, limit: int = 20) -> List[Dict[str, Any]]:
|
117
|
-
return await self.agent_instance.
|
102
|
+
return await self.agent_instance.get_neuro_history(limit)
|
@@ -67,11 +67,29 @@ class LettaAgent(BaseAgent):
|
|
67
67
|
raise ValueError("Letta agent ID (neuro_agent_id) is not configured.")
|
68
68
|
|
69
69
|
async def reset_memory(self):
|
70
|
+
"""Resets message history and clears the conversation_summary block."""
|
70
71
|
try:
|
72
|
+
# Reset message history
|
71
73
|
await asyncio.to_thread(self.client.agents.messages.reset, agent_id=self.agent_id)
|
72
74
|
logger.info(f"Letta Agent (ID: {self.agent_id}) message history has been reset.")
|
75
|
+
|
76
|
+
# Find and clear the conversation_summary block
|
77
|
+
blocks = await asyncio.to_thread(self.client.agents.blocks.list, agent_id=self.agent_id)
|
78
|
+
summary_block = next((block for block in blocks if block.name == "conversation_summary"), None)
|
79
|
+
|
80
|
+
if summary_block:
|
81
|
+
await asyncio.to_thread(
|
82
|
+
self.client.agents.blocks.modify,
|
83
|
+
agent_id=self.agent_id,
|
84
|
+
block_id=summary_block.id,
|
85
|
+
content=""
|
86
|
+
)
|
87
|
+
logger.info(f"Cleared content of 'conversation_summary' block (ID: {summary_block.id}) for Letta Agent.")
|
88
|
+
else:
|
89
|
+
logger.warning("'conversation_summary' block not found for Letta Agent, skipping clearing.")
|
90
|
+
|
73
91
|
except Exception as e:
|
74
|
-
logger.warning(f"Failed
|
92
|
+
logger.warning(f"Failed during Letta Agent memory reset: {e}")
|
75
93
|
|
76
94
|
async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
77
95
|
# Check if this is a superchat message based on the specific structure
|
@@ -11,6 +11,28 @@ from ..utils.state import app_state
|
|
11
11
|
|
12
12
|
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
13
13
|
|
14
|
+
_WORKING_DIR = os.getcwd()
|
15
|
+
_WELCOME_VIDEO_PATH_BACKEND = os.path.join(_WORKING_DIR, "assets", "neuro_start.mp4")
|
16
|
+
_WELCOME_VIDEO_DURATION_SEC_DEFAULT = 10.0
|
17
|
+
|
18
|
+
@staticmethod
|
19
|
+
def _get_video_duration(video_path: str) -> float:
|
20
|
+
"""Gets the duration of an MP4 video file using mutagen."""
|
21
|
+
if not os.path.exists(video_path):
|
22
|
+
logger.warning(f"Video file '{video_path}' not found. Using default duration.")
|
23
|
+
return _WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
24
|
+
try:
|
25
|
+
video = MP4(video_path)
|
26
|
+
duration = video.info.length
|
27
|
+
logger.info(f"Successfully read video duration for '{video_path}': {duration:.2f}s.")
|
28
|
+
return duration
|
29
|
+
except MP4StreamInfoError:
|
30
|
+
logger.warning(f"Could not parse stream info for '{video_path}'. Using default duration.")
|
31
|
+
return _WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
32
|
+
except Exception as e:
|
33
|
+
logger.error(f"Error getting video duration: {e}. Using default duration.")
|
34
|
+
return _WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
35
|
+
|
14
36
|
class LiveStreamManager:
|
15
37
|
class NeuroAvatarStage:
|
16
38
|
HIDDEN = "hidden"
|
@@ -25,28 +47,9 @@ class LiveStreamManager:
|
|
25
47
|
|
26
48
|
event_queue: asyncio.Queue = asyncio.Queue()
|
27
49
|
|
28
|
-
|
29
|
-
_WELCOME_VIDEO_PATH_BACKEND = os.path.join(
|
50
|
+
_WORKING_DIR = os.getcwd()
|
51
|
+
_WELCOME_VIDEO_PATH_BACKEND = os.path.join(_WORKING_DIR, "assets", "neuro_start.mp4")
|
30
52
|
_WELCOME_VIDEO_DURATION_SEC_DEFAULT = 10.0
|
31
|
-
|
32
|
-
@staticmethod
|
33
|
-
def _get_video_duration(video_path: str) -> float:
|
34
|
-
"""Gets the duration of an MP4 video file using mutagen."""
|
35
|
-
if not os.path.exists(video_path):
|
36
|
-
logger.warning(f"Video file '{video_path}' not found. Using default duration.")
|
37
|
-
return LiveStreamManager._WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
38
|
-
try:
|
39
|
-
video = MP4(video_path)
|
40
|
-
duration = video.info.length
|
41
|
-
logger.info(f"Successfully read video duration for '{video_path}': {duration:.2f}s.")
|
42
|
-
return duration
|
43
|
-
except MP4StreamInfoError:
|
44
|
-
logger.warning(f"Could not parse stream info for '{video_path}'. Using default duration.")
|
45
|
-
return LiveStreamManager._WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
46
|
-
except Exception as e:
|
47
|
-
logger.error(f"Error getting video duration: {e}. Using default duration.")
|
48
|
-
return LiveStreamManager._WELCOME_VIDEO_DURATION_SEC_DEFAULT
|
49
|
-
|
50
53
|
_WELCOME_VIDEO_DURATION_SEC = _get_video_duration(_WELCOME_VIDEO_PATH_BACKEND)
|
51
54
|
AVATAR_INTRO_TOTAL_DURATION_SEC = 3.0
|
52
55
|
|
neuro_simulator/utils/logging.py
CHANGED
@@ -87,4 +87,13 @@ def configure_server_logging():
|
|
87
87
|
uvicorn_logger.handlers = [server_queue_handler, console_handler]
|
88
88
|
uvicorn_logger.propagate = False # Prevent double-logging
|
89
89
|
|
90
|
+
# Configure the neuro_agent logger
|
91
|
+
neuro_agent_logger = logging.getLogger("neuro_agent")
|
92
|
+
neuro_agent_queue_handler = QueueLogHandler(agent_log_queue)
|
93
|
+
neuro_agent_queue_handler.setFormatter(queue_formatter)
|
94
|
+
neuro_agent_logger.addHandler(neuro_agent_queue_handler)
|
95
|
+
neuro_agent_logger.addHandler(console_handler) # Also send agent logs to console
|
96
|
+
neuro_agent_logger.setLevel(logging.INFO)
|
97
|
+
neuro_agent_logger.propagate = False # Prevent double-logging
|
98
|
+
|
90
99
|
root_logger.info("Server logging configured with unified formatting for queue and console.")
|
neuro_simulator/utils/queue.py
CHANGED
@@ -3,16 +3,39 @@
|
|
3
3
|
|
4
4
|
import logging
|
5
5
|
from collections import deque
|
6
|
-
from pathlib import Path
|
7
6
|
|
8
7
|
from ..core.config import config_manager
|
9
8
|
from ..utils.state import app_state
|
10
9
|
|
11
10
|
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
12
11
|
|
13
|
-
#
|
14
|
-
audience_chat_buffer: deque[dict] = deque(
|
15
|
-
neuro_input_queue: deque[dict] = deque(
|
12
|
+
# Deques for chat messages
|
13
|
+
audience_chat_buffer: deque[dict] = deque()
|
14
|
+
neuro_input_queue: deque[dict] = deque()
|
15
|
+
|
16
|
+
def initialize_queues():
|
17
|
+
"""
|
18
|
+
Initializes the chat queues with sizes from the loaded configuration.
|
19
|
+
This must be called after the config is loaded.
|
20
|
+
"""
|
21
|
+
global audience_chat_buffer, neuro_input_queue
|
22
|
+
|
23
|
+
settings = config_manager.settings
|
24
|
+
if not settings:
|
25
|
+
logger.error("Queue initialization failed: Config not loaded.")
|
26
|
+
return
|
27
|
+
|
28
|
+
logger.info("Initializing queues with configured sizes.")
|
29
|
+
|
30
|
+
# Re-initialize the deques with the correct maxlen
|
31
|
+
audience_chat_buffer = deque(
|
32
|
+
audience_chat_buffer,
|
33
|
+
maxlen=settings.performance.audience_chat_buffer_max_size
|
34
|
+
)
|
35
|
+
neuro_input_queue = deque(
|
36
|
+
neuro_input_queue,
|
37
|
+
maxlen=settings.performance.neuro_input_queue_max_size
|
38
|
+
)
|
16
39
|
|
17
40
|
def clear_all_queues():
|
18
41
|
"""Clears all chat queues."""
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# neuro_simulator/utils/websocket.py
|
2
|
-
import asyncio
|
3
2
|
import json
|
4
3
|
import logging
|
5
|
-
from collections import deque
|
6
4
|
|
7
5
|
from fastapi import WebSocket
|
8
6
|
from starlette.websockets import WebSocketState
|
@@ -39,7 +37,7 @@ class WebSocketManager:
|
|
39
37
|
|
40
38
|
async def broadcast(self, message: dict):
|
41
39
|
for connection in self.active_connections:
|
42
|
-
await
|
40
|
+
await self.send_personal_message(message, connection)
|
43
41
|
|
44
42
|
async def broadcast_to_admins(self, message: dict):
|
45
43
|
for connection in self.admin_connections:
|
@@ -0,0 +1,46 @@
|
|
1
|
+
neuro_simulator/__init__.py,sha256=-tposzyvg6UckPcfSvtc03UjxBa9oCe_zRvlKf8splk,31
|
2
|
+
neuro_simulator/cli.py,sha256=ma7kxzQqPaCbnUrZ516avIAsVk43PEc7-wH7BtfFe4E,5464
|
3
|
+
neuro_simulator/agent/__init__.py,sha256=t52CZlyTGWqcGjMs90qvpFpRckY2WSSlO7r_H3K_mSY,32
|
4
|
+
neuro_simulator/agent/core.py,sha256=_UkxMV1S33VHjD1JFlKz17GpaDhHQGNA9s9dsRgd3j0,9561
|
5
|
+
neuro_simulator/agent/llm.py,sha256=xPBEXpZ19WOt7YkERNaY9rscNI-ePASTjIt-_sZV7UI,4262
|
6
|
+
neuro_simulator/agent/memory_prompt.txt,sha256=wdpdnbOYhMKgPJnnGlcSejGdt2uItrXzDgz9_8cKnqw,824
|
7
|
+
neuro_simulator/agent/neuro_prompt.txt,sha256=WSN5Fa6AwVnJaSFhz98fQTb2EtQ35U6w2qga_C-s1Go,1438
|
8
|
+
neuro_simulator/agent/memory/__init__.py,sha256=YJ7cynQJI6kD7vjyv3rKc-CZqmoYSuGQtRZl_XdGEps,39
|
9
|
+
neuro_simulator/agent/memory/manager.py,sha256=tnwxxAlpS0wEd0jfX2MG9hnyAcuhUiZOIP7mz9LCPh8,5388
|
10
|
+
neuro_simulator/agent/tools/__init__.py,sha256=1WZy6PADfi6o1avyy1y-ThWBFAPJ_bBqtkobyYpf5ao,38
|
11
|
+
neuro_simulator/agent/tools/add_temp_memory.py,sha256=R6K-iVMD6ykd7pS3uI811dZIkuuAf7nPn9c6xCzqiHc,2127
|
12
|
+
neuro_simulator/agent/tools/add_to_core_memory_block.py,sha256=dt7y3w-qr379gS79mdata9w3hHG45wxdVIDXPnROO1Y,2306
|
13
|
+
neuro_simulator/agent/tools/base.py,sha256=Xj9ABTAtSFazhQf-qCMMWTCQVLsq48MZtKFVuBsYLQg,1799
|
14
|
+
neuro_simulator/agent/tools/create_core_memory_block.py,sha256=uM2vF71Ai3NH2-Qbr7u8nRmo4EoRzWYsTurfUH2X6s8,2766
|
15
|
+
neuro_simulator/agent/tools/delete_core_memory_block.py,sha256=_t2NZWZaxuWJsm9Uof3Zscvucyz-xJfyC0KrssSXPGI,1379
|
16
|
+
neuro_simulator/agent/tools/get_core_memory_block.py,sha256=vFK6lrbOqdxWVpYa7yF96wkXKMcpQAn4E68GCZlu0Ow,1432
|
17
|
+
neuro_simulator/agent/tools/get_core_memory_blocks.py,sha256=UbK-GkPgha35n703bCKzdnoIKgXR4U4YxqsSYWimW6c,1059
|
18
|
+
neuro_simulator/agent/tools/manager.py,sha256=-GFeGR1wO2GteIXOwWcFgZ-iPTj1syzGfL7coGSWf4I,7124
|
19
|
+
neuro_simulator/agent/tools/remove_from_core_memory_block.py,sha256=uiY69y2iIJYp3Zh8EdtP2_G5Zqfs4YKSmTBd9zcIp_c,2372
|
20
|
+
neuro_simulator/agent/tools/speak.py,sha256=7vPbfWjllxSz39HmOUbKRPwPx64q2vsYrqDu-JsfZJk,1870
|
21
|
+
neuro_simulator/agent/tools/update_core_memory_block.py,sha256=CQgimyPNLmOuK_pDAiAxV9qEf3Tx6u7OKludlIR08BA,2272
|
22
|
+
neuro_simulator/api/__init__.py,sha256=5LWyDSayPGdQS8Rv13nmAKLyhPnMVPyTYDdvoMPB4xw,56
|
23
|
+
neuro_simulator/api/system.py,sha256=OJT6m6HIYATMCAKrgeBRhficaiUjIDl9f-WyUT-RoBw,1874
|
24
|
+
neuro_simulator/core/__init__.py,sha256=-ojq25c8XA0CU25b0OxcGjH4IWFEDHR-HXSRSZIuKe8,57
|
25
|
+
neuro_simulator/core/agent_factory.py,sha256=p3IKT6sNzSpUojQjvbZJu0pbmyyb258sGn_YNSJlqwI,1717
|
26
|
+
neuro_simulator/core/agent_interface.py,sha256=ZXUCtkQUvsBQ5iCb0gTILJaShn5KmSrEgKhd7PK18HE,2794
|
27
|
+
neuro_simulator/core/application.py,sha256=oJUuKW3U8Uadr1IlzZThef8CgNoW1zncW_U94OFOUPQ,25518
|
28
|
+
neuro_simulator/core/config.py,sha256=QRbkm0h_FBTtYd-z5RRauSa1B0cp0LkNHVLqGBOBTYY,15324
|
29
|
+
neuro_simulator/core/path_manager.py,sha256=hfjI4s8-WXlgwvPWDSLYMQ2d0OaXPOMYWWlP5xe5NLg,2954
|
30
|
+
neuro_simulator/services/__init__.py,sha256=s3ZrAHg5TpJakadAAGY1h0wDw_xqN4Je4aJwJyRBmk4,61
|
31
|
+
neuro_simulator/services/audience.py,sha256=sAmvkz1ip1MNqaw7t-9abqfnmp0yh8EdG5bS5KYR-Us,4999
|
32
|
+
neuro_simulator/services/audio.py,sha256=EElBue80njZY8_CKQhVtZEcGchJUdmS4EDOvJEEu_LY,2909
|
33
|
+
neuro_simulator/services/builtin.py,sha256=NrdQhf4BuB8_evxnz8m6wiY9DgLwhY3l7Yp5TnmHGYo,5156
|
34
|
+
neuro_simulator/services/letta.py,sha256=OEM4qYeRRPoj8qY645YZwCHmMfg1McLp0eTbSAu3KL0,11976
|
35
|
+
neuro_simulator/services/stream.py,sha256=eueAaxhAwX_n0mT1W0W4YOVCe7fH5uPoMdRf2o0wQMI,5937
|
36
|
+
neuro_simulator/utils/__init__.py,sha256=xSEFzjT827W81mNyQ_DLtr00TgFlttqfFgpz9pSxFXQ,58
|
37
|
+
neuro_simulator/utils/logging.py,sha256=fAM8mW4czr0RLgAUS6oN-YTWKj5daqwc5g7yjyAgaPw,3892
|
38
|
+
neuro_simulator/utils/process.py,sha256=9OYWx8fzaJZqmFUcjQX37AnBhl7YWvrLxDWBa30vqwU,3192
|
39
|
+
neuro_simulator/utils/queue.py,sha256=bg-mIFF8ycClwmmfcKSPjAXtvjImzTKf6z7xZc2VX20,2196
|
40
|
+
neuro_simulator/utils/state.py,sha256=DdBqSAYfjOFtJfB1hEGhYPh32r1ZvFuVlN_-29_-luA,664
|
41
|
+
neuro_simulator/utils/websocket.py,sha256=fjC-qipzjgM_e7XImP12DFmLhvxkMOSr2GnUWws7esE,2058
|
42
|
+
neuro_simulator-0.4.0.dist-info/METADATA,sha256=n3zf_b6P5veJQ-VPxlqlCsxCfZXTd2-loTHn6XB4N3w,8643
|
43
|
+
neuro_simulator-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
44
|
+
neuro_simulator-0.4.0.dist-info/entry_points.txt,sha256=qVd5ypnRRgU8Cw7rWfZ7o0OXyS9P9hgY-cRoN_mgz9g,51
|
45
|
+
neuro_simulator-0.4.0.dist-info/top_level.txt,sha256=V8awSKpcrFnjJDiJxSfy7jtOrnuE2BgAR9hLmfMDWK8,16
|
46
|
+
neuro_simulator-0.4.0.dist-info/RECORD,,
|
neuro_simulator/agent/base.py
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# agent/base.py
|
2
|
-
"""Base classes for Neuro Simulator Agent"""
|
3
|
-
|
4
|
-
from abc import ABC, abstractmethod
|
5
|
-
from typing import List, Dict, Any, Optional
|
6
|
-
|
7
|
-
|
8
|
-
class BaseAgent(ABC):
|
9
|
-
"""Abstract base class for all agents"""
|
10
|
-
|
11
|
-
@abstractmethod
|
12
|
-
async def initialize(self):
|
13
|
-
"""Initialize the agent"""
|
14
|
-
pass
|
15
|
-
|
16
|
-
@abstractmethod
|
17
|
-
async def reset_memory(self):
|
18
|
-
"""Reset agent memory"""
|
19
|
-
pass
|
20
|
-
|
21
|
-
@abstractmethod
|
22
|
-
async def get_response(self, chat_messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
23
|
-
"""Get response from the agent
|
24
|
-
|
25
|
-
Args:
|
26
|
-
chat_messages: List of message dictionaries with 'username' and 'text' keys
|
27
|
-
|
28
|
-
Returns:
|
29
|
-
Dictionary containing processing details including tool executions and final response
|
30
|
-
"""
|
31
|
-
pass
|
32
|
-
|
33
|
-
@abstractmethod
|
34
|
-
async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
35
|
-
"""Process messages and generate a response
|
36
|
-
|
37
|
-
Args:
|
38
|
-
messages: List of message dictionaries with 'username' and 'text' keys
|
39
|
-
|
40
|
-
Returns:
|
41
|
-
Dictionary containing processing details including tool executions and final response
|
42
|
-
"""
|
43
|
-
pass
|
neuro_simulator/agent/factory.py
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# agent/factory.py
|
2
|
-
"""Factory for creating agent instances"""
|
3
|
-
|
4
|
-
from .base import BaseAgent
|
5
|
-
from ..config import config_manager
|
6
|
-
|
7
|
-
|
8
|
-
async def create_agent() -> BaseAgent:
|
9
|
-
"""Create an agent instance based on the configuration"""
|
10
|
-
agent_type = config_manager.settings.agent_type
|
11
|
-
|
12
|
-
if agent_type == "builtin":
|
13
|
-
from ..builtin_agent import local_agent, BuiltinAgentWrapper, initialize_builtin_agent
|
14
|
-
if local_agent is None:
|
15
|
-
# Try to initialize the builtin agent
|
16
|
-
await initialize_builtin_agent()
|
17
|
-
# Re-import local_agent after initialization
|
18
|
-
from ..builtin_agent import local_agent
|
19
|
-
if local_agent is None:
|
20
|
-
raise RuntimeError("Failed to initialize Builtin agent")
|
21
|
-
return BuiltinAgentWrapper(local_agent)
|
22
|
-
elif agent_type == "letta":
|
23
|
-
from ..letta import get_letta_agent, initialize_letta_client
|
24
|
-
# Try to initialize the letta client
|
25
|
-
initialize_letta_client()
|
26
|
-
agent = get_letta_agent()
|
27
|
-
await agent.initialize()
|
28
|
-
return agent
|
29
|
-
else:
|
30
|
-
raise ValueError(f"Unknown agent type: {agent_type}")
|
neuro_simulator/api/stream.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
# This file is now empty as all stream control API endpoints have been migrated to WebSockets.
|