ErrorAI 1.5.0__tar.gz → 1.6.0__tar.gz
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.
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/PKG-INFO +1 -1
- {errorai-1.5.0 → errorai-1.6.0}/PKG-INFO +1 -1
- errorai-1.6.0/errorai/config.py +311 -0
- errorai-1.6.0/errorai/providers.py +212 -0
- {errorai-1.5.0 → errorai-1.6.0}/pyproject.toml +1 -1
- errorai-1.5.0/errorai/config.py +0 -155
- errorai-1.5.0/errorai/providers.py +0 -112
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/entry_points.txt +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/requires.txt +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/ErrorAI.egg-info/top_level.txt +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/README.md +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/__init__.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/__main__.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/bootstrap.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/cli.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/compat.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/environment.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/pipeline.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/runtime.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/errorai/universal.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/setup.cfg +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/tests/test_bootstrap.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/tests/test_config.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/tests/test_runtime.py +0 -0
- {errorai-1.5.0 → errorai-1.6.0}/tests/test_safety.py +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import linecache
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import traceback
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .bootstrap import BootstrapStatus, ensure_model, model_status
|
|
13
|
+
from .config import ErrorAIConfig, autostart_enabled, load_config
|
|
14
|
+
from .environment import Capabilities, detect_capabilities
|
|
15
|
+
from .pipeline import Analyzer, Applier, Planner, Reporter, Watcher
|
|
16
|
+
from .providers import ModelProvider, RulesOnlyProvider, HttpApiProvider
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from .providers import LlamaCppProvider
|
|
20
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
21
|
+
LlamaCppProvider = None # type: ignore[assignment,misc]
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .providers import OnnxProvider
|
|
25
|
+
except ImportError: # pragma: no cover - optional dependency
|
|
26
|
+
OnnxProvider = None # type: ignore[assignment,misc]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RuntimeManager:
|
|
30
|
+
_instance: "RuntimeManager | None" = None
|
|
31
|
+
_instance_lock = threading.Lock()
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._init_lock = threading.Lock()
|
|
35
|
+
self._initialized = False
|
|
36
|
+
self.config: ErrorAIConfig | None = None
|
|
37
|
+
self.capabilities: Capabilities | None = None
|
|
38
|
+
self.reporter: Reporter | None = None
|
|
39
|
+
self.analyzer: Analyzer | None = None
|
|
40
|
+
self.planner: Planner | None = None
|
|
41
|
+
self.applier: Applier | None = None
|
|
42
|
+
self.watcher: Watcher | None = None
|
|
43
|
+
self.provider: ModelProvider = RulesOnlyProvider()
|
|
44
|
+
self.bootstrap_status = BootstrapStatus(False, "rules-only", "Not initialized.", None)
|
|
45
|
+
self.mode = "rules-only"
|
|
46
|
+
self._orig_sys_hook = sys.excepthook
|
|
47
|
+
self._orig_thread_hook = getattr(threading, "excepthook", None)
|
|
48
|
+
self._orig_idle_print_exception = None
|
|
49
|
+
self._idle_hook_installed = False
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_instance(cls) -> "RuntimeManager":
|
|
53
|
+
with cls._instance_lock:
|
|
54
|
+
if cls._instance is None:
|
|
55
|
+
cls._instance = cls()
|
|
56
|
+
return cls._instance
|
|
57
|
+
|
|
58
|
+
def initialize(self, config_override: ErrorAIConfig | None = None) -> "RuntimeManager":
|
|
59
|
+
if not autostart_enabled():
|
|
60
|
+
return self
|
|
61
|
+
with self._init_lock:
|
|
62
|
+
if self._initialized:
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
self.config = config_override or load_config()
|
|
66
|
+
self.capabilities = detect_capabilities()
|
|
67
|
+
self.reporter = Reporter(self.config.runtime.project_root)
|
|
68
|
+
self.analyzer = Analyzer()
|
|
69
|
+
self.applier = Applier(self.config.runtime)
|
|
70
|
+
if self.config.model.provider in ("onnx", "llama_cpp"):
|
|
71
|
+
self.bootstrap_status = ensure_model(self.config.model, explicit=False)
|
|
72
|
+
else:
|
|
73
|
+
self.bootstrap_status = BootstrapStatus(
|
|
74
|
+
True, "remote", "Using remote HTTP API provider; no local model needed.", None
|
|
75
|
+
)
|
|
76
|
+
self.provider = self._select_provider(self.bootstrap_status)
|
|
77
|
+
self.planner = Planner(self.provider)
|
|
78
|
+
self.watcher = Watcher(self.capabilities, self.reporter)
|
|
79
|
+
self._register_hooks()
|
|
80
|
+
watch_started = False
|
|
81
|
+
if self.config.runtime.auto_watch:
|
|
82
|
+
watch_started = self.watcher.start()
|
|
83
|
+
self.mode = "full" if self.bootstrap_status.ready and watch_started else "analyze-only"
|
|
84
|
+
if not self.bootstrap_status.ready:
|
|
85
|
+
self.mode = "rules-only"
|
|
86
|
+
self.reporter.log(
|
|
87
|
+
"runtime.initialized",
|
|
88
|
+
{
|
|
89
|
+
"environment": self.capabilities.environment,
|
|
90
|
+
"mode": self.mode,
|
|
91
|
+
"model_mode": self.bootstrap_status.mode,
|
|
92
|
+
"idle_hook_installed": self._idle_hook_installed,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
self._initialized = True
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def _select_provider(self, status: BootstrapStatus) -> ModelProvider:
|
|
99
|
+
if self.config is None:
|
|
100
|
+
return RulesOnlyProvider()
|
|
101
|
+
|
|
102
|
+
provider_name = self.config.model.provider
|
|
103
|
+
|
|
104
|
+
if provider_name == "http_api":
|
|
105
|
+
return HttpApiProvider(self.config.model)
|
|
106
|
+
|
|
107
|
+
if not status.ready or status.model_path is None:
|
|
108
|
+
return RulesOnlyProvider()
|
|
109
|
+
|
|
110
|
+
if provider_name == "onnx":
|
|
111
|
+
if OnnxProvider is None:
|
|
112
|
+
return RulesOnlyProvider()
|
|
113
|
+
try:
|
|
114
|
+
return OnnxProvider(self.config.model, status.model_path)
|
|
115
|
+
except Exception:
|
|
116
|
+
return RulesOnlyProvider()
|
|
117
|
+
|
|
118
|
+
if provider_name == "llama_cpp":
|
|
119
|
+
if LlamaCppProvider is None:
|
|
120
|
+
return RulesOnlyProvider()
|
|
121
|
+
try:
|
|
122
|
+
return LlamaCppProvider(self.config.model, status.model_path)
|
|
123
|
+
except Exception:
|
|
124
|
+
return RulesOnlyProvider()
|
|
125
|
+
|
|
126
|
+
return RulesOnlyProvider()
|
|
127
|
+
|
|
128
|
+
def _register_hooks(self) -> None:
|
|
129
|
+
self._orig_sys_hook = sys.excepthook
|
|
130
|
+
sys.excepthook = self._sys_excepthook
|
|
131
|
+
if hasattr(threading, "excepthook"):
|
|
132
|
+
self._orig_thread_hook = threading.excepthook
|
|
133
|
+
threading.excepthook = self._thread_excepthook
|
|
134
|
+
|
|
135
|
+
if self.capabilities and self.capabilities.environment == "idle":
|
|
136
|
+
self._install_idle_hook()
|
|
137
|
+
|
|
138
|
+
def _install_idle_hook(self) -> None:
|
|
139
|
+
"""IDLE runs user code inside idlelib.run's own exec/except loop and
|
|
140
|
+
prints tracebacks itself via idlelib.run.print_exception, so the
|
|
141
|
+
exception never reaches sys.excepthook. Patch that function directly.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
import idlelib.run as idle_run
|
|
145
|
+
except ImportError: # pragma: no cover - not actually running under IDLE
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if self._idle_hook_installed:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
self._orig_idle_print_exception = idle_run.print_exception
|
|
152
|
+
|
|
153
|
+
def patched_print_exception() -> None:
|
|
154
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
155
|
+
handled = False
|
|
156
|
+
if exc_type is not None:
|
|
157
|
+
handled = self.process_exception(exc_type, exc_value, exc_tb)
|
|
158
|
+
if handled:
|
|
159
|
+
print("[errorai] exception intercepted")
|
|
160
|
+
return
|
|
161
|
+
if self._orig_idle_print_exception is not None:
|
|
162
|
+
self._orig_idle_print_exception()
|
|
163
|
+
|
|
164
|
+
idle_run.print_exception = patched_print_exception
|
|
165
|
+
self._idle_hook_installed = True
|
|
166
|
+
|
|
167
|
+
def _uninstall_idle_hook(self) -> None:
|
|
168
|
+
if not self._idle_hook_installed:
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
import idlelib.run as idle_run
|
|
172
|
+
except ImportError: # pragma: no cover
|
|
173
|
+
self._idle_hook_installed = False
|
|
174
|
+
return
|
|
175
|
+
if self._orig_idle_print_exception is not None:
|
|
176
|
+
idle_run.print_exception = self._orig_idle_print_exception
|
|
177
|
+
self._idle_hook_installed = False
|
|
178
|
+
|
|
179
|
+
def _sys_excepthook(self, exc_type, exc_value, exc_tb):
|
|
180
|
+
handled = self.process_exception(exc_type, exc_value, exc_tb)
|
|
181
|
+
if handled:
|
|
182
|
+
print("[errorai] exception intercepted")
|
|
183
|
+
return
|
|
184
|
+
if self._orig_sys_hook:
|
|
185
|
+
self._orig_sys_hook(exc_type, exc_value, exc_tb)
|
|
186
|
+
|
|
187
|
+
def _thread_excepthook(self, args):
|
|
188
|
+
handled = self.process_exception(args.exc_type, args.exc_value, args.exc_traceback)
|
|
189
|
+
if handled:
|
|
190
|
+
print("[errorai] thread exception intercepted")
|
|
191
|
+
return
|
|
192
|
+
if self._orig_thread_hook:
|
|
193
|
+
self._orig_thread_hook(args)
|
|
194
|
+
|
|
195
|
+
def process_exception(self, exc_type, exc_value, exc_tb: TracebackType | None) -> bool:
|
|
196
|
+
if not exc_tb or not self.analyzer or not self.planner or not self.applier or not self.reporter:
|
|
197
|
+
return False
|
|
198
|
+
tb_list = traceback.extract_tb(exc_tb)
|
|
199
|
+
if not tb_list:
|
|
200
|
+
return False
|
|
201
|
+
frame = tb_list[-1]
|
|
202
|
+
filename = frame.filename
|
|
203
|
+
line = linecache.getline(filename, frame.lineno).strip() if filename else ""
|
|
204
|
+
analysis = self.analyzer.analyze_exception(exc_type, exc_value, exc_tb)
|
|
205
|
+
try:
|
|
206
|
+
plan = self.planner.plan_line_fix(line or frame.line or "", analysis["message"])
|
|
207
|
+
except Exception as provider_exc:
|
|
208
|
+
# Never let a broken/missing model provider raise out of an
|
|
209
|
+
# exception hook -- that can crash the interpreter (or IDLE's
|
|
210
|
+
# subprocess) while it's in the middle of handling an exception.
|
|
211
|
+
self.reporter.log(
|
|
212
|
+
"provider.error",
|
|
213
|
+
{"analysis": analysis, "error": f"{type(provider_exc).__name__}: {provider_exc}"},
|
|
214
|
+
)
|
|
215
|
+
print(f"[errorai] Model provider failed ({type(provider_exc).__name__}: {provider_exc})")
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
if not plan or not filename or "<" in filename:
|
|
219
|
+
self.reporter.log("exception.analyzed", {"analysis": analysis, "fixed": False})
|
|
220
|
+
print(f"[errorai] Caught {analysis['type']}: {analysis['message']}")
|
|
221
|
+
print("[errorai] No automatic fix rule matched this error.")
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
print(f"[errorai] Caught {analysis['type']}: {analysis['message']}")
|
|
225
|
+
print(f"[errorai] Suggested fix -> {plan}")
|
|
226
|
+
|
|
227
|
+
if self.capabilities and self.capabilities.can_prompt_user:
|
|
228
|
+
try:
|
|
229
|
+
answer = input("[errorai] Should I fix it [Y/N]: ").strip().lower()
|
|
230
|
+
except (EOFError, OSError):
|
|
231
|
+
answer = "n"
|
|
232
|
+
if answer not in {"y", "yes"}:
|
|
233
|
+
self.reporter.log("exception.declined", {"analysis": analysis})
|
|
234
|
+
print("[errorai] Skipped.")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
result = self.applier.apply_line_change(Path(filename), frame.lineno, plan)
|
|
238
|
+
self.reporter.log(
|
|
239
|
+
"exception.handled",
|
|
240
|
+
{
|
|
241
|
+
"analysis": analysis,
|
|
242
|
+
"changed": result.changed,
|
|
243
|
+
"detail": result.detail,
|
|
244
|
+
"preview": result.preview,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
if result.preview and not result.changed:
|
|
248
|
+
print("[errorai] suggested fix (dry-run, not applied):")
|
|
249
|
+
print(result.preview)
|
|
250
|
+
print("[errorai] set dry_run = false in .errorai.toml to auto-apply")
|
|
251
|
+
elif result.changed:
|
|
252
|
+
print(f"[errorai] Applied fix to {filename}:{frame.lineno}")
|
|
253
|
+
return result.changed
|
|
254
|
+
|
|
255
|
+
def status_report(self) -> dict[str, Any]:
|
|
256
|
+
cfg = self.config or load_config()
|
|
257
|
+
caps = self.capabilities or detect_capabilities()
|
|
258
|
+
model = model_status(cfg.model)
|
|
259
|
+
return {
|
|
260
|
+
"environment": caps.environment,
|
|
261
|
+
"can_watch_fs": caps.can_watch_fs,
|
|
262
|
+
"can_apply_patches": caps.can_apply_patches,
|
|
263
|
+
"mode": self.mode,
|
|
264
|
+
"model_ready": model.ready,
|
|
265
|
+
"model_mode": self.bootstrap_status.mode,
|
|
266
|
+
"model_detail": self.bootstrap_status.detail,
|
|
267
|
+
"dry_run": cfg.runtime.dry_run,
|
|
268
|
+
"project_root": str(cfg.runtime.project_root),
|
|
269
|
+
"idle_hook_installed": self._idle_hook_installed,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def install_model(self) -> BootstrapStatus:
|
|
273
|
+
if self.config is None:
|
|
274
|
+
self.config = load_config()
|
|
275
|
+
self.bootstrap_status = ensure_model(self.config.model, explicit=True)
|
|
276
|
+
return self.bootstrap_status
|
|
277
|
+
|
|
278
|
+
def configure(self, **overrides) -> None:
|
|
279
|
+
if self.config is None:
|
|
280
|
+
self.config = load_config()
|
|
281
|
+
runtime_values = overrides.get("runtime", {})
|
|
282
|
+
model_values = overrides.get("model", {})
|
|
283
|
+
self.config = ErrorAIConfig(
|
|
284
|
+
runtime=replace(self.config.runtime, **runtime_values),
|
|
285
|
+
model=replace(self.config.model, **model_values),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def shutdown(self) -> None:
|
|
289
|
+
sys.excepthook = self._orig_sys_hook
|
|
290
|
+
if hasattr(threading, "excepthook") and self._orig_thread_hook is not None:
|
|
291
|
+
threading.excepthook = self._orig_thread_hook
|
|
292
|
+
self._uninstall_idle_hook()
|
|
293
|
+
self._initialized = False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_runtime() -> RuntimeManager:
|
|
297
|
+
return RuntimeManager.get_instance()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def configure(**overrides) -> RuntimeManager:
|
|
301
|
+
runtime = get_runtime()
|
|
302
|
+
runtime.configure(**overrides)
|
|
303
|
+
return runtime
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _reset_for_tests() -> None:
|
|
307
|
+
runtime = RuntimeManager._instance
|
|
308
|
+
if runtime is not None:
|
|
309
|
+
runtime.shutdown()
|
|
310
|
+
RuntimeManager._instance = None
|
|
311
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from .config import ModelConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModelProvider(ABC):
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RulesOnlyProvider(ModelProvider):
|
|
19
|
+
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
20
|
+
lowered = error_message.lower()
|
|
21
|
+
if "expected ':'" in lowered and not snippet.rstrip().endswith(":"):
|
|
22
|
+
return f"{snippet.rstrip()}:"
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class LlamaCppProvider(ModelProvider):
|
|
28
|
+
config: ModelConfig
|
|
29
|
+
model_path: Path
|
|
30
|
+
_llm: object | None = None
|
|
31
|
+
|
|
32
|
+
def _get_llm(self):
|
|
33
|
+
if self._llm is not None:
|
|
34
|
+
return self._llm
|
|
35
|
+
from llama_cpp import Llama # type: ignore
|
|
36
|
+
|
|
37
|
+
self._llm = Llama(
|
|
38
|
+
model_path=str(self.model_path),
|
|
39
|
+
n_ctx=self.config.context_window,
|
|
40
|
+
verbose=False,
|
|
41
|
+
)
|
|
42
|
+
return self._llm
|
|
43
|
+
|
|
44
|
+
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
45
|
+
llm = self._get_llm()
|
|
46
|
+
prompt = (
|
|
47
|
+
"You are fixing Python code. Return only the fixed single line.\n"
|
|
48
|
+
f"Error: {error_message}\n"
|
|
49
|
+
f"Code: {snippet}\n"
|
|
50
|
+
"Fix:"
|
|
51
|
+
)
|
|
52
|
+
response = llm(
|
|
53
|
+
prompt=prompt,
|
|
54
|
+
temperature=self.config.temperature,
|
|
55
|
+
max_tokens=128,
|
|
56
|
+
stop=["\n\n"],
|
|
57
|
+
)
|
|
58
|
+
text = response["choices"][0]["text"].strip()
|
|
59
|
+
return text or None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class OnnxProvider(ModelProvider):
|
|
64
|
+
config: ModelConfig
|
|
65
|
+
model_path: Path
|
|
66
|
+
_model: object | None = None
|
|
67
|
+
_tokenizer: object | None = None
|
|
68
|
+
|
|
69
|
+
def _get_model(self):
|
|
70
|
+
if self._model is not None and self._tokenizer is not None:
|
|
71
|
+
return self._model, self._tokenizer
|
|
72
|
+
|
|
73
|
+
# optimum wraps onnxruntime with a HF-style generate() API, so we
|
|
74
|
+
# don't have to hand-roll KV-cache decoding against a raw .onnx graph.
|
|
75
|
+
from optimum.onnxruntime import ORTModelForCausalLM # type: ignore
|
|
76
|
+
from transformers import AutoTokenizer # type: ignore
|
|
77
|
+
|
|
78
|
+
model_dir = self.model_path if self.model_path.is_dir() else self.model_path.parent
|
|
79
|
+
|
|
80
|
+
self._tokenizer = AutoTokenizer.from_pretrained(model_dir)
|
|
81
|
+
self._model = ORTModelForCausalLM.from_pretrained(
|
|
82
|
+
model_dir,
|
|
83
|
+
subfolder="onnx",
|
|
84
|
+
file_name="model_int8.onnx",
|
|
85
|
+
use_cache=True,
|
|
86
|
+
)
|
|
87
|
+
return self._model, self._tokenizer
|
|
88
|
+
|
|
89
|
+
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
90
|
+
model, tokenizer = self._get_model()
|
|
91
|
+
|
|
92
|
+
prompt = (
|
|
93
|
+
"You are fixing Python code. Return only the fixed single line.\n"
|
|
94
|
+
f"Error: {error_message}\n"
|
|
95
|
+
f"Code: {snippet}\n"
|
|
96
|
+
"Fix:"
|
|
97
|
+
)
|
|
98
|
+
inputs = tokenizer(prompt, return_tensors="pt")
|
|
99
|
+
max_new_tokens = 64
|
|
100
|
+
eos_token_id = tokenizer.eos_token_id
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
output_ids = model.generate(
|
|
104
|
+
**inputs,
|
|
105
|
+
max_new_tokens=max_new_tokens,
|
|
106
|
+
do_sample=self.config.temperature > 0,
|
|
107
|
+
temperature=max(self.config.temperature, 1e-4),
|
|
108
|
+
eos_token_id=eos_token_id,
|
|
109
|
+
pad_token_id=eos_token_id,
|
|
110
|
+
)
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
generated = output_ids[0][inputs["input_ids"].shape[-1]:]
|
|
115
|
+
text = tokenizer.decode(generated, skip_special_tokens=True).strip()
|
|
116
|
+
# Model can wander onto a second line/explanation; keep only the fix.
|
|
117
|
+
text = text.splitlines()[0].strip() if text else ""
|
|
118
|
+
return text or None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_PROSE_PREFIXES = (
|
|
122
|
+
"here", "here's", "sure", "the fix", "this fixes", "note:", "note that",
|
|
123
|
+
"explanation", "you should", "to fix", "i ", "i'd", "the corrected",
|
|
124
|
+
"certainly", "of course", "this line", "this code", "in python",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _clean_line_response(text: str | None, original_line: str) -> str | None:
|
|
129
|
+
"""Reduce a chat-model reply down to a single bare code line.
|
|
130
|
+
|
|
131
|
+
Models asked for "just the fix" routinely still wrap it in a markdown
|
|
132
|
+
fence, prepend "Here's the fix:", or tack on an explanatory comment.
|
|
133
|
+
None of that should ever reach Applier.apply_line_change, since the user
|
|
134
|
+
is agreeing to apply exactly one corrected line, not a paragraph.
|
|
135
|
+
"""
|
|
136
|
+
if not text:
|
|
137
|
+
return None
|
|
138
|
+
text = text.strip()
|
|
139
|
+
if text.startswith("```"):
|
|
140
|
+
text = re.sub(r"^```[a-zA-Z0-9]*\n?", "", text)
|
|
141
|
+
text = re.sub(r"\n?```$", "", text).strip()
|
|
142
|
+
|
|
143
|
+
lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
|
|
144
|
+
lines = [ln for ln in lines if not ln.startswith("```")]
|
|
145
|
+
if not lines:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
candidate = lines[0]
|
|
149
|
+
lowered = candidate.lower()
|
|
150
|
+
if any(lowered.startswith(p) for p in _PROSE_PREFIXES):
|
|
151
|
+
# Try the next non-prose line instead of giving up outright.
|
|
152
|
+
remaining = [ln for ln in lines[1:] if not any(ln.lower().startswith(p) for p in _PROSE_PREFIXES)]
|
|
153
|
+
if not remaining:
|
|
154
|
+
return None
|
|
155
|
+
candidate = remaining[0]
|
|
156
|
+
|
|
157
|
+
# Strip a trailing comment the model added that wasn't in the original --
|
|
158
|
+
# that's commentary, not part of the fix.
|
|
159
|
+
if "#" not in original_line and "#" in candidate:
|
|
160
|
+
candidate = candidate.split("#", 1)[0].rstrip()
|
|
161
|
+
|
|
162
|
+
return candidate or None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class HttpApiProvider(ModelProvider):
|
|
167
|
+
"""Uses a free, keyless, hosted chat-completions API instead of a local model.
|
|
168
|
+
|
|
169
|
+
Default target is OVHcloud AI Endpoints' anonymous tier (no signup, no
|
|
170
|
+
API key, ~2 req/min per IP): https://endpoints.ai.cloud.ovh.net
|
|
171
|
+
Trade-off vs the local ONNX provider: the error line + message leave the
|
|
172
|
+
machine over the network. Only use this provider if that's acceptable.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
config: ModelConfig
|
|
176
|
+
|
|
177
|
+
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
178
|
+
prompt = (
|
|
179
|
+
"Fix this single line of Python code so it no longer raises the "
|
|
180
|
+
"error below. Reply with ONLY the corrected line of code and "
|
|
181
|
+
"nothing else -- no explanation, no markdown fences, no comments, "
|
|
182
|
+
"no extra lines.\n\n"
|
|
183
|
+
f"Error: {error_message}\n"
|
|
184
|
+
f"Line: {snippet}\n"
|
|
185
|
+
"Fixed line:"
|
|
186
|
+
)
|
|
187
|
+
payload = json.dumps(
|
|
188
|
+
{
|
|
189
|
+
"model": self.config.http_api_model,
|
|
190
|
+
"messages": [
|
|
191
|
+
{"role": "system", "content": "You output only corrected code, never prose."},
|
|
192
|
+
{"role": "user", "content": prompt},
|
|
193
|
+
],
|
|
194
|
+
"max_tokens": 100,
|
|
195
|
+
"temperature": 0.0,
|
|
196
|
+
}
|
|
197
|
+
).encode("utf-8")
|
|
198
|
+
|
|
199
|
+
request = urllib.request.Request(
|
|
200
|
+
self.config.http_api_base_url,
|
|
201
|
+
data=payload,
|
|
202
|
+
headers={"Content-Type": "application/json"},
|
|
203
|
+
method="POST",
|
|
204
|
+
)
|
|
205
|
+
try:
|
|
206
|
+
with urllib.request.urlopen(request, timeout=self.config.http_api_timeout) as response:
|
|
207
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
208
|
+
text = body["choices"][0]["message"]["content"]
|
|
209
|
+
except (urllib.error.URLError, TimeoutError, KeyError, IndexError, ValueError, OSError):
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
return _clean_line_response(text, snippet)
|
errorai-1.5.0/errorai/config.py
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass, field, replace
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
|
-
from typing import Any, Dict
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _default_project_root() -> Path:
|
|
11
|
-
"""Prefer the directory of the script actually being run.
|
|
12
|
-
|
|
13
|
-
Path.cwd() is wrong in IDLE (and many IDEs): "Run Module" does not chdir
|
|
14
|
-
into the script's folder, it just sets sys.argv[0] to the script path.
|
|
15
|
-
Falling back to cwd() there means the running file is almost never
|
|
16
|
-
considered "inside" project_root, and every fix silently gets blocked by
|
|
17
|
-
Applier.can_edit().
|
|
18
|
-
"""
|
|
19
|
-
try:
|
|
20
|
-
candidate = sys.argv[0]
|
|
21
|
-
if candidate and Path(candidate).is_file():
|
|
22
|
-
return Path(candidate).resolve().parent
|
|
23
|
-
except Exception:
|
|
24
|
-
pass
|
|
25
|
-
return Path.cwd()
|
|
26
|
-
|
|
27
|
-
try:
|
|
28
|
-
import tomllib
|
|
29
|
-
except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
|
|
30
|
-
import tomli as tomllib
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _default_cache_dir() -> Path:
|
|
34
|
-
try:
|
|
35
|
-
from platformdirs import user_cache_dir
|
|
36
|
-
|
|
37
|
-
return Path(user_cache_dir("errorai", "ErrorAI"))
|
|
38
|
-
except Exception:
|
|
39
|
-
return Path.home() / ".cache" / "errorai"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@dataclass(frozen=True)
|
|
43
|
-
class RuntimeConfig:
|
|
44
|
-
auto_watch: bool = True
|
|
45
|
-
safe_mode: bool = True
|
|
46
|
-
dry_run: bool = True
|
|
47
|
-
project_root: Path = field(default_factory=_default_project_root)
|
|
48
|
-
ignore_patterns: tuple[str, ...] = (
|
|
49
|
-
".git",
|
|
50
|
-
"__pycache__",
|
|
51
|
-
".env",
|
|
52
|
-
".venv",
|
|
53
|
-
"venv",
|
|
54
|
-
"node_modules",
|
|
55
|
-
"*.lock",
|
|
56
|
-
".errorai",
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass(frozen=True)
|
|
61
|
-
class ModelConfig:
|
|
62
|
-
provider: str = "onnx"
|
|
63
|
-
name: str = "onnx-qwen2.5-coder-0.5b"
|
|
64
|
-
repo_id: str = "onnx-community/Qwen2.5-Coder-0.5B-Instruct"
|
|
65
|
-
model_url: str = (
|
|
66
|
-
"https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct/resolve/main/"
|
|
67
|
-
"onnx/model.onnx"
|
|
68
|
-
)
|
|
69
|
-
context_window: int = 4096
|
|
70
|
-
temperature: float = 0.1
|
|
71
|
-
auto_bootstrap: bool = True
|
|
72
|
-
download_timeout: int = 600
|
|
73
|
-
cache_dir: Path = field(default_factory=_default_cache_dir)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass(frozen=True)
|
|
77
|
-
class ErrorAIConfig:
|
|
78
|
-
runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
|
|
79
|
-
model: ModelConfig = field(default_factory=ModelConfig)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _read_toml(path: Path) -> Dict[str, Any]:
|
|
83
|
-
if not path.exists():
|
|
84
|
-
return {}
|
|
85
|
-
with path.open("rb") as handle:
|
|
86
|
-
return tomllib.load(handle)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfig:
|
|
90
|
-
data = {}
|
|
91
|
-
for key in ("auto_watch", "safe_mode", "dry_run"):
|
|
92
|
-
if key in values:
|
|
93
|
-
data[key] = bool(values[key])
|
|
94
|
-
if "ignore_patterns" in values:
|
|
95
|
-
data["ignore_patterns"] = tuple(str(v) for v in values["ignore_patterns"])
|
|
96
|
-
if "project_root" in values:
|
|
97
|
-
data["project_root"] = Path(values["project_root"]).resolve()
|
|
98
|
-
return replace(base, **data)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
|
|
102
|
-
data = {}
|
|
103
|
-
for key in ("provider", "name", "repo_id", "model_url"):
|
|
104
|
-
if key in values:
|
|
105
|
-
data[key] = str(values[key])
|
|
106
|
-
if "context_window" in values:
|
|
107
|
-
data["context_window"] = int(values["context_window"])
|
|
108
|
-
if "temperature" in values:
|
|
109
|
-
data["temperature"] = float(values["temperature"])
|
|
110
|
-
if "auto_bootstrap" in values:
|
|
111
|
-
data["auto_bootstrap"] = bool(values["auto_bootstrap"])
|
|
112
|
-
if "download_timeout" in values:
|
|
113
|
-
data["download_timeout"] = int(values["download_timeout"])
|
|
114
|
-
if "cache_dir" in values:
|
|
115
|
-
data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
|
|
116
|
-
return replace(base, **data)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def load_config(project_root: Path | None = None) -> ErrorAIConfig:
|
|
120
|
-
root = (project_root or Path.cwd()).resolve()
|
|
121
|
-
config = ErrorAIConfig(runtime=RuntimeConfig(project_root=root))
|
|
122
|
-
pyproject = _read_toml(root / "pyproject.toml")
|
|
123
|
-
pyproject_settings = pyproject.get("tool", {}).get("errorai", {})
|
|
124
|
-
local_settings = _read_toml(root / ".errorai.toml")
|
|
125
|
-
|
|
126
|
-
runtime_values = {}
|
|
127
|
-
model_values = {}
|
|
128
|
-
for source in (pyproject_settings, local_settings):
|
|
129
|
-
runtime_values.update(source.get("runtime", {}))
|
|
130
|
-
model_values.update(source.get("model", {}))
|
|
131
|
-
|
|
132
|
-
return ErrorAIConfig(
|
|
133
|
-
runtime=_coerce_runtime(config.runtime, runtime_values),
|
|
134
|
-
model=_coerce_model(config.model, model_values),
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def config_template() -> str:
|
|
139
|
-
return """[runtime]
|
|
140
|
-
auto_watch = true
|
|
141
|
-
safe_mode = true
|
|
142
|
-
dry_run = true
|
|
143
|
-
ignore_patterns = [".git", "__pycache__", ".env", ".venv", "venv", "node_modules", "*.lock", ".errorai"]
|
|
144
|
-
|
|
145
|
-
[model]
|
|
146
|
-
provider = "onnx"
|
|
147
|
-
name = "onnx-default-python-expert"
|
|
148
|
-
context_window = 4096
|
|
149
|
-
temperature = 0.1
|
|
150
|
-
auto_bootstrap = true
|
|
151
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def autostart_enabled() -> bool:
|
|
155
|
-
return os.environ.get("ERRORAI_AUTOSTART", "1") != "0"
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from .config import ModelConfig
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class ModelProvider(ABC):
|
|
9
|
-
@abstractmethod
|
|
10
|
-
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
11
|
-
raise NotImplementedError
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class RulesOnlyProvider(ModelProvider):
|
|
15
|
-
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
16
|
-
lowered = error_message.lower()
|
|
17
|
-
if "expected ':'" in lowered and not snippet.rstrip().endswith(":"):
|
|
18
|
-
return f"{snippet.rstrip()}:"
|
|
19
|
-
return None
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@dataclass
|
|
23
|
-
class LlamaCppProvider(ModelProvider):
|
|
24
|
-
config: ModelConfig
|
|
25
|
-
model_path: Path
|
|
26
|
-
_llm: object | None = None
|
|
27
|
-
|
|
28
|
-
def _get_llm(self):
|
|
29
|
-
if self._llm is not None:
|
|
30
|
-
return self._llm
|
|
31
|
-
from llama_cpp import Llama # type: ignore
|
|
32
|
-
|
|
33
|
-
self._llm = Llama(
|
|
34
|
-
model_path=str(self.model_path),
|
|
35
|
-
n_ctx=self.config.context_window,
|
|
36
|
-
verbose=False,
|
|
37
|
-
)
|
|
38
|
-
return self._llm
|
|
39
|
-
|
|
40
|
-
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
41
|
-
llm = self._get_llm()
|
|
42
|
-
prompt = (
|
|
43
|
-
"You are fixing Python code. Return only the fixed single line.\n"
|
|
44
|
-
f"Error: {error_message}\n"
|
|
45
|
-
f"Code: {snippet}\n"
|
|
46
|
-
"Fix:"
|
|
47
|
-
)
|
|
48
|
-
response = llm(
|
|
49
|
-
prompt=prompt,
|
|
50
|
-
temperature=self.config.temperature,
|
|
51
|
-
max_tokens=128,
|
|
52
|
-
stop=["\n\n"],
|
|
53
|
-
)
|
|
54
|
-
text = response["choices"][0]["text"].strip()
|
|
55
|
-
return text or None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass
|
|
59
|
-
class OnnxProvider(ModelProvider):
|
|
60
|
-
config: ModelConfig
|
|
61
|
-
model_path: Path
|
|
62
|
-
_model: object | None = None
|
|
63
|
-
_tokenizer: object | None = None
|
|
64
|
-
|
|
65
|
-
def _get_model(self):
|
|
66
|
-
if self._model is not None and self._tokenizer is not None:
|
|
67
|
-
return self._model, self._tokenizer
|
|
68
|
-
|
|
69
|
-
# optimum wraps onnxruntime with a HF-style generate() API, so we
|
|
70
|
-
# don't have to hand-roll KV-cache decoding against a raw .onnx graph.
|
|
71
|
-
from optimum.onnxruntime import ORTModelForCausalLM # type: ignore
|
|
72
|
-
from transformers import AutoTokenizer # type: ignore
|
|
73
|
-
|
|
74
|
-
model_dir = self.model_path if self.model_path.is_dir() else self.model_path.parent
|
|
75
|
-
|
|
76
|
-
self._tokenizer = AutoTokenizer.from_pretrained(model_dir)
|
|
77
|
-
self._model = ORTModelForCausalLM.from_pretrained(
|
|
78
|
-
model_dir,
|
|
79
|
-
use_cache=True,
|
|
80
|
-
)
|
|
81
|
-
return self._model, self._tokenizer
|
|
82
|
-
|
|
83
|
-
def suggest_patch(self, snippet: str, error_message: str) -> str | None:
|
|
84
|
-
model, tokenizer = self._get_model()
|
|
85
|
-
|
|
86
|
-
prompt = (
|
|
87
|
-
"You are fixing Python code. Return only the fixed single line.\n"
|
|
88
|
-
f"Error: {error_message}\n"
|
|
89
|
-
f"Code: {snippet}\n"
|
|
90
|
-
"Fix:"
|
|
91
|
-
)
|
|
92
|
-
inputs = tokenizer(prompt, return_tensors="pt")
|
|
93
|
-
max_new_tokens = 64
|
|
94
|
-
eos_token_id = tokenizer.eos_token_id
|
|
95
|
-
|
|
96
|
-
try:
|
|
97
|
-
output_ids = model.generate(
|
|
98
|
-
**inputs,
|
|
99
|
-
max_new_tokens=max_new_tokens,
|
|
100
|
-
do_sample=self.config.temperature > 0,
|
|
101
|
-
temperature=max(self.config.temperature, 1e-4),
|
|
102
|
-
eos_token_id=eos_token_id,
|
|
103
|
-
pad_token_id=eos_token_id,
|
|
104
|
-
)
|
|
105
|
-
except Exception:
|
|
106
|
-
return None
|
|
107
|
-
|
|
108
|
-
generated = output_ids[0][inputs["input_ids"].shape[-1]:]
|
|
109
|
-
text = tokenizer.decode(generated, skip_special_tokens=True).strip()
|
|
110
|
-
# Model can wander onto a second line/explanation; keep only the fix.
|
|
111
|
-
text = text.splitlines()[0].strip() if text else ""
|
|
112
|
-
return text or None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|