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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. neuro_simulator/__init__.py +1 -10
  2. neuro_simulator/agent/__init__.py +1 -8
  3. neuro_simulator/agent/base.py +43 -0
  4. neuro_simulator/agent/core.py +111 -374
  5. neuro_simulator/agent/factory.py +30 -0
  6. neuro_simulator/agent/llm.py +34 -31
  7. neuro_simulator/agent/memory/__init__.py +1 -4
  8. neuro_simulator/agent/memory/manager.py +64 -230
  9. neuro_simulator/agent/tools/__init__.py +1 -4
  10. neuro_simulator/agent/tools/core.py +8 -18
  11. neuro_simulator/api/__init__.py +1 -0
  12. neuro_simulator/api/agent.py +163 -0
  13. neuro_simulator/api/stream.py +55 -0
  14. neuro_simulator/api/system.py +90 -0
  15. neuro_simulator/cli.py +53 -142
  16. neuro_simulator/core/__init__.py +1 -0
  17. neuro_simulator/core/agent_factory.py +52 -0
  18. neuro_simulator/core/agent_interface.py +91 -0
  19. neuro_simulator/core/application.py +278 -0
  20. neuro_simulator/services/__init__.py +1 -0
  21. neuro_simulator/{chatbot.py → services/audience.py} +24 -24
  22. neuro_simulator/{audio_synthesis.py → services/audio.py} +18 -15
  23. neuro_simulator/services/builtin.py +87 -0
  24. neuro_simulator/services/letta.py +206 -0
  25. neuro_simulator/{stream_manager.py → services/stream.py} +39 -47
  26. neuro_simulator/utils/__init__.py +1 -0
  27. neuro_simulator/utils/logging.py +90 -0
  28. neuro_simulator/utils/process.py +67 -0
  29. neuro_simulator/{stream_chat.py → utils/queue.py} +17 -4
  30. neuro_simulator/utils/state.py +14 -0
  31. neuro_simulator/{websocket_manager.py → utils/websocket.py} +18 -14
  32. {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/METADATA +176 -176
  33. neuro_simulator-0.2.0.dist-info/RECORD +37 -0
  34. neuro_simulator/agent/api.py +0 -737
  35. neuro_simulator/agent/memory.py +0 -137
  36. neuro_simulator/agent/tools.py +0 -69
  37. neuro_simulator/builtin_agent.py +0 -83
  38. neuro_simulator/config.yaml.example +0 -157
  39. neuro_simulator/letta.py +0 -164
  40. neuro_simulator/log_handler.py +0 -43
  41. neuro_simulator/main.py +0 -673
  42. neuro_simulator/media/neuro_start.mp4 +0 -0
  43. neuro_simulator/process_manager.py +0 -70
  44. neuro_simulator/shared_state.py +0 -11
  45. neuro_simulator-0.1.2.dist-info/RECORD +0 -31
  46. /neuro_simulator/{config.py → core/config.py} +0 -0
  47. {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/WHEEL +0 -0
  48. {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/entry_points.txt +0 -0
  49. {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,10 +1 @@
1
- # neuro_simulator/__init__.py
2
-
3
- # 导出主要模块以便于使用
4
- from .builtin_agent import initialize_builtin_agent, get_builtin_response, reset_builtin_agent_memory
5
-
6
- __all__ = [
7
- "initialize_builtin_agent",
8
- "get_builtin_response",
9
- "reset_builtin_agent_memory"
10
- ]
1
+ # neuro_simulator package root
@@ -1,8 +1 @@
1
- # agent/__init__.py
2
- """
3
- Agent module for Neuro Simulator Server
4
- """
5
-
6
- from .core import Agent
7
-
8
- __all__ = ["Agent"]
1
+ # neuro_simulator.agent package
@@ -0,0 +1,43 @@
1
+ # agent/base.py
2
+ """Base classes for Neuro Simulator Agent"""
3
+
4
+ from abc import ABC, abstractmethod
5
+ from typing import List, Dict, Any, Optional
6
+
7
+
8
+ class BaseAgent(ABC):
9
+ """Abstract base class for all agents"""
10
+
11
+ @abstractmethod
12
+ async def initialize(self):
13
+ """Initialize the agent"""
14
+ pass
15
+
16
+ @abstractmethod
17
+ async def reset_memory(self):
18
+ """Reset agent memory"""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def get_response(self, chat_messages: List[Dict[str, str]]) -> Dict[str, Any]:
23
+ """Get response from the agent
24
+
25
+ Args:
26
+ chat_messages: List of message dictionaries with 'username' and 'text' keys
27
+
28
+ Returns:
29
+ Dictionary containing processing details including tool executions and final response
30
+ """
31
+ pass
32
+
33
+ @abstractmethod
34
+ async def process_messages(self, messages: List[Dict[str, str]]) -> Dict[str, Any]:
35
+ """Process messages and generate a response
36
+
37
+ Args:
38
+ messages: List of message dictionaries with 'username' and 'text' keys
39
+
40
+ Returns:
41
+ Dictionary containing processing details including tool executions and final response
42
+ """
43
+ pass
@@ -1,18 +1,24 @@
1
- # agent/core.py
1
+ # neuro_simulator/agent/core.py
2
2
  """
3
- Core module for the Neuro Simulator Agent
3
+ Core module for the Neuro Simulator's built-in agent.
4
4
  """
5
5
 
6
- import os
7
- import json
8
6
  import asyncio
9
- from typing import Dict, List, Any, Optional
10
- from datetime import datetime
11
- import sys
7
+ import json
12
8
  import logging
9
+ import re
10
+ import sys
11
+ from datetime import datetime
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ # Updated imports for the new structure
15
+ from ..utils.logging import QueueLogHandler, agent_log_queue
16
+ from ..utils.websocket import connection_manager
13
17
 
14
- # Import the shared log queue from the main log_handler
15
- from ..log_handler import agent_log_queue, QueueLogHandler
18
+ # --- Agent-specific imports ---
19
+ from .llm import LLMClient
20
+ from .memory.manager import MemoryManager
21
+ from .tools.core import ToolManager
16
22
 
17
23
  # Create a logger for the agent
18
24
  agent_logger = logging.getLogger("neuro_agent")
@@ -20,452 +26,183 @@ agent_logger.setLevel(logging.DEBUG)
20
26
 
21
27
  # Configure agent logging to use the shared queue
22
28
  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
29
+ """Configure agent logging to use the shared agent_log_queue."""
30
30
  if agent_logger.hasHandlers():
31
31
  agent_logger.handlers.clear()
32
32
 
33
- # Add the queue handler
33
+ agent_queue_handler = QueueLogHandler(agent_log_queue)
34
+ # Use the same format as the server for consistency
35
+ formatter = logging.Formatter('%(asctime)s - [%(name)-24s] - %(levelname)-8s - %(message)s', datefmt='%H:%M:%S')
36
+ agent_queue_handler.setFormatter(formatter)
34
37
  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
+ agent_logger.propagate = False
39
+ agent_logger.info("Agent logging configured to use agent_log_queue.")
38
40
 
39
- # Configure agent logging when module is imported
40
41
  configure_agent_logging()
41
42
 
42
43
  class Agent:
43
- """Main Agent class that integrates LLM, memory, and tools"""
44
+ """Main Agent class that integrates LLM, memory, and tools. This is the concrete implementation."""
44
45
 
45
46
  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
47
  self.memory_manager = MemoryManager(working_dir)
52
48
  self.tool_manager = ToolManager(self.memory_manager)
53
49
  self.llm_client = LLMClient()
54
50
  self._initialized = False
55
-
56
- # Log agent initialization
57
- agent_logger.info("Agent initialized")
51
+ agent_logger.info("Agent instance created.")
58
52
  agent_logger.debug(f"Agent working directory: {working_dir}")
59
53
 
60
54
  async def initialize(self):
61
- """Initialize the agent, loading any persistent memory"""
55
+ """Initialize the agent, loading any persistent memory."""
62
56
  if not self._initialized:
63
- agent_logger.info("Initializing agent memory manager")
57
+ agent_logger.info("Initializing agent memory manager...")
64
58
  await self.memory_manager.initialize()
65
59
  self._initialized = True
66
- agent_logger.info("Agent initialized successfully")
60
+ agent_logger.info("Agent initialized successfully.")
67
61
 
68
62
  async def reset_all_memory(self):
69
- """Reset all agent memory types"""
70
- # Reset temp memory
63
+ """Reset all agent memory types."""
71
64
  await self.memory_manager.reset_temp_memory()
72
-
73
- # Reset context (dialog history)
74
65
  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()
66
+ agent_logger.info("All agent memory has been reset.")
82
67
 
83
68
  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
69
+ """Process incoming messages and generate a response with tool usage."""
94
70
  await self.initialize()
95
-
96
- agent_logger.info(f"Processing {len(messages)} messages")
97
-
98
- # Add messages to context
71
+ agent_logger.info(f"Processing {len(messages)} messages.")
72
+
99
73
  for msg in messages:
100
74
  content = f"{msg['username']}: {msg['text']}"
101
75
  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
76
+
106
77
  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
- })
78
+ await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": context_messages})
112
79
 
113
- # Add detailed context entry for the start of processing
114
80
  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"
81
+ input_messages=messages, prompt="Processing started", llm_response="",
82
+ tool_executions=[], final_response="Processing started"
120
83
  )
121
84
 
122
- # Get full context for LLM
123
85
  context = await self.memory_manager.get_full_context()
124
86
  tool_descriptions = self.tool_manager.get_tool_descriptions()
125
87
 
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}
88
+ # --- CORRECTED HISTORY GATHERING ---
89
+ recent_history = await self.memory_manager.get_detailed_context_history()
90
+ assistant_responses = []
91
+ for entry in reversed(recent_history):
92
+ if entry.get("type") == "llm_interaction":
93
+ for tool in entry.get("tool_executions", []):
94
+ if tool.get("name") == "speak" and tool.get("result"):
95
+ assistant_responses.append(tool["result"])
138
96
 
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.
97
+ # Create LLM prompt
98
+ prompt_parts = [
99
+ f"You are {self.memory_manager.init_memory.get('name', 'Neuro-Sama')}, an AI VTuber.",
100
+ f"Your personality: {self.memory_manager.init_memory.get('personality', 'Friendly and curious')}",
101
+ "\n=== CONTEXT ===", context,
102
+ "\n=== AVAILABLE TOOLS ===", tool_descriptions,
103
+ "\n=== YOUR RECENT SPEAK HISTORY (for context) ==="
104
+ ]
105
+ for response in assistant_responses[:5]: # Get last 5 responses
106
+ prompt_parts.append(f"- {response}")
149
107
 
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
108
+ prompt_parts.extend([
109
+ "\n=== INSTRUCTIONS ===",
110
+ "Process the user messages and respond. Use the 'speak' tool to talk to the user.",
111
+ "You are fully responsible for managing your own memory using the available tools.",
112
+ "\nUser messages to respond to:",
113
+ ])
155
114
 
