neuro-simulator 0.3.0__py3-none-any.whl → 0.3.1__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 +163 -130
- neuro_simulator/agent/memory/manager.py +26 -26
- neuro_simulator/api/stream.py +1 -55
- neuro_simulator/api/system.py +30 -60
- neuro_simulator/cli.py +3 -2
- neuro_simulator/core/agent_interface.py +1 -1
- neuro_simulator/core/application.py +163 -5
- neuro_simulator/services/builtin.py +34 -4
- neuro_simulator/utils/websocket.py +11 -14
- {neuro_simulator-0.3.0.dist-info → neuro_simulator-0.3.1.dist-info}/METADATA +3 -4
- {neuro_simulator-0.3.0.dist-info → neuro_simulator-0.3.1.dist-info}/RECORD +14 -15
- neuro_simulator/api/agent.py +0 -163
- {neuro_simulator-0.3.0.dist-info → neuro_simulator-0.3.1.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.3.0.dist-info → neuro_simulator-0.3.1.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.3.0.dist-info → neuro_simulator-0.3.1.dist-info}/top_level.txt +0 -0
neuro_simulator/agent/core.py
CHANGED
@@ -1,22 +1,19 @@
|
|
1
1
|
# neuro_simulator/agent/core.py
|
2
2
|
"""
|
3
3
|
Core module for the Neuro Simulator's built-in agent.
|
4
|
+
Implements a dual-LLM "Actor/Thinker" architecture for responsive interaction
|
5
|
+
and asynchronous memory consolidation.
|
4
6
|
"""
|
5
7
|
|
6
8
|
import asyncio
|
7
9
|
import json
|
8
10
|
import logging
|
9
11
|
import re
|
10
|
-
import sys
|
11
12
|
from pathlib import Path
|
12
|
-
from
|
13
|
-
from typing import Any, Dict, List, Optional
|
13
|
+
from typing import Any, Dict, List
|
14
14
|
|
15
|
-
# Updated imports for the new structure
|
16
15
|
from ..utils.logging import QueueLogHandler, agent_log_queue
|
17
16
|
from ..utils.websocket import connection_manager
|
18
|
-
|
19
|
-
# --- Agent-specific imports ---
|
20
17
|
from .llm import LLMClient
|
21
18
|
from .memory.manager import MemoryManager
|
22
19
|
from .tools.core import ToolManager
|
@@ -32,8 +29,7 @@ def configure_agent_logging():
|
|
32
29
|
agent_logger.handlers.clear()
|
33
30
|
|
34
31
|
agent_queue_handler = QueueLogHandler(agent_log_queue)
|
35
|
-
|
36
|
-
formatter = logging.Formatter('%(asctime)s - [%(name)-24s] - %(levelname)-8s - %(message)s', datefmt='%H:%M:%S')
|
32
|
+
formatter = logging.Formatter('%(asctime)s - [%(name)-32s] - %(levelname)-8s - %(message)s', datefmt='%H:%M:%S')
|
37
33
|
agent_queue_handler.setFormatter(formatter)
|
38
34
|
agent_logger.addHandler(agent_queue_handler)
|
39
35
|
agent_logger.propagate = False
|
@@ -42,14 +38,25 @@ def configure_agent_logging():
|
|
42
38
|
configure_agent_logging()
|
43
39
|
|
44
40
|
class Agent:
|
45
|
-
"""
|
41
|
+
"""
|
42
|
+
Main Agent class implementing the Actor/Thinker model.
|
43
|
+
- The "Neuro" part (Actor) handles real-time interaction.
|
44
|
+
- The "Memory" part (Thinker) handles background memory consolidation.
|
45
|
+
"""
|
46
46
|
|
47
47
|
def __init__(self, working_dir: str = None):
|
48
48
|
self.memory_manager = MemoryManager(working_dir)
|
49
49
|
self.tool_manager = ToolManager(self.memory_manager)
|
50
|
-
|
50
|
+
|
51
|
+
# Dual LLM clients
|
52
|
+
self.neuro_llm = LLMClient()
|
53
|
+
self.memory_llm = LLMClient()
|
54
|
+
|
51
55
|
self._initialized = False
|
52
|
-
|
56
|
+
self.turn_counter = 0
|
57
|
+
self.reflection_threshold = 3 # Trigger reflection every 3 turns
|
58
|
+
|
59
|
+
agent_logger.info("Agent instance created with dual-LLM architecture.")
|
53
60
|
agent_logger.debug(f"Agent working directory: {working_dir}")
|
54
61
|
|
55
62
|
async def initialize(self):
|
@@ -63,139 +70,165 @@ class Agent:
|
|
63
70
|
async def reset_all_memory(self):
|
64
71
|
"""Reset all agent memory types."""
|
65
72
|
await self.memory_manager.reset_temp_memory()
|
66
|
-
await self.memory_manager.
|
73
|
+
await self.memory_manager.reset_chat_history()
|
67
74
|
agent_logger.info("All agent memory has been reset.")
|
68
|
-
|
69
|
-
async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
70
|
-
"""Process incoming messages and generate a response with tool usage."""
|
71
|
-
await self.initialize()
|
72
|
-
agent_logger.info(f"Processing {len(messages)} messages.")
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
context_messages = await self.memory_manager.get_recent_context()
|
79
|
-
await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": context_messages})
|
80
|
-
|
81
|
-
processing_entry_id = await self.memory_manager.add_detailed_context_entry(
|
82
|
-
input_messages=messages, prompt="Processing started", llm_response="",
|
83
|
-
tool_executions=[], final_response="Processing started"
|
84
|
-
)
|
85
|
-
|
86
|
-
context = await self.memory_manager.get_full_context()
|
87
|
-
tool_descriptions = self.tool_manager.get_tool_descriptions()
|
88
|
-
|
89
|
-
# --- CORRECTED HISTORY GATHERING ---
|
90
|
-
recent_history = await self.memory_manager.get_detailed_context_history()
|
91
|
-
assistant_responses = []
|
92
|
-
for entry in reversed(recent_history):
|
93
|
-
if entry.get("type") == "llm_interaction":
|
94
|
-
for tool in entry.get("tool_executions", []):
|
95
|
-
if tool.get("name") == "speak" and tool.get("result"):
|
96
|
-
assistant_responses.append(tool["result"])
|
97
|
-
|
98
|
-
# Create LLM prompt from template
|
99
|
-
template_path = Path(self.memory_manager.memory_dir).parent / "prompt_template.txt"
|
76
|
+
async def _build_neuro_prompt(self, messages: List[Dict[str, str]]) -> str:
|
77
|
+
"""Builds the prompt for the Neuro (Actor) LLM."""
|
78
|
+
template_path = Path(self.memory_manager.memory_dir).parent / "neuro_prompt.txt"
|
100
79
|
with open(template_path, 'r', encoding='utf-8') as f:
|
101
80
|
prompt_template = f.read()
|
102
81
|
|
103
|
-
|
82
|
+
# Gather context
|
83
|
+
tool_descriptions = self.tool_manager.get_tool_descriptions()
|
84
|
+
|
85
|
+
# Format Core Memory from blocks
|
86
|
+
core_memory_blocks = await self.memory_manager.get_core_memory_blocks()
|
87
|
+
core_memory_parts = []
|
88
|
+
if core_memory_blocks:
|
89
|
+
for block_id, block in core_memory_blocks.items():
|
90
|
+
core_memory_parts.append(f"\nBlock: {block.get('title', '')} ({block_id})")
|
91
|
+
core_memory_parts.append(f"Description: {block.get('description', '')}")
|
92
|
+
content_items = block.get("content", [])
|
93
|
+
if content_items:
|
94
|
+
core_memory_parts.append("Content:")
|
95
|
+
for item in content_items:
|
96
|
+
core_memory_parts.append(f" - {item}")
|
97
|
+
core_memory_text = "\n".join(core_memory_parts) if core_memory_parts else "Not set."
|
98
|
+
|
99
|
+
# Format Temp Memory
|
100
|
+
temp_memory_items = self.memory_manager.temp_memory
|
101
|
+
temp_memory_text = "\n".join(
|
102
|
+
[f"[{item.get('role', 'system')}] {item.get('content', '')}" for item in temp_memory_items]
|
103
|
+
) if temp_memory_items else "Empty."
|
104
|
+
|
105
|
+
recent_history = await self.memory_manager.get_recent_chat(entries=10)
|
106
|
+
|
104
107
|
user_messages_text = "\n".join([f"{msg['username']}: {msg['text']}" for msg in messages])
|
108
|
+
recent_history_text = "\n".join([f"{msg['role']}: {msg['content']}" for msg in recent_history])
|
105
109
|
|
106
|
-
|
107
|
-
full_context=context,
|
110
|
+
return prompt_template.format(
|
108
111
|
tool_descriptions=tool_descriptions,
|
109
|
-
|
112
|
+
core_memory=core_memory_text,
|
113
|
+
temp_memory=temp_memory_text,
|
114
|
+
recent_history=recent_history_text,
|
110
115
|
user_messages=user_messages_text
|
111
116
|
)
|
117
|
+
|
118
|
+
async def _build_memory_prompt(self, conversation_history: List[Dict[str, str]]) -> str:
|
119
|
+
"""Builds the prompt for the Memory (Thinker) LLM."""
|
120
|
+
template_path = Path(self.memory_manager.memory_dir).parent / "memory_prompt.txt"
|
121
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
122
|
+
prompt_template = f.read()
|
112
123
|
|
113
|
-
|
114
|
-
input_messages=messages, prompt=prompt, llm_response="", tool_executions=[],
|
115
|
-
final_response="Prompt sent to LLM", entry_id=processing_entry_id
|
116
|
-
)
|
117
|
-
|
118
|
-
response_text = await self.llm_client.generate(prompt)
|
119
|
-
agent_logger.debug(f"LLM raw response: {response_text[:100] if response_text else 'None'}...")
|
120
|
-
|
121
|
-
await self.memory_manager.add_detailed_context_entry(
|
122
|
-
input_messages=messages, prompt=prompt, llm_response=response_text, tool_executions=[],
|
123
|
-
final_response="LLM response received", entry_id=processing_entry_id
|
124
|
-
)
|
125
|
-
|
126
|
-
processing_result = {
|
127
|
-
"input_messages": messages, "llm_response": response_text,
|
128
|
-
"tool_executions": [], "final_response": ""
|
129
|
-
}
|
130
|
-
|
131
|
-
if response_text:
|
132
|
-
tool_calls = self._parse_tool_calls(response_text)
|
133
|
-
for tool_call in tool_calls:
|
134
|
-
agent_logger.info(f"Executing tool: {tool_call['name']}")
|
135
|
-
await self._execute_parsed_tool(tool_call, processing_result)
|
136
|
-
|
137
|
-
await self.memory_manager.add_detailed_context_entry(
|
138
|
-
input_messages=messages, prompt=prompt, llm_response=response_text,
|
139
|
-
tool_executions=processing_result["tool_executions"],
|
140
|
-
final_response=processing_result["final_response"], entry_id=processing_entry_id
|
141
|
-
)
|
142
|
-
|
143
|
-
final_context = await self.memory_manager.get_recent_context()
|
144
|
-
await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": final_context})
|
145
|
-
|
146
|
-
agent_logger.info("Message processing completed.")
|
147
|
-
return processing_result
|
124
|
+
history_text = "\n".join([f"{msg['role']}: {msg['content']}" for msg in conversation_history])
|
148
125
|
|
149
|
-
|
150
|
-
|
126
|
+
return prompt_template.format(conversation_history=history_text)
|
127
|
+
|
128
|
+
def _parse_tool_calls(self, response_text: str) -> List[Dict[str, Any]]:
|
129
|
+
"""Parses LLM response for JSON tool calls."""
|
151
130
|
try:
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
131
|
+
# The LLM is prompted to return a JSON array of tool calls.
|
132
|
+
# Find the JSON block, which might be wrapped in markdown.
|
133
|
+
match = re.search(r'''```json\s*([\s\S]*?)\s*```|(\[[\s\S]*\])''', response_text)
|
134
|
+
if not match:
|
135
|
+
agent_logger.warning(f"No valid JSON tool call block found in response: {response_text}")
|
136
|
+
return []
|
137
|
+
|
138
|
+
json_str = match.group(1) or match.group(2)
|
139
|
+
tool_calls = json.loads(json_str)
|
161
140
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
141
|
+
if isinstance(tool_calls, list):
|
142
|
+
return tool_calls
|
143
|
+
return []
|
144
|
+
except json.JSONDecodeError as e:
|
145
|
+
agent_logger.error(f"Failed to decode JSON from LLM response: {e}\nResponse text: {response_text}")
|
146
|
+
return []
|
147
|
+
except Exception as e:
|
148
|
+
agent_logger.error(f"An unexpected error occurred while parsing tool calls: {e}")
|
149
|
+
return []
|
150
|
+
|
151
|
+
async def _execute_tool_calls(self, tool_calls: List[Dict[str, Any]]) -> Dict[str, Any]:
|
152
|
+
"""Executes a list of parsed tool calls."""
|
153
|
+
execution_results = []
|
154
|
+
final_response = ""
|
155
|
+
for tool_call in tool_calls:
|
156
|
+
tool_name = tool_call.get("name")
|
157
|
+
params = tool_call.get("params", {})
|
158
|
+
if not tool_name:
|
159
|
+
continue
|
160
|
+
|
161
|
+
agent_logger.info(f"Executing tool: {tool_name} with params: {params}")
|
168
162
|
try:
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
# Get the quoted string part
|
178
|
-
quoted_string = inner_content[len("text="):
|
179
|
-
].strip()
|
180
|
-
|
181
|
-
# Use ast.literal_eval to safely parse the Python string literal
|
182
|
-
parsed_text = ast.literal_eval(quoted_string)
|
183
|
-
|
184
|
-
if isinstance(parsed_text, str):
|
185
|
-
calls.append({
|
186
|
-
"name": "speak",
|
187
|
-
"params": {"text": parsed_text}
|
188
|
-
})
|
189
|
-
|
190
|
-
except (ValueError, SyntaxError, TypeError) as e:
|
191
|
-
agent_logger.warning(f"Could not parse tool call using ast.literal_eval: {text}. Error: {e}")
|
192
|
-
|
193
|
-
return calls
|
163
|
+
result = await self.tool_manager.execute_tool(tool_name, params)
|
164
|
+
execution_results.append({"name": tool_name, "params": params, "result": result})
|
165
|
+
if tool_name == "speak":
|
166
|
+
final_response = params.get("text", "")
|
167
|
+
except Exception as e:
|
168
|
+
agent_logger.error(f"Error executing tool {tool_name}: {e}")
|
169
|
+
execution_results.append({"name": tool_name, "params": params, "error": str(e)})
|
194
170
|
|
195
|
-
|
196
|
-
|
171
|
+
return {"tool_executions": execution_results, "final_response": final_response}
|
172
|
+
|
173
|
+
async def process_and_respond(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
174
|
+
"""
|
175
|
+
The main entry point for the "Neuro" (Actor) flow.
|
176
|
+
Handles real-time interaction and triggers background reflection.
|
177
|
+
"""
|
197
178
|
await self.initialize()
|
198
|
-
agent_logger.
|
199
|
-
|
200
|
-
|
201
|
-
|
179
|
+
agent_logger.info(f"Processing {len(messages)} messages in Actor flow.")
|
180
|
+
|
181
|
+
# Add user messages to context
|
182
|
+
for msg in messages:
|
183
|
+
await self.memory_manager.add_chat_entry("user", f"{msg['username']}: {msg['text']}")
|
184
|
+
|
185
|
+
# Build prompt and get response from Neuro LLM
|
186
|
+
prompt = await self._build_neuro_prompt(messages)
|
187
|
+
response_text = await self.neuro_llm.generate(prompt)
|
188
|
+
agent_logger.debug(f"Neuro LLM raw response: {response_text[:150] if response_text else 'None'}...")
|
189
|
+
|
190
|
+
# Parse and execute tools
|
191
|
+
tool_calls = self._parse_tool_calls(response_text)
|
192
|
+
processing_result = await self._execute_tool_calls(tool_calls)
|
193
|
+
|
194
|
+
# Add agent's response to context
|
195
|
+
if processing_result["final_response"]:
|
196
|
+
await self.memory_manager.add_chat_entry("assistant", processing_result["final_response"])
|
197
|
+
|
198
|
+
# Update dashboard/UI
|
199
|
+
final_context = await self.memory_manager.get_recent_chat()
|
200
|
+
# Broadcast to stream clients
|
201
|
+
await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": final_context})
|
202
|
+
# Broadcast to admin clients (Dashboard)
|
203
|
+
await connection_manager.broadcast_to_admins({"type": "agent_context", "action": "update", "messages": final_context})
|
204
|
+
|
205
|
+
# Handle reflection trigger
|
206
|
+
self.turn_counter += 1
|
207
|
+
if self.turn_counter >= self.reflection_threshold:
|
208
|
+
agent_logger.info(f"Reflection threshold reached ({self.turn_counter}/{self.reflection_threshold}). Scheduling background reflection.")
|
209
|
+
history_for_reflection = await self.memory_manager.get_recent_chat(entries=self.reflection_threshold * 2) # Get a bit more context
|
210
|
+
asyncio.create_task(self.reflect_on_context(history_for_reflection))
|
211
|
+
self.turn_counter = 0
|
212
|
+
|
213
|
+
agent_logger.info("Actor flow completed.")
|
214
|
+
return processing_result
|
215
|
+
|
216
|
+
async def reflect_on_context(self, conversation_history: List[Dict[str, str]]):
|
217
|
+
"""
|
218
|
+
The main entry point for the "Memory" (Thinker) flow.
|
219
|
+
Runs in the background to consolidate memories.
|
220
|
+
"""
|
221
|
+
agent_logger.info("Thinker flow started: Reflecting on recent context.")
|
222
|
+
|
223
|
+
prompt = await self._build_memory_prompt(conversation_history)
|
224
|
+
response_text = await self.memory_llm.generate(prompt)
|
225
|
+
agent_logger.debug(f"Memory LLM raw response: {response_text[:150] if response_text else 'None'}...")
|
226
|
+
|
227
|
+
tool_calls = self._parse_tool_calls(response_text)
|
228
|
+
if not tool_calls:
|
229
|
+
agent_logger.info("Thinker flow: No memory operations were suggested by the LLM.")
|
230
|
+
return
|
231
|
+
|
232
|
+
agent_logger.info(f"Thinker flow: Executing {len(tool_calls)} memory operations.")
|
233
|
+
await self._execute_tool_calls(tool_calls)
|
234
|
+
agent_logger.info("Thinker flow completed, memory has been updated.")
|
@@ -31,12 +31,12 @@ class MemoryManager:
|
|
31
31
|
|
32
32
|
self.init_memory_file = os.path.join(self.memory_dir, "init_memory.json")
|
33
33
|
self.core_memory_file = os.path.join(self.memory_dir, "core_memory.json")
|
34
|
-
self.
|
34
|
+
self.chat_history_file = os.path.join(self.memory_dir, "chat_history.json")
|
35
35
|
self.temp_memory_file = os.path.join(self.memory_dir, "temp_memory.json")
|
36
36
|
|
37
37
|
self.init_memory: Dict[str, Any] = {}
|
38
38
|
self.core_memory: Dict[str, Any] = {}
|
39
|
-
self.
|
39
|
+
self.chat_history: List[Dict[str, Any]] = []
|
40
40
|
self.temp_memory: List[Dict[str, Any]] = []
|
41
41
|
|
42
42
|
async def initialize(self):
|
@@ -61,12 +61,12 @@ class MemoryManager:
|
|
61
61
|
self.core_memory = {"blocks": {}}
|
62
62
|
await self._save_core_memory()
|
63
63
|
|
64
|
-
# Load
|
65
|
-
if os.path.exists(self.
|
66
|
-
with open(self.
|
67
|
-
self.
|
64
|
+
# Load chat history
|
65
|
+
if os.path.exists(self.chat_history_file):
|
66
|
+
with open(self.chat_history_file, 'r', encoding='utf-8') as f:
|
67
|
+
self.chat_history = json.load(f)
|
68
68
|
else:
|
69
|
-
self.
|
69
|
+
self.chat_history = []
|
70
70
|
|
71
71
|
# Load temp memory
|
72
72
|
if os.path.exists(self.temp_memory_file):
|
@@ -89,20 +89,20 @@ class MemoryManager:
|
|
89
89
|
with open(self.core_memory_file, 'w', encoding='utf-8') as f:
|
90
90
|
json.dump(self.core_memory, f, ensure_ascii=False, indent=2)
|
91
91
|
|
92
|
-
async def
|
93
|
-
with open(self.
|
94
|
-
json.dump(self.
|
92
|
+
async def _save_chat_history(self):
|
93
|
+
with open(self.chat_history_file, 'w', encoding='utf-8') as f:
|
94
|
+
json.dump(self.chat_history, f, ensure_ascii=False, indent=2)
|
95
95
|
|
96
96
|
async def _save_temp_memory(self):
|
97
97
|
with open(self.temp_memory_file, 'w', encoding='utf-8') as f:
|
98
98
|
json.dump(self.temp_memory, f, ensure_ascii=False, indent=2)
|
99
99
|
|
100
|
-
async def
|
100
|
+
async def add_chat_entry(self, role: str, content: str):
|
101
101
|
entry = {"id": generate_id(), "role": role, "content": content, "timestamp": datetime.now().isoformat()}
|
102
|
-
self.
|
103
|
-
await self.
|
102
|
+
self.chat_history.append(entry)
|
103
|
+
await self._save_chat_history()
|
104
104
|
|
105
|
-
async def
|
105
|
+
async def add_detailed_chat_entry(self, input_messages: List[Dict[str, str]],
|
106
106
|
prompt: str, llm_response: str,
|
107
107
|
tool_executions: List[Dict[str, Any]],
|
108
108
|
final_response: str, entry_id: str = None):
|
@@ -112,25 +112,25 @@ class MemoryManager:
|
|
112
112
|
"timestamp": datetime.now().isoformat()
|
113
113
|
}
|
114
114
|
if entry_id:
|
115
|
-
for entry in self.
|
115
|
+
for entry in self.chat_history:
|
116
116
|
if entry.get("id") == entry_id:
|
117
117
|
entry.update(update_data)
|
118
|
-
await self.
|
118
|
+
await self._save_chat_history()
|
119
119
|
return entry_id
|
120
120
|
|
121
121
|
new_entry = {"id": entry_id or generate_id(), "type": "llm_interaction", "role": "assistant", **update_data}
|
122
|
-
self.
|
123
|
-
await self.
|
122
|
+
self.chat_history.append(new_entry)
|
123
|
+
await self._save_chat_history()
|
124
124
|
return new_entry["id"]
|
125
125
|
|
126
|
-
async def
|
127
|
-
return self.
|
126
|
+
async def get_recent_chat(self, entries: int = 10) -> List[Dict[str, Any]]:
|
127
|
+
return self.chat_history[-entries:]
|
128
128
|
|
129
|
-
async def
|
130
|
-
return self.
|
129
|
+
async def get_detailed_chat_history(self) -> List[Dict[str, Any]]:
|
130
|
+
return self.chat_history
|
131
131
|
|
132
132
|
async def get_last_agent_response(self) -> Optional[str]:
|
133
|
-
for entry in reversed(self.
|
133
|
+
for entry in reversed(self.chat_history):
|
134
134
|
if entry.get("type") == "llm_interaction":
|
135
135
|
final_response = entry.get("final_response", "")
|
136
136
|
if final_response and final_response not in ["Processing started", "Prompt sent to LLM", "LLM response received"]:
|
@@ -141,9 +141,9 @@ class MemoryManager:
|
|
141
141
|
return content
|
142
142
|
return None
|
143
143
|
|
144
|
-
async def
|
145
|
-
self.
|
146
|
-
await self.
|
144
|
+
async def reset_chat_history(self):
|
145
|
+
self.chat_history = []
|
146
|
+
await self._save_chat_history()
|
147
147
|
|
148
148
|
async def reset_temp_memory(self):
|
149
149
|
"""Reset temp memory to a default empty state."""
|
neuro_simulator/api/stream.py
CHANGED
@@ -1,55 +1 @@
|
|
1
|
-
#
|
2
|
-
"""API endpoints for controlling the live stream lifecycle."""
|
3
|
-
|
4
|
-
import asyncio
|
5
|
-
import logging
|
6
|
-
from pathlib import Path
|
7
|
-
|
8
|
-
from fastapi import APIRouter, Depends
|
9
|
-
|
10
|
-
from ..core.agent_factory import create_agent
|
11
|
-
from ..utils.process import process_manager
|
12
|
-
from .agent import get_api_token # Re-using the auth dependency from agent API
|
13
|
-
|
14
|
-
logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
15
|
-
router = APIRouter(prefix="/api/stream", tags=["Stream Control"])
|
16
|
-
|
17
|
-
@router.post("/start", dependencies=[Depends(get_api_token)])
|
18
|
-
async def start_stream():
|
19
|
-
"""Starts the live stream processes."""
|
20
|
-
try:
|
21
|
-
agent = await create_agent()
|
22
|
-
await agent.reset_memory()
|
23
|
-
except Exception as e:
|
24
|
-
logger.error(f"Could not reset agent memory on stream start: {e}", exc_info=True)
|
25
|
-
|
26
|
-
if not process_manager.is_running:
|
27
|
-
process_manager.start_live_processes()
|
28
|
-
return {"status": "success", "message": "Stream started"}
|
29
|
-
else:
|
30
|
-
return {"status": "info", "message": "Stream is already running"}
|
31
|
-
|
32
|
-
@router.post("/stop", dependencies=[Depends(get_api_token)])
|
33
|
-
async def stop_stream():
|
34
|
-
"""Stops the live stream processes."""
|
35
|
-
if process_manager.is_running:
|
36
|
-
await process_manager.stop_live_processes()
|
37
|
-
return {"status": "success", "message": "Stream stopped"}
|
38
|
-
else:
|
39
|
-
return {"status": "info", "message": "Stream is not running"}
|
40
|
-
|
41
|
-
@router.post("/restart", dependencies=[Depends(get_api_token)])
|
42
|
-
async def restart_stream():
|
43
|
-
"""Restarts the live stream processes."""
|
44
|
-
await process_manager.stop_live_processes()
|
45
|
-
await asyncio.sleep(1) # Give time for tasks to cancel
|
46
|
-
process_manager.start_live_processes()
|
47
|
-
return {"status": "success", "message": "Stream restarted"}
|
48
|
-
|
49
|
-
@router.get("/status", dependencies=[Depends(get_api_token)])
|
50
|
-
async def get_stream_status():
|
51
|
-
"""Gets the current status of the stream."""
|
52
|
-
return {
|
53
|
-
"is_running": process_manager.is_running,
|
54
|
-
"backend_status": "running" if process_manager.is_running else "stopped"
|
55
|
-
}
|
1
|
+
# This file is now empty as all stream control API endpoints have been migrated to WebSockets.
|
neuro_simulator/api/system.py
CHANGED
@@ -1,71 +1,41 @@
|
|
1
1
|
# neuro_simulator/api/system.py
|
2
2
|
"""API endpoints for system, config, and utility functions."""
|
3
3
|
|
4
|
-
from fastapi import APIRouter, Depends, HTTPException
|
5
|
-
from pydantic import BaseModel
|
4
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
6
5
|
import time
|
7
6
|
|
8
|
-
from ..core.config import config_manager
|
9
|
-
from ..services.audio import synthesize_audio_segment
|
10
|
-
from .agent import get_api_token # Re-using the auth dependency
|
7
|
+
from ..core.config import config_manager
|
11
8
|
|
12
9
|
router = APIRouter(tags=["System & Utilities"])
|
13
10
|
|
14
|
-
# --- TTS Endpoint ---
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
'performance': {'neuro_input_queue_max_size', 'audience_chat_buffer_max_size', 'initial_chat_backlog_limit'}
|
44
|
-
})
|
45
|
-
|
46
|
-
@router.get("/api/configs", dependencies=[Depends(get_api_token)])
|
47
|
-
async def get_configs():
|
48
|
-
"""Gets the current, frontend-safe configuration."""
|
49
|
-
return filter_config_for_frontend(config_manager.settings)
|
50
|
-
|
51
|
-
@router.patch("/api/configs", dependencies=[Depends(get_api_token)])
|
52
|
-
async def update_configs(new_settings: dict):
|
53
|
-
"""Updates the configuration with new values from the frontend."""
|
54
|
-
try:
|
55
|
-
await config_manager.update_settings(new_settings)
|
56
|
-
return filter_config_for_frontend(config_manager.settings)
|
57
|
-
except Exception as e:
|
58
|
-
raise HTTPException(status_code=500, detail=f"Failed to update settings: {str(e)}")
|
59
|
-
|
60
|
-
@router.post("/api/configs/reload", dependencies=[Depends(get_api_token)])
|
61
|
-
async def reload_configs():
|
62
|
-
"""Triggers a reload of the configuration from the config.yaml file."""
|
63
|
-
try:
|
64
|
-
# Passing an empty dict forces a reload and triggers callbacks
|
65
|
-
await config_manager.update_settings({})
|
66
|
-
return {"status": "success", "message": "Configuration reloaded"}
|
67
|
-
except Exception as e:
|
68
|
-
raise HTTPException(status_code=500, detail=f"Failed to reload settings: {str(e)}")
|
12
|
+
# --- Utility function to filter config for frontend ---
|
13
|
+
def filter_config_for_frontend(settings):
|
14
|
+
"""Filters the full settings object to remove sensitive fields before sending to the frontend."""
|
15
|
+
# Create a dictionary representation of the settings
|
16
|
+
config_dict = settings.model_dump()
|
17
|
+
|
18
|
+
# Remove sensitive fields
|
19
|
+
config_dict.pop('api_keys', None)
|
20
|
+
|
21
|
+
return config_dict
|
22
|
+
|
23
|
+
|
24
|
+
# --- Auth Dependency ---
|
25
|
+
|
26
|
+
async def get_api_token(request: Request):
|
27
|
+
"""FastAPI dependency to check for the API token in headers."""
|
28
|
+
password = config_manager.settings.server.panel_password
|
29
|
+
if not password:
|
30
|
+
return True
|
31
|
+
header_token = request.headers.get("X-API-Token")
|
32
|
+
if header_token and header_token == password:
|
33
|
+
return True
|
34
|
+
raise HTTPException(
|
35
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
36
|
+
detail="Invalid API token",
|
37
|
+
headers={"WWW-Authenticate": "Bearer"},
|
38
|
+
)
|
69
39
|
|
70
40
|
# --- System Endpoints ---
|
71
41
|
|
@@ -87,4 +57,4 @@ async def root():
|
|
87
57
|
"message": "Neuro-Sama Simulator Backend",
|
88
58
|
"version": "2.0",
|
89
59
|
"api_docs": "/docs",
|
90
|
-
}
|
60
|
+
}
|
neuro_simulator/cli.py
CHANGED
@@ -60,12 +60,13 @@ def main():
|
|
60
60
|
# Ensure agent directory and its contents exist
|
61
61
|
agent_dir = work_dir / "agent"
|
62
62
|
agent_dir.mkdir(parents=True, exist_ok=True)
|
63
|
-
copy_resource('neuro_simulator', 'agent/
|
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')
|
64
65
|
|
65
66
|
# Ensure agent memory directory and its contents exist
|
66
67
|
agent_memory_dir = agent_dir / "memory"
|
67
68
|
agent_memory_dir.mkdir(parents=True, exist_ok=True)
|
68
|
-
for filename in ["
|
69
|
+
for filename in ["chat_history.json", "core_memory.json", "init_memory.json"]:
|
69
70
|
copy_resource('neuro_simulator', f'agent/memory/{filename}', agent_memory_dir / filename)
|
70
71
|
|
71
72
|
# 3. Validate essential files
|
@@ -16,7 +16,7 @@ class BaseAgent(ABC):
|
|
16
16
|
pass
|
17
17
|
|
18
18
|
@abstractmethod
|
19
|
-
async def
|
19
|
+
async def process_and_respond(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
20
20
|
"""Process messages and generate a response."""
|
21
21
|
pass
|
22
22
|
|
@@ -21,8 +21,6 @@ from ..services.letta import LettaAgent
|
|
21
21
|
from ..services.builtin import BuiltinAgentWrapper
|
22
22
|
|
23
23
|
# --- API Routers ---
|
24
|
-
from ..api.agent import router as agent_router
|
25
|
-
from ..api.stream import router as stream_router
|
26
24
|
from ..api.system import router as system_router
|
27
25
|
|
28
26
|
# --- Services and Utilities ---
|
@@ -60,8 +58,6 @@ app.add_middleware(
|
|
60
58
|
expose_headers=["X-API-Token"],
|
61
59
|
)
|
62
60
|
|
63
|
-
app.include_router(agent_router)
|
64
|
-
app.include_router(stream_router)
|
65
61
|
app.include_router(system_router)
|
66
62
|
|
67
63
|
# --- Background Task Definitions ---
|
@@ -167,7 +163,7 @@ async def neuro_response_cycle():
|
|
167
163
|
if not selected_chats:
|
168
164
|
continue
|
169
165
|
|
170
|
-
response_result = await asyncio.wait_for(agent.
|
166
|
+
response_result = await asyncio.wait_for(agent.process_and_respond(selected_chats), timeout=20.0)
|
171
167
|
|
172
168
|
response_text = response_result.get("final_response", "").strip()
|
173
169
|
if not response_text:
|
@@ -280,7 +276,10 @@ async def websocket_stream_endpoint(websocket: WebSocket):
|
|
280
276
|
@app.websocket("/ws/admin")
|
281
277
|
async def websocket_admin_endpoint(websocket: WebSocket):
|
282
278
|
await websocket.accept()
|
279
|
+
# Add the new admin client to a dedicated list
|
280
|
+
connection_manager.admin_connections.append(websocket)
|
283
281
|
try:
|
282
|
+
# Send initial state
|
284
283
|
for log_entry in list(server_log_queue): await websocket.send_json({"type": "server_log", "data": log_entry})
|
285
284
|
for log_entry in list(agent_log_queue): await websocket.send_json({"type": "agent_log", "data": log_entry})
|
286
285
|
|
@@ -288,15 +287,174 @@ async def websocket_admin_endpoint(websocket: WebSocket):
|
|
288
287
|
initial_context = await agent.get_message_history()
|
289
288
|
await websocket.send_json({"type": "agent_context", "action": "update", "messages": initial_context})
|
290
289
|
|
290
|
+
# Main loop for receiving messages from the client and pushing log updates
|
291
291
|
while websocket.client_state == WebSocketState.CONNECTED:
|
292
|
+
# Check for incoming messages
|
293
|
+
try:
|
294
|
+
raw_data = await asyncio.wait_for(websocket.receive_text(), timeout=0.01)
|
295
|
+
data = json.loads(raw_data)
|
296
|
+
await handle_admin_ws_message(websocket, data)
|
297
|
+
except asyncio.TimeoutError:
|
298
|
+
pass # No message received, continue to push logs
|
299
|
+
|
300
|
+
# Push log updates
|
292
301
|
if server_log_queue: await websocket.send_json({"type": "server_log", "data": server_log_queue.popleft()})
|
293
302
|
if agent_log_queue: await websocket.send_json({"type": "agent_log", "data": agent_log_queue.popleft()})
|
294
303
|
await asyncio.sleep(0.1)
|
304
|
+
|
295
305
|
except WebSocketDisconnect:
|
296
306
|
pass
|
297
307
|
finally:
|
308
|
+
connection_manager.admin_connections.remove(websocket)
|
298
309
|
logger.info("Admin WebSocket client disconnected.")
|
299
310
|
|
311
|
+
async def handle_admin_ws_message(websocket: WebSocket, data: dict):
|
312
|
+
"""Handles incoming messages from the admin WebSocket."""
|
313
|
+
action = data.get("action")
|
314
|
+
payload = data.get("payload", {})
|
315
|
+
request_id = data.get("request_id")
|
316
|
+
|
317
|
+
agent = await create_agent()
|
318
|
+
response = {"type": "response", "request_id": request_id, "payload": {}}
|
319
|
+
|
320
|
+
try:
|
321
|
+
# Core Memory Actions
|
322
|
+
if action == "get_core_memory_blocks":
|
323
|
+
blocks = await agent.get_memory_blocks()
|
324
|
+
response["payload"] = blocks
|
325
|
+
|
326
|
+
elif action == "create_core_memory_block":
|
327
|
+
block_id = await agent.create_memory_block(**payload)
|
328
|
+
response["payload"] = {"status": "success", "block_id": block_id}
|
329
|
+
# Broadcast the update to all admins
|
330
|
+
updated_blocks = await agent.get_memory_blocks()
|
331
|
+
from ..utils.websocket import connection_manager
|
332
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
333
|
+
|
334
|
+
elif action == "update_core_memory_block":
|
335
|
+
await agent.update_memory_block(**payload)
|
336
|
+
response["payload"] = {"status": "success"}
|
337
|
+
# Broadcast the update to all admins
|
338
|
+
updated_blocks = await agent.get_memory_blocks()
|
339
|
+
from ..utils.websocket import connection_manager
|
340
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
341
|
+
|
342
|
+
elif action == "delete_core_memory_block":
|
343
|
+
await agent.delete_memory_block(**payload)
|
344
|
+
response["payload"] = {"status": "success"}
|
345
|
+
# Broadcast the update to all admins
|
346
|
+
updated_blocks = await agent.get_memory_blocks()
|
347
|
+
from ..utils.websocket import connection_manager
|
348
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
349
|
+
|
350
|
+
# Temp Memory Actions
|
351
|
+
elif action == "get_temp_memory":
|
352
|
+
temp_mem = await agent.get_temp_memory()
|
353
|
+
response["payload"] = temp_mem
|
354
|
+
|
355
|
+
elif action == "add_temp_memory":
|
356
|
+
await agent.add_temp_memory(**payload)
|
357
|
+
response["payload"] = {"status": "success"}
|
358
|
+
updated_temp_mem = await agent.get_temp_memory()
|
359
|
+
from ..utils.websocket import connection_manager
|
360
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
361
|
+
|
362
|
+
elif action == "clear_temp_memory":
|
363
|
+
await agent.clear_temp_memory()
|
364
|
+
response["payload"] = {"status": "success"}
|
365
|
+
updated_temp_mem = await agent.get_temp_memory()
|
366
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
367
|
+
|
368
|
+
# Init Memory Actions
|
369
|
+
elif action == "get_init_memory":
|
370
|
+
init_mem = await agent.get_init_memory()
|
371
|
+
response["payload"] = init_mem
|
372
|
+
|
373
|
+
elif action == "update_init_memory":
|
374
|
+
await agent.update_init_memory(**payload)
|
375
|
+
response["payload"] = {"status": "success"}
|
376
|
+
updated_init_mem = await agent.get_init_memory()
|
377
|
+
from ..utils.websocket import connection_manager
|
378
|
+
await connection_manager.broadcast_to_admins({"type": "init_memory_updated", "payload": updated_init_mem})
|
379
|
+
|
380
|
+
# Tool Actions
|
381
|
+
elif action == "get_tools":
|
382
|
+
tools = await agent.get_available_tools()
|
383
|
+
response["payload"] = {"tools": tools}
|
384
|
+
|
385
|
+
elif action == "execute_tool":
|
386
|
+
result = await agent.execute_tool(**payload)
|
387
|
+
response["payload"] = {"result": result}
|
388
|
+
|
389
|
+
# Stream Control Actions
|
390
|
+
elif action == "start_stream":
|
391
|
+
if not process_manager.is_running:
|
392
|
+
process_manager.start_live_processes()
|
393
|
+
response["payload"] = {"status": "success", "message": "Stream started"}
|
394
|
+
|
395
|
+
elif action == "stop_stream":
|
396
|
+
if process_manager.is_running:
|
397
|
+
await process_manager.stop_live_processes()
|
398
|
+
response["payload"] = {"status": "success", "message": "Stream stopped"}
|
399
|
+
|
400
|
+
elif action == "restart_stream":
|
401
|
+
await process_manager.stop_live_processes()
|
402
|
+
await asyncio.sleep(1)
|
403
|
+
process_manager.start_live_processes()
|
404
|
+
response["payload"] = {"status": "success", "message": "Stream restarted"}
|
405
|
+
|
406
|
+
elif action == "get_stream_status":
|
407
|
+
status = {"is_running": process_manager.is_running, "backend_status": "running" if process_manager.is_running else "stopped"}
|
408
|
+
response["payload"] = status
|
409
|
+
|
410
|
+
# Config Management Actions
|
411
|
+
elif action == "get_configs":
|
412
|
+
from ..api.system import filter_config_for_frontend
|
413
|
+
configs = filter_config_for_frontend(config_manager.settings)
|
414
|
+
response["payload"] = configs
|
415
|
+
|
416
|
+
elif action == "update_configs":
|
417
|
+
from ..api.system import filter_config_for_frontend
|
418
|
+
await config_manager.update_settings(payload)
|
419
|
+
updated_configs = filter_config_for_frontend(config_manager.settings)
|
420
|
+
response["payload"] = updated_configs
|
421
|
+
await connection_manager.broadcast_to_admins({"type": "config_updated", "payload": updated_configs})
|
422
|
+
|
423
|
+
elif action == "reload_configs":
|
424
|
+
await config_manager.update_settings({})
|
425
|
+
response["payload"] = {"status": "success", "message": "Configuration reloaded"}
|
426
|
+
from ..api.system import filter_config_for_frontend
|
427
|
+
updated_configs = filter_config_for_frontend(config_manager.settings)
|
428
|
+
await connection_manager.broadcast_to_admins({"type": "config_updated", "payload": updated_configs})
|
429
|
+
|
430
|
+
# Other Agent Actions
|
431
|
+
elif action == "get_agent_context":
|
432
|
+
context = await agent.get_message_history()
|
433
|
+
response["payload"] = context
|
434
|
+
|
435
|
+
elif action == "reset_agent_memory":
|
436
|
+
await agent.reset_memory()
|
437
|
+
response["payload"] = {"status": "success"}
|
438
|
+
# Broadcast updates for all memory types
|
439
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": await agent.get_memory_blocks()})
|
440
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": await agent.get_temp_memory()})
|
441
|
+
await connection_manager.broadcast_to_admins({"type": "init_memory_updated", "payload": await agent.get_init_memory()})
|
442
|
+
await connection_manager.broadcast_to_admins({"type": "agent_context", "action": "update", "messages": await agent.get_message_history()})
|
443
|
+
|
444
|
+
else:
|
445
|
+
response["payload"] = {"status": "error", "message": f"Unknown action: {action}"}
|
446
|
+
|
447
|
+
# Send the direct response to the requesting client
|
448
|
+
if request_id:
|
449
|
+
await websocket.send_json(response)
|
450
|
+
|
451
|
+
except Exception as e:
|
452
|
+
logger.error(f"Error handling admin WS message (action: {action}): {e}", exc_info=True)
|
453
|
+
if request_id:
|
454
|
+
response["payload"] = {"status": "error", "message": str(e)}
|
455
|
+
await websocket.send_json(response)
|
456
|
+
|
457
|
+
|
300
458
|
# --- Server Entrypoint ---
|
301
459
|
|
302
460
|
def run_server(host: str = None, port: int = None):
|
@@ -37,8 +37,8 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
37
37
|
async def reset_memory(self):
|
38
38
|
await self.agent_instance.reset_all_memory()
|
39
39
|
|
40
|
-
async def
|
41
|
-
return await self.agent_instance.
|
40
|
+
async def process_and_respond(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
|
41
|
+
return await self.agent_instance.process_and_respond(messages)
|
42
42
|
|
43
43
|
# Memory Block Management
|
44
44
|
async def get_memory_blocks(self) -> List[Dict[str, Any]]:
|
@@ -50,13 +50,25 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
50
50
|
|
51
51
|
async def create_memory_block(self, title: str, description: str, content: List[str]) -> Dict[str, str]:
|
52
52
|
block_id = await self.agent_instance.memory_manager.create_core_memory_block(title, description, content)
|
53
|
+
# Broadcast core_memory_updated event
|
54
|
+
updated_blocks = await self.get_memory_blocks()
|
55
|
+
from ..utils.websocket import connection_manager
|
56
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
53
57
|
return {"block_id": block_id}
|
54
58
|
|
55
59
|
async def update_memory_block(self, block_id: str, title: Optional[str], description: Optional[str], content: Optional[List[str]]):
|
56
60
|
await self.agent_instance.memory_manager.update_core_memory_block(block_id, title, description, content)
|
61
|
+
# Broadcast core_memory_updated event
|
62
|
+
updated_blocks = await self.get_memory_blocks()
|
63
|
+
from ..utils.websocket import connection_manager
|
64
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
57
65
|
|
58
66
|
async def delete_memory_block(self, block_id: str):
|
59
67
|
await self.agent_instance.memory_manager.delete_core_memory_block(block_id)
|
68
|
+
# Broadcast core_memory_updated event
|
69
|
+
updated_blocks = await self.get_memory_blocks()
|
70
|
+
from ..utils.websocket import connection_manager
|
71
|
+
await connection_manager.broadcast_to_admins({"type": "core_memory_updated", "payload": updated_blocks})
|
60
72
|
|
61
73
|
# Init Memory Management
|
62
74
|
async def get_init_memory(self) -> Dict[str, Any]:
|
@@ -64,6 +76,10 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
64
76
|
|
65
77
|
async def update_init_memory(self, memory: Dict[str, Any]):
|
66
78
|
await self.agent_instance.memory_manager.update_init_memory(memory)
|
79
|
+
# Broadcast init_memory_updated event
|
80
|
+
updated_init_mem = await self.get_init_memory()
|
81
|
+
from ..utils.websocket import connection_manager
|
82
|
+
await connection_manager.broadcast_to_admins({"type": "init_memory_updated", "payload": updated_init_mem})
|
67
83
|
|
68
84
|
# Temp Memory Management
|
69
85
|
async def get_temp_memory(self) -> List[Dict[str, Any]]:
|
@@ -71,17 +87,31 @@ class BuiltinAgentWrapper(BaseAgent):
|
|
71
87
|
|
72
88
|
async def add_temp_memory(self, content: str, role: str):
|
73
89
|
await self.agent_instance.memory_manager.add_temp_memory(content, role)
|
90
|
+
# Broadcast temp_memory_updated event
|
91
|
+
updated_temp_mem = await self.get_temp_memory()
|
92
|
+
from ..utils.websocket import connection_manager
|
93
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
74
94
|
|
75
95
|
async def clear_temp_memory(self):
|
76
96
|
await self.agent_instance.memory_manager.reset_temp_memory()
|
97
|
+
# Broadcast temp_memory_updated event
|
98
|
+
updated_temp_mem = await self.get_temp_memory()
|
99
|
+
from ..utils.websocket import connection_manager
|
100
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
77
101
|
|
78
102
|
# Tool Management
|
79
103
|
async def get_available_tools(self) -> str:
|
80
104
|
return self.agent_instance.tool_manager.get_tool_descriptions()
|
81
105
|
|
82
106
|
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
83
|
-
|
107
|
+
result = await self.agent_instance.execute_tool(tool_name, params)
|
108
|
+
# If the tool was add_temp_memory, broadcast temp_memory_updated event
|
109
|
+
if tool_name == "add_temp_memory":
|
110
|
+
updated_temp_mem = await self.get_temp_memory()
|
111
|
+
from ..utils.websocket import connection_manager
|
112
|
+
await connection_manager.broadcast_to_admins({"type": "temp_memory_updated", "payload": updated_temp_mem})
|
113
|
+
return result
|
84
114
|
|
85
115
|
# Context/Message History
|
86
116
|
async def get_message_history(self, limit: int = 20) -> List[Dict[str, Any]]:
|
87
|
-
return await self.agent_instance.memory_manager.
|
117
|
+
return await self.agent_instance.memory_manager.get_recent_chat(limit)
|
@@ -12,7 +12,8 @@ logger = logging.getLogger(__name__.replace("neuro_simulator", "server", 1))
|
|
12
12
|
class WebSocketManager:
|
13
13
|
"""Manages all active WebSocket connections and provides broadcasting capabilities."""
|
14
14
|
def __init__(self):
|
15
|
-
self.active_connections:
|
15
|
+
self.active_connections: list[WebSocket] = []
|
16
|
+
self.admin_connections: list[WebSocket] = []
|
16
17
|
logger.info("WebSocketManager initialized.")
|
17
18
|
|
18
19
|
async def connect(self, websocket: WebSocket):
|
@@ -37,19 +38,15 @@ class WebSocketManager:
|
|
37
38
|
self.disconnect(websocket)
|
38
39
|
|
39
40
|
async def broadcast(self, message: dict):
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
disconnected_sockets.append(connection)
|
50
|
-
|
51
|
-
for disconnected_socket in disconnected_sockets:
|
52
|
-
self.disconnect(disconnected_socket)
|
41
|
+
for connection in self.active_connections:
|
42
|
+
await connection.send_json(message)
|
43
|
+
|
44
|
+
async def broadcast_to_admins(self, message: dict):
|
45
|
+
for connection in self.admin_connections:
|
46
|
+
try:
|
47
|
+
await connection.send_json(message)
|
48
|
+
except Exception as e:
|
49
|
+
logger.error(f"Failed to send message to admin connection: {e}")
|
53
50
|
|
54
51
|
# Global singleton instance
|
55
52
|
connection_manager = WebSocketManager()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: neuro_simulator
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.1
|
4
4
|
Summary: Neuro Simulator Server
|
5
5
|
Author-email: Moha-Master <hongkongreporter@outlook.com>
|
6
6
|
License-Expression: MIT
|
@@ -69,7 +69,7 @@ neuro_simulator/
|
|
69
69
|
│ ├── memory/ # 记忆管理模块
|
70
70
|
│ │ ├── __init__.py
|
71
71
|
│ │ ├── manager.py # 记忆管理器
|
72
|
-
│ │ ├──
|
72
|
+
│ │ ├── chat_history.json # 上下文记忆文件
|
73
73
|
│ │ ├── core_memory.json # 核心记忆文件
|
74
74
|
│ │ ├── init_memory.json # 初始化记忆文件
|
75
75
|
│ │ └── temp_memory.json # 临时记忆文件
|
@@ -109,7 +109,7 @@ working_dir_example/ # 工作目录结构,请将这个目录重命名和
|
|
109
109
|
├── config.yaml.example # 自动生成的配置文件模板,必须手动重命名和填写
|
110
110
|
└── agent/ # Agent相关文件夹
|
111
111
|
└── memory/ # Agent记忆文件夹
|
112
|
-
├──
|
112
|
+
├── chat_history.json # 上下文记忆文件
|
113
113
|
├── core_memory.json # 核心记忆文件
|
114
114
|
├── init_memory.json # 初始化记忆文件
|
115
115
|
└── temp_memory.json # 临时记忆文件
|
@@ -191,7 +191,6 @@ neuro -D /path/to/your/config -H 0.0.0.0 -P 8080
|
|
191
191
|
- `/api/configs/*` - 配置管理接口(获取/更新/重载配置)
|
192
192
|
- `api_keys` `server` 等敏感配置项无法从接口获取和修改
|
193
193
|
- `/api/logs` - 日志获取接口
|
194
|
-
- `/api/tts/synthesize` - TTS 合成接口
|
195
194
|
- `/api/system/health` - 健康检查接口
|
196
195
|
- `/ws/stream` - 客户端使用的直播接口
|
197
196
|
- `/ws/admin` - 日志和内建 Agent的 Context 流接口
|
@@ -1,27 +1,26 @@
|
|
1
1
|
neuro_simulator/__init__.py,sha256=-tposzyvg6UckPcfSvtc03UjxBa9oCe_zRvlKf8splk,31
|
2
|
-
neuro_simulator/cli.py,sha256=
|
2
|
+
neuro_simulator/cli.py,sha256=Nc-udO0jdRVZjlbebKBjDVOIUzd6tJr13ApBGeBEg2k,3967
|
3
3
|
neuro_simulator/agent/__init__.py,sha256=t52CZlyTGWqcGjMs90qvpFpRckY2WSSlO7r_H3K_mSY,32
|
4
4
|
neuro_simulator/agent/base.py,sha256=6v2ZO5UpGCwJEkJ23Oe96Rs510tK4ZOEpZ2DB49IZmM,1262
|
5
|
-
neuro_simulator/agent/core.py,sha256=
|
5
|
+
neuro_simulator/agent/core.py,sha256=2mPVCcNKZDRsyYd6L8RzQYlm8LwzoKoP9FJg0W4qcbc,10702
|
6
6
|
neuro_simulator/agent/factory.py,sha256=e0IBnqJQM7OuKtglrf-pWwqwmg98wh7tOq5LxF2rV-w,1146
|
7
7
|
neuro_simulator/agent/llm.py,sha256=vLz8hp2h2R0JaNfS1RLGYGkri_YoUdlEdNfFVbxeEuI,4261
|
8
8
|
neuro_simulator/agent/memory/__init__.py,sha256=YJ7cynQJI6kD7vjyv3rKc-CZqmoYSuGQtRZl_XdGEps,39
|
9
|
-
neuro_simulator/agent/memory/manager.py,sha256=
|
9
|
+
neuro_simulator/agent/memory/manager.py,sha256=UiUJgjiTV7SHCmb3V5sc4OUa_ksEeXBOyvl-WBQviqs,9266
|
10
10
|
neuro_simulator/agent/tools/__init__.py,sha256=1WZy6PADfi6o1avyy1y-ThWBFAPJ_bBqtkobyYpf5ao,38
|
11
11
|
neuro_simulator/agent/tools/core.py,sha256=o6Oyis-HFD-g6Z_u3T--tkmr9ylKJvybKqMRSMUwi1Q,5555
|
12
12
|
neuro_simulator/api/__init__.py,sha256=5LWyDSayPGdQS8Rv13nmAKLyhPnMVPyTYDdvoMPB4xw,56
|
13
|
-
neuro_simulator/api/
|
14
|
-
neuro_simulator/api/
|
15
|
-
neuro_simulator/api/system.py,sha256=hXznMcThuFhwopYWgpzrRxwtBuFnF_b_vinkOaE5XOs,3712
|
13
|
+
neuro_simulator/api/stream.py,sha256=hM66flSUygpE-NH9X-ZOV6SiGipBzN1-wjd_wZRpQm4,94
|
14
|
+
neuro_simulator/api/system.py,sha256=TV_3DzP-VgLnHdue0pKYFFQNOV_V7tr4aMuxL802vQg,1783
|
16
15
|
neuro_simulator/core/__init__.py,sha256=-ojq25c8XA0CU25b0OxcGjH4IWFEDHR-HXSRSZIuKe8,57
|
17
16
|
neuro_simulator/core/agent_factory.py,sha256=qMFidwT5IrOkyNHwmpO8_fRv20KLbaIBfWF-VTFCLNA,1742
|
18
|
-
neuro_simulator/core/agent_interface.py,sha256=
|
19
|
-
neuro_simulator/core/application.py,sha256=
|
17
|
+
neuro_simulator/core/agent_interface.py,sha256=ZXUCtkQUvsBQ5iCb0gTILJaShn5KmSrEgKhd7PK18HE,2794
|
18
|
+
neuro_simulator/core/application.py,sha256=lhTn5KR7ek8-kuyD1qWtppImUUz8pOExH9KRNW1SQ3M,21115
|
20
19
|
neuro_simulator/core/config.py,sha256=brA8kiekV_995mpz04JiGj1swIWbZZuWWLNYtbroMyE,14884
|
21
20
|
neuro_simulator/services/__init__.py,sha256=s3ZrAHg5TpJakadAAGY1h0wDw_xqN4Je4aJwJyRBmk4,61
|
22
21
|
neuro_simulator/services/audience.py,sha256=0phlhsujh_GMXm_UMiyOntY-ZMtoseRa_FroIfc5A6w,5028
|
23
22
|
neuro_simulator/services/audio.py,sha256=ZscQA25wVYpm9FUl4Hya7tKH8t0TjR3th9-OEZ0G7xk,2934
|
24
|
-
neuro_simulator/services/builtin.py,sha256=
|
23
|
+
neuro_simulator/services/builtin.py,sha256=nn3sJFPy09JxQkw35icdyGU9hzLTXXazAJkNpdcz6Zs,5848
|
25
24
|
neuro_simulator/services/letta.py,sha256=6jBvOTsLMlRILDv-fvX9fhHMONSYeu-ImJGFcKU00kc,11067
|
26
25
|
neuro_simulator/services/stream.py,sha256=dG7RuNI_ICohPkqKZ-zlBppo54BgWm_KYBs-ezzc73E,5907
|
27
26
|
neuro_simulator/utils/__init__.py,sha256=xSEFzjT827W81mNyQ_DLtr00TgFlttqfFgpz9pSxFXQ,58
|
@@ -29,9 +28,9 @@ neuro_simulator/utils/logging.py,sha256=BO-q_cCcoeamsc8eJqq2-L3Z8nhApze_v6LnmD-O
|
|
29
28
|
neuro_simulator/utils/process.py,sha256=9w2JQH59Wy6L8ADrig2QF88iajdykGPZIYJVJe6Al2Y,2603
|
30
29
|
neuro_simulator/utils/queue.py,sha256=vSkh-BrgfsGN_gDAx2mfK44ydmMapdVyLsDG-7LIJZQ,1643
|
31
30
|
neuro_simulator/utils/state.py,sha256=DdBqSAYfjOFtJfB1hEGhYPh32r1ZvFuVlN_-29_-luA,664
|
32
|
-
neuro_simulator/utils/websocket.py,sha256=
|
33
|
-
neuro_simulator-0.3.
|
34
|
-
neuro_simulator-0.3.
|
35
|
-
neuro_simulator-0.3.
|
36
|
-
neuro_simulator-0.3.
|
37
|
-
neuro_simulator-0.3.
|
31
|
+
neuro_simulator/utils/websocket.py,sha256=1gtVoH1hafBUfVYmwkVDAbjwETeqyC3sWx706nQzSRo,2085
|
32
|
+
neuro_simulator-0.3.1.dist-info/METADATA,sha256=lQv9x0KvOZJAA8NwZ8byIaHYTVnSxuHRPyhUyqLC6vw,8643
|
33
|
+
neuro_simulator-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
34
|
+
neuro_simulator-0.3.1.dist-info/entry_points.txt,sha256=qVd5ypnRRgU8Cw7rWfZ7o0OXyS9P9hgY-cRoN_mgz9g,51
|
35
|
+
neuro_simulator-0.3.1.dist-info/top_level.txt,sha256=V8awSKpcrFnjJDiJxSfy7jtOrnuE2BgAR9hLmfMDWK8,16
|
36
|
+
neuro_simulator-0.3.1.dist-info/RECORD,,
|
neuro_simulator/api/agent.py
DELETED
@@ -1,163 +0,0 @@
|
|
1
|
-
# neuro_simulator/api/agent.py
|
2
|
-
"""Unified API endpoints for agent management, decoupled from implementation."""
|
3
|
-
|
4
|
-
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
5
|
-
from typing import Dict, Any, List, Optional
|
6
|
-
from pydantic import BaseModel
|
7
|
-
|
8
|
-
# Imports for the new structure
|
9
|
-
from ..core.config import config_manager
|
10
|
-
from ..core.agent_factory import create_agent
|
11
|
-
from ..core.agent_interface import BaseAgent
|
12
|
-
|
13
|
-
router = APIRouter(prefix="/api/agent", tags=["Agent Management"])
|
14
|
-
|
15
|
-
# Security dependency (remains the same)
|
16
|
-
async def get_api_token(request: Request):
|
17
|
-
password = config_manager.settings.server.panel_password
|
18
|
-
if not password:
|
19
|
-
return True
|
20
|
-
header_token = request.headers.get("X-API-Token")
|
21
|
-
if header_token and header_token == password:
|
22
|
-
return True
|
23
|
-
raise HTTPException(
|
24
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
25
|
-
detail="Invalid API token",
|
26
|
-
headers={"WWW-Authenticate": "Bearer"},
|
27
|
-
)
|
28
|
-
|
29
|
-
# Pydantic models (remains the same)
|
30
|
-
class MessageItem(BaseModel):
|
31
|
-
username: str
|
32
|
-
text: str
|
33
|
-
role: str = "user"
|
34
|
-
|
35
|
-
class ToolExecutionRequest(BaseModel):
|
36
|
-
tool_name: str
|
37
|
-
params: Dict[str, Any]
|
38
|
-
|
39
|
-
class MemoryUpdateRequest(BaseModel):
|
40
|
-
title: Optional[str] = None
|
41
|
-
description: Optional[str] = None
|
42
|
-
content: Optional[List[str]] = None
|
43
|
-
|
44
|
-
class MemoryCreateRequest(BaseModel):
|
45
|
-
title: str
|
46
|
-
description: str
|
47
|
-
content: List[str]
|
48
|
-
|
49
|
-
class InitMemoryUpdateRequest(BaseModel):
|
50
|
-
memory: Dict[str, Any]
|
51
|
-
|
52
|
-
class TempMemoryItem(BaseModel):
|
53
|
-
content: str
|
54
|
-
role: str = "system"
|
55
|
-
|
56
|
-
# Dependency to get the agent instance, making endpoints cleaner
|
57
|
-
async def get_agent() -> BaseAgent:
|
58
|
-
return await create_agent()
|
59
|
-
|
60
|
-
# A single dependency for both auth and agent instance
|
61
|
-
class AgentDeps:
|
62
|
-
def __init__(self, token: bool = Depends(get_api_token), agent: BaseAgent = Depends(get_agent)):
|
63
|
-
self.agent = agent
|
64
|
-
|
65
|
-
# --- Refactored Agent Endpoints ---
|
66
|
-
|
67
|
-
@router.get("/messages", response_model=List[Dict[str, Any]])
|
68
|
-
async def get_agent_messages(deps: AgentDeps = Depends()):
|
69
|
-
"""Get agent's detailed message processing history."""
|
70
|
-
return await deps.agent.get_message_history()
|
71
|
-
|
72
|
-
@router.get("/context", response_model=List[Dict[str, Any]])
|
73
|
-
async def get_agent_context(deps: AgentDeps = Depends()):
|
74
|
-
"""Get agent's recent conversation context (last 20 entries)."""
|
75
|
-
return await deps.agent.get_message_history(limit=20)
|
76
|
-
|
77
|
-
@router.delete("/messages")
|
78
|
-
async def clear_agent_messages(deps: AgentDeps = Depends()):
|
79
|
-
"""Clear agent's message history."""
|
80
|
-
await deps.agent.reset_memory()
|
81
|
-
return {"status": "success", "message": "Agent memory reset successfully"}
|
82
|
-
|
83
|
-
@router.post("/messages")
|
84
|
-
async def send_message_to_agent(message: MessageItem, deps: AgentDeps = Depends()):
|
85
|
-
"""Send a message to the agent."""
|
86
|
-
response = await deps.agent.process_messages([message.dict()])
|
87
|
-
return {"response": response}
|
88
|
-
|
89
|
-
@router.get("/memory/init", response_model=Dict[str, Any])
|
90
|
-
async def get_init_memory(deps: AgentDeps = Depends()):
|
91
|
-
"""Get initialization memory content."""
|
92
|
-
return await deps.agent.get_init_memory()
|
93
|
-
|
94
|
-
@router.put("/memory/init")
|
95
|
-
async def update_init_memory(request: InitMemoryUpdateRequest, deps: AgentDeps = Depends()):
|
96
|
-
"""Update initialization memory content."""
|
97
|
-
await deps.agent.update_init_memory(request.memory)
|
98
|
-
return {"status": "success", "message": "Initialization memory updated"}
|
99
|
-
|
100
|
-
@router.get("/memory/temp", response_model=List[Dict[str, Any]])
|
101
|
-
async def get_temp_memory(deps: AgentDeps = Depends()):
|
102
|
-
"""Get all temporary memory content."""
|
103
|
-
return await deps.agent.get_temp_memory()
|
104
|
-
|
105
|
-
@router.post("/memory/temp")
|
106
|
-
async def add_temp_memory_item(request: TempMemoryItem, deps: AgentDeps = Depends()):
|
107
|
-
"""Add an item to temporary memory."""
|
108
|
-
await deps.agent.add_temp_memory(request.content, request.role)
|
109
|
-
return {"status": "success", "message": "Item added to temporary memory"}
|
110
|
-
|
111
|
-
@router.delete("/memory/temp")
|
112
|
-
async def clear_temp_memory(deps: AgentDeps = Depends()):
|
113
|
-
"""Clear temporary memory."""
|
114
|
-
await deps.agent.clear_temp_memory()
|
115
|
-
return {"status": "success", "message": "Temporary memory cleared"}
|
116
|
-
|
117
|
-
@router.get("/memory/blocks", response_model=List[Dict[str, Any]])
|
118
|
-
async def get_memory_blocks(deps: AgentDeps = Depends()):
|
119
|
-
"""Get all memory blocks."""
|
120
|
-
return await deps.agent.get_memory_blocks()
|
121
|
-
|
122
|
-
@router.get("/memory/blocks/{block_id}", response_model=Dict[str, Any])
|
123
|
-
async def get_memory_block(block_id: str, deps: AgentDeps = Depends()):
|
124
|
-
"""Get a specific memory block."""
|
125
|
-
block = await deps.agent.get_memory_block(block_id)
|
126
|
-
if block is None:
|
127
|
-
raise HTTPException(status_code=404, detail="Memory block not found")
|
128
|
-
return block
|
129
|
-
|
130
|
-
@router.post("/memory/blocks", response_model=Dict[str, str])
|
131
|
-
async def create_memory_block(request: MemoryCreateRequest, deps: AgentDeps = Depends()):
|
132
|
-
"""Create a new memory block."""
|
133
|
-
return await deps.agent.create_memory_block(request.title, request.description, request.content)
|
134
|
-
|
135
|
-
@router.put("/memory/blocks/{block_id}")
|
136
|
-
async def update_memory_block(block_id: str, request: MemoryUpdateRequest, deps: AgentDeps = Depends()):
|
137
|
-
"""Update a memory block."""
|
138
|
-
await deps.agent.update_memory_block(block_id, request.title, request.description, request.content)
|
139
|
-
return {"status": "success"}
|
140
|
-
|
141
|
-
@router.delete("/memory/blocks/{block_id}")
|
142
|
-
async def delete_memory_block(block_id: str, deps: AgentDeps = Depends()):
|
143
|
-
"""Delete a memory block."""
|
144
|
-
await deps.agent.delete_memory_block(block_id)
|
145
|
-
return {"status": "success"}
|
146
|
-
|
147
|
-
@router.post("/reset_memory")
|
148
|
-
async def reset_agent_memory(deps: AgentDeps = Depends()):
|
149
|
-
"""Reset all agent memory types."""
|
150
|
-
await deps.agent.reset_memory()
|
151
|
-
return {"status": "success", "message": "Agent memory reset successfully"}
|
152
|
-
|
153
|
-
@router.get("/tools")
|
154
|
-
async def get_available_tools(deps: AgentDeps = Depends()):
|
155
|
-
"""Get list of available tools."""
|
156
|
-
# Return in the format expected by the frontend
|
157
|
-
return {"tools": await deps.agent.get_available_tools()}
|
158
|
-
|
159
|
-
@router.post("/tools/execute")
|
160
|
-
async def execute_tool(request: ToolExecutionRequest, deps: AgentDeps = Depends()):
|
161
|
-
"""Execute a tool with given parameters."""
|
162
|
-
result = await deps.agent.execute_tool(request.tool_name, request.params)
|
163
|
-
return {"result": result}
|
File without changes
|
File without changes
|
File without changes
|