ErrorAI 0.9.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-0.9.0/ErrorAI.egg-info/PKG-INFO +64 -0
- errorai-0.9.0/ErrorAI.egg-info/SOURCES.txt +23 -0
- errorai-0.9.0/ErrorAI.egg-info/dependency_links.txt +1 -0
- errorai-0.9.0/ErrorAI.egg-info/entry_points.txt +2 -0
- errorai-0.9.0/ErrorAI.egg-info/requires.txt +5 -0
- errorai-0.9.0/ErrorAI.egg-info/top_level.txt +1 -0
- errorai-0.9.0/PKG-INFO +64 -0
- errorai-0.9.0/README.md +46 -0
- errorai-0.9.0/errorai/__init__.py +9 -0
- errorai-0.9.0/errorai/__main__.py +3 -0
- errorai-0.9.0/errorai/bootstrap.py +52 -0
- errorai-0.9.0/errorai/cli.py +67 -0
- errorai-0.9.0/errorai/compat.py +36 -0
- errorai-0.9.0/errorai/config.py +132 -0
- errorai-0.9.0/errorai/environment.py +38 -0
- errorai-0.9.0/errorai/pipeline.py +118 -0
- errorai-0.9.0/errorai/providers.py +57 -0
- errorai-0.9.0/errorai/runtime.py +186 -0
- errorai-0.9.0/errorai/universal.py +8 -0
- errorai-0.9.0/pyproject.toml +33 -0
- errorai-0.9.0/setup.cfg +4 -0
- errorai-0.9.0/tests/test_bootstrap.py +34 -0
- errorai-0.9.0/tests/test_config.py +31 -0
- errorai-0.9.0/tests/test_runtime.py +33 -0
- errorai-0.9.0/tests/test_safety.py +36 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErrorAI
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks.
|
|
5
|
+
Author: Aswanth R
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NodeX-AR/ErrorAI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: platformdirs>=3.0
|
|
15
|
+
Requires-Dist: onnxruntime>=1.18.0
|
|
16
|
+
Provides-Extra: watch
|
|
17
|
+
Requires-Dist: watchdog>=4.0.0; extra == "watch"
|
|
18
|
+
|
|
19
|
+
# ErrorAI v2
|
|
20
|
+
|
|
21
|
+
`ErrorAI` is a Python-only autonomous runtime that starts on import, catches exceptions, and operates with safe-by-default watch/read/write behavior.
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install ErrorAI
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import errorai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That import initializes a singleton runtime, registers exception hooks, and starts background watch services when supported.
|
|
34
|
+
|
|
35
|
+
## Optional setup commands
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
errorai init
|
|
40
|
+
errorai doctor
|
|
41
|
+
errorai install-model
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `init`: writes default `.errorai.toml`
|
|
45
|
+
- `doctor`: reports environment/runtime/model readiness and fallback mode
|
|
46
|
+
- `install-model`: retries local model bootstrap
|
|
47
|
+
|
|
48
|
+
## Model bootstrap strategy
|
|
49
|
+
|
|
50
|
+
- Core package does not bundle large model weights.
|
|
51
|
+
- On import/first use, ErrorAI attempts to bootstrap a lightweight local coding model into user cache.
|
|
52
|
+
- If bootstrap fails (offline/network/permissions), runtime continues in `rules-only` mode with clear status.
|
|
53
|
+
|
|
54
|
+
## Safety defaults
|
|
55
|
+
|
|
56
|
+
- Safe mode is enabled by default.
|
|
57
|
+
- Writes are restricted to the project root.
|
|
58
|
+
- Sensitive/common ignore patterns are blocked by default.
|
|
59
|
+
- Dry-run mode is on by default to preview edits before writing.
|
|
60
|
+
|
|
61
|
+
## Migration notes from v1 beta
|
|
62
|
+
|
|
63
|
+
- `import errorai` now auto-starts the runtime; explicit `global_activate()` is optional.
|
|
64
|
+
- `@watch` and `catch_errors` remain available for compatibility but are no longer required.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ErrorAI.egg-info/PKG-INFO
|
|
4
|
+
ErrorAI.egg-info/SOURCES.txt
|
|
5
|
+
ErrorAI.egg-info/dependency_links.txt
|
|
6
|
+
ErrorAI.egg-info/entry_points.txt
|
|
7
|
+
ErrorAI.egg-info/requires.txt
|
|
8
|
+
ErrorAI.egg-info/top_level.txt
|
|
9
|
+
errorai/__init__.py
|
|
10
|
+
errorai/__main__.py
|
|
11
|
+
errorai/bootstrap.py
|
|
12
|
+
errorai/cli.py
|
|
13
|
+
errorai/compat.py
|
|
14
|
+
errorai/config.py
|
|
15
|
+
errorai/environment.py
|
|
16
|
+
errorai/pipeline.py
|
|
17
|
+
errorai/providers.py
|
|
18
|
+
errorai/runtime.py
|
|
19
|
+
errorai/universal.py
|
|
20
|
+
tests/test_bootstrap.py
|
|
21
|
+
tests/test_config.py
|
|
22
|
+
tests/test_runtime.py
|
|
23
|
+
tests/test_safety.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
errorai
|
errorai-0.9.0/PKG-INFO
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ErrorAI
|
|
3
|
+
Version: 0.9.0
|
|
4
|
+
Summary: Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks.
|
|
5
|
+
Author: Aswanth R
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NodeX-AR/ErrorAI
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: platformdirs>=3.0
|
|
15
|
+
Requires-Dist: onnxruntime>=1.18.0
|
|
16
|
+
Provides-Extra: watch
|
|
17
|
+
Requires-Dist: watchdog>=4.0.0; extra == "watch"
|
|
18
|
+
|
|
19
|
+
# ErrorAI v2
|
|
20
|
+
|
|
21
|
+
`ErrorAI` is a Python-only autonomous runtime that starts on import, catches exceptions, and operates with safe-by-default watch/read/write behavior.
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install ErrorAI
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import errorai
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That import initializes a singleton runtime, registers exception hooks, and starts background watch services when supported.
|
|
34
|
+
|
|
35
|
+
## Optional setup commands
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
errorai init
|
|
40
|
+
errorai doctor
|
|
41
|
+
errorai install-model
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- `init`: writes default `.errorai.toml`
|
|
45
|
+
- `doctor`: reports environment/runtime/model readiness and fallback mode
|
|
46
|
+
- `install-model`: retries local model bootstrap
|
|
47
|
+
|
|
48
|
+
## Model bootstrap strategy
|
|
49
|
+
|
|
50
|
+
- Core package does not bundle large model weights.
|
|
51
|
+
- On import/first use, ErrorAI attempts to bootstrap a lightweight local coding model into user cache.
|
|
52
|
+
- If bootstrap fails (offline/network/permissions), runtime continues in `rules-only` mode with clear status.
|
|
53
|
+
|
|
54
|
+
## Safety defaults
|
|
55
|
+
|
|
56
|
+
- Safe mode is enabled by default.
|
|
57
|
+
- Writes are restricted to the project root.
|
|
58
|
+
- Sensitive/common ignore patterns are blocked by default.
|
|
59
|
+
- Dry-run mode is on by default to preview edits before writing.
|
|
60
|
+
|
|
61
|
+
## Migration notes from v1 beta
|
|
62
|
+
|
|
63
|
+
- `import errorai` now auto-starts the runtime; explicit `global_activate()` is optional.
|
|
64
|
+
- `@watch` and `catch_errors` remain available for compatibility but are no longer required.
|
errorai-0.9.0/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ErrorAI v2
|
|
2
|
+
|
|
3
|
+
`ErrorAI` is a Python-only autonomous runtime that starts on import, catches exceptions, and operates with safe-by-default watch/read/write behavior.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ErrorAI
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import errorai
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That import initializes a singleton runtime, registers exception hooks, and starts background watch services when supported.
|
|
16
|
+
|
|
17
|
+
## Optional setup commands
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
errorai init
|
|
22
|
+
errorai doctor
|
|
23
|
+
errorai install-model
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `init`: writes default `.errorai.toml`
|
|
27
|
+
- `doctor`: reports environment/runtime/model readiness and fallback mode
|
|
28
|
+
- `install-model`: retries local model bootstrap
|
|
29
|
+
|
|
30
|
+
## Model bootstrap strategy
|
|
31
|
+
|
|
32
|
+
- Core package does not bundle large model weights.
|
|
33
|
+
- On import/first use, ErrorAI attempts to bootstrap a lightweight local coding model into user cache.
|
|
34
|
+
- If bootstrap fails (offline/network/permissions), runtime continues in `rules-only` mode with clear status.
|
|
35
|
+
|
|
36
|
+
## Safety defaults
|
|
37
|
+
|
|
38
|
+
- Safe mode is enabled by default.
|
|
39
|
+
- Writes are restricted to the project root.
|
|
40
|
+
- Sensitive/common ignore patterns are blocked by default.
|
|
41
|
+
- Dry-run mode is on by default to preview edits before writing.
|
|
42
|
+
|
|
43
|
+
## Migration notes from v1 beta
|
|
44
|
+
|
|
45
|
+
- `import errorai` now auto-starts the runtime; explicit `global_activate()` is optional.
|
|
46
|
+
- `@watch` and `catch_errors` remain available for compatibility but are no longer required.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .compat import catch_errors, global_activate, watch
|
|
2
|
+
from .runtime import configure, get_runtime
|
|
3
|
+
|
|
4
|
+
__version__ = "2.0.0"
|
|
5
|
+
__all__ = ["get_runtime", "configure", "watch", "catch_errors", "global_activate"]
|
|
6
|
+
|
|
7
|
+
# Import-time auto-start for zero-decorator usage.
|
|
8
|
+
runtime = get_runtime()
|
|
9
|
+
runtime.initialize()
|
|
@@ -0,0 +1,52 @@
|
|
|
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)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .bootstrap import ensure_model
|
|
7
|
+
from .config import config_template, load_config
|
|
8
|
+
from .runtime import get_runtime
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_init(args) -> int:
|
|
12
|
+
root = Path.cwd()
|
|
13
|
+
config_path = root / ".errorai.toml"
|
|
14
|
+
if not config_path.exists() or args.force:
|
|
15
|
+
config_path.write_text(config_template(), encoding="utf-8")
|
|
16
|
+
print(f"[errorai] Wrote {config_path}")
|
|
17
|
+
else:
|
|
18
|
+
print(f"[errorai] Config already exists: {config_path}")
|
|
19
|
+
return 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def cmd_doctor(_args) -> int:
|
|
23
|
+
runtime = get_runtime().initialize()
|
|
24
|
+
status = runtime.status_report()
|
|
25
|
+
print("ErrorAI Doctor")
|
|
26
|
+
for key in (
|
|
27
|
+
"environment",
|
|
28
|
+
"can_watch_fs",
|
|
29
|
+
"can_apply_patches",
|
|
30
|
+
"mode",
|
|
31
|
+
"model_ready",
|
|
32
|
+
"model_mode",
|
|
33
|
+
"model_detail",
|
|
34
|
+
"dry_run",
|
|
35
|
+
"project_root",
|
|
36
|
+
):
|
|
37
|
+
print(f"- {key}: {status[key]}")
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_install_model(_args) -> int:
|
|
42
|
+
cfg = load_config()
|
|
43
|
+
status = ensure_model(cfg.model, explicit=True)
|
|
44
|
+
print(status.detail)
|
|
45
|
+
return 0 if status.ready else 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
49
|
+
parser = argparse.ArgumentParser(prog="errorai")
|
|
50
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
51
|
+
|
|
52
|
+
init_parser = sub.add_parser("init", help="Create default .errorai.toml config.")
|
|
53
|
+
init_parser.add_argument("--force", action="store_true", help="Overwrite existing config.")
|
|
54
|
+
init_parser.set_defaults(func=cmd_init)
|
|
55
|
+
|
|
56
|
+
doctor_parser = sub.add_parser("doctor", help="Check runtime readiness and fallback state.")
|
|
57
|
+
doctor_parser.set_defaults(func=cmd_doctor)
|
|
58
|
+
|
|
59
|
+
install_parser = sub.add_parser("install-model", help="Install or retry local model bootstrap.")
|
|
60
|
+
install_parser.set_defaults(func=cmd_install_model)
|
|
61
|
+
return parser
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main(argv: list[str] | None = None) -> int:
|
|
65
|
+
parser = build_parser()
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
return args.func(args)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .runtime import get_runtime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def watch(func):
|
|
10
|
+
@functools.wraps(func)
|
|
11
|
+
def wrapper(*args, **kwargs):
|
|
12
|
+
try:
|
|
13
|
+
return func(*args, **kwargs)
|
|
14
|
+
except Exception:
|
|
15
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
16
|
+
get_runtime().process_exception(exc_type, exc_value, exc_tb)
|
|
17
|
+
raise
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class catch_errors:
|
|
23
|
+
def __enter__(self):
|
|
24
|
+
return self
|
|
25
|
+
|
|
26
|
+
def __exit__(self, exc_type, exc_value, exc_tb):
|
|
27
|
+
if exc_type is None:
|
|
28
|
+
return False
|
|
29
|
+
get_runtime().process_exception(exc_type, exc_value, exc_tb)
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def global_activate():
|
|
34
|
+
runtime = get_runtime()
|
|
35
|
+
runtime.initialize()
|
|
36
|
+
return runtime
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, replace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import tomllib
|
|
10
|
+
except ModuleNotFoundError: # pragma: no cover - py<3.11 fallback
|
|
11
|
+
import tomli as tomllib
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _default_cache_dir() -> Path:
|
|
15
|
+
try:
|
|
16
|
+
from platformdirs import user_cache_dir
|
|
17
|
+
|
|
18
|
+
return Path(user_cache_dir("errorai", "ErrorAI"))
|
|
19
|
+
except Exception:
|
|
20
|
+
return Path.home() / ".cache" / "errorai"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class RuntimeConfig:
|
|
25
|
+
auto_watch: bool = True
|
|
26
|
+
safe_mode: bool = True
|
|
27
|
+
dry_run: bool = True
|
|
28
|
+
project_root: Path = field(default_factory=lambda: Path.cwd())
|
|
29
|
+
ignore_patterns: tuple[str, ...] = (
|
|
30
|
+
".git",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
".env",
|
|
33
|
+
".venv",
|
|
34
|
+
"venv",
|
|
35
|
+
"node_modules",
|
|
36
|
+
"*.lock",
|
|
37
|
+
".errorai",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ModelConfig:
|
|
43
|
+
provider: str = "onnx"
|
|
44
|
+
name: str = "onnx-default-python-expert"
|
|
45
|
+
model_url: str = (
|
|
46
|
+
"https://huggingface.co/onnx-community/Qwen2.5-Coder-1.5B-Instruct-ONNX/resolve/main/"
|
|
47
|
+
"model.onnx"
|
|
48
|
+
)
|
|
49
|
+
context_window: int = 4096
|
|
50
|
+
temperature: float = 0.1
|
|
51
|
+
auto_bootstrap: bool = True
|
|
52
|
+
cache_dir: Path = field(default_factory=_default_cache_dir)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ErrorAIConfig:
|
|
57
|
+
runtime: RuntimeConfig = field(default_factory=RuntimeConfig)
|
|
58
|
+
model: ModelConfig = field(default_factory=ModelConfig)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_toml(path: Path) -> Dict[str, Any]:
|
|
62
|
+
if not path.exists():
|
|
63
|
+
return {}
|
|
64
|
+
with path.open("rb") as handle:
|
|
65
|
+
return tomllib.load(handle)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _coerce_runtime(base: RuntimeConfig, values: Dict[str, Any]) -> RuntimeConfig:
|
|
69
|
+
data = {}
|
|
70
|
+
for key in ("auto_watch", "safe_mode", "dry_run"):
|
|
71
|
+
if key in values:
|
|
72
|
+
data[key] = bool(values[key])
|
|
73
|
+
if "ignore_patterns" in values:
|
|
74
|
+
data["ignore_patterns"] = tuple(str(v) for v in values["ignore_patterns"])
|
|
75
|
+
if "project_root" in values:
|
|
76
|
+
data["project_root"] = Path(values["project_root"]).resolve()
|
|
77
|
+
return replace(base, **data)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _coerce_model(base: ModelConfig, values: Dict[str, Any]) -> ModelConfig:
|
|
81
|
+
data = {}
|
|
82
|
+
for key in ("provider", "name", "model_url"):
|
|
83
|
+
if key in values:
|
|
84
|
+
data[key] = str(values[key])
|
|
85
|
+
if "context_window" in values:
|
|
86
|
+
data["context_window"] = int(values["context_window"])
|
|
87
|
+
if "temperature" in values:
|
|
88
|
+
data["temperature"] = float(values["temperature"])
|
|
89
|
+
if "auto_bootstrap" in values:
|
|
90
|
+
data["auto_bootstrap"] = bool(values["auto_bootstrap"])
|
|
91
|
+
if "cache_dir" in values:
|
|
92
|
+
data["cache_dir"] = Path(values["cache_dir"]).expanduser().resolve()
|
|
93
|
+
return replace(base, **data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_config(project_root: Path | None = None) -> ErrorAIConfig:
|
|
97
|
+
root = (project_root or Path.cwd()).resolve()
|
|
98
|
+
config = ErrorAIConfig(runtime=RuntimeConfig(project_root=root))
|
|
99
|
+
pyproject = _read_toml(root / "pyproject.toml")
|
|
100
|
+
pyproject_settings = pyproject.get("tool", {}).get("errorai", {})
|
|
101
|
+
local_settings = _read_toml(root / ".errorai.toml")
|
|
102
|
+
|
|
103
|
+
runtime_values = {}
|
|
104
|
+
model_values = {}
|
|
105
|
+
for source in (pyproject_settings, local_settings):
|
|
106
|
+
runtime_values.update(source.get("runtime", {}))
|
|
107
|
+
model_values.update(source.get("model", {}))
|
|
108
|
+
|
|
109
|
+
return ErrorAIConfig(
|
|
110
|
+
runtime=_coerce_runtime(config.runtime, runtime_values),
|
|
111
|
+
model=_coerce_model(config.model, model_values),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def config_template() -> str:
|
|
116
|
+
return """[runtime]
|
|
117
|
+
auto_watch = true
|
|
118
|
+
safe_mode = true
|
|
119
|
+
dry_run = true
|
|
120
|
+
ignore_patterns = [".git", "__pycache__", ".env", ".venv", "venv", "node_modules", "*.lock", ".errorai"]
|
|
121
|
+
|
|
122
|
+
[model]
|
|
123
|
+
provider = "onnx"
|
|
124
|
+
name = "onnx-default-python-expert"
|
|
125
|
+
context_window = 4096
|
|
126
|
+
temperature = 0.1
|
|
127
|
+
auto_bootstrap = true
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def autostart_enabled() -> bool:
|
|
132
|
+
return os.environ.get("ERRORAI_AUTOSTART", "1") != "0"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Capabilities:
|
|
10
|
+
environment: str
|
|
11
|
+
can_watch_fs: bool
|
|
12
|
+
can_apply_patches: bool
|
|
13
|
+
can_prompt_user: bool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def detect_capabilities() -> Capabilities:
|
|
17
|
+
env = "generic"
|
|
18
|
+
interactive = bool(getattr(sys, "ps1", None) or sys.flags.interactive)
|
|
19
|
+
if "ipykernel" in sys.modules or "JPY_PARENT_PID" in os.environ:
|
|
20
|
+
env = "notebook"
|
|
21
|
+
elif "idlelib" in sys.modules:
|
|
22
|
+
env = "idle"
|
|
23
|
+
elif interactive:
|
|
24
|
+
env = "interactive"
|
|
25
|
+
elif sys.stdin and sys.stdin.isatty():
|
|
26
|
+
env = "terminal"
|
|
27
|
+
else:
|
|
28
|
+
env = "ide"
|
|
29
|
+
|
|
30
|
+
can_watch_fs = env not in {"notebook"}
|
|
31
|
+
can_apply_patches = env != "notebook"
|
|
32
|
+
can_prompt_user = env in {"terminal", "interactive", "idle", "ide"}
|
|
33
|
+
return Capabilities(
|
|
34
|
+
environment=env,
|
|
35
|
+
can_watch_fs=can_watch_fs,
|
|
36
|
+
can_apply_patches=can_apply_patches,
|
|
37
|
+
can_prompt_user=can_prompt_user,
|
|
38
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import fnmatch
|
|
7
|
+
import json
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .config import RuntimeConfig
|
|
12
|
+
from .environment import Capabilities
|
|
13
|
+
from .providers import ModelProvider
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Reporter:
|
|
17
|
+
def __init__(self, project_root: Path):
|
|
18
|
+
self.log_path = project_root / ".errorai" / "logs" / "operations.log"
|
|
19
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
def log(self, event: str, payload: dict[str, Any]) -> None:
|
|
23
|
+
record = {
|
|
24
|
+
"time": datetime.now(timezone.utc).isoformat(),
|
|
25
|
+
"event": event,
|
|
26
|
+
"payload": payload,
|
|
27
|
+
}
|
|
28
|
+
with self._lock, self.log_path.open("a", encoding="utf-8") as handle:
|
|
29
|
+
handle.write(json.dumps(record) + "\n")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Analyzer:
|
|
33
|
+
def analyze_exception(self, exc_type, exc_value, exc_tb) -> dict[str, Any]:
|
|
34
|
+
return {
|
|
35
|
+
"type": getattr(exc_type, "__name__", str(exc_type)),
|
|
36
|
+
"message": str(exc_value),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Planner:
|
|
41
|
+
def __init__(self, provider: ModelProvider):
|
|
42
|
+
self.provider = provider
|
|
43
|
+
|
|
44
|
+
def plan_line_fix(self, line: str, error_message: str) -> str | None:
|
|
45
|
+
return self.provider.suggest_patch(line, error_message)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class ApplyResult:
|
|
50
|
+
changed: bool
|
|
51
|
+
detail: str
|
|
52
|
+
preview: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Applier:
|
|
56
|
+
def __init__(self, config: RuntimeConfig):
|
|
57
|
+
self.config = config
|
|
58
|
+
self.project_root = config.project_root.resolve()
|
|
59
|
+
|
|
60
|
+
def _is_ignored(self, path: Path) -> bool:
|
|
61
|
+
rel = str(path)
|
|
62
|
+
for pattern in self.config.ignore_patterns:
|
|
63
|
+
if fnmatch.fnmatch(path.name, pattern) or fnmatch.fnmatch(rel, pattern):
|
|
64
|
+
return True
|
|
65
|
+
if pattern in path.parts:
|
|
66
|
+
return True
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def _is_within_root(self, path: Path) -> bool:
|
|
70
|
+
try:
|
|
71
|
+
path.resolve().relative_to(self.project_root)
|
|
72
|
+
except ValueError:
|
|
73
|
+
return False
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def can_edit(self, file_path: Path) -> bool:
|
|
77
|
+
path = file_path.resolve()
|
|
78
|
+
return self._is_within_root(path) and not self._is_ignored(path)
|
|
79
|
+
|
|
80
|
+
def apply_line_change(self, file_path: Path, lineno: int, new_line: str) -> ApplyResult:
|
|
81
|
+
path = file_path.resolve()
|
|
82
|
+
if not self.can_edit(path):
|
|
83
|
+
return ApplyResult(False, "Blocked by safe mode restrictions.")
|
|
84
|
+
|
|
85
|
+
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
86
|
+
if lineno < 1 or lineno > len(lines):
|
|
87
|
+
return ApplyResult(False, "Line number out of range.")
|
|
88
|
+
|
|
89
|
+
old_line = lines[lineno - 1]
|
|
90
|
+
indentation = old_line[: len(old_line) - len(old_line.lstrip())]
|
|
91
|
+
candidate = f"{indentation}{new_line.strip()}\n"
|
|
92
|
+
preview = f"- {old_line.rstrip()}\n+ {candidate.rstrip()}"
|
|
93
|
+
if self.config.dry_run:
|
|
94
|
+
return ApplyResult(False, "Dry-run mode enabled; no write applied.", preview=preview)
|
|
95
|
+
|
|
96
|
+
lines[lineno - 1] = candidate
|
|
97
|
+
path.write_text("".join(lines), encoding="utf-8")
|
|
98
|
+
return ApplyResult(True, "Edit applied.", preview=preview)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Watcher:
|
|
102
|
+
def __init__(self, capabilities: Capabilities, reporter: Reporter):
|
|
103
|
+
self.capabilities = capabilities
|
|
104
|
+
self.reporter = reporter
|
|
105
|
+
self.started = False
|
|
106
|
+
|
|
107
|
+
def start(self) -> bool:
|
|
108
|
+
if not self.capabilities.can_watch_fs:
|
|
109
|
+
self.reporter.log("watcher.skipped", {"reason": "environment unsupported"})
|
|
110
|
+
return False
|
|
111
|
+
try:
|
|
112
|
+
from watchdog.observers import Observer # type: ignore # noqa: F401
|
|
113
|
+
except Exception:
|
|
114
|
+
self.reporter.log("watcher.skipped", {"reason": "watchdog unavailable"})
|
|
115
|
+
return False
|
|
116
|
+
self.started = True
|
|
117
|
+
self.reporter.log("watcher.started", {})
|
|
118
|
+
return True
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import linecache
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import traceback
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .bootstrap import BootstrapStatus, ensure_model, model_status
|
|
13
|
+
from .config import ErrorAIConfig, autostart_enabled, load_config
|
|
14
|
+
from .environment import Capabilities, detect_capabilities
|
|
15
|
+
from .pipeline import Analyzer, Applier, Planner, Reporter, Watcher
|
|
16
|
+
from .providers import LlamaCppProvider, ModelProvider, RulesOnlyProvider
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RuntimeManager:
|
|
20
|
+
_instance: "RuntimeManager | None" = None
|
|
21
|
+
_instance_lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._init_lock = threading.Lock()
|
|
25
|
+
self._initialized = False
|
|
26
|
+
self.config: ErrorAIConfig | None = None
|
|
27
|
+
self.capabilities: Capabilities | None = None
|
|
28
|
+
self.reporter: Reporter | None = None
|
|
29
|
+
self.analyzer: Analyzer | None = None
|
|
30
|
+
self.planner: Planner | None = None
|
|
31
|
+
self.applier: Applier | None = None
|
|
32
|
+
self.watcher: Watcher | None = None
|
|
33
|
+
self.provider: ModelProvider = RulesOnlyProvider()
|
|
34
|
+
self.bootstrap_status = BootstrapStatus(False, "rules-only", "Not initialized.", None)
|
|
35
|
+
self.mode = "rules-only"
|
|
36
|
+
self._orig_sys_hook = sys.excepthook
|
|
37
|
+
self._orig_thread_hook = getattr(threading, "excepthook", None)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get_instance(cls) -> "RuntimeManager":
|
|
41
|
+
with cls._instance_lock:
|
|
42
|
+
if cls._instance is None:
|
|
43
|
+
cls._instance = cls()
|
|
44
|
+
return cls._instance
|
|
45
|
+
|
|
46
|
+
def initialize(self, config_override: ErrorAIConfig | None = None) -> "RuntimeManager":
|
|
47
|
+
if not autostart_enabled():
|
|
48
|
+
return self
|
|
49
|
+
with self._init_lock:
|
|
50
|
+
if self._initialized:
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
self.config = config_override or load_config()
|
|
54
|
+
self.capabilities = detect_capabilities()
|
|
55
|
+
self.reporter = Reporter(self.config.runtime.project_root)
|
|
56
|
+
self.analyzer = Analyzer()
|
|
57
|
+
self.applier = Applier(self.config.runtime)
|
|
58
|
+
self.bootstrap_status = ensure_model(self.config.model, explicit=False)
|
|
59
|
+
self.provider = self._select_provider(self.bootstrap_status)
|
|
60
|
+
self.planner = Planner(self.provider)
|
|
61
|
+
self.watcher = Watcher(self.capabilities, self.reporter)
|
|
62
|
+
self._register_hooks()
|
|
63
|
+
watch_started = False
|
|
64
|
+
if self.config.runtime.auto_watch:
|
|
65
|
+
watch_started = self.watcher.start()
|
|
66
|
+
self.mode = "full" if self.bootstrap_status.ready and watch_started else "analyze-only"
|
|
67
|
+
if not self.bootstrap_status.ready:
|
|
68
|
+
self.mode = "rules-only"
|
|
69
|
+
self.reporter.log(
|
|
70
|
+
"runtime.initialized",
|
|
71
|
+
{
|
|
72
|
+
"environment": self.capabilities.environment,
|
|
73
|
+
"mode": self.mode,
|
|
74
|
+
"model_mode": self.bootstrap_status.mode,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
self._initialized = True
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def _select_provider(self, status: BootstrapStatus) -> ModelProvider:
|
|
81
|
+
if not status.ready or status.model_path is None or self.config is None:
|
|
82
|
+
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()
|
|
89
|
+
|
|
90
|
+
def _register_hooks(self) -> None:
|
|
91
|
+
self._orig_sys_hook = sys.excepthook
|
|
92
|
+
sys.excepthook = self._sys_excepthook
|
|
93
|
+
if hasattr(threading, "excepthook"):
|
|
94
|
+
self._orig_thread_hook = threading.excepthook
|
|
95
|
+
threading.excepthook = self._thread_excepthook
|
|
96
|
+
|
|
97
|
+
def _sys_excepthook(self, exc_type, exc_value, exc_tb):
|
|
98
|
+
self.process_exception(exc_type, exc_value, exc_tb)
|
|
99
|
+
if self._orig_sys_hook:
|
|
100
|
+
self._orig_sys_hook(exc_type, exc_value, exc_tb)
|
|
101
|
+
|
|
102
|
+
def _thread_excepthook(self, args):
|
|
103
|
+
self.process_exception(args.exc_type, args.exc_value, args.exc_traceback)
|
|
104
|
+
if self._orig_thread_hook:
|
|
105
|
+
self._orig_thread_hook(args)
|
|
106
|
+
|
|
107
|
+
def process_exception(self, exc_type, exc_value, exc_tb: TracebackType | None) -> bool:
|
|
108
|
+
if not exc_tb or not self.analyzer or not self.planner or not self.applier or not self.reporter:
|
|
109
|
+
return False
|
|
110
|
+
tb_list = traceback.extract_tb(exc_tb)
|
|
111
|
+
if not tb_list:
|
|
112
|
+
return False
|
|
113
|
+
frame = tb_list[-1]
|
|
114
|
+
filename = frame.filename
|
|
115
|
+
line = linecache.getline(filename, frame.lineno).strip() if filename else ""
|
|
116
|
+
analysis = self.analyzer.analyze_exception(exc_type, exc_value, exc_tb)
|
|
117
|
+
plan = self.planner.plan_line_fix(line or frame.line or "", analysis["message"])
|
|
118
|
+
if not plan or not filename or "<" in filename:
|
|
119
|
+
self.reporter.log("exception.analyzed", {"analysis": analysis, "fixed": False})
|
|
120
|
+
return False
|
|
121
|
+
result = self.applier.apply_line_change(Path(filename), frame.lineno, plan)
|
|
122
|
+
self.reporter.log(
|
|
123
|
+
"exception.handled",
|
|
124
|
+
{
|
|
125
|
+
"analysis": analysis,
|
|
126
|
+
"changed": result.changed,
|
|
127
|
+
"detail": result.detail,
|
|
128
|
+
"preview": result.preview,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
return result.changed
|
|
132
|
+
|
|
133
|
+
def status_report(self) -> dict[str, Any]:
|
|
134
|
+
cfg = self.config or load_config()
|
|
135
|
+
caps = self.capabilities or detect_capabilities()
|
|
136
|
+
model = model_status(cfg.model)
|
|
137
|
+
return {
|
|
138
|
+
"environment": caps.environment,
|
|
139
|
+
"can_watch_fs": caps.can_watch_fs,
|
|
140
|
+
"can_apply_patches": caps.can_apply_patches,
|
|
141
|
+
"mode": self.mode,
|
|
142
|
+
"model_ready": model.ready,
|
|
143
|
+
"model_mode": self.bootstrap_status.mode,
|
|
144
|
+
"model_detail": self.bootstrap_status.detail,
|
|
145
|
+
"dry_run": cfg.runtime.dry_run,
|
|
146
|
+
"project_root": str(cfg.runtime.project_root),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def install_model(self) -> BootstrapStatus:
|
|
150
|
+
if self.config is None:
|
|
151
|
+
self.config = load_config()
|
|
152
|
+
self.bootstrap_status = ensure_model(self.config.model, explicit=True)
|
|
153
|
+
return self.bootstrap_status
|
|
154
|
+
|
|
155
|
+
def configure(self, **overrides) -> None:
|
|
156
|
+
if self.config is None:
|
|
157
|
+
self.config = load_config()
|
|
158
|
+
runtime_values = overrides.get("runtime", {})
|
|
159
|
+
model_values = overrides.get("model", {})
|
|
160
|
+
self.config = ErrorAIConfig(
|
|
161
|
+
runtime=replace(self.config.runtime, **runtime_values),
|
|
162
|
+
model=replace(self.config.model, **model_values),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def shutdown(self) -> None:
|
|
166
|
+
sys.excepthook = self._orig_sys_hook
|
|
167
|
+
if hasattr(threading, "excepthook") and self._orig_thread_hook is not None:
|
|
168
|
+
threading.excepthook = self._orig_thread_hook
|
|
169
|
+
self._initialized = False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_runtime() -> RuntimeManager:
|
|
173
|
+
return RuntimeManager.get_instance()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def configure(**overrides) -> RuntimeManager:
|
|
177
|
+
runtime = get_runtime()
|
|
178
|
+
runtime.configure(**overrides)
|
|
179
|
+
return runtime
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _reset_for_tests() -> None:
|
|
183
|
+
runtime = RuntimeManager._instance
|
|
184
|
+
if runtime is not None:
|
|
185
|
+
runtime.shutdown()
|
|
186
|
+
RuntimeManager._instance = None
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ErrorAI"
|
|
7
|
+
version = "0.9.0"
|
|
8
|
+
description = "Python-only autonomous runtime that watches/analyzes errors with safe local auto-fix fallbacks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Aswanth R" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.9"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"platformdirs>=3.0",
|
|
23
|
+
"onnxruntime>=1.18.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
watch = ["watchdog>=4.0.0"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
errorai = "errorai.cli:main"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/NodeX-AR/ErrorAI"
|
errorai-0.9.0/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
os.environ["ERRORAI_AUTOSTART"] = "0"
|
|
7
|
+
|
|
8
|
+
from errorai import bootstrap
|
|
9
|
+
from errorai.config import ModelConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BootstrapTests(unittest.TestCase):
|
|
13
|
+
def test_bootstrap_fallback_to_rules_only_on_failure(self):
|
|
14
|
+
with tempfile.TemporaryDirectory() as td:
|
|
15
|
+
cfg = ModelConfig(cache_dir=Path(td))
|
|
16
|
+
|
|
17
|
+
original_download = bootstrap._download_model
|
|
18
|
+
|
|
19
|
+
def fail_download(url, destination):
|
|
20
|
+
raise OSError("offline")
|
|
21
|
+
|
|
22
|
+
bootstrap._download_model = fail_download
|
|
23
|
+
try:
|
|
24
|
+
status = bootstrap.ensure_model(cfg, explicit=True)
|
|
25
|
+
finally:
|
|
26
|
+
bootstrap._download_model = original_download
|
|
27
|
+
|
|
28
|
+
self.assertFalse(status.ready)
|
|
29
|
+
self.assertEqual(status.mode, "rules-only")
|
|
30
|
+
self.assertIn("failed", status.detail.lower())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
unittest.main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
os.environ["ERRORAI_AUTOSTART"] = "0"
|
|
7
|
+
|
|
8
|
+
from errorai.config import load_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigTests(unittest.TestCase):
|
|
12
|
+
def test_config_parsing_with_file_override(self):
|
|
13
|
+
with tempfile.TemporaryDirectory() as td:
|
|
14
|
+
root = Path(td)
|
|
15
|
+
(root / "pyproject.toml").write_text(
|
|
16
|
+
"[tool.errorai.runtime]\nauto_watch = false\n\n[tool.errorai.model]\ncontext_window = 2048\n",
|
|
17
|
+
encoding="utf-8",
|
|
18
|
+
)
|
|
19
|
+
(root / ".errorai.toml").write_text(
|
|
20
|
+
"[runtime]\ndry_run = false\n\n[model]\ntemperature = 0.2\n",
|
|
21
|
+
encoding="utf-8",
|
|
22
|
+
)
|
|
23
|
+
config = load_config(root)
|
|
24
|
+
self.assertFalse(config.runtime.auto_watch)
|
|
25
|
+
self.assertFalse(config.runtime.dry_run)
|
|
26
|
+
self.assertEqual(config.model.context_window, 2048)
|
|
27
|
+
self.assertEqual(config.model.temperature, 0.2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
unittest.main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
os.environ["ERRORAI_AUTOSTART"] = "0"
|
|
6
|
+
|
|
7
|
+
from errorai.runtime import _reset_for_tests, get_runtime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuntimeTests(unittest.TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
os.environ["ERRORAI_AUTOSTART"] = "1"
|
|
13
|
+
_reset_for_tests()
|
|
14
|
+
self._original_sys_hook = sys.excepthook
|
|
15
|
+
|
|
16
|
+
def tearDown(self):
|
|
17
|
+
_reset_for_tests()
|
|
18
|
+
os.environ.pop("ERRORAI_AUTOSTART", None)
|
|
19
|
+
sys.excepthook = self._original_sys_hook
|
|
20
|
+
|
|
21
|
+
def test_singleton_runtime_manager(self):
|
|
22
|
+
first = get_runtime()
|
|
23
|
+
second = get_runtime()
|
|
24
|
+
self.assertIs(first, second)
|
|
25
|
+
|
|
26
|
+
def test_hook_registration_on_initialize(self):
|
|
27
|
+
runtime = get_runtime().initialize()
|
|
28
|
+
self.assertIsNot(sys.excepthook, self._original_sys_hook)
|
|
29
|
+
self.assertTrue(runtime._initialized)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
unittest.main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
os.environ["ERRORAI_AUTOSTART"] = "0"
|
|
7
|
+
|
|
8
|
+
from errorai.config import RuntimeConfig
|
|
9
|
+
from errorai.pipeline import Applier
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SafetyTests(unittest.TestCase):
|
|
13
|
+
def test_applier_restricts_writes_to_project_root(self):
|
|
14
|
+
with tempfile.TemporaryDirectory() as root_dir, tempfile.TemporaryDirectory() as outside_dir:
|
|
15
|
+
root = Path(root_dir)
|
|
16
|
+
outside = Path(outside_dir)
|
|
17
|
+
target = root / "main.py"
|
|
18
|
+
target.write_text("print('x')\n", encoding="utf-8")
|
|
19
|
+
outside_target = outside / "outside.py"
|
|
20
|
+
outside_target.write_text("print('y')\n", encoding="utf-8")
|
|
21
|
+
|
|
22
|
+
applier = Applier(RuntimeConfig(project_root=root, dry_run=False))
|
|
23
|
+
self.assertTrue(applier.can_edit(target))
|
|
24
|
+
self.assertFalse(applier.can_edit(outside_target))
|
|
25
|
+
|
|
26
|
+
def test_applier_respects_sensitive_ignore_defaults(self):
|
|
27
|
+
with tempfile.TemporaryDirectory() as td:
|
|
28
|
+
root = Path(td)
|
|
29
|
+
sensitive = root / ".env"
|
|
30
|
+
sensitive.write_text("A=B\n", encoding="utf-8")
|
|
31
|
+
applier = Applier(RuntimeConfig(project_root=root))
|
|
32
|
+
self.assertFalse(applier.can_edit(sensitive))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
unittest.main()
|