mindroot 10.5.0__py3-none-any.whl → 10.14.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.
- mindroot/coreplugins/admin/static/js/agent-form.js +26 -1
- mindroot/coreplugins/admin/static/js/persona-editor.js +14 -1
- mindroot/coreplugins/agent/agent.py +46 -21
- mindroot/coreplugins/agent/buagentz1.py +540 -0
- mindroot/coreplugins/agent/speech_to_speech.py +77 -0
- mindroot/coreplugins/agent/speech_to_speech.py.backup +46 -0
- mindroot/coreplugins/chat/buservices.py +625 -0
- mindroot/coreplugins/chat/commands.py +4 -0
- mindroot/coreplugins/chat/services.py +116 -10
- mindroot/coreplugins/chat/static/css/dark.css +90 -1
- mindroot/coreplugins/chat/static/css/default.css +90 -1
- mindroot/coreplugins/chat/static/css/light.css +724 -0
- mindroot/coreplugins/chat/static/js/chat.js +114 -86
- mindroot/coreplugins/chat/static/js/throttle.js +4 -2
- mindroot/coreplugins/chat/templates/chat.jinja2 +8 -0
- mindroot/lib/chatcontext.py +1 -0
- mindroot/lib/chatlog.py +40 -0
- mindroot/lib/logging/logfiles.py +1 -0
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/METADATA +1 -1
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/RECORD +24 -19
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/WHEEL +0 -0
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/entry_points.txt +0 -0
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/licenses/LICENSE +0 -0
- {mindroot-10.5.0.dist-info → mindroot-10.14.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
from json import JSONDecodeError
|
|
7
|
+
from jinja2 import Template
|
|
8
|
+
from lib.providers.commands import command_manager, command
|
|
9
|
+
from lib.providers.hooks import hook_manager
|
|
10
|
+
from lib.pipelines.pipe import pipeline_manager
|
|
11
|
+
from lib.providers.services import service
|
|
12
|
+
from lib.providers.services import service_manager
|
|
13
|
+
from lib.json_str_block import replace_raw_blocks
|
|
14
|
+
import sys
|
|
15
|
+
from lib.utils.check_args import *
|
|
16
|
+
from .command_parser import parse_streaming_commands, invalid_start_format
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
import pytz
|
|
19
|
+
import traceback
|
|
20
|
+
from lib.logging.logfiles import logger
|
|
21
|
+
from lib.utils.debug import debug_box
|
|
22
|
+
from .init_models import *
|
|
23
|
+
from lib.chatcontext import ChatContext
|
|
24
|
+
from .cmd_start_example import *
|
|
25
|
+
from lib.templates import render
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
error_result = """
|
|
29
|
+
[SYSTEM]: ERROR, invalid response format.
|
|
30
|
+
|
|
31
|
+
Your response does not appear to adhere to the command list format.
|
|
32
|
+
|
|
33
|
+
Common causes:
|
|
34
|
+
|
|
35
|
+
- replied with JSON inside of fenced code blocks instead of JSON or RAW string format as below
|
|
36
|
+
|
|
37
|
+
- ONLY if your model supports this, for complex multiline string arguments, use the RAW format described in system instructions, e.g.:
|
|
38
|
+
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
{ "json_encoded_md": { "markdown": START_RAW
|
|
42
|
+
The moon, so bright
|
|
43
|
+
It's shining light
|
|
44
|
+
Like a pizza pie
|
|
45
|
+
In the sky
|
|
46
|
+
END_RAW
|
|
47
|
+
} }
|
|
48
|
+
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
- iF your model does not support RAW format or it is not a complex multiline string like code, you MUST properly escape JSON strings!
|
|
52
|
+
- remember newlines, double quotes, etc. must be escaped (but not double escaped)!
|
|
53
|
+
|
|
54
|
+
- plain text response before JSON.
|
|
55
|
+
|
|
56
|
+
- some JSON args with unescaped newlines, etc.
|
|
57
|
+
|
|
58
|
+
- multiple command lists. Only one command list response is allowed!
|
|
59
|
+
- This is a frequent cause of parse errors.
|
|
60
|
+
|
|
61
|
+
- some characters escaped that did not need to be/invalid
|
|
62
|
+
|
|
63
|
+
Please adhere to the system JSON command list response format carefully.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@service()
|
|
67
|
+
async def get_agent_data(agent_name, context=None):
|
|
68
|
+
logger.info("Agent name: {agent_name}", agent_name=agent_name)
|
|
69
|
+
|
|
70
|
+
agent_path = os.path.join('data/agents', 'local', agent_name)
|
|
71
|
+
|
|
72
|
+
if not os.path.exists(agent_path):
|
|
73
|
+
agent_path = os.path.join('data/agents', 'shared', agent_name)
|
|
74
|
+
if not os.path.exists(agent_path):
|
|
75
|
+
return {}
|
|
76
|
+
agent_file = os.path.join(agent_path, 'agent.json')
|
|
77
|
+
if not os.path.exists(agent_file):
|
|
78
|
+
return {}
|
|
79
|
+
with open(agent_file, 'r') as f:
|
|
80
|
+
agent_data = json.load(f)
|
|
81
|
+
|
|
82
|
+
# Ensure required_plugins is present
|
|
83
|
+
if 'required_plugins' not in agent_data:
|
|
84
|
+
agent_data['required_plugins'] = []
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
agent_data["persona"] = await service_manager.get_persona_data(agent_data["persona"])
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error("Error getting persona data", extra={"error": str(e)})
|
|
90
|
+
raise e
|
|
91
|
+
|
|
92
|
+
agent_data["flags"] = agent_data["flags"]
|
|
93
|
+
agent_data["flags"] = list(dict.fromkeys(agent_data["flags"]))
|
|
94
|
+
return agent_data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def find_new_substring(s1, s2):
|
|
99
|
+
if s1 in s2:
|
|
100
|
+
return s2.replace(s1, '', 1)
|
|
101
|
+
return s2
|
|
102
|
+
|
|
103
|
+
class Agent:
|
|
104
|
+
|
|
105
|
+
def __init__(self, model=None, sys_core_template=None, agent=None, clear_model=False, commands=[], context=None):
|
|
106
|
+
if model is None:
|
|
107
|
+
if os.environ.get('AH_DEFAULT_LLM'):
|
|
108
|
+
self.model = os.environ.get('AH_DEFAULT_LLM')
|
|
109
|
+
else:
|
|
110
|
+
self.model = 'llama3'
|
|
111
|
+
else:
|
|
112
|
+
self.model = model
|
|
113
|
+
|
|
114
|
+
self.agent = agent
|
|
115
|
+
|
|
116
|
+
#if sys_core_template is None:
|
|
117
|
+
# system_template_path = os.path.join(os.path.dirname(__file__), "system.j2")
|
|
118
|
+
# with open(system_template_path, "r") as f:
|
|
119
|
+
# self.sys_core_template = f.read()
|
|
120
|
+
#else:
|
|
121
|
+
# self.sys_core_template = sys_core_template
|
|
122
|
+
|
|
123
|
+
#self.sys_template = Template(self.sys_core_template)
|
|
124
|
+
|
|
125
|
+
self.cmd_handler = {}
|
|
126
|
+
self.context = context
|
|
127
|
+
|
|
128
|
+
#if clear_model:
|
|
129
|
+
# logger.debug("Unloading model")
|
|
130
|
+
# asyncio.create_task(use_ollama.unload(self.model))
|
|
131
|
+
|
|
132
|
+
def use_model(self, model_id, local=True):
|
|
133
|
+
self.current_model = model_id
|
|
134
|
+
|
|
135
|
+
async def set_cmd_handler(self, cmd_name, callback):
|
|
136
|
+
self.cmd_handler[cmd_name] = callback
|
|
137
|
+
logger.info("Recorded handler for command: {command}", command=cmd_name)
|
|
138
|
+
|
|
139
|
+
async def unload_llm_if_needed(self):
|
|
140
|
+
logger.info("Not unloading LLM")
|
|
141
|
+
#await use_ollama.unload(self.model)
|
|
142
|
+
#await asyncio.sleep(1)
|
|
143
|
+
|
|
144
|
+
async def handle_cmds(self, cmd_name, cmd_args, json_cmd=None, context=None):
|
|
145
|
+
# Check both permanent finish and temporary cancellation
|
|
146
|
+
if context.data.get('finished_conversation') or context.data.get('cancel_current_turn'):
|
|
147
|
+
logger.warning("Conversation is finished, not executing command")
|
|
148
|
+
print("\033[91mConversation is finished, not executing command\033[0m")
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
logger.info("Command execution: {command}", command=cmd_name)
|
|
152
|
+
logger.debug("Command details: {details}", details={
|
|
153
|
+
"command": cmd_name,
|
|
154
|
+
"arguments": cmd_args,
|
|
155
|
+
"context": str(context)
|
|
156
|
+
})
|
|
157
|
+
context.chat_log.add_message({"role": "assistant", "content": [{"type": "text",
|
|
158
|
+
"text": '['+json_cmd+']' }]})
|
|
159
|
+
command_manager.context = context
|
|
160
|
+
|
|
161
|
+
if cmd_name == "reasoning":
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
# cmd_args might be a single arg like integer or string, or it may be an array, or an object/dict with named args
|
|
165
|
+
try:
|
|
166
|
+
if isinstance(cmd_args, list):
|
|
167
|
+
#filter out empty strings
|
|
168
|
+
cmd_args = [x for x in cmd_args if x != '']
|
|
169
|
+
logger.debug("Executing command with list arguments", extra={"step": 1})
|
|
170
|
+
await context.running_command(cmd_name, cmd_args)
|
|
171
|
+
logger.debug("Executing command with list arguments", extra={"step": 2})
|
|
172
|
+
return await command_manager.execute(cmd_name, *cmd_args)
|
|
173
|
+
elif isinstance(cmd_args, dict):
|
|
174
|
+
logger.debug("Executing command with dict arguments", extra={"step": 1})
|
|
175
|
+
await context.running_command(cmd_name, cmd_args)
|
|
176
|
+
logger.debug("Executing command with dict arguments", extra={"step": 2})
|
|
177
|
+
return await command_manager.execute(cmd_name, **cmd_args)
|
|
178
|
+
else:
|
|
179
|
+
logger.debug("Executing command with single argument", extra={"step": 1})
|
|
180
|
+
await context.running_command(cmd_name, cmd_args)
|
|
181
|
+
logger.debug("Executing command with single argument", extra={"step": 2})
|
|
182
|
+
return await command_manager.execute(cmd_name, cmd_args)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
trace = traceback.format_exc()
|
|
186
|
+
print("\033[96mError in handle_cmds: " + str(e) + "\033[0m")
|
|
187
|
+
print("\033[96m" + trace + "\033[0m")
|
|
188
|
+
logger.error("Error in handle_cmds", extra={
|
|
189
|
+
"error": str(e),
|
|
190
|
+
"command": cmd_name,
|
|
191
|
+
"arguments": cmd_args,
|
|
192
|
+
"traceback": trace
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
return {"error": str(e)}
|
|
196
|
+
|
|
197
|
+
def remove_braces(self, buffer):
|
|
198
|
+
if buffer.endswith("\n"):
|
|
199
|
+
buffer = buffer[:-1]
|
|
200
|
+
if buffer.startswith('[ '):
|
|
201
|
+
buffer = buffer[2:]
|
|
202
|
+
if buffer.startswith(' ['):
|
|
203
|
+
buffer = buffer[2:]
|
|
204
|
+
if buffer.endswith(','):
|
|
205
|
+
buffer = buffer[:-1]
|
|
206
|
+
if buffer.endswith(']'):
|
|
207
|
+
buffer = buffer[:-1]
|
|
208
|
+
if buffer.startswith('['):
|
|
209
|
+
buffer = buffer[1:]
|
|
210
|
+
if buffer.endswith('},'):
|
|
211
|
+
buffer = buffer[:-1]
|
|
212
|
+
return buffer
|
|
213
|
+
|
|
214
|
+
async def parse_single_cmd(self, json_str, context, buffer, match=None):
|
|
215
|
+
cmd_name = '?'
|
|
216
|
+
try:
|
|
217
|
+
cmd_obj = json.loads(json_str)
|
|
218
|
+
cmd_name = next(iter(cmd_obj))
|
|
219
|
+
if isinstance(cmd_obj, list):
|
|
220
|
+
cmd_obj = cmd_obj[0]
|
|
221
|
+
cmd_name = next(iter(cmd_obj))
|
|
222
|
+
|
|
223
|
+
cmd_args = cmd_obj[cmd_name]
|
|
224
|
+
# make sure that cmd_name is in self.agent["commands"]
|
|
225
|
+
if cmd_name not in self.agent["commands"]:
|
|
226
|
+
logger.warning("Command not found in agent commands", extra={"command": cmd_name})
|
|
227
|
+
return None, buffer
|
|
228
|
+
if check_empty_args(cmd_args):
|
|
229
|
+
logger.info("Empty arguments for command", extra={"command": cmd_name})
|
|
230
|
+
return None, buffer
|
|
231
|
+
else:
|
|
232
|
+
logger.info("Non-empty arguments for command", extra={"command": cmd_name, "arguments": cmd_args})
|
|
233
|
+
# Handle the full command
|
|
234
|
+
result = await self.handle_cmds(cmd_name, cmd_args, json_cmd=json_str, context=context)
|
|
235
|
+
await context.command_result(cmd_name, result)
|
|
236
|
+
|
|
237
|
+
cmd = {"cmd": cmd_name, "result": result}
|
|
238
|
+
# Remove the processed JSON object from the buffer
|
|
239
|
+
if match is not None:
|
|
240
|
+
buffer = buffer[match.end():]
|
|
241
|
+
buffer = buffer.lstrip(',').rstrip(',')
|
|
242
|
+
return [cmd], buffer
|
|
243
|
+
except Exception as e:
|
|
244
|
+
trace = traceback.format_exc()
|
|
245
|
+
logger.error("Error processing command", extra={"error": str(e) + "\n\n" + trace})
|
|
246
|
+
|
|
247
|
+
json_str = '[' + json_str + ']'
|
|
248
|
+
|
|
249
|
+
return None, buffer
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def parse_cmd_stream(self, stream, context):
|
|
253
|
+
buffer = ""
|
|
254
|
+
results = []
|
|
255
|
+
full_cmds = []
|
|
256
|
+
|
|
257
|
+
num_processed = 0
|
|
258
|
+
parse_failed = False
|
|
259
|
+
debug_box("Parsing command stream")
|
|
260
|
+
debug_box(str(context))
|
|
261
|
+
original_buffer = ""
|
|
262
|
+
|
|
263
|
+
async for part in stream:
|
|
264
|
+
buffer += part
|
|
265
|
+
original_buffer += part
|
|
266
|
+
|
|
267
|
+
logger.debug(f"Current buffer: ||{buffer}||")
|
|
268
|
+
|
|
269
|
+
if invalid_start_format(buffer):
|
|
270
|
+
print("Found invalid start to buffer", buffer)
|
|
271
|
+
context.chat_log.add_message({"role": "assistant", "content": buffer})
|
|
272
|
+
started_with = f"Your invalid command started with: {buffer[0:20]}"
|
|
273
|
+
results.append({"cmd": "UNKNOWN", "args": { "invalid": "(" }, "result": error_result + "\n\n" + started_with})
|
|
274
|
+
return results, full_cmds
|
|
275
|
+
|
|
276
|
+
if len(buffer) > 0 and buffer[0] == '{':
|
|
277
|
+
buffer = "[" + buffer
|
|
278
|
+
|
|
279
|
+
# happened with Qwen 3 for some reason
|
|
280
|
+
buffer = buffer.replace('}] <>\n\n[{','}, {')
|
|
281
|
+
buffer = buffer.replace('}] <>\n[{','}, {')
|
|
282
|
+
|
|
283
|
+
commands, partial_cmd = parse_streaming_commands(buffer)
|
|
284
|
+
|
|
285
|
+
if isinstance(commands, int):
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
if not isinstance(commands, list):
|
|
289
|
+
commands = [commands]
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
if len(commands) == 1 and 'commands' in commands[0]:
|
|
293
|
+
commands = commands[0]['commands']
|
|
294
|
+
except Exception as e:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
logger.debug(f"commands: {commands}, partial_cmd: {partial_cmd}")
|
|
298
|
+
|
|
299
|
+
# Check for cancellation (either permanent or current turn)
|
|
300
|
+
if context.data.get('finished_conversation') or context.data.get('cancel_current_turn'):
|
|
301
|
+
# Clear the temporary cancel flag so next turn can proceed
|
|
302
|
+
if 'cancel_current_turn' in context.data:
|
|
303
|
+
del context.data['cancel_current_turn']
|
|
304
|
+
logger.warning("Conversation is finished or halted, exiting stream parsing")
|
|
305
|
+
debug_box(f"""Conversation is finished or halted, exiting stream""")
|
|
306
|
+
debug_box(str(context))
|
|
307
|
+
# stream is actually a generator
|
|
308
|
+
if partial_cmd is not None:
|
|
309
|
+
cmd_name = next(iter(partial_cmd))
|
|
310
|
+
if cmd_name in ["say", "json_encoded_md", "think"]:
|
|
311
|
+
context.chat_log.add_message({"role": "assistant", "content": str(partial_cmd[cmd_name])})
|
|
312
|
+
else:
|
|
313
|
+
context.chat_log.add_message({"role": "assistant", "content": str(partial_cmd) + "(Interrupted)"})
|
|
314
|
+
try:
|
|
315
|
+
stream.close()
|
|
316
|
+
except Exception as e:
|
|
317
|
+
print("\033[91mError closing stream\033[0m")
|
|
318
|
+
|
|
319
|
+
return results, full_cmds
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if len(commands) > num_processed:
|
|
323
|
+
logger.debug("New command(s) found")
|
|
324
|
+
logger.debug(f"Commands: {commands}")
|
|
325
|
+
for i in range(num_processed, len(commands)):
|
|
326
|
+
try:
|
|
327
|
+
cmd = commands[i]
|
|
328
|
+
try:
|
|
329
|
+
cmd_name = next(iter(cmd))
|
|
330
|
+
except Exception as e:
|
|
331
|
+
print("next iter failed. cmd is")
|
|
332
|
+
print(cmd)
|
|
333
|
+
break
|
|
334
|
+
if isinstance(cmd, str):
|
|
335
|
+
print("\033[91m" + "Invalid command format, expected object, trying to parse anyway" + "\033[0m")
|
|
336
|
+
print("\033[91m" + str(cmd) + "\033[0m")
|
|
337
|
+
cmd = json.loads(cmd)
|
|
338
|
+
cmd_name = next(iter(cmd))
|
|
339
|
+
cmd_args = cmd[cmd_name]
|
|
340
|
+
logger.debug(f"Processing command: {cmd}")
|
|
341
|
+
await context.partial_command(cmd_name, json.dumps(cmd_args), cmd_args)
|
|
342
|
+
|
|
343
|
+
self.handle_cmds(cmd_name, cmd_args, json_cmd=json.dumps(cmd), context=context)
|
|
344
|
+
|
|
345
|
+
cmd_task = asyncio.create_task(
|
|
346
|
+
self.handle_cmds(cmd_name, cmd_args, json_cmd=json.dumps(cmd), context=context)
|
|
347
|
+
)
|
|
348
|
+
context.data['active_command_task'] = cmd_task
|
|
349
|
+
try:
|
|
350
|
+
result = await cmd_task
|
|
351
|
+
finally:
|
|
352
|
+
# Clear the task from context once it's done or cancelled
|
|
353
|
+
if context.data.get('active_command_task') == cmd_task:
|
|
354
|
+
del context.data['active_command_task']
|
|
355
|
+
|
|
356
|
+
await context.command_result(cmd_name, result)
|
|
357
|
+
sys_header = "Note: tool command results follow, not user replies"
|
|
358
|
+
sys_header = ""
|
|
359
|
+
|
|
360
|
+
if result == "SYSTEM: WARNING - Command interrupted!\n\n":
|
|
361
|
+
logger.warning("Command was interrupted. Skipping any extra commands in list.")
|
|
362
|
+
await context.chat_log.drop_last('assistant')
|
|
363
|
+
return results, full_cmds
|
|
364
|
+
break
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
full_cmds.append({ "SYSTEM": sys_header, "cmd": cmd_name, "args": cmd_args, "result": result})
|
|
368
|
+
if result is not None:
|
|
369
|
+
results.append({"SYSTEM": sys_header, "cmd": cmd_name, "args": { "omitted": "(see command msg.)"}, "result": result})
|
|
370
|
+
|
|
371
|
+
num_processed = len(commands)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
trace = traceback.format_exc()
|
|
374
|
+
logger.error(f"Error processing command: {e} \n{trace}")
|
|
375
|
+
logger.error(str(e))
|
|
376
|
+
pass
|
|
377
|
+
else:
|
|
378
|
+
logger.debug("No new commands found")
|
|
379
|
+
# sometimes partial_cmd is actually a string for some reason
|
|
380
|
+
# definitely skip that
|
|
381
|
+
# check if partial_cmd is a string
|
|
382
|
+
is_string = isinstance(partial_cmd, str)
|
|
383
|
+
if partial_cmd is not None and partial_cmd != {} and not is_string:
|
|
384
|
+
logger.debug(f"Partial command {partial_cmd}")
|
|
385
|
+
try:
|
|
386
|
+
cmd_name = next(iter(partial_cmd))
|
|
387
|
+
cmd_args = partial_cmd[cmd_name]
|
|
388
|
+
logger.debug(f"Partial command detected: {partial_cmd}")
|
|
389
|
+
await context.partial_command(cmd_name, json.dumps(cmd_args), cmd_args)
|
|
390
|
+
except Exception as de:
|
|
391
|
+
logger.error("Failed to parse partial command")
|
|
392
|
+
logger.error(str(de))
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
#print("\033[92m" + str(full_cmds) + "\033[0m")
|
|
396
|
+
# getting false positive on this check
|
|
397
|
+
reasonOnly = False
|
|
398
|
+
try:
|
|
399
|
+
cmd_name = next(iter(full_cmds[0]))
|
|
400
|
+
if cmd_name == 'reasoning':
|
|
401
|
+
reasonOnly = True
|
|
402
|
+
for cmd in full_cmds:
|
|
403
|
+
if cmd_name != 'reasoning':
|
|
404
|
+
reasonOnly = False
|
|
405
|
+
break
|
|
406
|
+
except Exception as e:
|
|
407
|
+
pass
|
|
408
|
+
if len(full_cmds) == 0 or reasonOnly:
|
|
409
|
+
print("\033[91m" + "No results and parse failed" + "\033[0m")
|
|
410
|
+
try:
|
|
411
|
+
buffer = replace_raw_blocks(buffer)
|
|
412
|
+
parse_ok = json.loads(buffer)
|
|
413
|
+
parse_fail_reason = ""
|
|
414
|
+
tried_to_parse = ""
|
|
415
|
+
except JSONDecodeError as e:
|
|
416
|
+
print("final parse fail")
|
|
417
|
+
print(buffer)
|
|
418
|
+
parse_fail_reason = str(e)
|
|
419
|
+
context.chat_log.add_message({"role": "assistant", "content": buffer})
|
|
420
|
+
print(parse_fail_reason)
|
|
421
|
+
await asyncio.sleep(1)
|
|
422
|
+
tried_to_parse = f"\n\nTried to parse the following input: {original_buffer}"
|
|
423
|
+
results.append({"cmd": "UNKNOWN", "args": { "invalid": "("}, "result": error_result + '\n\nJSON parse error was: ' + parse_fail_reason +
|
|
424
|
+
tried_to_parse })
|
|
425
|
+
|
|
426
|
+
return results, full_cmds
|
|
427
|
+
|
|
428
|
+
async def render_system_msg(self):
|
|
429
|
+
logger.debug("Docstrings:")
|
|
430
|
+
logger.debug(command_manager.get_some_docstrings(self.agent["commands"]))
|
|
431
|
+
now = datetime.now()
|
|
432
|
+
|
|
433
|
+
formatted_time = now.strftime("~ %Y-%m-%d %I %p %Z%z")
|
|
434
|
+
|
|
435
|
+
data = {
|
|
436
|
+
"command_docs": command_manager.get_some_docstrings(self.agent["commands"]),
|
|
437
|
+
"agent": self.agent,
|
|
438
|
+
"persona": self.agent['persona'],
|
|
439
|
+
"formatted_datetime": formatted_time,
|
|
440
|
+
"context_data": self.context.data
|
|
441
|
+
}
|
|
442
|
+
# is say in the command_manager
|
|
443
|
+
if 'say' in command_manager.functions.keys():
|
|
444
|
+
print("I found say! in the functions!")
|
|
445
|
+
else:
|
|
446
|
+
print("Say is not in the functions!")
|
|
447
|
+
if 'say' in data['command_docs'].keys():
|
|
448
|
+
print("I found say in the command docs!")
|
|
449
|
+
|
|
450
|
+
# we need to be doubly sure to remove anything from command_docs that is not in command_manager.functions.keys()
|
|
451
|
+
for cmd in data['command_docs']:
|
|
452
|
+
if cmd not in command_manager.functions.keys():
|
|
453
|
+
print("Removing " + cmd + " from command_docs")
|
|
454
|
+
del data['command_docs'][cmd]
|
|
455
|
+
|
|
456
|
+
#self.system_message = self.sys_template.render(data)
|
|
457
|
+
self.system_message = await render('system', data)
|
|
458
|
+
|
|
459
|
+
additional_instructions = await hook_manager.add_instructions(self.context)
|
|
460
|
+
|
|
461
|
+
for instruction in additional_instructions:
|
|
462
|
+
self.system_message += instruction + "\n\n"
|
|
463
|
+
|
|
464
|
+
return self.system_message
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
async def chat_commands(self, model, context,
|
|
468
|
+
temperature=0, max_tokens=4000, messages=[]):
|
|
469
|
+
|
|
470
|
+
self.context = context
|
|
471
|
+
content = [ { "type": "text", "text": await self.render_system_msg() } ]
|
|
472
|
+
messages = [{"role": "system", "content": content }] + demo_boot_msgs() + messages
|
|
473
|
+
|
|
474
|
+
#logger.info("Messages for chat", extra={"messages": messages})
|
|
475
|
+
|
|
476
|
+
json_messages = json.dumps(messages)
|
|
477
|
+
new_messages = json.loads(json_messages)
|
|
478
|
+
|
|
479
|
+
if os.environ.get("AH_DEFAULT_MAX_TOKENS"):
|
|
480
|
+
max_tokens = int(os.environ.get("AH_DEFAULT_MAX_TOKENS"))
|
|
481
|
+
try:
|
|
482
|
+
tmp_data = { "messages": new_messages }
|
|
483
|
+
debug_box("Filtering messages")
|
|
484
|
+
#debug_box(tmp_data)
|
|
485
|
+
|
|
486
|
+
tmp_data = await pipeline_manager.filter_messages(tmp_data, context=context)
|
|
487
|
+
new_messages = tmp_data['messages']
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error("Error filtering messages")
|
|
490
|
+
logger.error(str(e))
|
|
491
|
+
|
|
492
|
+
if new_messages[0]['role'] != 'system':
|
|
493
|
+
logger.error("First message is not a system message")
|
|
494
|
+
print("\033[91mFirst message is not a system message\033[0m")
|
|
495
|
+
return None, None
|
|
496
|
+
|
|
497
|
+
if not isinstance(context.agent, dict):
|
|
498
|
+
context.agent = await get_agent_data(context.agent, context=context)
|
|
499
|
+
|
|
500
|
+
if 'max_tokens' in context.agent and context.agent['max_tokens'] is not None and context.agent['max_tokens'] != '':
|
|
501
|
+
logger.info(f"Using agent max tokens {max_tokens}")
|
|
502
|
+
max_tokens = context.agent['max_tokens']
|
|
503
|
+
else:
|
|
504
|
+
logger.info(f"Using default max tokens {max_tokens}")
|
|
505
|
+
|
|
506
|
+
if model is None:
|
|
507
|
+
if 'service_models' in context.agent and context.agent['service_models'] is not None:
|
|
508
|
+
if context.agent['service_models'].get('stream_chat', None) is None:
|
|
509
|
+
model = os.environ.get("DEFAULT_LLM_MODEL")
|
|
510
|
+
|
|
511
|
+
# we need to be able to abort this task if necessary
|
|
512
|
+
stream = await context.stream_chat(model,
|
|
513
|
+
temperature=temperature,
|
|
514
|
+
max_tokens=max_tokens,
|
|
515
|
+
messages=new_messages,
|
|
516
|
+
context=context)
|
|
517
|
+
|
|
518
|
+
ret, full_cmds = await self.parse_cmd_stream(stream, context)
|
|
519
|
+
logger.debug("System message was:")
|
|
520
|
+
logger.debug(await self.render_system_msg())
|
|
521
|
+
|
|
522
|
+
# use green text
|
|
523
|
+
print("\033[92m" + "Just after stream chat, last two messages in chat log:")
|
|
524
|
+
print("------------------------------------")
|
|
525
|
+
print(context.chat_log.messages[-1])
|
|
526
|
+
print(context.chat_log.messages[-2])
|
|
527
|
+
# switch back to normal text
|
|
528
|
+
print("\033[0m")
|
|
529
|
+
|
|
530
|
+
return ret, full_cmds
|
|
531
|
+
|
|
532
|
+
@service()
|
|
533
|
+
async def run_command(cmd_name, cmd_args, context=None):
|
|
534
|
+
if context is None:
|
|
535
|
+
raise Exception("run_command: No context provided")
|
|
536
|
+
|
|
537
|
+
agent = Agent(agent=context.agent)
|
|
538
|
+
json_cmd = json.dumps({cmd_name: cmd_args})
|
|
539
|
+
asyncio.create_task(agent.handle_cmds(cmd_name, cmd_args, json_cmd, context=context))
|
|
540
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from .agent import Agent, get_agent_data
|
|
2
|
+
import traceback
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
class SpeechToSpeechAgent(Agent):
|
|
6
|
+
|
|
7
|
+
def __init__(self, agent_name=None, context=None):
|
|
8
|
+
super().__init__(context)
|
|
9
|
+
self.context = context
|
|
10
|
+
if agent_name:
|
|
11
|
+
self.agent_name = agent_name
|
|
12
|
+
self.on_sip_call = False # Track if we're on a SIP call
|
|
13
|
+
|
|
14
|
+
async def on_audio_chunk_callback(self, audio_bytes: bytes, context=None):
|
|
15
|
+
"""Route audio output to SIP if on a call."""
|
|
16
|
+
if self.on_sip_call:
|
|
17
|
+
try:
|
|
18
|
+
# Send audio to active SIP session
|
|
19
|
+
from lib.providers.services import service_manager
|
|
20
|
+
await service_manager.sip_audio_out_chunk(
|
|
21
|
+
audio_chunk=audio_bytes,
|
|
22
|
+
context=self.context
|
|
23
|
+
)
|
|
24
|
+
except Exception as e:
|
|
25
|
+
print(f"Error routing audio to SIP: {e}")
|
|
26
|
+
# If not on call, audio plays locally (handled by ah_openai)
|
|
27
|
+
|
|
28
|
+
async def handle_s2s_cmd(self, cmd:dict, context=None):
|
|
29
|
+
try:
|
|
30
|
+
print('Received S2S command:')
|
|
31
|
+
print(json.dumps(cmd, indent=2))
|
|
32
|
+
|
|
33
|
+
# Track call state for audio routing
|
|
34
|
+
if 'call' in cmd:
|
|
35
|
+
self.on_sip_call = True
|
|
36
|
+
elif 'hangup' in cmd:
|
|
37
|
+
self.on_sip_call = False
|
|
38
|
+
|
|
39
|
+
json_str = json.dumps(cmd)
|
|
40
|
+
buffer = ''
|
|
41
|
+
results = await self.parse_single_cmd(json_str, self.context, buffer)
|
|
42
|
+
print()
|
|
43
|
+
print()
|
|
44
|
+
print('#########################################')
|
|
45
|
+
print(results)
|
|
46
|
+
await self.send_message([{
|
|
47
|
+
"type": "text",
|
|
48
|
+
"text": f"[SYSTEM: Command executed successfully]\n{json.dumps(str(results))}"
|
|
49
|
+
}])
|
|
50
|
+
except Exception as e:
|
|
51
|
+
trace = traceback.format_exc()
|
|
52
|
+
print(f"Error executing S2S command: {e}")
|
|
53
|
+
await self.send_message([{
|
|
54
|
+
"type": "text",
|
|
55
|
+
"text": f"[SYSTEM: Error executing command: {str(e)}\n{str(trace)}]"
|
|
56
|
+
}])
|
|
57
|
+
|
|
58
|
+
async def connect(self):
|
|
59
|
+
self.agent = await get_agent_data(self.context.agent_name)
|
|
60
|
+
sys_msg = await self.render_system_msg()
|
|
61
|
+
|
|
62
|
+
# Pass audio callback to route output to SIP when on call
|
|
63
|
+
await self.context.start_s2s(
|
|
64
|
+
self.model,
|
|
65
|
+
sys_msg,
|
|
66
|
+
self.handle_s2s_cmd,
|
|
67
|
+
play_local=False,
|
|
68
|
+
on_audio_chunk=self.on_audio_chunk_callback,
|
|
69
|
+
context=self.context
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def send_message(self, content, context=None):
|
|
73
|
+
msg = { "role": "user", "content": content }
|
|
74
|
+
print("calling send_s2s_message", msg)
|
|
75
|
+
await self.context.send_s2s_message(msg)
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from .agent import Agent, get_agent_data
|
|
2
|
+
import traceback
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
class SpeechToSpeechAgent(Agent):
|
|
6
|
+
|
|
7
|
+
def __init__(self, agent_name=None, context=None):
|
|
8
|
+
super().__init__(context)
|
|
9
|
+
self.context = context
|
|
10
|
+
if agent_name:
|
|
11
|
+
self.agent_name = agent_name
|
|
12
|
+
|
|
13
|
+
async def handle_s2s_cmd(self, cmd:dict, context=None):
|
|
14
|
+
try:
|
|
15
|
+
print('Received S2S command:')
|
|
16
|
+
print(json.dumps(cmd, indent=2))
|
|
17
|
+
json_str = json.dumps(cmd)
|
|
18
|
+
buffer = ''
|
|
19
|
+
results = await self.parse_single_cmd(json_str, self.context, buffer)
|
|
20
|
+
print()
|
|
21
|
+
print()
|
|
22
|
+
print('#########################################')
|
|
23
|
+
print(results)
|
|
24
|
+
await self.send_message([{
|
|
25
|
+
"type": "text",
|
|
26
|
+
"text": f"[SYSTEM: Command executed successfully]\n{json.dumps(str(results))}"
|
|
27
|
+
}])
|
|
28
|
+
except Exception as e:
|
|
29
|
+
trace = traceback.format_exc()
|
|
30
|
+
print(f"Error executing S2S command: {e}")
|
|
31
|
+
await self.send_message([{
|
|
32
|
+
"type": "text",
|
|
33
|
+
"text": f"[SYSTEM: Error executing command: {str(e)}\n{str(trace)}]"
|
|
34
|
+
}])
|
|
35
|
+
|
|
36
|
+
async def connect(self):
|
|
37
|
+
self.agent = await get_agent_data(self.context.agent_name)
|
|
38
|
+
sys_msg = await self.render_system_msg()
|
|
39
|
+
|
|
40
|
+
await self.context.start_s2s(self.model, sys_msg, self.handle_s2s_cmd, context=self.context)
|
|
41
|
+
|
|
42
|
+
async def send_message(self, content, context=None):
|
|
43
|
+
msg = { "role": "user", "content": content }
|
|
44
|
+
print("calling send_s2s_message", msg)
|
|
45
|
+
await self.context.send_s2s_message(msg)
|
|
46
|
+
|