neuro-simulator 0.0.4__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,471 @@
1
+ # agent/core.py
2
+ """
3
+ Core module for the Neuro Simulator Agent
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import asyncio
9
+ from typing import Dict, List, Any, Optional
10
+ from datetime import datetime
11
+ import sys
12
+ import logging
13
+
14
+ # Import the shared log queue from the main log_handler
15
+ from ..log_handler import agent_log_queue, QueueLogHandler
16
+
17
+ # Create a logger for the agent
18
+ agent_logger = logging.getLogger("neuro_agent")
19
+ agent_logger.setLevel(logging.DEBUG)
20
+
21
+ # Configure agent logging to use the shared queue
22
+ def configure_agent_logging():
23
+ """Configure agent logging to use the shared agent_log_queue"""
24
+ # Create a handler for the agent queue
25
+ agent_queue_handler = QueueLogHandler(agent_log_queue)
26
+ formatter = logging.Formatter('%(asctime)s - [AGENT] - %(levelname)s - %(message)s', datefmt='%H:%M:%S')
27
+ agent_queue_handler.setFormatter(formatter)
28
+
29
+ # Clear any existing handlers
30
+ if agent_logger.hasHandlers():
31
+ agent_logger.handlers.clear()
32
+
33
+ # Add the queue handler
34
+ agent_logger.addHandler(agent_queue_handler)
35
+ agent_logger.propagate = False # Prevent logs from propagating to root logger
36
+
37
+ print("Agent日志系统已配置,将日志输出到 agent_log_queue。")
38
+
39
+ # Configure agent logging when module is imported
40
+ configure_agent_logging()
41
+
42
+ class Agent:
43
+ """Main Agent class that integrates LLM, memory, and tools"""
44
+
45
+ def __init__(self, working_dir: str = None):
46
+ # Lazy imports to avoid circular dependencies
47
+ from .memory.manager import MemoryManager
48
+ from .tools.core import ToolManager
49
+ from .llm import LLMClient
50
+
51
+ self.memory_manager = MemoryManager(working_dir)
52
+ self.tool_manager = ToolManager(self.memory_manager)
53
+ self.llm_client = LLMClient()
54
+ self._initialized = False
55
+
56
+ # Log agent initialization
57
+ agent_logger.info("Agent initialized")
58
+ agent_logger.debug(f"Agent working directory: {working_dir}")
59
+
60
+ async def initialize(self):
61
+ """Initialize the agent, loading any persistent memory"""
62
+ if not self._initialized:
63
+ agent_logger.info("Initializing agent memory manager")
64
+ await self.memory_manager.initialize()
65
+ self._initialized = True
66
+ agent_logger.info("Agent initialized successfully")
67
+
68
+ async def reset_all_memory(self):
69
+ """Reset all agent memory types"""
70
+ # Reset temp memory
71
+ await self.memory_manager.reset_temp_memory()
72
+
73
+ # Reset context (dialog history)
74
+ await self.memory_manager.reset_context()
75
+
76
+ agent_logger.info("All agent memory reset successfully")
77
+ print("All agent memory reset successfully")
78
+
79
+ async def reset_memory(self):
80
+ """Reset agent temp memory (alias for backward compatibility)"""
81
+ await self.reset_all_memory()
82
+
83
+ async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
84
+ """
85
+ Process incoming messages and generate a response
86
+
87
+ Args:
88
+ messages: List of message dictionaries with 'username' and 'text' keys
89
+
90
+ Returns:
91
+ Dictionary containing processing details including tool executions and final response
92
+ """
93
+ # Ensure agent is initialized
94
+ await self.initialize()
95
+
96
+ agent_logger.info(f"Processing {len(messages)} messages")
97
+
98
+ # Add messages to context
99
+ for msg in messages:
100
+ content = f"{msg['username']}: {msg['text']}"
101
+ await self.memory_manager.add_context_entry("user", content)
102
+ agent_logger.debug(f"Added message to context: {content}")
103
+
104
+ # Send context update via WebSocket after adding user messages
105
+ from ..websocket_manager import connection_manager
106
+ context_messages = await self.memory_manager.get_recent_context()
107
+ await connection_manager.broadcast({
108
+ "type": "agent_context",
109
+ "action": "update",
110
+ "messages": context_messages
111
+ })
112
+
113
+ # Add detailed context entry for the start of processing
114
+ processing_entry_id = await self.memory_manager.add_detailed_context_entry(
115
+ input_messages=messages,
116
+ prompt="Processing started",
117
+ llm_response="",
118
+ tool_executions=[],
119
+ final_response="Processing started"
120
+ )
121
+
122
+ # Get full context for LLM
123
+ context = await self.memory_manager.get_full_context()
124
+ tool_descriptions = self.tool_manager.get_tool_descriptions()
125
+
126
+ # Get last agent response to avoid repetition
127
+ last_response = await self.memory_manager.get_last_agent_response()
128
+
129
+ # Create LLM prompt with context and tools
130
+ prompt = f"""You are {self.memory_manager.init_memory.get('name', 'Neuro-Sama')}, an AI VTuber.
131
+ Your personality: {self.memory_manager.init_memory.get('personality', 'Friendly and curious')}
132
+
133
+ === CONTEXT ===
134
+ {context}
135
+
136
+ === AVAILABLE TOOLS ===
137
+ {tool_descriptions}
138
+
139
+ === INSTRUCTIONS ===
140
+ Process the user messages and respond appropriately. You can use tools to manage memory or output responses.
141
+ When you want to speak to the user, use the 'speak' tool with your response as the text parameter.
142
+ When you want to update memory, use the appropriate memory management tools.
143
+ You are fully responsible for managing your own memory. Use the memory tools proactively when you need to:
144
+ - Remember important information from the conversation
145
+ - Update your knowledge or personality
146
+ - Store observations about users or events
147
+ - Retrieve relevant information to inform your responses
148
+ Always think about whether you need to use tools before responding.
149
+
150
+ IMPORTANT GUIDELINES:
151
+ - Be creative and engaging in your responses
152
+ - Avoid repeating the same phrases or ideas from your last response: "{last_response}" (if available)
153
+ - Keep responses concise and conversational
154
+ - Maintain your character's personality
155
+
156
+ User messages:
157
+ """
158
+
159
+ for msg in messages:
160
+ prompt += f"{msg['username']}: {msg['text']}\n"
161
+
162
+ prompt += "\nYour response (use tools as needed):"
163
+
164
+ agent_logger.debug("Sending prompt to LLM")
165
+
166
+ # Add detailed context entry for the prompt
167
+ await self.memory_manager.add_detailed_context_entry(
168
+ input_messages=messages,
169
+ prompt=prompt,
170
+ llm_response="",
171
+ tool_executions=[],
172
+ final_response="Prompt sent to LLM",
173
+ entry_id=processing_entry_id
174
+ )
175
+
176
+ # Generate response using LLM
177
+ response = await self.llm_client.generate(prompt)
178
+ agent_logger.debug(f"LLM response received: {response[:100] if response else 'None'}...")
179
+
180
+ # Add detailed context entry for the LLM response
181
+ await self.memory_manager.add_detailed_context_entry(
182
+ input_messages=messages,
183
+ prompt=prompt,
184
+ llm_response=response,
185
+ tool_executions=[],
186
+ final_response="LLM response received",
187
+ entry_id=processing_entry_id
188
+ )
189
+
190
+ # Parse the response to handle tool calls
191
+ # This is a simplified parser - in a full implementation, you would use a more robust method
192
+ processing_result = {
193
+ "input_messages": messages,
194
+ "llm_response": response,
195
+ "tool_executions": [],
196
+ "final_response": ""
197
+ }
198
+
199
+ # Extract tool calls from the response
200
+ # Look for tool calls in the response
201
+ lines = response.split('\n') if response else []
202
+ i = 0
203
+ json_buffer = "" # Buffer to accumulate multi-line JSON
204
+ in_json_block = False # Flag to track if we're inside a JSON block
205
+
206
+ while i < len(lines):
207
+ line = lines[i].strip()
208
+ agent_logger.debug(f"Parsing line: {line}")
209
+
210
+ # Handle JSON blocks
211
+ if line.startswith('```json'):
212
+ in_json_block = True
213
+ json_buffer = line + '\n'
214
+ elif line == '```' and in_json_block:
215
+ # End of JSON block
216
+ json_buffer += line
217
+ in_json_block = False
218
+ # Process the complete JSON block
219
+ tool_call = self._parse_tool_call(json_buffer)
220
+ if tool_call:
221
+ agent_logger.info(f"Executing tool: {tool_call['name']}")
222
+ await self._execute_parsed_tool(tool_call, processing_result)
223
+ # Update detailed context entry for tool execution
224
+ await self.memory_manager.add_detailed_context_entry(
225
+ input_messages=messages,
226
+ prompt=prompt,
227
+ llm_response=response,
228
+ tool_executions=processing_result["tool_executions"].copy(), # Pass a copy of current tool executions
229
+ final_response=f"Executed tool: {tool_call['name']}",
230
+ entry_id=processing_entry_id
231
+ )
232
+ else:
233
+ agent_logger.warning(f"Failed to parse tool call from JSON block: {json_buffer}")
234
+ elif in_json_block:
235
+ # Accumulate lines for JSON block
236
+ json_buffer += line + '\n'
237
+ else:
238
+ # Check if line contains a tool call
239
+ if any(line.startswith(prefix) for prefix in ["get_", "create_", "update_", "delete_", "add_", "remove_", "speak("]):
240
+ # Parse tool call
241
+ tool_call = self._parse_tool_call(line)
242
+ if tool_call:
243
+ agent_logger.info(f"Executing tool: {tool_call['name']}")
244
+ await self._execute_parsed_tool(tool_call, processing_result)
245
+ # Update detailed context entry for tool execution
246
+ await self.memory_manager.add_detailed_context_entry(
247
+ input_messages=messages,
248
+ prompt=prompt,
249
+ llm_response=response,
250
+ tool_executions=processing_result["tool_executions"].copy(), # Pass a copy of current tool executions
251
+ final_response=f"Executed tool: {tool_call['name']}",
252
+ entry_id=processing_entry_id
253
+ )
254
+ else:
255
+ agent_logger.warning(f"Failed to parse tool call from line: {line}")
256
+ i += 1
257
+
258
+ # If we're still in a JSON block at the end, process it
259
+ if in_json_block and json_buffer:
260
+ tool_call = self._parse_tool_call(json_buffer)
261
+ if tool_call:
262
+ agent_logger.info(f"Executing tool: {tool_call['name']}")
263
+ await self._execute_parsed_tool(tool_call, processing_result)
264
+ # Update detailed context entry for tool execution
265
+ await self.memory_manager.add_detailed_context_entry(
266
+ input_messages=messages,
267
+ prompt=prompt,
268
+ llm_response=response,
269
+ tool_executions=processing_result["tool_executions"].copy(), # Pass a copy of current tool executions
270
+ final_response=f"Executed tool: {tool_call['name']}",
271
+ entry_id=processing_entry_id
272
+ )
273
+ else:
274
+ agent_logger.warning(f"Failed to parse tool call from incomplete JSON block: {json_buffer}")
275
+
276
+ # If we have a final response, add it to context
277
+ if processing_result["final_response"]:
278
+ await self.memory_manager.add_context_entry("assistant", processing_result["final_response"])
279
+
280
+ # Update the detailed context entry with final LLM interaction details
281
+ await self.memory_manager.add_detailed_context_entry(
282
+ input_messages=messages,
283
+ prompt=prompt,
284
+ llm_response=response,
285
+ tool_executions=processing_result["tool_executions"],
286
+ final_response=processing_result["final_response"],
287
+ entry_id=processing_entry_id
288
+ )
289
+
290
+ # Send context update via WebSocket
291
+ from ..websocket_manager import connection_manager
292
+ context_messages = await self.memory_manager.get_recent_context()
293
+ await connection_manager.broadcast({
294
+ "type": "agent_context",
295
+ "action": "update",
296
+ "messages": context_messages
297
+ })
298
+
299
+ agent_logger.info("Message processing completed")
300
+ return processing_result
301
+
302
+ async def _execute_parsed_tool(self, tool_call: Dict[str, Any], processing_result: Dict[str, Any]):
303
+ """Execute a parsed tool call and update processing result"""
304
+ # Only prevent duplicate speak tool executions to avoid repeated responses
305
+ if tool_call["name"] == "speak":
306
+ for executed_tool in processing_result["tool_executions"]:
307
+ if (executed_tool["name"] == "speak" and
308
+ executed_tool["params"].get("text") == tool_call["params"].get("text")):
309
+ agent_logger.debug(f"Skipping duplicate speak tool execution: {tool_call['params'].get('text')}")
310
+ return
311
+
312
+ # Execute the tool
313
+ try:
314
+ tool_result = await self.execute_tool(tool_call["name"], tool_call["params"])
315
+ tool_call["result"] = tool_result
316
+
317
+ # If this is the speak tool, capture the final response
318
+ if tool_call["name"] == "speak":
319
+ processing_result["final_response"] = tool_call["params"].get("text", "")
320
+ agent_logger.info(f"Speak tool executed with text: {processing_result['final_response']}")
321
+ else:
322
+ agent_logger.debug(f"Tool execution result: {tool_result}")
323
+
324
+ processing_result["tool_executions"].append(tool_call)
325
+ except Exception as e:
326
+ tool_call["error"] = str(e)
327
+ processing_result["tool_executions"].append(tool_call)
328
+ agent_logger.error(f"Error executing tool {tool_call['name']}: {e}")
329
+
330
+ def _parse_tool_call(self, line: str) -> Optional[Dict[str, Any]]:
331
+ """Parse a tool call from a line of text"""
332
+ import re
333
+ import json
334
+
335
+ # First try to parse as JSON if it looks like JSON
336
+ line = line.strip()
337
+ if line.startswith('```json'):
338
+ try:
339
+ # Extract JSON content
340
+ json_content = line[7:] # Remove ```json
341
+ if json_content.endswith('```'):
342
+ json_content = json_content[:-3] # Remove trailing ```
343
+ json_content = json_content.strip()
344
+
345
+ # Parse the JSON
346
+ tool_call_data = json.loads(json_content)
347
+
348
+ # Handle different JSON formats
349
+ if isinstance(tool_call_data, dict):
350
+ # Check if it's a tool_code format
351
+ if 'tool_code' in tool_call_data:
352
+ # Extract the tool call from tool_code
353
+ tool_code = tool_call_data['tool_code']
354
+ # Remove any wrapper functions like print()
355
+ tool_code = re.sub(r'^\w+\((.*)\)$', r'\1', tool_code)
356
+ # Now parse the tool call normally
357
+ pattern = r'(\w+)\((.*)\)'
358
+ match = re.match(pattern, tool_code)
359
+ if match:
360
+ tool_name = match.group(1)
361
+ params_str = match.group(2)
362
+
363
+ # Parse parameters
364
+ params = {}
365
+ param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
366
+ for param_match in re.finditer(param_pattern, params_str):
367
+ key, value = param_match.groups()
368
+ # Remove quotes if present
369
+ if (value.startswith('"') and value.endswith('"')) or \
370
+ (value.startswith("'") and value.endswith("'")):
371
+ value = value[1:-1]
372
+ params[key] = value
373
+
374
+ return {
375
+ "name": tool_name,
376
+ "params": params
377
+ }
378
+ # Check if it's a name/arguments format
379
+ elif 'name' in tool_call_data and 'arguments' in tool_call_data:
380
+ return {
381
+ "name": tool_call_data['name'],
382
+ "params": tool_call_data['arguments']
383
+ }
384
+ elif isinstance(tool_call_data, list) and len(tool_call_data) > 0:
385
+ # Handle array format - take the first item
386
+ first_item = tool_call_data[0]
387
+ if isinstance(first_item, dict):
388
+ if 'tool_code' in first_item:
389
+ # Extract the tool call from tool_code
390
+ tool_code = first_item['tool_code']
391
+ # Remove any wrapper functions like print()
392
+ tool_code = re.sub(r'^\w+\((.*)\)$', r'\1', tool_code)
393
+ # Now parse the tool call normally
394
+ pattern = r'(\w+)\((.*)\)'
395
+ match = re.match(pattern, tool_code)
396
+ if match:
397
+ tool_name = match.group(1)
398
+ params_str = match.group(2)
399
+
400
+ # Parse parameters
401
+ params = {}
402
+ param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
403
+ for param_match in re.finditer(param_pattern, params_str):
404
+ key, value = param_match.groups()
405
+ # Remove quotes if present
406
+ if (value.startswith('"') and value.endswith('"')) or \
407
+ (value.startswith("'") and value.endswith("'")):
408
+ value = value[1:-1]
409
+ params[key] = value
410
+
411
+ return {
412
+ "name": tool_name,
413
+ "params": params
414
+ }
415
+ elif 'name' in first_item and 'arguments' in first_item:
416
+ return {
417
+ "name": first_item['name'],
418
+ "params": first_item['arguments']
419
+ }
420
+
421
+ except (json.JSONDecodeError, KeyError, IndexError):
422
+ pass # Fall back to regex parsing
423
+
424
+ # Handle multi-line JSON that might be split across several lines
425
+ if line == '```json' or line == '{' or line == '}':
426
+ # Skip these lines as they're part of JSON structure
427
+ return None
428
+
429
+ # Pattern to match tool_name(param1=value1, param2=value2, ...)
430
+ pattern = r'(\w+)\((.*)\)'
431
+ match = re.match(pattern, line)
432
+
433
+ if match:
434
+ tool_name = match.group(1)
435
+ params_str = match.group(2)
436
+
437
+ # Parse parameters more robustly
438
+ params = {}
439
+
440
+ # Handle parameters one by one
441
+ # This handles quoted strings correctly, including special characters
442
+ param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
443
+ for param_match in re.finditer(param_pattern, params_str):
444
+ key, value = param_match.groups()
445
+ # Remove quotes if present
446
+ if (value.startswith('"') and value.endswith('"')) or \
447
+ (value.startswith("'") and value.endswith("'")):
448
+ value = value[1:-1]
449
+ params[key] = value
450
+
451
+ return {
452
+ "name": tool_name,
453
+ "params": params
454
+ }
455
+
456
+ return None
457
+
458
+ async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
459
+ """Execute a registered tool"""
460
+ # Ensure agent is initialized
461
+ await self.initialize()
462
+ agent_logger.debug(f"Executing tool: {tool_name} with params: {params}")
463
+ result = await self.tool_manager.execute_tool(tool_name, params)
464
+ agent_logger.debug(f"Tool execution result: {result}")
465
+ return result
466
+
467
+ # Function to get agent logs (now uses the shared queue)
468
+ def get_agent_logs(lines: int = 50) -> List[str]:
469
+ """Get recent agent logs from the shared queue"""
470
+ logs_list = list(agent_log_queue)
471
+ return logs_list[-lines:] if len(logs_list) > lines else logs_list
@@ -0,0 +1,104 @@
1
+ # agent/llm.py
2
+ """
3
+ LLM client for the Neuro Simulator Agent
4
+ """
5
+
6
+ from typing import Optional
7
+ import os
8
+ import sys
9
+
10
+ # Add project root to path
11
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
12
+
13
+ from google import genai
14
+ from google.genai import types
15
+ from openai import AsyncOpenAI
16
+ from ..config import config_manager
17
+
18
+ class LLMClient:
19
+ """A completely independent LLM client for the built-in agent."""
20
+
21
+ def __init__(self):
22
+ self.client = None
23
+ self.model_name = None
24
+ self._initialize_client()
25
+
26
+ def _initialize_client(self):
27
+ """Initializes the LLM client based on the 'agent' section of the config."""
28
+ settings = config_manager.settings
29
+ provider = settings.agent.agent_provider.lower()
30
+
31
+ if provider == "gemini":
32
+ api_key = settings.api_keys.gemini_api_key
33
+ if not api_key:
34
+ raise ValueError("GEMINI_API_KEY is not set in configuration for the agent.")
35
+
36
+ # Use the new client-based API as per the latest documentation
37
+ self.client = genai.Client(api_key=api_key)
38
+ self.model_name = settings.agent.agent_model
39
+ self._generate_func = self._generate_gemini
40
+
41
+ elif provider == "openai":
42
+ api_key = settings.api_keys.openai_api_key
43
+ if not api_key:
44
+ raise ValueError("OPENAI_API_KEY is not set in configuration for the agent.")
45
+
46
+ self.model_name = settings.agent.agent_model
47
+ self.client = AsyncOpenAI(
48
+ api_key=api_key,
49
+ base_url=settings.api_keys.openai_api_base_url
50
+ )
51
+ self._generate_func = self._generate_openai
52
+ else:
53
+ raise ValueError(f"Unsupported agent provider in config: {settings.agent.agent_provider}")
54
+
55
+ print(f"Agent LLM client initialized. Provider: {provider.upper()}, Model: {self.model_name}")
56
+
57
+ async def _generate_gemini(self, prompt: str, max_tokens: int) -> str:
58
+ """Generates text using the Gemini model with the new SDK."""
59
+ import asyncio
60
+
61
+ generation_config = types.GenerateContentConfig(
62
+ max_output_tokens=max_tokens,
63
+ # temperature can be added later if needed from config
64
+ )
65
+
66
+ try:
67
+ # The new client's generate_content is synchronous, run it in a thread
68
+ response = await asyncio.to_thread(
69
+ self.client.models.generate_content,
70
+ model=self.model_name,
71
+ contents=prompt,
72
+ config=generation_config
73
+ )
74
+ return response.text if response and response.text else ""
75
+ except Exception as e:
76
+ print(f"Error in _generate_gemini: {e}")
77
+ return ""
78
+
79
+ async def _generate_openai(self, prompt: str, max_tokens: int) -> str:
80
+ try:
81
+ response = await self.client.chat.completions.create(
82
+ model=self.model_name,
83
+ messages=[{"role": "user", "content": prompt}],
84
+ max_tokens=max_tokens,
85
+ # temperature can be added to config if needed
86
+ )
87
+ if response.choices and response.choices[0].message and response.choices[0].message.content:
88
+ return response.choices[0].message.content.strip()
89
+ return ""
90
+ except Exception as e:
91
+ print(f"Error in _generate_openai: {e}")
92
+ return ""
93
+
94
+ async def generate(self, prompt: str, max_tokens: int = 1000) -> str:
95
+ """Generate text using the configured LLM."""
96
+ if not self.client:
97
+ raise RuntimeError("LLM Client is not initialized.")
98
+ try:
99
+ result = await self._generate_func(prompt, max_tokens)
100
+ # Ensure we always return a string, even if the result is None
101
+ return result if result is not None else ""
102
+ except Exception as e:
103
+ print(f"Error generating text with Agent LLM: {e}")
104
+ return "My brain is not working, tell Vedal to check the logs."
@@ -0,0 +1,4 @@
1
+ # agent/memory/__init__.py
2
+ """
3
+ Memory module for the Neuro Simulator Agent
4
+ """