ErrorAI 1.5.0__tar.gz → 1.7.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.7.0}/ErrorAI.egg-info/PKG-INFO +1 -1
- {errorai-1.5.0 → errorai-1.7.0}/PKG-INFO +1 -1
- {errorai-1.5.0 → errorai-1.7.0}/errorai/config.py +12 -0
- errorai-1.7.0/errorai/providers.py +212 -0
- {errorai-1.5.0 → errorai-1.7.0}/pyproject.toml +1 -1
- errorai-1.5.0/errorai/providers.py +0 -112
- {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/entry_points.txt +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/requires.txt +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/top_level.txt +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/README.md +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/__init__.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/__main__.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/bootstrap.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/cli.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/compat.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/environment.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/pipeline.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/runtime.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/errorai/universal.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/setup.cfg +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/tests/test_bootstrap.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/tests/test_config.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/tests/test_runtime.py +0 -0
- {errorai-1.5.0 → errorai-1.7.0}/tests/test_safety.py +0 -0
|
@@ -71,6 +71,12 @@ class ModelConfig:
|
|
|
71
71
|
auto_bootstrap: bool = True
|
|
72
72
|
download_timeout: int = 600
|
|
73
73
|
cache_dir: Path = field(default_factory=_default_cache_dir)
|
|
74
|
+
# Only used when provider = "http_api". Sends the errroring line + message
|
|
75
|
+
# to this endpoint instead of running a local model. Opt-in only: unlike
|
|
76
|
+
# the local onnx/llama_cpp providers, this means code leaves the machine.
|
|
77
|
+
http_api_base_url: str = "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"
|
|
78
|
+
http_api_model: str = "qwen2.5-coder-32b-instruct"
|
|
79
|
+
http_api_timeout: float = 8.0
|
|
74
80
|
|
|
75
81
|
|
|
76
82
|
@dataclass(frozen=True)
|
|
@@ -113,6 +119,12 @@ def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
|
|
|
113
119
|
data["download_timeout"] = int(values["download_timeout"])
|
|
114
120
|
if "cache_dir" in values:
|
|
115
121
|
data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
|
|
122
|
+
if "http_api_base_url" in values:
|
|
123
|
+
data["http_api_base_url"] = str(values["http_api_base_url"])
|
|
124
|
+
if "http_api_model" in values:
|
|
125
|
+
data["http_api_model"] = str(values["http_api_model"])
|
|
126
|
+
if "http_api_timeout" in values:
|
|
127
|
+
data["http_api_timeout"] = float(values["http_api_timeout"])
|
|
116
128
|
return replace(base, **data)
|
|
117
129
|
|
|
118
130
|
|
|
@@ -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)
|
|
@@ -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
|