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.
- neuro_simulator/__init__.py +1 -10
- neuro_simulator/agent/__init__.py +1 -8
- neuro_simulator/agent/base.py +43 -0
- neuro_simulator/agent/core.py +111 -374
- neuro_simulator/agent/factory.py +30 -0
- neuro_simulator/agent/llm.py +34 -31
- neuro_simulator/agent/memory/__init__.py +1 -4
- neuro_simulator/agent/memory/manager.py +64 -230
- neuro_simulator/agent/tools/__init__.py +1 -4
- neuro_simulator/agent/tools/core.py +8 -18
- neuro_simulator/api/__init__.py +1 -0
- neuro_simulator/api/agent.py +163 -0
- neuro_simulator/api/stream.py +55 -0
- neuro_simulator/api/system.py +90 -0
- neuro_simulator/cli.py +53 -142
- neuro_simulator/core/__init__.py +1 -0
- neuro_simulator/core/agent_factory.py +52 -0
- neuro_simulator/core/agent_interface.py +91 -0
- neuro_simulator/core/application.py +278 -0
- neuro_simulator/services/__init__.py +1 -0
- neuro_simulator/{chatbot.py → services/audience.py} +24 -24
- neuro_simulator/{audio_synthesis.py → services/audio.py} +18 -15
- neuro_simulator/services/builtin.py +87 -0
- neuro_simulator/services/letta.py +206 -0
- neuro_simulator/{stream_manager.py → services/stream.py} +39 -47
- neuro_simulator/utils/__init__.py +1 -0
- neuro_simulator/utils/logging.py +90 -0
- neuro_simulator/utils/process.py +67 -0
- neuro_simulator/{stream_chat.py → utils/queue.py} +17 -4
- neuro_simulator/utils/state.py +14 -0
- neuro_simulator/{websocket_manager.py → utils/websocket.py} +18 -14
- {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/METADATA +176 -176
- neuro_simulator-0.2.0.dist-info/RECORD +37 -0
- neuro_simulator/agent/api.py +0 -737
- neuro_simulator/agent/memory.py +0 -137
- neuro_simulator/agent/tools.py +0 -69
- neuro_simulator/builtin_agent.py +0 -83
- neuro_simulator/config.yaml.example +0 -157
- neuro_simulator/letta.py +0 -164
- neuro_simulator/log_handler.py +0 -43
- neuro_simulator/main.py +0 -673
- neuro_simulator/media/neuro_start.mp4 +0 -0
- neuro_simulator/process_manager.py +0 -70
- neuro_simulator/shared_state.py +0 -11
- neuro_simulator-0.1.2.dist-info/RECORD +0 -31
- /neuro_simulator/{config.py → core/config.py} +0 -0
- {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.1.2.dist-info → neuro_simulator-0.2.0.dist-info}/top_level.txt +0 -0
neuro_simulator/__init__.py
CHANGED
@@ -1,10 +1 @@
|
|
1
|
-
# neuro_simulator
|
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
|
@@ -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
|
neuro_simulator/agent/core.py
CHANGED
@@ -1,18 +1,24 @@
|
|
1
|
-
# agent/core.py
|
1
|
+
# neuro_simulator/agent/core.py
|
2
2
|
"""
|
3
|
-
Core module for the Neuro Simulator
|
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
|
-
|
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
|
-
#
|
15
|
-
from
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
161
|
-
|
162
|
-
prompt
|
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
|
-
|
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
|
-
|
177
|
-
response
|
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
|
-
|
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
|
-
"
|
195
|
-
"tool_executions": [],
|
196
|
-
"final_response": ""
|
134
|
+
"input_messages": messages, "llm_response": response_text,
|
135
|
+
"tool_executions": [], "final_response": ""
|
197
136
|
}
|
198
137
|
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
-
|
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
|
-
|
291
|
-
|
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
|
331
|
-
"""Parse
|
332
|
-
import
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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
|
-
#
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
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
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
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
|