ErrorAI 1.2.0__tar.gz → 1.5.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.2.0 → errorai-1.5.0}/ErrorAI.egg-info/PKG-INFO +5 -1
  2. errorai-1.5.0/ErrorAI.egg-info/requires.txt +11 -0
  3. {errorai-1.2.0 → errorai-1.5.0}/PKG-INFO +5 -1
  4. errorai-1.5.0/errorai/bootstrap.py +95 -0
  5. {errorai-1.2.0 → errorai-1.5.0}/errorai/config.py +28 -5
  6. {errorai-1.2.0 → errorai-1.5.0}/errorai/pipeline.py +0 -3
  7. errorai-1.5.0/errorai/providers.py +112 -0
  8. {errorai-1.2.0 → errorai-1.5.0}/errorai/runtime.py +115 -8
  9. {errorai-1.2.0 → errorai-1.5.0}/pyproject.toml +5 -1
  10. errorai-1.2.0/ErrorAI.egg-info/requires.txt +0 -5
  11. errorai-1.2.0/errorai/bootstrap.py +0 -52
  12. errorai-1.2.0/errorai/providers.py +0 -57
  13. {errorai-1.2.0 → errorai-1.5.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
  14. {errorai-1.2.0 → errorai-1.5.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
  15. {errorai-1.2.0 → errorai-1.5.0}/ErrorAI.egg-info/entry_points.txt +0 -0
  16. {errorai-1.2.0 → errorai-1.5.0}/ErrorAI.egg-info/top_level.txt +0 -0
  17. {errorai-1.2.0 → errorai-1.5.0}/README.md +0 -0
  18. {errorai-1.2.0 → errorai-1.5.0}/errorai/__init__.py +0 -0
  19. {errorai-1.2.0 → errorai-1.5.0}/errorai/__main__.py +0 -0
  20. {errorai-1.2.0 → errorai-1.5.0}/errorai/cli.py +0 -0
  21. {errorai-1.2.0 → errorai-1.5.0}/errorai/compat.py +0 -0
  22. {errorai-1.2.0 → errorai-1.5.0}/errorai/environment.py +0 -0
  23. {errorai-1.2.0 → errorai-1.5.0}/errorai/universal.py +0 -0
  24. {errorai-1.2.0 → errorai-1.5.0}/setup.cfg +0 -0
  25. {errorai-1.2.0 → errorai-1.5.0}/tests/test_bootstrap.py +0 -0
  26. {errorai-1.2.0 → errorai-1.5.0}/tests/test_config.py +0 -0
  27. {errorai-1.2.0 → errorai-1.5.0}/tests/test_runtime.py +0 -0
  28. {errorai-1.2.0 → errorai-1.5.0}/tests/test_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.2.0
3
+ Version: 1.5.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.2.0
3
+ Version: 1.5.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,95 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import urllib.request
6
+
7
+ from .config import ModelConfig
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class BootstrapStatus:
12
+ ready: bool
13
+ mode: str
14
+ detail: str
15
+ model_path: Path | None = None
16
+
17
+
18
+ def model_path(config: ModelConfig) -> Path:
19
+ base = config.cache_dir / "models"
20
+ if config.provider == "onnx":
21
+ # ONNX text generation needs a full model directory (config.json,
22
+ # tokenizer files, model.onnx, etc.), not a single downloaded file.
23
+ return base / config.name
24
+ return base / f"{config.name}.gguf"
25
+
26
+
27
+ def _model_ready(path: Path, config: ModelConfig) -> bool:
28
+ if config.provider == "onnx":
29
+ return path.exists() and path.is_dir() and any(path.iterdir())
30
+ return path.exists()
31
+
32
+
33
+ def _download_onnx_model(config: ModelConfig, destination: Path) -> None:
34
+ from huggingface_hub import snapshot_download
35
+
36
+ destination.parent.mkdir(parents=True, exist_ok=True)
37
+ downloaded_dir = Path(
38
+ snapshot_download(
39
+ repo_id=config.repo_id,
40
+ cache_dir=str(config.cache_dir / "hf_cache"),
41
+ )
42
+ )
43
+ if destination.exists() or destination.is_symlink():
44
+ return
45
+ try:
46
+ destination.symlink_to(downloaded_dir, target_is_directory=True)
47
+ except OSError:
48
+ # Symlinks can fail on some Windows setups without dev mode/admin
49
+ # rights; fall back to a plain copy of the downloaded snapshot.
50
+ import shutil
51
+
52
+ shutil.copytree(downloaded_dir, destination)
53
+
54
+
55
+ def _download_gguf_model(config: ModelConfig, destination: Path) -> None:
56
+ destination.parent.mkdir(parents=True, exist_ok=True)
57
+ with urllib.request.urlopen(config.model_url, timeout=config.download_timeout) as response:
58
+ with destination.open("wb") as handle:
59
+ while True:
60
+ chunk = response.read(1 << 20)
61
+ if not chunk:
62
+ break
63
+ handle.write(chunk)
64
+
65
+
66
+ def ensure_model(config: ModelConfig, explicit: bool = False) -> BootstrapStatus:
67
+ path = model_path(config)
68
+ if _model_ready(path, config):
69
+ return BootstrapStatus(True, "model-ready", "Local model available.", path)
70
+ if not explicit and not config.auto_bootstrap:
71
+ return BootstrapStatus(False, "rules-only", "Auto-bootstrap is disabled.", None)
72
+ try:
73
+ if config.provider == "onnx":
74
+ _download_onnx_model(config, path)
75
+ else:
76
+ _download_gguf_model(config, path)
77
+ except Exception as exc:
78
+ return BootstrapStatus(
79
+ False,
80
+ "rules-only",
81
+ f"Model bootstrap failed ({exc.__class__.__name__}: {exc}); continuing without model.",
82
+ None,
83
+ )
84
+ if not _model_ready(path, config):
85
+ return BootstrapStatus(
86
+ False, "rules-only", "Model download finished but expected files are missing.", None
87
+ )
88
+ return BootstrapStatus(True, "model-ready", "Model bootstrap completed.", path)
89
+
90
+
91
+ def model_status(config: ModelConfig) -> BootstrapStatus:
92
+ path = model_path(config)
93
+ if _model_ready(path, config):
94
+ return BootstrapStatus(True, "model-ready", "Model files exist.", path)
95
+ return BootstrapStatus(False, "rules-only", "Model is not installed.", None)
@@ -3,8 +3,27 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass, field, replace
4
4
  from pathlib import Path
5
5
  import os
6
+ import sys
6
7
  from typing import Any, Dict
7
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
+
8
27
  try:
9
28
  import tomllib
10
29
  except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
@@ -25,7 +44,7 @@ class RuntimeConfig:
25
44
  auto_watch: bool = True
26
45
  safe_mode: bool = True
27
46
  dry_run: bool = True
28
- project_root: Path = field(default_factory=lambda: Path.cwd())
47
+ project_root: Path = field(default_factory=_default_project_root)
29
48
  ignore_patterns: tuple[str, ...] = (
30
49
  ".git",
31
50
  "__pycache__",
@@ -41,14 +60,16 @@ class RuntimeConfig:
41
60
  @dataclass(frozen=True)
42
61
  class ModelConfig:
43
62
  provider: str = "onnx"
44
- name: str = "onnx-default-python-expert"
63
+ name: str = "onnx-qwen2.5-coder-0.5b"
64
+ repo_id: str = "onnx-community/Qwen2.5-Coder-0.5B-Instruct"
45
65
  model_url: str = (
46
- "https://huggingface.co/onnx-community/Qwen2.5-Coder-1.5B-Instruct-ONNX/resolve/main/"
47
- "model.onnx"
66
+ "https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct/resolve/main/"
67
+ "onnx/model.onnx"
48
68
  )
49
69
  context_window: int = 4096
50
70
  temperature: float = 0.1
51
71
  auto_bootstrap: bool = True
72
+ download_timeout: int = 600
52
73
  cache_dir: Path = field(default_factory=_default_cache_dir)
53
74
 
54
75
 
@@ -79,7 +100,7 @@ def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfi
79
100
 
80
101
  def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
81
102
  data = {}
82
- for key in ("provider", "name", "model_url"):
103
+ for key in ("provider", "name", "repo_id", "model_url"):
83
104
  if key in values:
84
105
  data[key] = str(values[key])
85
106
  if "context_window" in values:
@@ -88,6 +109,8 @@ def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
88
109
  data["temperature"] = float(values["temperature"])
89
110
  if "auto_bootstrap" in values:
90
111
  data["auto_bootstrap"] = bool(values["auto_bootstrap"])
112
+ if "download_timeout" in values:
113
+ data["download_timeout"] = int(values["download_timeout"])
91
114
  if "cache_dir" in values:
92
115
  data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
93
116
  return replace(base, **data)
@@ -81,18 +81,15 @@ class Applier:
81
81
  path = file_path.resolve()
82
82
  if not self.can_edit(path):
83
83
  return ApplyResult(False, "Blocked by safe mode restrictions.")
84
-
85
84
  lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
86
85
  if lineno < 1 or lineno > len(lines):
87
86
  return ApplyResult(False, "Line number out of range.")
88
-
89
87
  old_line = lines[lineno - 1]
90
88
  indentation = old_line[: len(old_line) - len(old_line.lstrip())]
91
89
  candidate = f"{indentation}{new_line.strip()}\n"
92
90
  preview = f"- {old_line.rstrip()}\n+ {candidate.rstrip()}"
93
91
  if self.config.dry_run:
94
92
  return ApplyResult(False, "Dry-run mode enabled; no write applied.", preview=preview)
95
-
96
93
  lines[lineno - 1] = candidate
97
94
  path.write_text("".join(lines), encoding="utf-8")
98
95
  return ApplyResult(True, "Edit applied.", preview=preview)
@@ -0,0 +1,112 @@
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
@@ -13,7 +13,17 @@ from .bootstrap import BootstrapStatus, ensure_model, model_status
13
13
  from .config import ErrorAIConfig, autostart_enabled, load_config
14
14
  from .environment import Capabilities, detect_capabilities
15
15
  from .pipeline import Analyzer, Applier, Planner, Reporter, Watcher
16
- from .providers import LlamaCppProvider, ModelProvider, RulesOnlyProvider
16
+ from .providers import ModelProvider, RulesOnlyProvider
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]
17
27
 
18
28
 
19
29
  class RuntimeManager:
@@ -35,6 +45,8 @@ class RuntimeManager:
35
45
  self.mode = "rules-only"
36
46
  self._orig_sys_hook = sys.excepthook
37
47
  self._orig_thread_hook = getattr(threading, "excepthook", None)
48
+ self._orig_idle_print_exception = None
49
+ self._idle_hook_installed = False
38
50
 
39
51
  @classmethod
40
52
  def get_instance(cls) -> "RuntimeManager":
@@ -72,6 +84,7 @@ class RuntimeManager:
72
84
  "environment": self.capabilities.environment,
73
85
  "mode": self.mode,
74
86
  "model_mode": self.bootstrap_status.mode,
87
+ "idle_hook_installed": self._idle_hook_installed,
75
88
  },
76
89
  )