156
- User messages:
157
- """
158
-
159
115
  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")
116
+ prompt_parts.append(f"{msg['username']}: {msg['text']}")
117
+ prompt_parts.append("\nYour response (use tools as needed):")
118
+ prompt = "\n".join(prompt_parts)
165
119
 
166
- # Add detailed context entry for the prompt
167
120
  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
121
+ input_messages=messages, prompt=prompt, llm_response="", tool_executions=[],
122
+ final_response="Prompt sent to LLM", entry_id=processing_entry_id
174
123
  )
175
124
 
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'}...")
125
+ response_text = await self.llm_client.generate(prompt)
126
+ agent_logger.debug(f"LLM raw response: {response_text[:100] if response_text else 'None'}...")
179
127
 
180
- # Add detailed context entry for the LLM response
181
128
  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
129
+ input_messages=messages, prompt=prompt, llm_response=response_text, tool_executions=[],
130
+ final_response="LLM response received", entry_id=processing_entry_id
188
131
  )
189
132
 
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
133
  processing_result = {
193
- "input_messages": messages,
194
- "llm_response": response,
195
- "tool_executions": [],
196
- "final_response": ""
134
+ "input_messages": messages, "llm_response": response_text,
135
+ "tool_executions": [], "final_response": ""
197
136
  }
198
137
 
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:
138
+ if response_text:
139
+ tool_calls = self._parse_tool_calls(response_text)
140
+ for tool_call in tool_calls:
262
141
  agent_logger.info(f"Executing tool: {tool_call['name']}")
263
142
  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
143
+
281
144
  await self.memory_manager.add_detailed_context_entry(
282
- input_messages=messages,
283
- prompt=prompt,
284
- llm_response=response,
145
+ input_messages=messages, prompt=prompt, llm_response=response_text,
285
146
  tool_executions=processing_result["tool_executions"],
286
- final_response=processing_result["final_response"],
287
- entry_id=processing_entry_id
147
+ final_response=processing_result["final_response"], entry_id=processing_entry_id
288
148
  )
289
149
 
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
- })
150
+ final_context = await self.memory_manager.get_recent_context()
151
+ await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": final_context})
298
152
 
299
- agent_logger.info("Message processing completed")
153
+ agent_logger.info("Message processing completed.")
300
154
  return processing_result
301
155
 
302
156
  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
157
+ """Execute a parsed tool call and update processing result."""
313
158
  try:
314
159
  tool_result = await self.execute_tool(tool_call["name"], tool_call["params"])
315
160
  tool_call["result"] = tool_result
316
-
317
- # If this is the speak tool, capture the final response
318
161
  if tool_call["name"] == "speak":
319
162
  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
163
  processing_result["tool_executions"].append(tool_call)
325
164
  except Exception as e:
326
165
  tool_call["error"] = str(e)
327
166
  processing_result["tool_executions"].append(tool_call)
328
167
  agent_logger.error(f"Error executing tool {tool_call['name']}: {e}")
329
168
 
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'):
169
+ def _parse_tool_calls(self, text: str) -> List[Dict[str, Any]]:
170
+ """Parse tool calls using ast.literal_eval for robustness."""
171
+ import ast
172
+ calls = []
173
+ text = text.strip()
174
+ if text.startswith("speak(") and text.endswith(")"):
338
175
  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)
176
+ # Extract the content inside speak(...)
177
+ # e.g., "text='Hello, I'm here'"
178
+ inner_content = text[len("speak("):-1].strip()
179
+
180
+ # Ensure it's a text=... call
181
+ if not inner_content.startswith("text="):
182
+ return []
347
183
 
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
184
+ # Get the quoted string part
185
+ quoted_string = inner_content[len("text="):
186
+ ].strip()
187
+
188
+ # Use ast.literal_eval to safely parse the Python string literal
189
+ parsed_text = ast.literal_eval(quoted_string)
450
190
 
451
- return {
452
- "name": tool_name,
453
- "params": params
454
- }
455
-
456
- return None
191
+ if isinstance(parsed_text, str):
192
+ calls.append({
193
+ "name": "speak",
194
+ "params": {"text": parsed_text}
195
+ })
196
+
197
+ except (ValueError, SyntaxError, TypeError) as e:
198
+ agent_logger.warning(f"Could not parse tool call using ast.literal_eval: {text}. Error: {e}")
199
+
200
+ return calls
457
201
 
458
202
  async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
459
- """Execute a registered tool"""
460
- # Ensure agent is initialized
203
+ """Execute a registered tool."""
461
204
  await self.initialize()
462
205
  agent_logger.debug(f"Executing tool: {tool_name} with params: {params}")
463
206
  result = await self.tool_manager.execute_tool(tool_name, params)
464
207
  agent_logger.debug(f"Tool execution result: {result}")
465
208
  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