neuro-simulator 0.3.2__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.
Files changed (40) hide show
  1. neuro_simulator/agent/core.py +103 -136
  2. neuro_simulator/agent/llm.py +1 -1
  3. neuro_simulator/agent/memory/manager.py +24 -103
  4. neuro_simulator/agent/memory_prompt.txt +14 -0
  5. neuro_simulator/agent/neuro_prompt.txt +32 -0
  6. neuro_simulator/agent/tools/add_temp_memory.py +61 -0
  7. neuro_simulator/agent/tools/add_to_core_memory_block.py +64 -0
  8. neuro_simulator/agent/tools/base.py +56 -0
  9. neuro_simulator/agent/tools/create_core_memory_block.py +78 -0
  10. neuro_simulator/agent/tools/delete_core_memory_block.py +44 -0
  11. neuro_simulator/agent/tools/get_core_memory_block.py +44 -0
  12. neuro_simulator/agent/tools/get_core_memory_blocks.py +30 -0
  13. neuro_simulator/agent/tools/manager.py +143 -0
  14. neuro_simulator/agent/tools/remove_from_core_memory_block.py +65 -0
  15. neuro_simulator/agent/tools/speak.py +56 -0
  16. neuro_simulator/agent/tools/update_core_memory_block.py +65 -0
  17. neuro_simulator/api/system.py +5 -2
  18. neuro_simulator/cli.py +83 -53
  19. neuro_simulator/core/agent_factory.py +0 -1
  20. neuro_simulator/core/application.py +72 -43
  21. neuro_simulator/core/config.py +66 -63
  22. neuro_simulator/core/path_manager.py +69 -0
  23. neuro_simulator/services/audience.py +0 -2
  24. neuro_simulator/services/audio.py +0 -1
  25. neuro_simulator/services/builtin.py +10 -25
  26. neuro_simulator/services/letta.py +19 -1
  27. neuro_simulator/services/stream.py +24 -21
  28. neuro_simulator/utils/logging.py +9 -0
  29. neuro_simulator/utils/queue.py +27 -4
  30. neuro_simulator/utils/websocket.py +1 -3
  31. {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/METADATA +1 -1
  32. neuro_simulator-0.4.0.dist-info/RECORD +46 -0
  33. neuro_simulator/agent/base.py +0 -43
  34. neuro_simulator/agent/factory.py +0 -30
  35. neuro_simulator/agent/tools/core.py +0 -102
  36. neuro_simulator/api/stream.py +0 -1
  37. neuro_simulator-0.3.2.dist-info/RECORD +0 -36
  38. {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/WHEEL +0 -0
  39. {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/entry_points.txt +0 -0
  40. {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/top_level.txt +0 -0
neuro_simulator/cli.py CHANGED
@@ -8,29 +8,7 @@ import shutil
8
8
  import sys
9
9
  from pathlib import Path
10
10
 
11
- def copy_resource(package_name: str, resource_path: str, destination_path: Path, is_dir: bool = False):
12
- """A helper function to copy a resource from the package to the working directory."""
13
- if destination_path.exists():
14
- return
15
11
 
16
- try:
17
- import pkg_resources
18
- source_path_str = pkg_resources.resource_filename(package_name, resource_path)
19
- source_path = Path(source_path_str)
20
- except (ModuleNotFoundError, KeyError):
21
- source_path = Path(__file__).parent / resource_path
22
-
23
- if source_path.exists():
24
- try:
25
- if is_dir:
26
- shutil.copytree(source_path, destination_path)
27
- else:
28
- shutil.copy(source_path, destination_path)
29
- logging.info(f"Created '{destination_path}' from package resource.")
30
- except Exception as e:
31
- logging.warning(f"Could not copy resource '{resource_path}'. Error: {e}")
32
- else:
33
- logging.warning(f"Resource '{resource_path}' not found in package or development folder.")
34
12
 
35
13
  def main():
36
14
  """Main entry point for the CLI."""
@@ -54,39 +32,91 @@ def main():
54
32
  os.chdir(work_dir)
55
33
  logging.info(f"Using working directory: {work_dir}")
56
34
 
57
- # 2. Ensure required assets and configs exist
58
- copy_resource('neuro_simulator', 'core/config.yaml.example', work_dir / 'config.yaml.example')
59
- copy_resource('neuro_simulator', 'assets', work_dir / 'assets', is_dir=True)
60
- # Ensure agent directory and its contents exist
61
- agent_dir = work_dir / "agent"
62
- agent_dir.mkdir(parents=True, exist_ok=True)
63
- copy_resource('neuro_simulator', 'agent/neuro_prompt.txt', agent_dir / 'neuro_prompt.txt')
64
- copy_resource('neuro_simulator', 'agent/memory_prompt.txt', agent_dir / 'memory_prompt.txt')
65
-
66
- # Ensure agent memory directory and its contents exist
67
- agent_memory_dir = agent_dir / "memory"
68
- agent_memory_dir.mkdir(parents=True, exist_ok=True)
69
- for filename in ["chat_history.json", "core_memory.json", "init_memory.json"]:
70
- copy_resource('neuro_simulator', f'agent/memory/{filename}', agent_memory_dir / filename)
71
-
72
- # 3. Validate essential files
73
- errors = []
74
- if not (work_dir / "config.yaml").exists():
75
- errors.append(f"'config.yaml' not found in '{work_dir}'. Please copy 'config.yaml.example' to 'config.yaml' and configure it.")
76
- if not (work_dir / "assets" / "neuro_start.mp4").exists():
77
- errors.append(f"Required file 'neuro_start.mp4' not found in '{work_dir / 'assets'}'.")
78
-
79
- if errors:
80
- for error in errors:
81
- logging.error(error)
82
- sys.exit(1)
35
+ # 2. Initialize paths and load configuration
36
+ from neuro_simulator.core import path_manager
37
+ from neuro_simulator.core.config import config_manager
38
+ import uvicorn
39
+
40
+ path_manager.initialize_path_manager(os.getcwd())
41
+
42
+ # Define example_path early for config loading
43
+ example_path = Path(__file__).parent / "core" / "config.yaml.example"
44
+
45
+ # 2.2. Copy default config.yaml.example if it doesn't exist
46
+ try:
47
+ source_config_example = example_path
48
+ destination_config_example = path_manager.path_manager.working_dir / "config.yaml.example"
49
+ if not destination_config_example.exists():
50
+ shutil.copy(source_config_example, destination_config_example)
51
+ logging.info(f"Copied default config.yaml.example to {destination_config_example}")
52
+ except Exception as e:
53
+ logging.warning(f"Could not copy default config.yaml.example: {e}")
54
+
55
+ main_config_path = path_manager.path_manager.working_dir / "config.yaml"
56
+ config_manager.load_and_validate(str(main_config_path), str(example_path))
57
+
58
+ # 2.5. Copy default prompt templates if they don't exist
59
+ try:
60
+ # Use Path(__file__).parent for robust path resolution
61
+ base_path = Path(__file__).parent
62
+ neuro_prompt_example = base_path / "agent" / "neuro_prompt.txt"
63
+ memory_prompt_example = base_path / "agent" / "memory_prompt.txt"
64
+
65
+ if not path_manager.path_manager.neuro_prompt_path.exists():
66
+ shutil.copy(neuro_prompt_example, path_manager.path_manager.neuro_prompt_path)
67
+ logging.info(f"Copied default neuro prompt to {path_manager.path_manager.neuro_prompt_path}")
68
+ if not path_manager.path_manager.memory_agent_prompt_path.exists():
69
+ shutil.copy(memory_prompt_example, path_manager.path_manager.memory_agent_prompt_path)
70
+ logging.info(f"Copied default memory prompt to {path_manager.path_manager.memory_agent_prompt_path}")
71
+
72
+ # Copy default memory JSON files if they don't exist
73
+ memory_files = {
74
+ "core_memory.json": path_manager.path_manager.core_memory_path,
75
+ "init_memory.json": path_manager.path_manager.init_memory_path,
76
+ "temp_memory.json": path_manager.path_manager.temp_memory_path,
77
+ }
78
+ for filename, dest_path in memory_files.items():
79
+ src_path = base_path / "agent" / "memory" / filename
80
+ if not dest_path.exists():
81
+ shutil.copy(src_path, dest_path)
82
+ logging.info(f"Copied default {filename} to {dest_path}")
83
+
84
+ # Copy default assets directory if it doesn't exist
85
+ source_assets_dir = base_path / "assets"
86
+ destination_assets_dir = path_manager.path_manager.assets_dir
87
+
88
+ # Ensure the destination assets directory exists
89
+ destination_assets_dir.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Copy individual files from source assets to destination assets
92
+ for item in source_assets_dir.iterdir():
93
+ if item.is_file():
94
+ dest_file = destination_assets_dir / item.name
95
+ if not dest_file.exists():
96
+ shutil.copy(item, dest_file)
97
+ logging.info(f"Copied asset {item.name} to {dest_file}")
98
+ elif item.is_dir():
99
+ # Recursively copy subdirectories if they don't exist
100
+ dest_subdir = destination_assets_dir / item.name
101
+ if not dest_subdir.exists():
102
+ shutil.copytree(item, dest_subdir)
103
+ logging.info(f"Copied asset directory {item.name} to {dest_subdir}")
104
+ except Exception as e:
105
+ logging.warning(f"Could not copy default prompt templates, memory files, or assets: {e}")
106
+
107
+ # 3. Determine server host and port
108
+ server_host = args.host or config_manager.settings.server.host
109
+ server_port = args.port or config_manager.settings.server.port
83
110
 
84
- # 4. Import and run the server
111
+ # 4. Run the server
112
+ logging.info(f"Starting Neuro-Simulator server on {server_host}:{server_port}...")
85
113
  try:
86
- from neuro_simulator.core.application import run_server
87
- logging.info("Starting Neuro-Simulator server...")
88
- # The full application logger will take over from here
89
- run_server(args.host, args.port)
114
+ uvicorn.run(
115
+ "neuro_simulator.core.application:app",
116
+ host=server_host,
117
+ port=server_port,
118
+ reload=False
119
+ )
90
120
  except ImportError as e:
91
121
  logging.error(f"Could not import the application. Make sure the package is installed correctly. Details: {e}", exc_info=True)
92
122
  sys.exit(1)
@@ -1,6 +1,5 @@
1
1
  # neuro_simulator/core/agent_factory.py
2
2
  import logging
3
- from pathlib import Path
4
3
 
5
4
  from .agent_interface import BaseAgent
6
5
  from .config import config_manager, AppSettings
@@ -7,7 +7,7 @@ import logging
7
7
  import random
8
8
  import re
9
9
  import time
10
- from pathlib import Path
10
+ import os
11
11
  from typing import List
12
12
 
13
13
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect
@@ -17,6 +17,7 @@ from starlette.websockets import WebSocketState
17
17
  # --- Core Imports ---
18
18
  from .config import config_manager, AppSettings
19
19
  from ..core.agent_factory import create_agent
20
+ from ..agent.core import Agent as LocalAgent
20
21
  from ..services.letta import LettaAgent
21
22
  from ..services.builtin import BuiltinAgentWrapper
22
23
 
@@ -34,7 +35,8 @@ from ..utils.queue import (
34
35
  add_to_neuro_input_queue,
35
36
  get_recent_audience_chats,
36
37
  is_neuro_input_queue_empty,
37
- get_all_neuro_input_chats
38
+ get_all_neuro_input_chats,
39
+ initialize_queues,
38
40
  )
39
41
  from ..utils.state import app_state
40
42
  from ..utils.websocket import connection_manager
@@ -169,6 +171,10 @@ async def neuro_response_cycle():
169
171
  if not response_text:
170
172
  continue
171
173
 
174
+ # Push updated agent context to admin clients immediately after processing
175
+ updated_context = await agent.get_message_history()
176
+ await connection_manager.broadcast_to_admins({"type": "agent_context", "action": "update", "messages": updated_context})
177
+
172
178
  async with app_state.neuro_last_speech_lock:
173
179
  app_state.neuro_last_speech = response_text
174
180
 
@@ -192,6 +198,7 @@ async def neuro_response_cycle():
192
198
 
193
199
  await connection_manager.broadcast({"type": "neuro_speech_segment", "is_end": True})
194
200
  live_stream_manager.set_neuro_speaking_status(False)
201
+
195
202
  await asyncio.sleep(config_manager.settings.neuro_behavior.post_speech_cooldown_sec)
196
203
 
197
204
  except asyncio.TimeoutError:
@@ -210,17 +217,24 @@ async def neuro_response_cycle():
210
217
  @app.on_event("startup")
211
218
  async def startup_event():
212
219
  """Actions to perform on application startup."""
213
- global chatbot_manager
220
+ # 1. Configure logging first
214
221
  configure_server_logging()
215
-
222
+
223
+ # 2. Initialize queues now that config is loaded
224
+ initialize_queues()
225
+
226
+ # 4. Initialize other managers/services that depend on config
227
+ global chatbot_manager
216
228
  chatbot_manager = AudienceChatbotManager()
217
229
 
230
+ # 5. Register callbacks
218
231
  async def metadata_callback(settings: AppSettings):
219
232
  await live_stream_manager.broadcast_stream_metadata()
220
233
 
221
234
  config_manager.register_update_callback(metadata_callback)
222
235
  config_manager.register_update_callback(chatbot_manager.handle_config_update)
223
236
 
237
+ # 6. Initialize agent (which will load its own configs)
224
238
  try:
225
239
  await create_agent()
226
240
  logger.info(f"Successfully initialized agent type: {config_manager.settings.agent_type}")
@@ -332,7 +346,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
332
346
  response["payload"] = {"status": "success", "block_id": block_id}
333
347
  # Broadcast the update to all admins
334
348
  updated_blocks = await agent.get_memory_blocks()
335
- from ..utils.websocket import connection_manager
336
349
  await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
337
350
 
338
351
  elif action == "update_core_memory_block":
@@ -340,7 +353,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
340
353
  response["payload"] = {"status": "success"}
341
354
  # Broadcast the update to all admins
342
355
  updated_blocks = await agent.get_memory_blocks()
343
- from ..utils.websocket import connection_manager
344
356
  await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
345
357
 
346
358
  elif action == "delete_core_memory_block":
@@ -348,7 +360,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
348
360
  response["payload"] = {"status": "success"}
349
361
  # Broadcast the update to all admins
350
362
  updated_blocks = await agent.get_memory_blocks()
351
- from ..utils.websocket import connection_manager
352
363
  await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
353
364
 
354
365
  # Temp Memory Actions
@@ -360,7 +371,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
360
371
  await agent.add_temp_memory(**payload)
361
372
  response["payload"] = {"status": "success"}
362
373
  updated_temp_mem = await agent.get_temp_memory()
363
- from ..utils.websocket import connection_manager
364
374
  await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
365
375
 
366
376
  elif action == "clear_temp_memory":
@@ -378,13 +388,35 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
378
388
  await agent.update_init_memory(**payload)
379
389
  response["payload"] = {"status": "success"}
380
390
  updated_init_mem = await agent.get_init_memory()
381
- from ..utils.websocket import connection_manager
382
391
  await connection_manager.broadcast_to_admins({"type": "init_memory_updated", "payload": updated_init_mem})
383
392
 
384
393
  # Tool Actions
385
- elif action == "get_tools":
386
- tools = await agent.get_available_tools()
387
- response["payload"] = {"tools": tools}
394
+ elif action == "get_all_tools":
395
+ agent_instance = getattr(agent, 'agent_instance', agent)
396
+ all_tools = agent_instance.tool_manager.get_all_tool_schemas()
397
+ response["payload"] = {"tools": all_tools}
398
+
399
+ elif action == "get_agent_tool_allocations":
400
+ agent_instance = getattr(agent, 'agent_instance', agent)
401
+ allocations = agent_instance.tool_manager.get_allocations()
402
+ response["payload"] = {"allocations": allocations}
403
+
404
+ elif action == "set_agent_tool_allocations":
405
+ agent_instance = getattr(agent, 'agent_instance', agent)
406
+ allocations_payload = payload.get("allocations", {})
407
+ agent_instance.tool_manager.set_allocations(allocations_payload)
408
+ response["payload"] = {"status": "success"}
409
+ # Broadcast the update to all admins
410
+ updated_allocations = agent_instance.tool_manager.get_allocations()
411
+ await connection_manager.broadcast_to_admins({"type": "agent_tool_allocations_updated", "payload": {"allocations": updated_allocations}})
412
+
413
+ elif action == "reload_tools":
414
+ agent_instance = getattr(agent, 'agent_instance', agent)
415
+ agent_instance.tool_manager.reload_tools()
416
+ response["payload"] = {"status": "success"}
417
+ # Broadcast an event to notify UI to refresh tool lists
418
+ all_tools = agent_instance.tool_manager.get_all_tool_schemas()
419
+ await connection_manager.broadcast_to_admins({"type": "available_tools_updated", "payload": {"tools": all_tools}})
388
420
 
389
421
  elif action == "execute_tool":
390
422
  result = await agent.execute_tool(**payload)
@@ -392,11 +424,12 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
392
424
 
393
425
  # Stream Control Actions
394
426
  elif action == "start_stream":
427
+ logger.info("Start stream action received. Resetting agent memory before starting processes...")
428
+ await agent.reset_memory()
395
429
  if not process_manager.is_running:
396
430
  process_manager.start_live_processes()
397
431
  response["payload"] = {"status": "success", "message": "Stream started"}
398
432
  # Broadcast stream status update
399
- from ..utils.websocket import connection_manager
400
433
  status = {"is_running": process_manager.is_running, "backend_status": "running" if process_manager.is_running else "stopped"}
401
434
  await connection_manager.broadcast_to_admins({"type": "stream_status", "payload": status})
402
435
 
@@ -405,7 +438,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
405
438
  await process_manager.stop_live_processes()
406
439
  response["payload"] = {"status": "success", "message": "Stream stopped"}
407
440
  # Broadcast stream status update
408
- from ..utils.websocket import connection_manager
409
441
  status = {"is_running": process_manager.is_running, "backend_status": "running" if process_manager.is_running else "stopped"}
410
442
  await connection_manager.broadcast_to_admins({"type": "stream_status", "payload": status})
411
443
 
@@ -415,7 +447,6 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
415
447
  process_manager.start_live_processes()
416
448
  response["payload"] = {"status": "success", "message": "Stream restarted"}
417
449
  # Broadcast stream status update
418
- from ..utils.websocket import connection_manager
419
450
  status = {"is_running": process_manager.is_running, "backend_status": "running" if process_manager.is_running else "stopped"}
420
451
  await connection_manager.broadcast_to_admins({"type": "stream_status", "payload": status})
421
452
 
@@ -449,22 +480,33 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
449
480
  response["payload"] = context
450
481
 
451
482
  elif action == "get_last_prompt":
452
- # Check if the agent supports prompt generation introspection
453
- agent_instance = getattr(agent, 'agent_instance', None) if hasattr(agent, 'agent_instance') else agent
454
- if not hasattr(agent_instance, 'memory_manager') or not hasattr(agent_instance.memory_manager, 'get_recent_chat') or not hasattr(agent_instance, '_build_neuro_prompt'):
455
- response["payload"] = {"status": "error", "message": "The active agent does not support prompt generation introspection."}
483
+ # This is specific to the builtin agent, as Letta doesn't expose its prompt.
484
+ agent_instance = getattr(agent, 'agent_instance', agent)
485
+ if not isinstance(agent_instance, LocalAgent):
486
+ response["payload"] = {"prompt": "The active agent does not support prompt generation introspection."}
456
487
  else:
457
- recent_history = await agent_instance.memory_manager.get_recent_chat(entries=10)
458
- messages_for_prompt = []
459
- for entry in recent_history:
460
- if entry.get('role') == 'user':
461
- parts = entry.get('content', '').split(':', 1)
462
- if len(parts) == 2:
463
- messages_for_prompt.append({'username': parts[0].strip(), 'text': parts[1].strip()})
464
- else:
465
- messages_for_prompt.append({'username': 'user', 'text': entry.get('content', '')})
466
- prompt = await agent_instance._build_neuro_prompt(messages_for_prompt)
467
- response["payload"] = {"prompt": prompt}
488
+ try:
489
+ # 1. Get the recent history from the agent itself
490
+ history = await agent_instance.get_neuro_history(limit=10)
491
+
492
+ # 2. Reconstruct the 'messages' list that _build_neuro_prompt expects
493
+ messages_for_prompt = []
494
+ for entry in history:
495
+ if entry.get('role') == 'user':
496
+ # Content is in the format "username: text"
497
+ content = entry.get('content', '')
498
+ parts = content.split(':', 1)
499
+ if len(parts) == 2:
500
+ messages_for_prompt.append({'username': parts[0].strip(), 'text': parts[1].strip()})
501
+ elif content: # Handle cases where there's no colon
502
+ messages_for_prompt.append({'username': 'user', 'text': content})
503
+
504
+ # 3. Build the prompt using the agent's own internal logic
505
+ prompt = await agent_instance._build_neuro_prompt(messages_for_prompt)
506
+ response["payload"] = {"prompt": prompt}
507
+ except Exception as e:
508
+ logger.error(f"Error generating last prompt: {e}", exc_info=True)
509
+ response["payload"] = {"prompt": f"Failed to generate prompt: {e}"}
468
510
 
469
511
  elif action == "reset_agent_memory":
470
512
  await agent.reset_memory()
@@ -489,17 +531,4 @@ async def handle_admin_ws_message(websocket: WebSocket, data: dict):
489
531
  await websocket.send_json(response)
490
532
 
491
533
 
492
- # --- Server Entrypoint ---
493
534
 
494
- def run_server(host: str = None, port: int = None):
495
- """Runs the FastAPI server with Uvicorn."""
496
- import uvicorn
497
- server_host = host or config_manager.settings.server.host
498
- server_port = port or config_manager.settings.server.port
499
-
500
- uvicorn.run(
501
- "neuro_simulator.core.application:app",
502
- host=server_host,
503
- port=server_port,
504
- reload=False
505
- )
@@ -1,8 +1,9 @@
1
1
  # backend/config.py
2
- import os
2
+ import shutil
3
+ from pathlib import Path
3
4
  import yaml
4
5
  from pydantic import BaseModel, Field
5
- from typing import List, Optional, Dict, Any
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
- 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
110
+ import sys
128
111
 
129
- def _load_config_from_yaml(self) -> dict:
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
-
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
- content = yaml.safe_load(f)
141
- if content is None:
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
- 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.")
168
+ self.settings = AppSettings.model_validate(yaml_config)
169
+ logging.info("Main configuration loaded successfully.")
164
170
 
165
- logging.info("Configuration loaded successfully.")
166
- return base_settings
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
- existing_config = self._load_config_from_yaml()
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,7 +1,5 @@
1
1
  # neuro_simulator/services/audience.py
2
- import asyncio
3
2
  import logging
4
- import random
5
3
 
6
4
  from google import genai
7
5
  from google.genai import types
@@ -3,7 +3,6 @@ import asyncio
3
3
  import base64
4
4
  import html
5
5
  import logging
6
- from pathlib import Path
7
6
 
8
7
  import azure.cognitiveservices.speech as speechsdk
9
8