77
90
  self._initialized = True
@@ -80,12 +93,26 @@ class RuntimeManager:
80
93
  def _select_provider(self, status: BootstrapStatus) -> ModelProvider:
81
94
  if not status.ready or status.model_path is None or self.config is None:
82
95
  return RulesOnlyProvider()
83
- if self.config.model.provider != "llama_cpp":
84
- return RulesOnlyProvider()
85
- try:
86
- return LlamaCppProvider(self.config.model, status.model_path)
87
- except Exception:
88
- return RulesOnlyProvider()
96
+
97
+ provider_name = self.config.model.provider
98
+
99
+ if provider_name == "onnx":
100
+ if OnnxProvider is None:
101
+ return RulesOnlyProvider()
102
+ try:
103
+ return OnnxProvider(self.config.model, status.model_path)
104
+ except Exception:
105
+ return RulesOnlyProvider()
106
+
107
+ if provider_name == "llama_cpp":
108
+ if LlamaCppProvider is None:
109
+ return RulesOnlyProvider()
110
+ try:
111
+ return LlamaCppProvider(self.config.model, status.model_path)
112
+ except Exception:
113
+ return RulesOnlyProvider()
114
+
115
+ return RulesOnlyProvider()
89
116
 
90
117
  def _register_hooks(self) -> None:
