ErrorAI 1.9.2__tar.gz → 1.10.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.9.2 → errorai-1.10.0}/ErrorAI.egg-info/PKG-INFO +1 -1
  2. {errorai-1.9.2 → errorai-1.10.0}/PKG-INFO +1 -1
  3. errorai-1.10.0/errorai/bootstrap.py +100 -0
  4. {errorai-1.9.2 → errorai-1.10.0}/errorai/runtime.py +1 -0
  5. {errorai-1.9.2 → errorai-1.10.0}/pyproject.toml +1 -1
  6. errorai-1.9.2/errorai/providers.py +0 -212
  7. {errorai-1.9.2 → errorai-1.10.0}/ErrorAI.egg-info/SOURCES.txt +0 -0
  8. {errorai-1.9.2 → errorai-1.10.0}/ErrorAI.egg-info/dependency_links.txt +0 -0
  9. {errorai-1.9.2 → errorai-1.10.0}/ErrorAI.egg-info/entry_points.txt +0 -0
  10. {errorai-1.9.2 → errorai-1.10.0}/ErrorAI.egg-info/requires.txt +0 -0
  11. {errorai-1.9.2 → errorai-1.10.0}/ErrorAI.egg-info/top_level.txt +0 -0
  12. {errorai-1.9.2 → errorai-1.10.0}/README.md +0 -0
  13. {errorai-1.9.2 → errorai-1.10.0}/errorai/__init__.py +0 -0
  14. {errorai-1.9.2 → errorai-1.10.0}/errorai/__main__.py +0 -0
  15. {errorai-1.9.2 → errorai-1.10.0}/errorai/cli.py +0 -0
  16. {errorai-1.9.2 → errorai-1.10.0}/errorai/compat.py +0 -0
  17. {errorai-1.9.2 → errorai-1.10.0}/errorai/config.py +0 -0
  18. {errorai-1.9.2 → errorai-1.10.0}/errorai/environment.py +0 -0
  19. {errorai-1.9.2 → errorai-1.10.0}/errorai/pipeline.py +0 -0
  20. /errorai-1.9.2/errorai/bootstrap.py → /errorai-1.10.0/errorai/providers.py +0 -0
  21. {errorai-1.9.2 → errorai-1.10.0}/errorai/universal.py +0 -0
  22. {errorai-1.9.2 → errorai-1.10.0}/setup.cfg +0 -0
  23. {errorai-1.9.2 → errorai-1.10.0}/tests/test_bootstrap.py +0 -0
  24. {errorai-1.9.2 → errorai-1.10.0}/tests/test_config.py +0 -0
  25. {errorai-1.9.2 → errorai-1.10.0}/tests/test_runtime.py +0 -0
  26. {errorai-1.9.2 → errorai-1.10.0}/tests/test_safety.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErrorAI
3
- Version: 1.9.2
3
+ Version: 1.10.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.9.2
3
+ Version: 1.10.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
@@ -0,0 +1,100 @@
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
+ allow_patterns=[
42
+ "*.json",
43
+ "*.txt",
44
+ "onnx/model_int8.onnx",
45
+ ],
46
+ )
47
+ )
48
+ if destination.exists() or destination.is_symlink():
49
+ return
50
+ try:
51
+ destination.symlink_to(downloaded_dir, target_is_directory=True)
52
+ except OSError:
53
+ # Symlinks can fail on some Windows setups without dev mode/admin
54
+ # rights; fall back to a plain copy of the downloaded snapshot.
55
+ import shutil
56
+
57
+ shutil.copytree(downloaded_dir, destination)
58
+
59
+
60
+ def _download_gguf_model(config: ModelConfig, destination: Path) -> None:
61
+ destination.parent.mkdir(parents=True, exist_ok=True)
62
+ with urllib.request.urlopen(config.model_url, timeout=config.download_timeout) as response:
63
+ with destination.open("wb") as handle:
64
+ while True:
65
+ chunk = response.read(1 << 20)
66
+ if not chunk:
67
+ break
68
+ handle.write(chunk)
69
+
70
+
71
+ def ensure_model(config: ModelConfig, explicit: bool = False) -> BootstrapStatus:
72
+ path = model_path(config)
73
+ if _model_ready(path, config):
74
+ return BootstrapStatus(True, "model-ready", "Local model available.", path)
75
+ if not explicit and not config.auto_bootstrap:
76
+ return BootstrapStatus(False, "rules-only", "Auto-bootstrap is disabled.", None)
77
+ try:
78
+ if config.provider == "onnx":
79
+ _download_onnx_model(config, path)
80
+ else:
81
+ _download_gguf_model(config, path)
82
+ except Exception as exc:
83
+ return BootstrapStatus(
84
+ False,
85
+ "rules-only",
86
+ f"Model bootstrap failed ({exc.__class__.__name__}: {exc}); continuing without model.",
87
+ None,
88
+ )
89
+ if not _model_ready(path, config):
90
+ return BootstrapStatus(
91
+ False, "rules-only", "Model download finished but expected files are missing.", None
92
+ )
93
+ return BootstrapStatus(True, "model-ready", "Model bootstrap completed.", path)
94
+
95
+
96
+ def model_status(config: ModelConfig) -> BootstrapStatus:
97
+ path = model_path(config)
98
+ if _model_ready(path, config):
99
+ return BootstrapStatus(True, "model-ready", "Model files exist.", path)
100
+ return BootstrapStatus(False, "rules-only", "Model is not installed.", None)
@@ -312,3 +312,4 @@ def _reset_for_tests() -> None:
312
312
  if runtime is not None:
313
313
  runtime.shutdown()
314
314
  RuntimeManager._instance = None
315
+
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ErrorAI"
7
- version = "1.9.2"
7
+ version = "1.10.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,212 +0,0 @@
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)
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