ErrorAI 1.3.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.
Files changed (28) hide show
  1. {errorai-1.3.0 → errorai-1.6.0}/ErrorAI.egg-info/PKG-INFO +5 -1
  2. errorai-1.6.0/ErrorAI.egg-info/requires.txt +11 -0
  3. {errorai-1.3.0 → errorai-1.6.0}/PKG-INFO +5 -1
  4. errorai-1.6.0/errorai/config.py +311 -0
  5. errorai-1.6.0/errorai/providers.py +212 -0
  6. {errorai-1.3.0 → errorai-1.6.0}/errorai/runtime.py +31 -1
  7. {errorai-1.3.0 → errorai-1.6.0}/pyproject.toml +5 -1
  8. errorai-1.3.0/ErrorAI.egg-info/requires.txt +0 -5
  9. errorai-1.3.0/errorai/config.py +0 -136
  10. errorai-1.3.0/errorai/providers.py +0 -112
  11. {errorai-1.3.0 → errorai-1.6.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
  12. {errorai-1.3.0 → errorai-1.6.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
  13. {errorai-1.3.0 → errorai-1.6.0}/ErrorAI.egg-info/entry_points.txt +0 -0
  14. {errorai-1.3.0 → errorai-1.6.0}/ErrorAI.egg-info/top_level.txt +0 -0
  15. {errorai-1.3.0 → errorai-1.6.0}/README.md +0 -0
  16. {errorai-1.3.0 → errorai-1.6.0}/errorai/__init__.py +0 -0
  17. {errorai-1.3.0 → errorai-1.6.0}/errorai/__main__.py +0 -0
  18. {errorai-1.3.0 → errorai-1.6.0}/errorai/bootstrap.py +0 -0
  19. {errorai-1.3.0 → errorai-1.6.0}/errorai/cli.py +0 -0
  20. {errorai-1.3.0 → errorai-1.6.0}/errorai/compat.py +0 -0
  21. {errorai-1.3.0 → errorai-1.6.0}/errorai/environment.py +0 -0
  22. {errorai-1.3.0 → errorai-1.6.0}/errorai/pipeline.py +0 -0
  23. {errorai-1.3.0 → errorai-1.6.0}/errorai/universal.py +0 -0
  24. {errorai-1.3.0 → errorai-1.6.0}/setup.cfg +0 -0
  25. {errorai-1.3.0 → errorai-1.6.0}/tests/test_bootstrap.py +0 -0
  26. {errorai-1.3.0 → errorai-1.6.0}/tests/test_config.py +0 -0
  27. {errorai-1.3.0 → errorai-1.6.0}/tests/test_runtime.py +0 -0
  28. {errorai-1.3.0 → errorai-1.6.0}/tests/test_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.3.0
3
+ Version: 1.6.0
4
4
  Summary: Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks.
5
5
  Author: Aswanth R
6
6
  License: MIT
@@ -13,6 +13,10 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: platformdirs>=3.0
15
15
  Requires-Dist: onnxruntime>=1.18.0
16
+ Requires-Dist: optimum[onnxruntime]>=1.20.0
17
+ Requires-Dist: transformers>=4.40.0
18
+ Requires-Dist: huggingface_hub>=0.23.0
19
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
16
20
  Provides-Extra: watch
17
21
  Requires-Dist: watchdog>=4.0.0; extra == "watch"
18
22
 
@@ -0,0 +1,11 @@
1
+ platformdirs>=3.0
2
+ onnxruntime>=1.18.0
3
+ optimum[onnxruntime]>=1.20.0
4
+ transformers>=4.40.0
5
+ huggingface_hub>=0.23.0
6
+
7
+ [:python_version < "3.11"]
8
+ tomli>=2.0.1
9
+
10
+ [watch]
11
+ watchdog>=4.0.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.3.0
3
+ Version: 1.6.0
4
4
  Summary: Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks.
5
5
  Author: Aswanth R
6
6
  License: MIT
@@ -13,6 +13,10 @@ Requires-Python: >=3.9
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: platformdirs>=3.0
15
15
  Requires-Dist: onnxruntime>=1.18.0
16
+ Requires-Dist: optimum[onnxruntime]>=1.20.0
17
+ Requires-Dist: transformers>=4.40.0
18
+ Requires-Dist: huggingface_hub>=0.23.0
19
+ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
16
20
  Provides-Extra: watch
17
21
  Requires-Dist: watchdog>=4.0.0; extra == "watch"
18
22
 
@@ -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)
@@ -191,10 +191,38 @@ class RuntimeManager:
191
191
  filename = frame.filename
192
192
  line = linecache.getline(filename, frame.lineno).strip() if filename else ""
193
193
  analysis = self.analyzer.analyze_exception(exc_type, exc_value, exc_tb)
194
- plan = self.planner.plan_line_fix(line or frame.line or "", analysis["message"])
194
+ try:
195
+ plan = self.planner.plan_line_fix(line or frame.line or "", analysis["message"])
196
+ except Exception as provider_exc:
197
+ # Never let a broken/missing model provider raise out of an
198
+ # exception hook -- that can crash the interpreter (or IDLE's
199
+ # subprocess) while it's in the middle of handling an exception.
200
+ self.reporter.log(
201
+ "provider.error",
202
+ {"analysis": analysis, "error": f"{type(provider_exc).__name__}: {provider_exc}"},
203
+ )
204
+ print(f"[errorai] Model provider failed ({type(provider_exc).__name__}: {provider_exc})")
205
+ return False
206
+
195
207
  if not plan or not filename or "<" in filename:
196
208
  self.reporter.log("exception.analyzed", {"analysis": analysis, "fixed": False})
209
+ print(f"[errorai] Caught {analysis['type']}: {analysis['message']}")
210
+ print("[errorai] No automatic fix rule matched this error.")
197
211
  return False
212
+
213
+ print(f"[errorai] Caught {analysis['type']}: {analysis['message']}")
214
+ print(f"[errorai] Suggested fix -> {plan}")
215
+
216
+ if self.capabilities and self.capabilities.can_prompt_user:
217
+ try:
218
+ answer = input("[errorai] Should I fix it [Y/N]: ").strip().lower()
219
+ except (EOFError, OSError):
220
+ answer = "n"
221
+ if answer not in {"y", "yes"}:
222
+ self.reporter.log("exception.declined", {"analysis": analysis})
223
+ print("[errorai] Skipped.")
224
+ return False
225
+
198
226
  result = self.applier.apply_line_change(Path(filename), frame.lineno, plan)
199
227
  self.reporter.log(
200
228
  "exception.handled",
@@ -209,6 +237,8 @@ class RuntimeManager:
209
237
  print("[errorai] suggested fix (dry-run, not applied):")
210
238
  print(result.preview)
211
239
  print("[errorai] set dry_run = false in .errorai.toml to auto-apply")
240
+ elif result.changed:
241
+ print(f"[errorai] Applied fix to {filename}:{frame.lineno}")
212
242
  return result.changed
213
243
 
214
244
  def status_report(self) -> dict[str, Any]:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ErrorAI"
7
- version = "1.3.0"
7
+ version = "1.6.0"
8
8
  description = "Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -21,6 +21,10 @@ requires-python = ">=3.9"
21
21
  dependencies = [
22
22
  "platformdirs>=3.0",
23
23
  "onnxruntime>=1.18.0",
24
+ "optimum[onnxruntime]>=1.20.0",
25
+ "transformers>=4.40.0",
26
+ "huggingface_hub>=0.23.0",
27
+ "tomli>=2.0.1; python_version < '3.11'",
24
28
  ]
25
29
 
26
30
  [project.optional-dependencies]
@@ -1,5 +0,0 @@
1
- platformdirs>=3.0
2
- onnxruntime>=1.18.0
3
-
4
- [watch]
5
- watchdog>=4.0.0
@@ -1,136 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field, replace
4
- from pathlib import Path
5
- import os
6
- from typing import Any, Dict
7
-
8
- try:
9
- import tomllib
10
- except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
11
- import tomli as tomllib
12
-
13
-
14
- def _default_cache_dir() -> Path:
15
- try:
16
- from platformdirs import user_cache_dir
17
-
18
- return Path(user_cache_dir("errorai", "ErrorAI"))
19
- except Exception:
20
- return Path.home() / ".cache" / "errorai"
21
-
22
-
23
- @dataclass(frozen=True)
24
- class RuntimeConfig:
25
- auto_watch: bool = True
26
- safe_mode: bool = True
27
- dry_run: bool = True
28
- project_root: Path = field(default_factory=lambda: Path.cwd())
29
- ignore_patterns: tuple[str, ...] = (
30
- ".git",
31
- "__pycache__",
32
- ".env",
33
- ".venv",
34
- "venv",
35
- "node_modules",
36
- "*.lock",
37
- ".errorai",
38
- )
39
-
40
-
41
- @dataclass(frozen=True)
42
- class ModelConfig:
43
- provider: str = "onnx"
44
- name: str = "onnx-qwen2.5-coder-0.5b"
45
- repo_id: str = "onnx-community/Qwen2.5-Coder-0.5B-Instruct-ONNX"
46
- model_url: str = (
47
- "https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct-ONNX/resolve/main/"
48
- "model.onnx"
49
- )
50
- context_window: int = 4096
51
- temperature: float = 0.1
52
- auto_bootstrap: bool = True
53
- download_timeout: int = 600
54
- cache_dir: Path = field(default_factory=_default_cache_dir)
55
-
56
-
57
- @dataclass(frozen=True)
58
- class ErrorAIConfig:
59
- runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
60
- model: ModelConfig = field(default_factory=ModelConfig)
61
-
62
-
63
- def _read_toml(path: Path) -> Dict[str, Any]:
64
- if not path.exists():
65
- return {}
66
- with path.open("rb") as handle:
67
- return tomllib.load(handle)
68
-
69
-
70
- def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfig:
71
- data = {}
72
- for key in ("auto_watch", "safe_mode", "dry_run"):
73
- if key in values:
74
- data[key] = bool(values[key])
75
- if "ignore_patterns" in values:
76
- data["ignore_patterns"] = tuple(str(v) for v in values["ignore_patterns"])
77
- if "project_root" in values:
78
- data["project_root"] = Path(values["project_root"]).resolve()
79
- return replace(base, **data)
80
-
81
-
82
- def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
83
- data = {}
84
- for key in ("provider", "name", "repo_id", "model_url"):
85
- if key in values:
86
- data[key] = str(values[key])
87
- if "context_window" in values:
88
- data["context_window"] = int(values["context_window"])
89
- if "temperature" in values:
90
- data["temperature"] = float(values["temperature"])
91
- if "auto_bootstrap" in values:
92
- data["auto_bootstrap"] = bool(values["auto_bootstrap"])
93
- if "download_timeout" in values:
94
- data["download_timeout"] = int(values["download_timeout"])
95
- if "cache_dir" in values:
96
- data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
97
- return replace(base, **data)
98
-
99
-
100
- def load_config(project_root: Path | None = None) -> ErrorAIConfig:
101
- root = (project_root or Path.cwd()).resolve()
102
- config = ErrorAIConfig(runtime=RuntimeConfig(project_root=root))
103
- pyproject = _read_toml(root / "pyproject.toml")
104
- pyproject_settings = pyproject.get("tool", {}).get("errorai", {})
105
- local_settings = _read_toml(root / ".errorai.toml")
106
-
107
- runtime_values = {}
108
- model_values = {}
109
- for source in (pyproject_settings, local_settings):
110
- runtime_values.update(source.get("runtime", {}))
111
- model_values.update(source.get("model", {}))
112
-
113
- return ErrorAIConfig(
114
- runtime=_coerce_runtime(config.runtime, runtime_values),
115
- model=_coerce_model(config.model, model_values),
116
- )
117
-
118
-
119
- def config_template() -> str:
120
- return """[runtime]
121
- auto_watch = true
122
- safe_mode = true
123
- dry_run = true
124
- ignore_patterns = [".git", "__pycache__", ".env", ".venv", "venv", "node_modules", "*.lock", ".errorai"]
125
-
126
- [model]
127
- provider = "onnx"
128
- name = "onnx-default-python-expert"
129
- context_window = 4096
130
- temperature = 0.1
131
- auto_bootstrap = true
132
- """
133
-
134
-
135
- def autostart_enabled() -> bool:
136
- 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