auto-coder 0.1.348__py3-none-any.whl → 0.1.349__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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/METADATA +1 -1
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/RECORD +35 -26
- autocoder/auto_coder_runner.py +14 -10
- autocoder/chat_auto_coder_lang.py +5 -3
- autocoder/common/model_speed_tester.py +392 -0
- autocoder/common/printer.py +7 -8
- autocoder/common/run_cmd.py +247 -0
- autocoder/common/test_run_cmd.py +110 -0
- autocoder/common/v2/agent/agentic_edit.py +61 -11
- autocoder/common/v2/agent/agentic_edit_conversation.py +9 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +21 -36
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +4 -7
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +2 -5
- autocoder/helper/rag_doc_creator.py +141 -0
- autocoder/ignorefiles/__init__.py +4 -0
- autocoder/ignorefiles/ignore_file_utils.py +63 -0
- autocoder/ignorefiles/test_ignore_file_utils.py +91 -0
- autocoder/models.py +49 -9
- autocoder/rag/cache/byzer_storage_cache.py +10 -4
- autocoder/rag/cache/file_monitor_cache.py +27 -24
- autocoder/rag/cache/local_byzer_storage_cache.py +11 -5
- autocoder/rag/cache/local_duckdb_storage_cache.py +203 -128
- autocoder/rag/cache/simple_cache.py +56 -37
- autocoder/rag/loaders/filter_utils.py +106 -0
- autocoder/rag/loaders/image_loader.py +45 -23
- autocoder/rag/loaders/pdf_loader.py +3 -3
- autocoder/rag/loaders/test_image_loader.py +209 -0
- autocoder/rag/qa_conversation_strategy.py +3 -5
- autocoder/rag/utils.py +20 -9
- autocoder/utils/_markitdown.py +35 -0
- autocoder/version.py +1 -1
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/LICENSE +0 -0
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/WHEEL +0 -0
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/entry_points.txt +0 -0
- {auto_coder-0.1.348.dist-info → auto_coder-0.1.349.dist-info}/top_level.txt +0 -0
autocoder/common/printer.py
CHANGED
|
@@ -15,19 +15,18 @@ class Printer:
|
|
|
15
15
|
|
|
16
16
|
def get_message_from_key(self, msg_key: str):
|
|
17
17
|
try:
|
|
18
|
-
|
|
18
|
+
v = get_message(msg_key)
|
|
19
|
+
if not v:
|
|
20
|
+
return get_chat_message(msg_key)
|
|
19
21
|
except Exception as e:
|
|
20
22
|
return get_chat_message(msg_key)
|
|
21
23
|
|
|
22
|
-
def get_message_from_key_with_format(self, msg_key: str, **kwargs):
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
except Exception as e:
|
|
26
|
-
return format_str_jinja2(self.get_chat_message_from_key(msg_key), **kwargs)
|
|
27
|
-
|
|
24
|
+
def get_message_from_key_with_format(self, msg_key: str, **kwargs):
|
|
25
|
+
return format_str_jinja2(self.get_message_from_key(msg_key), **kwargs)
|
|
26
|
+
|
|
28
27
|
def print_in_terminal(self, msg_key: str, style: str = None,**kwargs):
|
|
29
28
|
try:
|
|
30
|
-
if style:
|
|
29
|
+
if style:
|
|
31
30
|
self.console.print(format_str_jinja2(self.get_message_from_key(msg_key),**kwargs), style=style)
|
|
32
31
|
else:
|
|
33
32
|
self.console.print(format_str_jinja2(self.get_message_from_key(msg_key),**kwargs))
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
import pexpect
|
|
8
|
+
import psutil
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_cmd(command, verbose=False, error_print=None, cwd=None):
|
|
12
|
+
"""
|
|
13
|
+
执行一条命令,根据不同系统和环境选择最合适的执行方式(交互式或非交互式)。
|
|
14
|
+
|
|
15
|
+
适用场景:
|
|
16
|
+
- 需要跨平台运行命令,自动判断是否使用pexpect(支持交互式)还是subprocess。
|
|
17
|
+
- 希望获得命令的执行状态及输出内容。
|
|
18
|
+
- 在CLI工具、自动化脚本、REPL中执行shell命令。
|
|
19
|
+
|
|
20
|
+
参数:
|
|
21
|
+
- command (str): 需要执行的命令字符串。
|
|
22
|
+
- verbose (bool): 是否打印详细调试信息,默认为False。
|
|
23
|
+
- error_print (callable|None): 自定义错误打印函数,默认为None,使用print。
|
|
24
|
+
- cwd (str|None): 指定命令的工作目录,默认为None。
|
|
25
|
+
|
|
26
|
+
返回:
|
|
27
|
+
- tuple: (exit_code, output),其中exit_code为整型退出码,output为命令输出内容。
|
|
28
|
+
|
|
29
|
+
异常:
|
|
30
|
+
- 捕获OSError异常,返回错误信息。
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
if sys.stdin.isatty() and hasattr(pexpect, "spawn") and platform.system() != "Windows":
|
|
34
|
+
return run_cmd_pexpect(command, verbose, cwd)
|
|
35
|
+
|
|
36
|
+
return run_cmd_subprocess(command, verbose, cwd)
|
|
37
|
+
except OSError as e:
|
|
38
|
+
error_message = f"Error occurred while running command '{command}': {str(e)}"
|
|
39
|
+
if error_print is None:
|
|
40
|
+
print(error_message)
|
|
41
|
+
else:
|
|
42
|
+
error_print(error_message)
|
|
43
|
+
return 1, error_message
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_windows_parent_process_name():
|
|
47
|
+
"""
|
|
48
|
+
获取当前进程的父进程名(仅在Windows系统下有意义)。
|
|
49
|
+
|
|
50
|
+
适用场景:
|
|
51
|
+
- 判断命令是否由PowerShell或cmd.exe启动,以便调整命令格式。
|
|
52
|
+
- 在Windows平台上进行父进程分析。
|
|
53
|
+
|
|
54
|
+
参数:
|
|
55
|
+
- 无
|
|
56
|
+
|
|
57
|
+
返回:
|
|
58
|
+
- str|None: 父进程名(小写字符串,如"powershell.exe"或"cmd.exe"),如果无法获取则为None。
|
|
59
|
+
|
|
60
|
+
异常:
|
|
61
|
+
- 捕获所有异常,返回None。
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
current_process = psutil.Process()
|
|
65
|
+
while True:
|
|
66
|
+
parent = current_process.parent()
|
|
67
|
+
if parent is None:
|
|
68
|
+
break
|
|
69
|
+
parent_name = parent.name().lower()
|
|
70
|
+
if parent_name in ["powershell.exe", "cmd.exe"]:
|
|
71
|
+
return parent_name
|
|
72
|
+
current_process = parent
|
|
73
|
+
return None
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_cmd_subprocess(command, verbose=False, cwd=None, encoding=sys.stdout.encoding):
|
|
79
|
+
if verbose:
|
|
80
|
+
print("Using run_cmd_subprocess:", command)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
shell = os.environ.get("SHELL", "/bin/sh")
|
|
84
|
+
parent_process = None
|
|
85
|
+
|
|
86
|
+
# Determine the appropriate shell
|
|
87
|
+
if platform.system() == "Windows":
|
|
88
|
+
parent_process = get_windows_parent_process_name()
|
|
89
|
+
if parent_process == "powershell.exe":
|
|
90
|
+
command = f"powershell -Command {command}"
|
|
91
|
+
|
|
92
|
+
if verbose:
|
|
93
|
+
print("Running command:", command)
|
|
94
|
+
print("SHELL:", shell)
|
|
95
|
+
if platform.system() == "Windows":
|
|
96
|
+
print("Parent process:", parent_process)
|
|
97
|
+
|
|
98
|
+
process = subprocess.Popen(
|
|
99
|
+
command,
|
|
100
|
+
stdout=subprocess.PIPE,
|
|
101
|
+
stderr=subprocess.STDOUT,
|
|
102
|
+
text=True,
|
|
103
|
+
shell=True,
|
|
104
|
+
encoding=encoding,
|
|
105
|
+
errors="replace",
|
|
106
|
+
bufsize=0, # Set bufsize to 0 for unbuffered output
|
|
107
|
+
universal_newlines=True,
|
|
108
|
+
cwd=cwd,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
output = []
|
|
112
|
+
while True:
|
|
113
|
+
chunk = process.stdout.read(1)
|
|
114
|
+
if not chunk:
|
|
115
|
+
break
|
|
116
|
+
print(chunk, end="", flush=True) # Print the chunk in real-time
|
|
117
|
+
output.append(chunk) # Store the chunk for later use
|
|
118
|
+
|
|
119
|
+
process.wait()
|
|
120
|
+
return process.returncode, "".join(output)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return 1, str(e)
|
|
123
|
+
|
|
124
|
+
def run_cmd_subprocess_generator(command, verbose=False, cwd=None, encoding=sys.stdout.encoding):
|
|
125
|
+
"""
|
|
126
|
+
使用subprocess运行命令,将命令输出逐步以生成器方式yield出来。
|
|
127
|
+
|
|
128
|
+
适用场景:
|
|
129
|
+
- 运行无需交互的命令。
|
|
130
|
+
- 希望实时逐步处理命令输出(如日志打印、进度监控)。
|
|
131
|
+
- 在Linux、macOS、Windows等多平台环境下安全运行命令。
|
|
132
|
+
|
|
133
|
+
参数:
|
|
134
|
+
- command (str): 需要执行的命令字符串。
|
|
135
|
+
- verbose (bool): 是否打印详细调试信息,默认为False。
|
|
136
|
+
- cwd (str|None): 指定命令的工作目录,默认为None。
|
|
137
|
+
- encoding (str): 输出解码使用的字符编码,默认为当前stdout编码。
|
|
138
|
+
|
|
139
|
+
返回:
|
|
140
|
+
- 生成器: 逐块yield命令的输出字符串。
|
|
141
|
+
|
|
142
|
+
异常:
|
|
143
|
+
- 捕获所有异常,yield错误信息字符串。
|
|
144
|
+
"""
|
|
145
|
+
if verbose:
|
|
146
|
+
print("Using run_cmd_subprocess:", command)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
shell = os.environ.get("SHELL", "/bin/sh")
|
|
150
|
+
parent_process = None
|
|
151
|
+
|
|
152
|
+
# Windows下调整命令
|
|
153
|
+
if platform.system() == "Windows":
|
|
154
|
+
parent_process = get_windows_parent_process_name()
|
|
155
|
+
if parent_process == "powershell.exe":
|
|
156
|
+
command = f"powershell -Command {command}"
|
|
157
|
+
|
|
158
|
+
if verbose:
|
|
159
|
+
print("Running command:", command)
|
|
160
|
+
print("SHELL:", shell)
|
|
161
|
+
if platform.system() == "Windows":
|
|
162
|
+
print("Parent process:", parent_process)
|
|
163
|
+
|
|
164
|
+
process = subprocess.Popen(
|
|
165
|
+
command,
|
|
166
|
+
stdout=subprocess.PIPE,
|
|
167
|
+
stderr=subprocess.STDOUT,
|
|
168
|
+
text=True,
|
|
169
|
+
shell=True,
|
|
170
|
+
encoding=encoding,
|
|
171
|
+
errors="replace",
|
|
172
|
+
bufsize=0,
|
|
173
|
+
universal_newlines=True,
|
|
174
|
+
cwd=cwd,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
while True:
|
|
178
|
+
chunk = process.stdout.read(1)
|
|
179
|
+
if not chunk:
|
|
180
|
+
break
|
|
181
|
+
# 确保始终yield字符串,避免因字节或其他类型导致异常
|
|
182
|
+
if not isinstance(chunk, str):
|
|
183
|
+
chunk = str(chunk)
|
|
184
|
+
yield chunk
|
|
185
|
+
|
|
186
|
+
process.wait()
|
|
187
|
+
except Exception as e:
|
|
188
|
+
# 出错时yield异常信息,也可以raise
|
|
189
|
+
yield f"[run_cmd_subprocess error]: {str(e)}"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def run_cmd_pexpect(command, verbose=False, cwd=None):
|
|
193
|
+
"""
|
|
194
|
+
使用pexpect以交互方式运行命令,捕获完整输出。
|
|
195
|
+
|
|
196
|
+
适用场景:
|
|
197
|
+
- 执行需要用户交互的命令(如登录、密码输入等)。
|
|
198
|
+
- 在Linux、macOS等Unix系统下模拟终端操作。
|
|
199
|
+
- 希望完整捕获交互式命令的输出。
|
|
200
|
+
|
|
201
|
+
参数:
|
|
202
|
+
- command (str): 需要执行的命令字符串。
|
|
203
|
+
- verbose (bool): 是否打印详细调试信息,默认为False。
|
|
204
|
+
- cwd (str|None): 指定命令的工作目录,默认为None。
|
|
205
|
+
|
|
206
|
+
返回:
|
|
207
|
+
- tuple: (exit_code, output),exit_code为退出状态码,output为命令完整输出内容。
|
|
208
|
+
|
|
209
|
+
异常:
|
|
210
|
+
- 捕获pexpect相关异常,返回错误信息。
|
|
211
|
+
"""
|
|
212
|
+
if verbose:
|
|
213
|
+
print("Using run_cmd_pexpect:", command)
|
|
214
|
+
|
|
215
|
+
output = BytesIO()
|
|
216
|
+
|
|
217
|
+
def output_callback(b):
|
|
218
|
+
output.write(b)
|
|
219
|
+
return b
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# Use the SHELL environment variable, falling back to /bin/sh if not set
|
|
223
|
+
shell = os.environ.get("SHELL", "/bin/sh")
|
|
224
|
+
if verbose:
|
|
225
|
+
print("With shell:", shell)
|
|
226
|
+
|
|
227
|
+
if os.path.exists(shell):
|
|
228
|
+
# Use the shell from SHELL environment variable
|
|
229
|
+
if verbose:
|
|
230
|
+
print("Running pexpect.spawn with shell:", shell)
|
|
231
|
+
child = pexpect.spawn(shell, args=["-i", "-c", command], encoding="utf-8", cwd=cwd)
|
|
232
|
+
else:
|
|
233
|
+
# Fall back to spawning the command directly
|
|
234
|
+
if verbose:
|
|
235
|
+
print("Running pexpect.spawn without shell.")
|
|
236
|
+
child = pexpect.spawn(command, encoding="utf-8", cwd=cwd)
|
|
237
|
+
|
|
238
|
+
# Transfer control to the user, capturing output
|
|
239
|
+
child.interact(output_filter=output_callback)
|
|
240
|
+
|
|
241
|
+
# Wait for the command to finish and get the exit status
|
|
242
|
+
child.close()
|
|
243
|
+
return child.exitstatus, output.getvalue().decode("utf-8", errors="replace")
|
|
244
|
+
|
|
245
|
+
except (pexpect.ExceptionPexpect, TypeError, ValueError) as e:
|
|
246
|
+
error_msg = f"Error running command {command}: {e}"
|
|
247
|
+
return 1, error_msg
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import platform
|
|
4
|
+
from unittest import mock
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from autocoder.common.run_cmd import (
|
|
10
|
+
run_cmd,
|
|
11
|
+
run_cmd_subprocess,
|
|
12
|
+
run_cmd_subprocess_generator,
|
|
13
|
+
run_cmd_pexpect,
|
|
14
|
+
get_windows_parent_process_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def test_run_cmd_basic():
|
|
18
|
+
"""
|
|
19
|
+
测试run_cmd函数,确保其正确执行命令并返回预期结果。
|
|
20
|
+
"""
|
|
21
|
+
cmd = "echo hello"
|
|
22
|
+
exit_code, output = run_cmd(cmd)
|
|
23
|
+
assert exit_code == 0, f"命令退出码非零: {exit_code}"
|
|
24
|
+
assert "hello" in output, f"输出不包含hello: {output}"
|
|
25
|
+
|
|
26
|
+
def test_run_cmd_subprocess_normal():
|
|
27
|
+
"""
|
|
28
|
+
测试run_cmd_subprocess正常执行命令,逐步输出。
|
|
29
|
+
"""
|
|
30
|
+
cmd = "echo hello_subprocess"
|
|
31
|
+
gen = run_cmd_subprocess_generator(cmd)
|
|
32
|
+
output = ""
|
|
33
|
+
try:
|
|
34
|
+
for chunk in gen:
|
|
35
|
+
output += chunk
|
|
36
|
+
except Exception as e:
|
|
37
|
+
pytest.fail(f"run_cmd_subprocess异常: {e}")
|
|
38
|
+
assert "hello_subprocess" in output
|
|
39
|
+
|
|
40
|
+
def test_run_cmd_subprocess_error():
|
|
41
|
+
"""
|
|
42
|
+
测试run_cmd_subprocess执行错误命令时能否正确返回异常信息。
|
|
43
|
+
"""
|
|
44
|
+
cmd = "non_existing_command_xyz"
|
|
45
|
+
gen = run_cmd_subprocess_generator(cmd)
|
|
46
|
+
output = ""
|
|
47
|
+
for chunk in gen:
|
|
48
|
+
output += chunk
|
|
49
|
+
# 应该包含错误提示
|
|
50
|
+
assert "[run_cmd_subprocess error]" in output or "not found" in output or "无法" in output or "未找到" in output
|
|
51
|
+
|
|
52
|
+
def test_run_cmd_pexpect_mock():
|
|
53
|
+
"""
|
|
54
|
+
测试run_cmd_pexpect函数,mock pexpect交互行为。
|
|
55
|
+
"""
|
|
56
|
+
with mock.patch("pexpect.spawn") as mock_spawn:
|
|
57
|
+
mock_child = mock.MagicMock()
|
|
58
|
+
mock_child.exitstatus = 0
|
|
59
|
+
mock_child.interact.side_effect = lambda output_filter=None: output_filter(b"mock output\n")
|
|
60
|
+
mock_child.close.return_value = None
|
|
61
|
+
mock_child.exitstatus = 0
|
|
62
|
+
mock_child.getvalue = lambda: b"mock output\n"
|
|
63
|
+
mock_spawn.return_value = mock_child
|
|
64
|
+
|
|
65
|
+
# 由于run_cmd_pexpect内部会decode BytesIO内容
|
|
66
|
+
exit_code, output = run_cmd_pexpect("echo hello", verbose=False)
|
|
67
|
+
assert exit_code == 0
|
|
68
|
+
assert "mock output" in output
|
|
69
|
+
|
|
70
|
+
def test_get_windows_parent_process_name_mocked():
|
|
71
|
+
"""
|
|
72
|
+
测试get_windows_parent_process_name,模拟不同父进程
|
|
73
|
+
"""
|
|
74
|
+
if platform.system() != "Windows":
|
|
75
|
+
# 非Windows系统跳过
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# 构造mock进程树
|
|
79
|
+
class FakeProcess:
|
|
80
|
+
def __init__(self, name, parent=None):
|
|
81
|
+
self._name = name
|
|
82
|
+
self._parent = parent
|
|
83
|
+
|
|
84
|
+
def name(self):
|
|
85
|
+
return self._name
|
|
86
|
+
|
|
87
|
+
def parent(self):
|
|
88
|
+
return self._parent
|
|
89
|
+
|
|
90
|
+
powershell_proc = FakeProcess("powershell.exe")
|
|
91
|
+
cmd_proc = FakeProcess("cmd.exe")
|
|
92
|
+
other_proc = FakeProcess("python.exe")
|
|
93
|
+
|
|
94
|
+
# 模拟powershell父进程
|
|
95
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
96
|
+
MockProcess.return_value = FakeProcess("python.exe", powershell_proc)
|
|
97
|
+
assert get_windows_parent_process_name() == "powershell.exe"
|
|
98
|
+
|
|
99
|
+
# 模拟cmd父进程
|
|
100
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
101
|
+
MockProcess.return_value = FakeProcess("python.exe", cmd_proc)
|
|
102
|
+
assert get_windows_parent_process_name() == "cmd.exe"
|
|
103
|
+
|
|
104
|
+
# 模拟无匹配父进程
|
|
105
|
+
with mock.patch("psutil.Process") as MockProcess:
|
|
106
|
+
MockProcess.return_value = FakeProcess("python.exe", other_proc)
|
|
107
|
+
assert get_windows_parent_process_name() is None
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
sys.exit(pytest.main([__file__]))
|
|
@@ -771,12 +771,16 @@ class AgenticEdit:
|
|
|
771
771
|
Analyzes the user request, interacts with the LLM, parses responses,
|
|
772
772
|
executes tools, and yields structured events for visualization until completion or error.
|
|
773
773
|
"""
|
|
774
|
+
logger.info(f"Starting analyze method with user input: {request.user_input[:50]}...")
|
|
774
775
|
system_prompt = self._analyze.prompt(request)
|
|
776
|
+
logger.info(f"Generated system prompt with length: {len(system_prompt)}")
|
|
777
|
+
|
|
775
778
|
# print(system_prompt)
|
|
776
779
|
conversations = [
|
|
777
780
|
{"role": "system", "content": system_prompt},
|
|
778
781
|
]
|
|
779
782
|
|
|
783
|
+
logger.info("Adding initial files context to conversation")
|
|
780
784
|
conversations.append({
|
|
781
785
|
"role":"user","content":f'''
|
|
782
786
|
Below are some files the user is focused on, and the content is up to date. These entries show the file paths along with their full text content, which can help you better understand the user's needs. If the information is insufficient, you can use tools such as read_file to retrieve more details.
|
|
@@ -788,23 +792,36 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
788
792
|
conversations.append({
|
|
789
793
|
"role":"assistant","content":"Ok"
|
|
790
794
|
})
|
|
795
|
+
|
|
796
|
+
logger.info("Adding conversation history")
|
|
791
797
|
conversations.extend(self.conversation_manager.get_history())
|
|
792
798
|
conversations.append({
|
|
793
799
|
"role": "user", "content": request.user_input
|
|
794
800
|
})
|
|
795
801
|
self.conversation_manager.add_user_message(request.user_input)
|
|
796
802
|
|
|
797
|
-
logger.
|
|
803
|
+
logger.info(
|
|
798
804
|
f"Initial conversation history size: {len(conversations)}")
|
|
799
805
|
|
|
806
|
+
iteration_count = 0
|
|
800
807
|
tool_executed = False
|
|
801
808
|
while True:
|
|
809
|
+
iteration_count += 1
|
|
810
|
+
logger.info(f"Starting LLM interaction cycle #{iteration_count}")
|
|
802
811
|
global_cancel.check_and_raise()
|
|
812
|
+
last_message = conversations[-1]
|
|
813
|
+
if last_message["role"] == "assistant":
|
|
814
|
+
logger.info(f"Last message is assistant, skipping LLM interaction cycle")
|
|
815
|
+
yield CompletionEvent(completion=AttemptCompletionTool(
|
|
816
|
+
result=last_message["content"],
|
|
817
|
+
command=""
|
|
818
|
+
), completion_xml="")
|
|
819
|
+
break
|
|
803
820
|
logger.info(
|
|
804
821
|
f"Starting LLM interaction cycle. History size: {len(conversations)}")
|
|
805
822
|
|
|
806
823
|
assistant_buffer = ""
|
|
807
|
-
|
|
824
|
+
logger.info("Initializing stream chat with LLM")
|
|
808
825
|
llm_response_gen = stream_chat_with_continue(
|
|
809
826
|
llm=self.llm,
|
|
810
827
|
conversations=conversations,
|
|
@@ -813,21 +830,29 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
813
830
|
)
|
|
814
831
|
|
|
815
832
|
meta_holder = byzerllm.MetaHolder()
|
|
833
|
+
logger.info("Starting to parse LLM response stream")
|
|
816
834
|
parsed_events = self.stream_and_parse_llm_response(
|
|
817
835
|
llm_response_gen, meta_holder)
|
|
818
836
|
|
|
837
|
+
event_count = 0
|
|
819
838
|
for event in parsed_events:
|
|
839
|
+
event_count += 1
|
|
840
|
+
logger.info(f"Processing event #{event_count}: {type(event).__name__}")
|
|
820
841
|
global_cancel.check_and_raise()
|
|
821
842
|
if isinstance(event, (LLMOutputEvent, LLMThinkingEvent)):
|
|
822
843
|
assistant_buffer += event.text
|
|
823
|
-
|
|
844
|
+
logger.debug(f"Accumulated {len(assistant_buffer)} chars in assistant buffer")
|
|
845
|
+
yield event # Yield text/thinking immediately for display
|
|
824
846
|
|
|
825
847
|
elif isinstance(event, ToolCallEvent):
|
|
826
848
|
tool_executed = True
|
|
827
849
|
tool_obj = event.tool
|
|
850
|
+
tool_name = type(tool_obj).__name__
|
|
851
|
+
logger.info(f"Tool call detected: {tool_name}")
|
|
828
852
|
tool_xml = event.tool_xml # Already reconstructed by parser
|
|
829
853
|
|
|
830
854
|
# Append assistant's thoughts and the tool call to history
|
|
855
|
+
logger.info(f"Adding assistant message with tool call to conversation history")
|
|
831
856
|
conversations.append({
|
|
832
857
|
"role": "assistant",
|
|
833
858
|
"content": assistant_buffer + tool_xml
|
|
@@ -837,11 +862,13 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
837
862
|
assistant_buffer = "" # Reset buffer after tool call
|
|
838
863
|
|
|
839
864
|
yield event # Yield the ToolCallEvent for display
|
|
865
|
+
logger.info("Yielded ToolCallEvent")
|
|
840
866
|
|
|
841
867
|
# Handle AttemptCompletion separately as it ends the loop
|
|
842
868
|
if isinstance(tool_obj, AttemptCompletionTool):
|
|
843
869
|
logger.info(
|
|
844
870
|
"AttemptCompletionTool received. Finalizing session.")
|
|
871
|
+
logger.info(f"Completion result: {tool_obj.result[:50]}...")
|
|
845
872
|
yield CompletionEvent(completion=tool_obj, completion_xml=tool_xml)
|
|
846
873
|
logger.info(
|
|
847
874
|
"AgenticEdit analyze loop finished due to AttemptCompletion.")
|
|
@@ -850,6 +877,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
850
877
|
if isinstance(tool_obj, PlanModeRespondTool):
|
|
851
878
|
logger.info(
|
|
852
879
|
"PlanModeRespondTool received. Finalizing session.")
|
|
880
|
+
logger.info(f"Plan mode response: {tool_obj.response[:50]}...")
|
|
853
881
|
yield PlanModeRespondEvent(completion=tool_obj, completion_xml=tool_xml)
|
|
854
882
|
logger.info(
|
|
855
883
|
"AgenticEdit analyze loop finished due to PlanModeRespond.")
|
|
@@ -867,6 +895,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
867
895
|
error_xml = f"<tool_result tool_name='{type(tool_obj).__name__}' success='false'><message>Error: Tool resolver not implemented.</message><content></content></tool_result>"
|
|
868
896
|
else:
|
|
869
897
|
try:
|
|
898
|
+
logger.info(f"Creating resolver for tool: {tool_name}")
|
|
870
899
|
resolver = resolver_cls(
|
|
871
900
|
agent=self, tool=tool_obj, args=self.args)
|
|
872
901
|
logger.info(
|
|
@@ -878,6 +907,7 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
878
907
|
tool_obj).__name__, result=tool_result)
|
|
879
908
|
|
|
880
909
|
# Prepare XML for conversation history
|
|
910
|
+
logger.info("Preparing XML for conversation history")
|
|
881
911
|
escaped_message = xml.sax.saxutils.escape(
|
|
882
912
|
tool_result.message)
|
|
883
913
|
content_str = str(
|
|
@@ -902,40 +932,55 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
902
932
|
error_message)
|
|
903
933
|
error_xml = f"<tool_result tool_name='{type(tool_obj).__name__}' success='false'><message>{escaped_error}</message><content></content></tool_result>"
|
|
904
934
|
|
|
905
|
-
yield result_event # Yield the ToolResultEvent for display
|
|
935
|
+
yield result_event # Yield the ToolResultEvent for display
|
|
936
|
+
logger.info("Yielded ToolResultEvent")
|
|
906
937
|
|
|
907
938
|
# Append the tool result (as user message) to history
|
|
939
|
+
logger.info("Adding tool result to conversation history")
|
|
908
940
|
conversations.append({
|
|
909
941
|
"role": "user", # Simulating the user providing the tool result
|
|
910
942
|
"content": error_xml
|
|
911
943
|
})
|
|
912
944
|
self.conversation_manager.add_user_message(error_xml)
|
|
913
|
-
logger.
|
|
945
|
+
logger.info(
|
|
914
946
|
f"Added tool result to conversations for tool {type(tool_obj).__name__}")
|
|
947
|
+
logger.info(f"Breaking LLM cycle after executing tool: {tool_name}")
|
|
915
948
|
break # After tool execution and result, break to start a new LLM cycle
|
|
916
949
|
|
|
917
950
|
elif isinstance(event, ErrorEvent):
|
|
951
|
+
logger.error(f"Error event occurred: {event.message}")
|
|
918
952
|
yield event # Pass through errors
|
|
919
953
|
# Optionally stop the process on parsing errors
|
|
920
954
|
# logger.error("Stopping analyze loop due to parsing error.")
|
|
921
955
|
# return
|
|
922
956
|
|
|
957
|
+
logger.info("Yielding token usage event")
|
|
923
958
|
yield TokenUsageEvent(usage=meta_holder.meta)
|
|
959
|
+
|
|
924
960
|
if not tool_executed:
|
|
925
961
|
# No tool executed in this LLM response cycle
|
|
926
|
-
logger.info("LLM response finished without executing a tool.")
|
|
962
|
+
logger.info("LLM response finished without executing a tool.")
|
|
927
963
|
# Append any remaining assistant buffer to history if it wasn't followed by a tool
|
|
928
964
|
if assistant_buffer:
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
965
|
+
logger.info(f"Appending assistant buffer to history: {len(assistant_buffer)} chars")
|
|
966
|
+
last_message = conversations[-1]
|
|
967
|
+
if last_message["role"] != "assistant":
|
|
968
|
+
logger.info("Adding new assistant message")
|
|
969
|
+
conversations.append(
|
|
970
|
+
{"role": "assistant", "content": assistant_buffer})
|
|
971
|
+
self.conversation_manager.add_assistant_message(
|
|
972
|
+
assistant_buffer)
|
|
973
|
+
elif last_message["role"] == "assistant":
|
|
974
|
+
logger.info("Appending to existing assistant message")
|
|
975
|
+
last_message["content"] += assistant_buffer
|
|
976
|
+
self.conversation_manager.append_to_last_message(assistant_buffer)
|
|
933
977
|
# If the loop ends without AttemptCompletion, it means the LLM finished talking
|
|
934
978
|
# without signaling completion. We might just stop or yield a final message.
|
|
935
979
|
# Let's assume it stops here.
|
|
980
|
+
logger.info("No tool executed and LLM finished. Breaking out of main loop.")
|
|
936
981
|
break
|
|
937
982
|
|
|
938
|
-
logger.info("AgenticEdit analyze loop finished.")
|
|
983
|
+
logger.info(f"AgenticEdit analyze loop finished after {iteration_count} iterations.")
|
|
939
984
|
|
|
940
985
|
def stream_and_parse_llm_response(
|
|
941
986
|
self, generator: Generator[Tuple[str, Any], None, None], meta_holder: byzerllm.MetaHolder
|
|
@@ -1323,6 +1368,11 @@ Below are some files the user is focused on, and the content is up to date. Thes
|
|
|
1323
1368
|
Apply all tracked file changes to the original project directory.
|
|
1324
1369
|
"""
|
|
1325
1370
|
for (file_path, change) in self.get_all_file_changes().items():
|
|
1371
|
+
# Ensure the directory exists before writing the file
|
|
1372
|
+
dir_path = os.path.dirname(file_path)
|
|
1373
|
+
if dir_path: # Ensure dir_path is not empty (for files in root)
|
|
1374
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
1375
|
+
|
|
1326
1376
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
1327
1377
|
f.write(change.content)
|
|
1328
1378
|
|
|
@@ -68,6 +68,15 @@ class AgenticConversation:
|
|
|
68
68
|
"""Adds an assistant message (potentially containing text response)."""
|
|
69
69
|
self.add_message(role="assistant", content=content)
|
|
70
70
|
|
|
71
|
+
def append_to_last_message(self, content: str, role: str = "assistant"):
|
|
72
|
+
"""Appends content to the last message."""
|
|
73
|
+
if self._history:
|
|
74
|
+
last_message = self._history[-1]
|
|
75
|
+
if role and last_message["role"] == role:
|
|
76
|
+
last_message["content"] += content
|
|
77
|
+
elif not role:
|
|
78
|
+
last_message["content"] += content
|
|
79
|
+
|
|
71
80
|
def add_assistant_tool_call_message(self, tool_calls: List[Dict[str, Any]], content: Optional[str] = None):
|
|
72
81
|
"""
|
|
73
82
|
Adds a message representing one or more tool calls from the assistant.
|