ErrorAI 1.2.0__tar.gz → 1.3.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.2.0 → errorai-1.3.0}/ErrorAI.egg-info/PKG-INFO +1 -1
- {errorai-1.2.0 → errorai-1.3.0}/PKG-INFO +1 -1
- errorai-1.3.0/errorai/bootstrap.py +95 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/config.py +7 -3
- {errorai-1.2.0 → errorai-1.3.0}/errorai/pipeline.py +0 -3
- errorai-1.3.0/errorai/providers.py +112 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/runtime.py +84 -7
- {errorai-1.2.0 → errorai-1.3.0}/pyproject.toml +1 -1
- errorai-1.2.0/errorai/bootstrap.py +0 -52
- errorai-1.2.0/errorai/providers.py +0 -57
- {errorai-1.2.0 → errorai-1.3.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/ErrorAI.egg-info/entry_points.txt +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/ErrorAI.egg-info/requires.txt +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/ErrorAI.egg-info/top_level.txt +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/README.md +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/__init__.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/__main__.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/cli.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/compat.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/environment.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/errorai/universal.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/setup.cfg +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/tests/test_bootstrap.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/tests/test_config.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/tests/test_runtime.py +0 -0
- {errorai-1.2.0 → errorai-1.3.0}/tests/test_safety.py +0 -0
|
@@ -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)
|
|
@@ -41,14 +41,16 @@ class RuntimeConfig:
|
|
|
41
41
|
@dataclass(frozen=True)
|
|
42
42
|
class ModelConfig:
|
|
43
43
|
provider: str = "onnx"
|
|
44
|
-
name: str = "onnx-
|
|
44
|
+
name: str = "onnx-qwen2.5-coder-0.5b"
|
|
45
|
+
repo_id: str = "onnx-community/Qwen2.5-Coder-0.5B-Instruct-ONNX"
|
|
45
46
|
model_url: str = (
|
|
46
|
-
"https://huggingface.co/onnx-community/Qwen2.5-Coder-
|
|
47
|
+
"https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct-ONNX/resolve/main/"
|
|
47
48
|
"model.onnx"
|
|
48
49
|
)
|
|
49
50
|
context_window: int = 4096
|
|
50
51
|
temperature: float = 0.1
|
|
51
52
|
auto_bootstrap: bool = True
|
|
53
|
+
download_timeout: int = 600
|
|
52
54
|
cache_dir: Path = field(default_factory=_default_cache_dir)
|
|
53
55
|
|
|
54
56
|
|
|
@@ -79,7 +81,7 @@ def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfi
|
|
|
79
81
|
|
|
80
82
|
def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
|
|
81
83
|
data = {}
|
|
82
|
-
for key in ("provider", "name", "model_url"):
|
|
84
|
+
for key in ("provider", "name", "repo_id", "model_url"):
|
|
83
85
|
if key in values:
|
|
84
86
|
data[key] = str(values[key])
|
|
85
87
|
if "context_window" in values:
|
|
@@ -88,6 +90,8 @@ def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
|
|
|
88
90
|
data["temperature"] = float(values["temperature"])
|
|
89
91
|
if "auto_bootstrap" in values:
|
|
90
92
|
data["auto_bootstrap"] = bool(values["auto_bootstrap"])
|
|
93
|
+
if "download_timeout" in values:
|
|
94
|
+
data["download_timeout"] = int(values["download_timeout"])
|
|
91
95
|
if "cache_dir" in values:
|
|
92
96
|
data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
|
|
93
97
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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:
|
|
@@ -134,6 +205,10 @@ class RuntimeManager:
|
|
|
134
205
|
"preview": result.preview,
|
|
135
206
|
},
|
|
136
207
|
)
|
|
208
|
+
if result.preview and not result.changed:
|
|
209
|
+
print("[errorai] suggested fix (dry-run, not applied):")
|
|
210
|
+
print(result.preview)
|
|
211
|
+
print("[errorai] set dry_run = false in .errorai.toml to auto-apply")
|
|
137
212
|
return result.changed
|
|
138
213
|
|
|
139
214
|
def status_report(self) -> dict[str, Any]:
|
|
@@ -150,6 +225,7 @@ class RuntimeManager:
|
|
|
150
225
|
"model_detail": self.bootstrap_status.detail,
|
|
151
226
|
"dry_run": cfg.runtime.dry_run,
|
|
152
227
|
"project_root": str(cfg.runtime.project_root),
|
|
228
|
+
"idle_hook_installed": self._idle_hook_installed,
|
|
153
229
|
}
|
|
154
230
|
|
|
155
231
|
def install_model(self) -> BootstrapStatus:
|
|
@@ -172,6 +248,7 @@ class RuntimeManager:
|
|
|
172
248
|
sys.excepthook = self._orig_sys_hook
|
|
173
249
|
if hasattr(threading, "excepthook") and self._orig_thread_hook is not None:
|
|
174
250
|
threading.excepthook = self._orig_thread_hook
|
|
251
|
+
self._uninstall_idle_hook()
|
|
175
252
|
self._initialized = False
|
|
176
253
|
|
|
177
254
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|