91
118
  self._orig_sys_hook = sys.excepthook
@@ -94,6 +121,50 @@ class RuntimeManager:
94
121
  self._orig_thread_hook = threading.excepthook
95
122
  threading.excepthook = self._thread_excepthook
96
123
 
124
+ if self.capabilities and self.capabilities.environment == "idle":
125
+ self._install_idle_hook()
126
+
127
+ def _install_idle_hook(self) -> None:
128
+ """IDLE runs user code inside idlelib.run's own exec/except loop and
129
+ prints tracebacks itself via idlelib.run.print_exception, so the
130
+ exception never reaches sys.excepthook. Patch that function directly.
131
+ """
132
+ try:
133
+ import idlelib.run as idle_run
134
+ except ImportError: # pragma: no cover - not actually running under IDLE
135
+ return
136
+
137
+ if self._idle_hook_installed:
138
+ return
139
+
140
+ self._orig_idle_print_exception = idle_run.print_exception
141
+
142
+ def patched_print_exception() -> None:
143
+ exc_type, exc_value, exc_tb = sys.exc_info()
144
+ handled = False
145
+ if exc_type is not None:
146
+ handled = self.process_exception(exc_type, exc_value, exc_tb)
147
+ if handled:
148
+ print("[errorai] exception intercepted")
149
+ return
150
+ if self._orig_idle_print_exception is not None:
151
+ self._orig_idle_print_exception()
152
+
153
+ idle_run.print_exception = patched_print_exception
154
+ self._idle_hook_installed = True
155
+
156
+ def _uninstall_idle_hook(self) -> None:
157
+ if not self._idle_hook_installed:
158
+ return
159
+ try:
160
+ import idlelib.run as idle_run
161
+ except ImportError: # pragma: no cover
162
+ self._idle_hook_installed = False
163
+ return
164
+ if self._orig_idle_print_exception is not None:
165
+ idle_run.print_exception = self._orig_idle_print_exception
166
+ self._idle_hook_installed = False
167
+
97
168
  def _sys_excepthook(self, exc_type, exc_value, exc_tb):
