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.
Files changed (26) hide show
  1. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/PKG-INFO +1 -1
  2. {errorai-1.5.0 → errorai-1.7.0}/PKG-INFO +1 -1
  3. {errorai-1.5.0 → errorai-1.7.0}/errorai/config.py +12 -0
  4. errorai-1.7.0/errorai/providers.py +212 -0
  5. {errorai-1.5.0 → errorai-1.7.0}/pyproject.toml +1 -1
  6. errorai-1.5.0/errorai/providers.py +0 -112
  7. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
  8. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
  9. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/entry_points.txt +0 -0
  10. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/requires.txt +0 -0
  11. {errorai-1.5.0 → errorai-1.7.0}/ErrorAI.egg-info/top_level.txt +0 -0
  12. {errorai-1.5.0 → errorai-1.7.0}/README.md +0 -0
  13. {errorai-1.5.0 → errorai-1.7.0}/errorai/__init__.py +0 -0
  14. {errorai-1.5.0 → errorai-1.7.0}/errorai/__main__.py +0 -0
  15. {errorai-1.5.0 → errorai-1.7.0}/errorai/bootstrap.py +0 -0
  16. {errorai-1.5.0 → errorai-1.7.0}/errorai/cli.py +0 -0
  17. {errorai-1.5.0 → errorai-1.7.0}/errorai/compat.py +0 -0
  18. {errorai-1.5.0 → errorai-1.7.0}/errorai/environment.py +0 -0
  19. {errorai-1.5.0 → errorai-1.7.0}/errorai/pipeline.py +0 -0
  20. {errorai-1.5.0 → errorai-1.7.0}/errorai/runtime.py +0 -0
  21. {errorai-1.5.0 → errorai-1.7.0}/errorai/universal.py +0 -0
  22. {errorai-1.5.0 → errorai-1.7.0}/setup.cfg +0 -0
  23. {errorai-1.5.0 → errorai-1.7.0}/tests/test_bootstrap.py +0 -0
  24. {errorai-1.5.0 → errorai-1.7.0}/tests/test_config.py +0 -0
  25. {errorai-1.5.0 → errorai-1.7.0}/tests/test_runtime.py +0 -0
  26. {errorai-1.5.0 → errorai-1.7.0}/tests/test_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.5.0
3
+ Version: 1.7.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.5.0
3
+ Version: 1.7.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
@@ -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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ErrorAI"
7
- version = "1.5.0"
7
+ version = "1.7.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 = [
@@ -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