neuro-simulator 0.1.3__py3-none-any.whl → 0.2.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/__init__.py +1 -10
- neuro_simulator/agent/__init__.py +1 -8
- neuro_simulator/agent/base.py +43 -0
- neuro_simulator/agent/core.py +105 -398
- 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 +61 -203
- 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 +60 -143
- 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.3.dist-info → neuro_simulator-0.2.1.dist-info}/METADATA +83 -33
- neuro_simulator-0.2.1.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.3.dist-info/RECORD +0 -31
- /neuro_simulator/{config.py → core/config.py} +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/WHEEL +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/entry_points.txt +0 -0
- {neuro_simulator-0.1.3.dist-info → neuro_simulator-0.2.1.dist-info}/top_level.txt +0 -0
neuro_simulator/agent/core.py
CHANGED
@@ -1,18 +1,25 @@
|
|
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 pathlib import Path
|
12
|
+
from datetime import datetime
|
13
|
+
from typing import Any, Dict, List, Optional
|
13
14
|
|
14
|
-
#
|
15
|
-
from ..
|
15
|
+
# Updated imports for the new structure
|
16
|
+
from ..utils.logging import QueueLogHandler, agent_log_queue
|
17
|
+
from ..utils.websocket import connection_manager
|
18
|
+
|
19
|
+
# --- Agent-specific imports ---
|
20
|
+
from .llm import LLMClient
|
21
|
+
from .memory.manager import MemoryManager
|
22
|
+
from .tools.core import ToolManager
|
16
23
|
|
17
24
|
# Create a logger for the agent
|
18
25
|
agent_logger = logging.getLogger("neuro_agent")
|
@@ -20,475 +27,175 @@ agent_logger.setLevel(logging.DEBUG)
|
|
20
27
|
|
21
28
|
# Configure agent logging to use the shared queue
|
22
29
|
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
|
+
"""Configure agent logging to use the shared agent_log_queue."""
|
30
31
|
if agent_logger.hasHandlers():
|
31
32
|
agent_logger.handlers.clear()
|
32
33
|
|
33
|
-
|
34
|
+
agent_queue_handler = QueueLogHandler(agent_log_queue)
|
35
|
+
# Use the same format as the server for consistency
|
36
|
+
formatter = logging.Formatter('%(asctime)s - [%(name)-24s] - %(levelname)-8s - %(message)s', datefmt='%H:%M:%S')
|
37
|
+
agent_queue_handler.setFormatter(formatter)
|
34
38
|
agent_logger.addHandler(agent_queue_handler)
|
35
|
-
agent_logger.propagate = False
|
36
|
-
|
37
|
-
print("Agent日志系统已配置,将日志输出到 agent_log_queue。")
|
39
|
+
agent_logger.propagate = False
|
40
|
+
agent_logger.info("Agent logging configured to use agent_log_queue.")
|
38
41
|
|
39
|
-
# Configure agent logging when module is imported
|
40
42
|
configure_agent_logging()
|
41
43
|
|
42
44
|
class Agent:
|
43
|
-
"""Main Agent class that integrates LLM, memory, and tools"""
|
45
|
+
"""Main Agent class that integrates LLM, memory, and tools. This is the concrete implementation."""
|
44
46
|
|
45
47
|
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
48
|
self.memory_manager = MemoryManager(working_dir)
|
52
49
|
self.tool_manager = ToolManager(self.memory_manager)
|
53
50
|
self.llm_client = LLMClient()
|
54
51
|
self._initialized = False
|
55
|
-
|
56
|
-
# Log agent initialization
|
57
|
-
agent_logger.info("Agent initialized")
|
52
|
+
agent_logger.info("Agent instance created.")
|
58
53
|
agent_logger.debug(f"Agent working directory: {working_dir}")
|
59
54
|
|
60
55
|
async def initialize(self):
|
61
|
-
"""Initialize the agent, loading any persistent memory"""
|
56
|
+
"""Initialize the agent, loading any persistent memory."""
|
62
57
|
if not self._initialized:
|
63
|
-
agent_logger.info("Initializing agent memory manager")
|
58
|
+
agent_logger.info("Initializing agent memory manager...")
|
64
59
|
await self.memory_manager.initialize()
|
65
60
|
self._initialized = True
|
66
|
-
agent_logger.info("Agent initialized successfully")
|
61
|
+
agent_logger.info("Agent initialized successfully.")
|
67
62
|
|
68
63
|
async def reset_all_memory(self):
|
69
|
-
"""Reset all agent memory types"""
|
70
|
-
# Reset temp memory
|
64
|
+
"""Reset all agent memory types."""
|
71
65
|
await self.memory_manager.reset_temp_memory()
|
72
|
-
|
73
|
-
# Reset context (dialog history)
|
74
66
|
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()
|
67
|
+
agent_logger.info("All agent memory has been reset.")
|
82
68
|
|
83
69
|
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
|
70
|
+
"""Process incoming messages and generate a response with tool usage."""
|
94
71
|
await self.initialize()
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
# Add messages to context
|
72
|
+
agent_logger.info(f"Processing {len(messages)} messages.")
|
73
|
+
|
99
74
|
for msg in messages:
|
100
75
|
content = f"{msg['username']}: {msg['text']}"
|
101
76
|
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
|
77
|
+
|
106
78
|
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
|
-
})
|
79
|
+
await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": context_messages})
|
112
80
|
|
113
|
-
# Add detailed context entry for the start of processing
|
114
81
|
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"
|
82
|
+
input_messages=messages, prompt="Processing started", llm_response="",
|
83
|
+
tool_executions=[], final_response="Processing started"
|
120
84
|
)
|
121
85
|
|
122
|
-
# Get full context for LLM
|
123
86
|
context = await self.memory_manager.get_full_context()
|
124
87
|
tool_descriptions = self.tool_manager.get_tool_descriptions()
|
125
88
|
|
126
|
-
#
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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.
|
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"])
|
149
97
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
- Pay close attention to the conversation history in the context to understand the flow of the dialogue
|
155
|
-
- Respond to the most recent user messages while considering the overall context of the conversation
|
98
|
+
# Create LLM prompt from template
|
99
|
+
template_path = Path(self.memory_manager.memory_dir).parent / "prompt_template.txt"
|
100
|
+
with open(template_path, 'r', encoding='utf-8') as f:
|
101
|
+
prompt_template = f.read()
|
156
102
|
|
157
|
-
|
158
|
-
"""
|
103
|
+
recent_speak_history_text = "\n".join([f"- {response}" for response in assistant_responses[:5]]) if assistant_responses else "You haven't said anything yet."
|
104
|
+
user_messages_text = "\n".join([f"{msg['username']}: {msg['text']}" for msg in messages])
|
159
105
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
for entry in reversed(recent_context): # Reverse to get newest first
|
167
|
-
if entry.get("type") == "llm_interaction" and entry.get("role") == "assistant":
|
168
|
-
tool_executions = entry.get("tool_executions", [])
|
169
|
-
for tool_execution in tool_executions:
|
170
|
-
if tool_execution.get("name") == "speak":
|
171
|
-
result = tool_execution.get("result")
|
172
|
-
if result:
|
173
|
-
assistant_responses.append(result)
|
174
|
-
|
175
|
-
# Add up to 64 most recent assistant responses
|
176
|
-
for i, response in enumerate(assistant_responses[:64]):
|
177
|
-
prompt += f"{i+1}. {response}\n"
|
178
|
-
|
179
|
-
prompt += f"\nUser messages to respond to:\n"
|
180
|
-
|
181
|
-
for msg in messages:
|
182
|
-
prompt += f"{msg['username']}: {msg['text']}\n"
|
183
|
-
|
184
|
-
prompt += "\nYour response (use tools as needed):"
|
185
|
-
|
186
|
-
agent_logger.debug("Sending prompt to LLM")
|
106
|
+
prompt = prompt_template.format(
|
107
|
+
full_context=context,
|
108
|
+
tool_descriptions=tool_descriptions,
|
109
|
+
recent_speak_history=recent_speak_history_text,
|
110
|
+
user_messages=user_messages_text
|
111
|
+
)
|
187
112
|
|
188
|
-
# Add detailed context entry for the prompt
|
189
113
|
await self.memory_manager.add_detailed_context_entry(
|
190
|
-
input_messages=messages,
|
191
|
-
|
192
|
-
llm_response="",
|
193
|
-
tool_executions=[],
|
194
|
-
final_response="Prompt sent to LLM",
|
195
|
-
entry_id=processing_entry_id
|
114
|
+
input_messages=messages, prompt=prompt, llm_response="", tool_executions=[],
|
115
|
+
final_response="Prompt sent to LLM", entry_id=processing_entry_id
|
196
116
|
)
|
197
117
|
|
198
|
-
|
199
|
-
response
|
200
|
-
agent_logger.debug(f"LLM response received: {response[:100] if response else 'None'}...")
|
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'}...")
|
201
120
|
|
202
|
-
# Add detailed context entry for the LLM response
|
203
121
|
await self.memory_manager.add_detailed_context_entry(
|
204
|
-
input_messages=messages,
|
205
|
-
|
206
|
-
llm_response=response,
|
207
|
-
tool_executions=[],
|
208
|
-
final_response="LLM response received",
|
209
|
-
entry_id=processing_entry_id
|
122
|
+
input_messages=messages, prompt=prompt, llm_response=response_text, tool_executions=[],
|
123
|
+
final_response="LLM response received", entry_id=processing_entry_id
|
210
124
|
)
|
211
125
|
|
212
|
-
# Parse the response to handle tool calls
|
213
|
-
# This is a simplified parser - in a full implementation, you would use a more robust method
|
214
126
|
processing_result = {
|
215
|
-
"input_messages": messages,
|
216
|
-
"
|
217
|
-
"tool_executions": [],
|
218
|
-
"final_response": ""
|
127
|
+
"input_messages": messages, "llm_response": response_text,
|
128
|
+
"tool_executions": [], "final_response": ""
|
219
129
|
}
|
220
130
|
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
i = 0
|
225
|
-
json_buffer = "" # Buffer to accumulate multi-line JSON
|
226
|
-
in_json_block = False # Flag to track if we're inside a JSON block
|
227
|
-
|
228
|
-
while i < len(lines):
|
229
|
-
line = lines[i].strip()
|
230
|
-
agent_logger.debug(f"Parsing line: {line}")
|
231
|
-
|
232
|
-
# Handle JSON blocks
|
233
|
-
if line.startswith('```json'):
|
234
|
-
in_json_block = True
|
235
|
-
json_buffer = line + '\n'
|
236
|
-
elif line == '```' and in_json_block:
|
237
|
-
# End of JSON block
|
238
|
-
json_buffer += line
|
239
|
-
in_json_block = False
|
240
|
-
# Process the complete JSON block
|
241
|
-
tool_call = self._parse_tool_call(json_buffer)
|
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 JSON block: {json_buffer}")
|
256
|
-
elif in_json_block:
|
257
|
-
# Accumulate lines for JSON block
|
258
|
-
json_buffer += line + '\n'
|
259
|
-
else:
|
260
|
-
# Check if line contains a tool call
|
261
|
-
if any(line.startswith(prefix) for prefix in ["get_", "create_", "update_", "delete_", "add_", "remove_", "speak("]):
|
262
|
-
# Parse tool call
|
263
|
-
tool_call = self._parse_tool_call(line)
|
264
|
-
if tool_call:
|
265
|
-
agent_logger.info(f"Executing tool: {tool_call['name']}")
|
266
|
-
await self._execute_parsed_tool(tool_call, processing_result)
|
267
|
-
# Update detailed context entry for tool execution
|
268
|
-
await self.memory_manager.add_detailed_context_entry(
|
269
|
-
input_messages=messages,
|
270
|
-
prompt=prompt,
|
271
|
-
llm_response=response,
|
272
|
-
tool_executions=processing_result["tool_executions"].copy(), # Pass a copy of current tool executions
|
273
|
-
final_response=f"Executed tool: {tool_call['name']}",
|
274
|
-
entry_id=processing_entry_id
|
275
|
-
)
|
276
|
-
else:
|
277
|
-
agent_logger.warning(f"Failed to parse tool call from line: {line}")
|
278
|
-
i += 1
|
279
|
-
|
280
|
-
# If we're still in a JSON block at the end, process it
|
281
|
-
if in_json_block and json_buffer:
|
282
|
-
tool_call = self._parse_tool_call(json_buffer)
|
283
|
-
if tool_call:
|
131
|
+
if response_text:
|
132
|
+
tool_calls = self._parse_tool_calls(response_text)
|
133
|
+
for tool_call in tool_calls:
|
284
134
|
agent_logger.info(f"Executing tool: {tool_call['name']}")
|
285
135
|
await self._execute_parsed_tool(tool_call, processing_result)
|
286
|
-
|
287
|
-
await self.memory_manager.add_detailed_context_entry(
|
288
|
-
input_messages=messages,
|
289
|
-
prompt=prompt,
|
290
|
-
llm_response=response,
|
291
|
-
tool_executions=processing_result["tool_executions"].copy(), # Pass a copy of current tool executions
|
292
|
-
final_response=f"Executed tool: {tool_call['name']}",
|
293
|
-
entry_id=processing_entry_id
|
294
|
-
)
|
295
|
-
else:
|
296
|
-
agent_logger.warning(f"Failed to parse tool call from incomplete JSON block: {json_buffer}")
|
297
|
-
|
298
|
-
# If we have a final response, we don't need to add it to context here
|
299
|
-
# because add_detailed_context_entry will add a short entry for us
|
300
|
-
# if processing_result["final_response"]:
|
301
|
-
# await self.memory_manager.add_context_entry("assistant", processing_result["final_response"])
|
302
|
-
|
303
|
-
# Update the detailed context entry with final LLM interaction details
|
136
|
+
|
304
137
|
await self.memory_manager.add_detailed_context_entry(
|
305
|
-
input_messages=messages,
|
306
|
-
prompt=prompt,
|
307
|
-
llm_response=response,
|
138
|
+
input_messages=messages, prompt=prompt, llm_response=response_text,
|
308
139
|
tool_executions=processing_result["tool_executions"],
|
309
|
-
final_response=processing_result["final_response"],
|
310
|
-
entry_id=processing_entry_id
|
140
|
+
final_response=processing_result["final_response"], entry_id=processing_entry_id
|
311
141
|
)
|
312
142
|
|
313
|
-
|
314
|
-
|
315
|
-
context_messages = await self.memory_manager.get_recent_context()
|
316
|
-
await connection_manager.broadcast({
|
317
|
-
"type": "agent_context",
|
318
|
-
"action": "update",
|
319
|
-
"messages": context_messages
|
320
|
-
})
|
143
|
+
final_context = await self.memory_manager.get_recent_context()
|
144
|
+
await connection_manager.broadcast({"type": "agent_context", "action": "update", "messages": final_context})
|
321
145
|
|
322
|
-
agent_logger.info("Message processing completed")
|
146
|
+
agent_logger.info("Message processing completed.")
|
323
147
|
return processing_result
|
324
148
|
|
325
149
|
async def _execute_parsed_tool(self, tool_call: Dict[str, Any], processing_result: Dict[str, Any]):
|
326
|
-
"""Execute a parsed tool call and update processing result"""
|
327
|
-
# Only prevent duplicate speak tool executions to avoid repeated responses
|
328
|
-
if tool_call["name"] == "speak":
|
329
|
-
for executed_tool in processing_result["tool_executions"]:
|
330
|
-
if (executed_tool["name"] == "speak" and
|
331
|
-
executed_tool["params"].get("text") == tool_call["params"].get("text")):
|
332
|
-
agent_logger.debug(f"Skipping duplicate speak tool execution: {tool_call['params'].get('text')}")
|
333
|
-
return
|
334
|
-
|
335
|
-
# Execute the tool
|
150
|
+
"""Execute a parsed tool call and update processing result."""
|
336
151
|
try:
|
337
152
|
tool_result = await self.execute_tool(tool_call["name"], tool_call["params"])
|
338
153
|
tool_call["result"] = tool_result
|
339
|
-
|
340
|
-
# If this is the speak tool, capture the final response
|
341
154
|
if tool_call["name"] == "speak":
|
342
155
|
processing_result["final_response"] = tool_call["params"].get("text", "")
|
343
|
-
agent_logger.info(f"Speak tool executed with text: {processing_result['final_response']}")
|
344
|
-
else:
|
345
|
-
agent_logger.debug(f"Tool execution result: {tool_result}")
|
346
|
-
|
347
156
|
processing_result["tool_executions"].append(tool_call)
|
348
157
|
except Exception as e:
|
349
158
|
tool_call["error"] = str(e)
|
350
159
|
processing_result["tool_executions"].append(tool_call)
|
351
160
|
agent_logger.error(f"Error executing tool {tool_call['name']}: {e}")
|
352
161
|
|
353
|
-
def
|
354
|
-
"""Parse
|
355
|
-
import
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
line = line.strip()
|
360
|
-
if line.startswith('```json'):
|
162
|
+
def _parse_tool_calls(self, text: str) -> List[Dict[str, Any]]:
|
163
|
+
"""Parse tool calls using ast.literal_eval for robustness."""
|
164
|
+
import ast
|
165
|
+
calls = []
|
166
|
+
text = text.strip()
|
167
|
+
if text.startswith("speak(") and text.endswith(")"):
|
361
168
|
try:
|
362
|
-
# Extract
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
tool_call_data = json.loads(json_content)
|
169
|
+
# Extract the content inside speak(...)
|
170
|
+
# e.g., "text='Hello, I'm here'"
|
171
|
+
inner_content = text[len("speak("):-1].strip()
|
172
|
+
|
173
|
+
# Ensure it's a text=... call
|
174
|
+
if not inner_content.startswith("text="):
|
175
|
+
return []
|
370
176
|
|
371
|
-
#
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
# Remove any wrapper functions like print()
|
378
|
-
tool_code = re.sub(r'^\w+\((.*)\)$', r'\1', tool_code)
|
379
|
-
# Now parse the tool call normally
|
380
|
-
pattern = r'(\w+)\((.*)\)'
|
381
|
-
match = re.match(pattern, tool_code)
|
382
|
-
if match:
|
383
|
-
tool_name = match.group(1)
|
384
|
-
params_str = match.group(2)
|
385
|
-
|
386
|
-
# Parse parameters
|
387
|
-
params = {}
|
388
|
-
param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
|
389
|
-
for param_match in re.finditer(param_pattern, params_str):
|
390
|
-
key, value = param_match.groups()
|
391
|
-
# Remove quotes if present
|
392
|
-
if (value.startswith('"') and value.endswith('"')) or \
|
393
|
-
(value.startswith("'") and value.endswith("'")):
|
394
|
-
value = value[1:-1]
|
395
|
-
params[key] = value
|
396
|
-
|
397
|
-
return {
|
398
|
-
"name": tool_name,
|
399
|
-
"params": params
|
400
|
-
}
|
401
|
-
# Check if it's a name/arguments format
|
402
|
-
elif 'name' in tool_call_data and 'arguments' in tool_call_data:
|
403
|
-
return {
|
404
|
-
"name": tool_call_data['name'],
|
405
|
-
"params": tool_call_data['arguments']
|
406
|
-
}
|
407
|
-
elif isinstance(tool_call_data, list) and len(tool_call_data) > 0:
|
408
|
-
# Handle array format - take the first item
|
409
|
-
first_item = tool_call_data[0]
|
410
|
-
if isinstance(first_item, dict):
|
411
|
-
if 'tool_code' in first_item:
|
412
|
-
# Extract the tool call from tool_code
|
413
|
-
tool_code = first_item['tool_code']
|
414
|
-
# Remove any wrapper functions like print()
|
415
|
-
tool_code = re.sub(r'^\w+\((.*)\)$', r'\1', tool_code)
|
416
|
-
# Now parse the tool call normally
|
417
|
-
pattern = r'(\w+)\((.*)\)'
|
418
|
-
match = re.match(pattern, tool_code)
|
419
|
-
if match:
|
420
|
-
tool_name = match.group(1)
|
421
|
-
params_str = match.group(2)
|
422
|
-
|
423
|
-
# Parse parameters
|
424
|
-
params = {}
|
425
|
-
param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
|
426
|
-
for param_match in re.finditer(param_pattern, params_str):
|
427
|
-
key, value = param_match.groups()
|
428
|
-
# Remove quotes if present
|
429
|
-
if (value.startswith('"') and value.endswith('"')) or \
|
430
|
-
(value.startswith("'") and value.endswith("'")):
|
431
|
-
value = value[1:-1]
|
432
|
-
params[key] = value
|
433
|
-
|
434
|
-
return {
|
435
|
-
"name": tool_name,
|
436
|
-
"params": params
|
437
|
-
}
|
438
|
-
elif 'name' in first_item and 'arguments' in first_item:
|
439
|
-
return {
|
440
|
-
"name": first_item['name'],
|
441
|
-
"params": first_item['arguments']
|
442
|
-
}
|
443
|
-
|
444
|
-
except (json.JSONDecodeError, KeyError, IndexError):
|
445
|
-
pass # Fall back to regex parsing
|
446
|
-
|
447
|
-
# Handle multi-line JSON that might be split across several lines
|
448
|
-
if line == '```json' or line == '{' or line == '}':
|
449
|
-
# Skip these lines as they're part of JSON structure
|
450
|
-
return None
|
451
|
-
|
452
|
-
# Pattern to match tool_name(param1=value1, param2=value2, ...)
|
453
|
-
pattern = r'(\w+)\((.*)\)'
|
454
|
-
match = re.match(pattern, line)
|
455
|
-
|
456
|
-
if match:
|
457
|
-
tool_name = match.group(1)
|
458
|
-
params_str = match.group(2)
|
459
|
-
|
460
|
-
# Parse parameters more robustly
|
461
|
-
params = {}
|
462
|
-
|
463
|
-
# Handle parameters one by one
|
464
|
-
# This handles quoted strings correctly, including special characters
|
465
|
-
param_pattern = r'(\w+)\s*=\s*(".*?"|\'.*?\'|[^,]+?)(?:,|$)'
|
466
|
-
for param_match in re.finditer(param_pattern, params_str):
|
467
|
-
key, value = param_match.groups()
|
468
|
-
# Remove quotes if present
|
469
|
-
if (value.startswith('"') and value.endswith('"')) or \
|
470
|
-
(value.startswith("'") and value.endswith("'")):
|
471
|
-
value = value[1:-1]
|
472
|
-
params[key] = value
|
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)
|
473
183
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
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
|
480
194
|
|
481
195
|
async def execute_tool(self, tool_name: str, params: Dict[str, Any]) -> Any:
|
482
|
-
"""Execute a registered tool"""
|
483
|
-
# Ensure agent is initialized
|
196
|
+
"""Execute a registered tool."""
|
484
197
|
await self.initialize()
|
485
198
|
agent_logger.debug(f"Executing tool: {tool_name} with params: {params}")
|
486
199
|
result = await self.tool_manager.execute_tool(tool_name, params)
|
487
200
|
agent_logger.debug(f"Tool execution result: {result}")
|
488
201
|
return result
|
489
|
-
|
490
|
-
# Function to get agent logs (now uses the shared queue)
|
491
|
-
def get_agent_logs(lines: int = 50) -> List[str]:
|
492
|
-
"""Get recent agent logs from the shared queue"""
|
493
|
-
logs_list = list(agent_log_queue)
|
494
|
-
return logs_list[-lines:] if len(logs_list) > lines else logs_list
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# agent/factory.py
|
2
|
+
"""Factory for creating agent instances"""
|
3
|
+
|
4
|
+
from .base import BaseAgent
|
5
|
+
from ..config import config_manager
|
6
|
+
|
7
|
+
|
8
|
+
async def create_agent() -> BaseAgent:
|
9
|
+
"""Create an agent instance based on the configuration"""
|
10
|
+
agent_type = config_manager.settings.agent_type
|
11
|
+
|
12
|
+
if agent_type == "builtin":
|
13
|
+
from ..builtin_agent import local_agent, BuiltinAgentWrapper, initialize_builtin_agent
|
14
|
+
if local_agent is None:
|
15
|
+
# Try to initialize the builtin agent
|
16
|
+
await initialize_builtin_agent()
|
17
|
+
# Re-import local_agent after initialization
|
18
|
+
from ..builtin_agent import local_agent
|
19
|
+
if local_agent is None:
|
20
|
+
raise RuntimeError("Failed to initialize Builtin agent")
|
21
|
+
return BuiltinAgentWrapper(local_agent)
|
22
|
+
elif agent_type == "letta":
|
23
|
+
from ..letta import get_letta_agent, initialize_letta_client
|
24
|
+
# Try to initialize the letta client
|
25
|
+
initialize_letta_client()
|
26
|
+
agent = get_letta_agent()
|
27
|
+
await agent.initialize()
|
28
|
+
return agent
|
29
|
+
else:
|
30
|
+
raise ValueError(f"Unknown agent type: {agent_type}")
|