98
169
  handled = self.process_exception(exc_type, exc_value, exc_tb)
99
170
  if handled:
@@ -120,10 +191,38 @@ class RuntimeManager:
120
191
  filename = frame.filename
121
192
  line = linecache.getline(filename, frame.lineno).strip() if filename else ""
122
193
  analysis = self.analyzer.analyze_exception(exc_type, exc_value, exc_tb)
123
- 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
+
124
207
  if not plan or not filename or "<" in filename:
125
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.")
126
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
+
127
226
  result = self.applier.apply_line_change(Path(filename), frame.lineno, plan)
128
227
  self.reporter.log(
129
228
  "exception.handled",
@@ -134,6 +233,12 @@ class RuntimeManager:
134
233
  "preview": result.preview,
135
234
  },
136
235
  )
236
+ if result.preview and not result.changed:
237
+ print("[errorai] suggested fix (dry-run, not applied):")
238
+ print(result.preview)
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}")
137
242
  return result.changed
138
243
 
139
244
  def status_report(self) -> dict[str, Any]:
@@ -150,6 +255,7 @@ class RuntimeManager:
150
255
  "model_detail": self.bootstrap_status.detail,
151
256
  "dry_run": cfg.runtime.dry_run,
152
257
  "project_root": str(cfg.runtime.project_root),
258
+ "idle_hook_installed": self._idle_hook_installed,
153
259
  }
154
260
 
155
261
  def install_model(self) -> BootstrapStatus:
