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.
- neuro_simulator/agent/core.py +103 -136
- neuro_simulator/agent/llm.py +1 -1
- neuro_simulator/agent/memory/manager.py +24 -103
- neuro_simulator/agent/memory_prompt.txt +14 -0
- neuro_simulator/agent/neuro_prompt.txt +32 -0
- neuro_simulator/agent/tools/add_temp_memory.py +61 -0
- neuro_simulator/agent/tools/add_to_core_memory_block.py +64 -0
- neuro_simulator/agent/tools/base.py +56 -0
- neuro_simulator/agent/tools/create_core_memory_block.py +78 -0
- neuro_simulator/agent/tools/delete_core_memory_block.py +44 -0
- neuro_simulator/agent/tools/get_core_memory_block.py +44 -0
- neuro_simulator/agent/tools/get_core_memory_blocks.py +30 -0
- neuro_simulator/agent/tools/manager.py +143 -0
- neuro_simulator/agent/tools/remove_from_core_memory_block.py +65 -0
- neuro_simulator/agent/tools/speak.py +56 -0
- neuro_simulator/agent/tools/update_core_memory_block.py +65 -0
- 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 +72 -43
- 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.2.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/agent/tools/core.py +0 -102
- neuro_simulator/api/stream.py +0 -1
- neuro_simulator-0.3.2.dist-info/RECORD +0 -36
- {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.3.2.dist-info → neuro_simulator-0.4.0.dist-info}/entry_points.txt +0 -0
- {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.
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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.
|
111
|
+
# 4. Run the server
|
112
|
+
logging.info(f"Starting Neuro-Simulator server on {server_host}:{server_port}...")
|
85
113
|
try:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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)
|
@@ -7,7 +7,7 @@ import logging
|
|
7
7
|
import random
|
8
8
|
import re
|
9
9
|
import time
|
10
|
-
|
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
|
-
|
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 == "
|
386
|
-
|
387
|
-
|
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
|
-
#
|
453
|
-
agent_instance = getattr(agent, 'agent_instance',
|
454
|
-
if not
|
455
|
-
|
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
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
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
|
-
)
|
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()
|