@@ -172,6 +278,7 @@ class RuntimeManager:
172
278
  sys.excepthook = self._orig_sys_hook
173
279
  if hasattr(threading, "excepthook") and self._orig_thread_hook is not None:
174
280
  threading.excepthook = self._orig_thread_hook
281
+ self._uninstall_idle_hook()
175
282
  self._initialized = False
176
283
 
177
284
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ErrorAI"
7
- version = "1.2.0"
7
+ version = "1.5.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,52 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- import urllib.request
6
-
7
- from .config import ModelConfig
8
-
9
-
10
- @dataclass(frozen=True)
11
- class BootstrapStatus:
12
- ready: bool
13
- mode: str
14
- detail: str
15
- model_path: Path | None = None
16
-
17
-
18
- def model_path(config: ModelConfig) -> Path:
19
- return config.cache_dir / "models" / f"{config.name}.gguf"
20
-
21
-
22
- def _download_model(url: str, destination: Path) -> None:
23
- destination.parent.mkdir(parents=True, exist_ok=True)
24
- with urllib.request.urlopen(url, timeout=5) as response, destination.open("wb") as handle:
25
- handle.write(response.read())
26
-
27
-
28
- def ensure_model(config: ModelConfig, explicit: bool = False) -> BootstrapStatus:
29
- path = model_path(config)
30
- if path.exists():
31
- return BootstrapStatus(True, "model-ready", "Local model available.", path)
32
-
33
- if not explicit and not config.auto_bootstrap:
34
- return BootstrapStatus(False, "rules-only", "Auto-bootstrap is disabled.", None)
35
-
36
- try:
37
- _download_model(config.model_url, path)
38
- except Exception as exc:
39
- return BootstrapStatus(
40
- False,
41
- "rules-only",
42
- f"Model bootstrap failed ({exc.__class__.__name__}); continuing without model.",
43
- None,
44
- )
45
- return BootstrapStatus(True, "model-ready", "Model bootstrap completed.", path)
46
-
47
-
48
- def model_status(config: ModelConfig) -> BootstrapStatus:
49
- path = model_path(config)
50
- if path.exists():
51
- return BootstrapStatus(True, "model-ready", "Model file exists.", path)
52
- return BootstrapStatus(False, "rules-only", "Model file is not installed.", None)
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from dataclasses import dataclass
5
- from pathlib import Path
6
-
7
- from .config import ModelConfig
8
-
9
-
10
- class ModelProvider(ABC):
11
- @abstractmethod
12
- def suggest_patch(self, snippet: str, error_message: str) -> str | None:
13
- raise NotImplementedError
14
-
15
-
16
- class RulesOnlyProvider(ModelProvider):
17
- def suggest_patch(self, snippet: str, error_message: str) -> str | None:
18
- lowered = error_message.lower()
19
- if "expected ':'" in lowered and not snippet.rstrip().endswith(":"):
20
- return f"{snippet.rstrip()}:"
21
- return None
22
-
23
-
24
- @dataclass
25
- class LlamaCppProvider(ModelProvider):
26
- config: ModelConfig
27
- model_path: Path
28
- _llm: object | None = None
29
-
30
- def _get_llm(self):
31
- if self._llm is not None:
32
- return self._llm
33
- from llama_cpp import Llama # type: ignore
34
-
35
- self._llm = Llama(
36
- model_path=str(self.model_path),
37
- n_ctx=self.config.context_window,
38
- verbose=False,
39
- )
40
- return self._llm
41
-
42
- def suggest_patch(self, snippet: str, error_message: str) -> str | None:
43
- llm = self._get_llm()
44
- prompt = (
45
- "You are fixing Python code. Return only the fixed single line.\n"
46
- f"Error: {error_message}\n"
47
- f"Code: {snippet}\n"
48
- "Fix:"
49
- )
50
- response = llm(
51
- prompt=prompt,
52
- temperature=self.config.temperature,
53
- max_tokens=128,
54
- stop=["\n\n"],
55
- )
56
- text = response["choices"][0]["text"].strip()
57
- 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