auto-code-fixer 0.2.5__tar.gz → 0.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.
Files changed (36) hide show
  1. auto_code_fixer-0.3.0/PKG-INFO +138 -0
  2. auto_code_fixer-0.3.0/README.md +117 -0
  3. auto_code_fixer-0.3.0/auto_code_fixer/__init__.py +1 -0
  4. auto_code_fixer-0.3.0/auto_code_fixer/cli.py +261 -0
  5. auto_code_fixer-0.3.0/auto_code_fixer/command_runner.py +45 -0
  6. auto_code_fixer-0.3.0/auto_code_fixer/fixer.py +79 -0
  7. auto_code_fixer-0.3.0/auto_code_fixer/installer.py +47 -0
  8. auto_code_fixer-0.3.0/auto_code_fixer/models.py +19 -0
  9. auto_code_fixer-0.3.0/auto_code_fixer/patcher.py +31 -0
  10. auto_code_fixer-0.3.0/auto_code_fixer/plan.py +61 -0
  11. auto_code_fixer-0.3.0/auto_code_fixer/runner.py +38 -0
  12. auto_code_fixer-0.3.0/auto_code_fixer/sandbox.py +49 -0
  13. auto_code_fixer-0.3.0/auto_code_fixer/traceback_utils.py +27 -0
  14. auto_code_fixer-0.3.0/auto_code_fixer/utils.py +106 -0
  15. auto_code_fixer-0.3.0/auto_code_fixer/venv_manager.py +33 -0
  16. auto_code_fixer-0.3.0/auto_code_fixer.egg-info/PKG-INFO +138 -0
  17. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/SOURCES.txt +10 -2
  18. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/pyproject.toml +1 -1
  19. auto_code_fixer-0.3.0/tests/test_fix_imported_file.py +48 -0
  20. auto_code_fixer-0.3.0/tests/test_internal_imports.py +30 -0
  21. auto_code_fixer-0.2.5/PKG-INFO +0 -51
  22. auto_code_fixer-0.2.5/README.md +0 -30
  23. auto_code_fixer-0.2.5/auto_code_fixer/__init__.py +0 -2
  24. auto_code_fixer-0.2.5/auto_code_fixer/cli.py +0 -183
  25. auto_code_fixer-0.2.5/auto_code_fixer/fixer.py +0 -60
  26. auto_code_fixer-0.2.5/auto_code_fixer/guard.py +0 -19
  27. auto_code_fixer-0.2.5/auto_code_fixer/installer.py +0 -25
  28. auto_code_fixer-0.2.5/auto_code_fixer/runner.py +0 -30
  29. auto_code_fixer-0.2.5/auto_code_fixer/utils.py +0 -57
  30. auto_code_fixer-0.2.5/auto_code_fixer.egg-info/PKG-INFO +0 -51
  31. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/LICENSE +0 -0
  32. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/dependency_links.txt +0 -0
  33. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/entry_points.txt +0 -0
  34. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/requires.txt +0 -0
  35. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/auto_code_fixer.egg-info/top_level.txt +0 -0
  36. {auto_code_fixer-0.2.5 → auto_code_fixer-0.3.0}/setup.cfg +0 -0
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: auto-code-fixer
3
+ Version: 0.3.0
4
+ Summary: Automatically fix Python code using ChatGPT
5
+ Author-email: Arif Shah <ashah7775@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/auto-code-fixer/
8
+ Project-URL: Source, https://bitbucket.org/arif_automation/auto_code_fixer
9
+ Project-URL: Issues, https://bitbucket.org/arif_automation/auto_code_fixer/issues
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: openai>=1.0.0
19
+ Requires-Dist: python-decouple
20
+ Dynamic: license-file
21
+
22
+ # Auto Code Fixer
23
+
24
+ Auto Code Fixer is a CLI tool that **detects runtime failures** and automatically fixes Python code using OpenAI.
25
+
26
+ It is designed for real projects where an entry script imports multiple local modules. The tool runs your code in an **isolated sandbox + venv**, installs missing external dependencies, and applies fixes only after the code executes successfully.
27
+
28
+ ---
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install auto-code-fixer
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick start
39
+
40
+ ```bash
41
+ auto-code-fixer path/to/main.py --project-root . --ask
42
+ ```
43
+
44
+ If you want it to overwrite without asking:
45
+
46
+ ```bash
47
+ auto-code-fixer path/to/main.py --project-root . --no-ask
48
+ ```
49
+
50
+ ---
51
+
52
+ ## What it fixes (core behavior)
53
+
54
+ ### ✅ Local/internal imports are treated as project code
55
+ If your entry file imports something like:
56
+
57
+ ```py
58
+ from mylib import add
59
+ ```
60
+
61
+ …and `mylib.py` exists inside the project, Auto Code Fixer will:
62
+ - copy `main.py` + `mylib.py` into a sandbox
63
+ - execute inside the sandbox
64
+ - if the traceback points to `mylib.py`, it will fix `mylib.py`
65
+ - then apply the fix back to your repo (with backups)
66
+
67
+ ### ✅ External imports are auto-installed
68
+ If execution fails with:
69
+
70
+ ```
71
+ ModuleNotFoundError: No module named 'requests'
72
+ ```
73
+
74
+ …it will run:
75
+
76
+ ```bash
77
+ pip install requests
78
+ ```
79
+
80
+ …but only inside the sandbox venv (so your system env isn’t polluted).
81
+
82
+ ---
83
+
84
+ ## Safety
85
+
86
+ ### Backups
87
+ Before overwriting any file, it creates a backup:
88
+ - `file.py.bak` (or `.bak1`, `.bak2`, ...)
89
+
90
+ ### Dry run
91
+ ```bash
92
+ auto-code-fixer path/to/main.py --project-root . --dry-run
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Advanced options
98
+
99
+ ### Run a custom command (pytest, etc.)
100
+ Instead of `python main.py`, run tests:
101
+
102
+ ```bash
103
+ auto-code-fixer . --project-root . --run "pytest -q" --no-ask
104
+ ```
105
+
106
+ ### Model selection
107
+ ```bash
108
+ export AUTO_CODE_FIXER_MODEL=gpt-4.1-mini
109
+ # or
110
+ auto-code-fixer main.py --model gpt-4.1-mini
111
+ ```
112
+
113
+ ### Max retries / timeout
114
+ ```bash
115
+ auto-code-fixer main.py --max-retries 8 --timeout 30
116
+ ```
117
+
118
+ ### Optional AI planning (which file to edit)
119
+ ```bash
120
+ auto-code-fixer main.py --ai-plan
121
+ ```
122
+ This enables a helper that can suggest which local file to edit. It is best-effort.
123
+
124
+ ---
125
+
126
+ ## Environment variables
127
+
128
+ - `OPENAI_API_KEY` (required unless you always pass `--api-key`)
129
+ - `AUTO_CODE_FIXER_MODEL` (default model)
130
+ - `AUTO_CODE_FIXER_ASK=true|false`
131
+ - `AUTO_CODE_FIXER_VERBOSE=true|false`
132
+
133
+ ---
134
+
135
+ ## Notes
136
+
137
+ - This tool edits code. Use it on a git repo so you can review diffs.
138
+ - For maximum safety, run with `--ask` and/or `--dry-run`.
@@ -0,0 +1,117 @@
1
+ # Auto Code Fixer
2
+
3
+ Auto Code Fixer is a CLI tool that **detects runtime failures** and automatically fixes Python code using OpenAI.
4
+
5
+ It is designed for real projects where an entry script imports multiple local modules. The tool runs your code in an **isolated sandbox + venv**, installs missing external dependencies, and applies fixes only after the code executes successfully.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install auto-code-fixer
13
+ ```
14
+
15
+ ---
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ auto-code-fixer path/to/main.py --project-root . --ask
21
+ ```
22
+
23
+ If you want it to overwrite without asking:
24
+
25
+ ```bash
26
+ auto-code-fixer path/to/main.py --project-root . --no-ask
27
+ ```
28
+
29
+ ---
30
+
31
+ ## What it fixes (core behavior)
32
+
33
+ ### ✅ Local/internal imports are treated as project code
34
+ If your entry file imports something like:
35
+
36
+ ```py
37
+ from mylib import add
38
+ ```
39
+
40
+ …and `mylib.py` exists inside the project, Auto Code Fixer will:
41
+ - copy `main.py` + `mylib.py` into a sandbox
42
+ - execute inside the sandbox
43
+ - if the traceback points to `mylib.py`, it will fix `mylib.py`
44
+ - then apply the fix back to your repo (with backups)
45
+
46
+ ### ✅ External imports are auto-installed
47
+ If execution fails with:
48
+
49
+ ```
50
+ ModuleNotFoundError: No module named 'requests'
51
+ ```
52
+
53
+ …it will run:
54
+
55
+ ```bash
56
+ pip install requests
57
+ ```
58
+
59
+ …but only inside the sandbox venv (so your system env isn’t polluted).
60
+
61
+ ---
62
+
63
+ ## Safety
64
+
65
+ ### Backups
66
+ Before overwriting any file, it creates a backup:
67
+ - `file.py.bak` (or `.bak1`, `.bak2`, ...)
68
+
69
+ ### Dry run
70
+ ```bash
71
+ auto-code-fixer path/to/main.py --project-root . --dry-run
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Advanced options
77
+
78
+ ### Run a custom command (pytest, etc.)
79
+ Instead of `python main.py`, run tests:
80
+
81
+ ```bash
82
+ auto-code-fixer . --project-root . --run "pytest -q" --no-ask
83
+ ```
84
+
85
+ ### Model selection
86
+ ```bash
87
+ export AUTO_CODE_FIXER_MODEL=gpt-4.1-mini
88
+ # or
89
+ auto-code-fixer main.py --model gpt-4.1-mini
90
+ ```
91
+
92
+ ### Max retries / timeout
93
+ ```bash
94
+ auto-code-fixer main.py --max-retries 8 --timeout 30
95
+ ```
96
+
97
+ ### Optional AI planning (which file to edit)
98
+ ```bash
99
+ auto-code-fixer main.py --ai-plan
100
+ ```
101
+ This enables a helper that can suggest which local file to edit. It is best-effort.
102
+
103
+ ---
104
+
105
+ ## Environment variables
106
+
107
+ - `OPENAI_API_KEY` (required unless you always pass `--api-key`)
108
+ - `AUTO_CODE_FIXER_MODEL` (default model)
109
+ - `AUTO_CODE_FIXER_ASK=true|false`
110
+ - `AUTO_CODE_FIXER_VERBOSE=true|false`
111
+
112
+ ---
113
+
114
+ ## Notes
115
+
116
+ - This tool edits code. Use it on a git repo so you can review diffs.
117
+ - For maximum safety, run with `--ask` and/or `--dry-run`.
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,261 @@
1
+ import argparse
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import time
6
+ from decouple import config
7
+
8
+ from auto_code_fixer.runner import run_code
9
+ from auto_code_fixer.fixer import fix_code_with_gpt
10
+ from auto_code_fixer.installer import check_and_install_missing_lib
11
+ from auto_code_fixer.utils import (
12
+ discover_all_files,
13
+ is_in_project,
14
+ log,
15
+ set_verbose,
16
+ )
17
+ from auto_code_fixer import __version__
18
+
19
+ DEFAULT_MAX_RETRIES = 8 # can be overridden via CLI
20
+
21
+
22
+ def fix_file(file_path, project_root, api_key, ask, verbose, *, dry_run: bool, model: str | None, timeout_s: int, max_retries: int, run_cmd: str | None) -> bool:
23
+ log(f"Processing entry file: {file_path}")
24
+
25
+ project_root = os.path.abspath(project_root)
26
+ file_path = os.path.abspath(file_path)
27
+
28
+ # Build sandbox with entry + imported local files
29
+ from auto_code_fixer.sandbox import make_sandbox
30
+ sandbox_root, sandbox_entry = make_sandbox(entry_file=file_path, project_root=project_root)
31
+
32
+ # Create isolated venv in sandbox
33
+ from auto_code_fixer.venv_manager import create_venv
34
+ from auto_code_fixer.patcher import backup_file
35
+
36
+ venv_python = create_venv(sandbox_root)
37
+
38
+ changed_sandbox_files: set[str] = set()
39
+
40
+ for attempt in range(max_retries):
41
+ log(f"Run attempt #{attempt + 1}")
42
+
43
+ # Ensure local modules resolve inside sandbox
44
+ if run_cmd:
45
+ from auto_code_fixer.command_runner import run_command
46
+
47
+ retcode, stdout, stderr = run_command(
48
+ run_cmd,
49
+ timeout_s=timeout_s,
50
+ python_exe=venv_python,
51
+ cwd=sandbox_root,
52
+ extra_env={"PYTHONPATH": sandbox_root},
53
+ )
54
+ else:
55
+ retcode, stdout, stderr = run_code(
56
+ sandbox_entry,
57
+ timeout_s=timeout_s,
58
+ python_exe=venv_python,
59
+ cwd=sandbox_root,
60
+ extra_env={"PYTHONPATH": sandbox_root},
61
+ )
62
+
63
+ if verbose:
64
+ if stdout:
65
+ log(f"STDOUT:\n{stdout}", "DEBUG")
66
+ if stderr:
67
+ log(f"STDERR:\n{stderr}", "DEBUG")
68
+
69
+ if retcode == 0:
70
+ log("Script executed successfully ✅")
71
+
72
+ # Apply sandbox changes back to project (only if we actually changed something)
73
+ if attempt > 0 and is_in_project(file_path, project_root) and changed_sandbox_files:
74
+ rel_changes = [os.path.relpath(p, sandbox_root) for p in sorted(changed_sandbox_files)]
75
+
76
+ if ask:
77
+ confirm = input(
78
+ "Overwrite original files with fixed versions?\n"
79
+ + "\n".join(f"- {c}" for c in rel_changes)
80
+ + "\n(y/n): "
81
+ ).strip().lower()
82
+
83
+ if confirm != "y":
84
+ log("User declined overwrite", "WARN")
85
+ shutil.rmtree(sandbox_root)
86
+ return False
87
+
88
+ if dry_run:
89
+ log("DRY RUN: would apply fixes:\n" + "\n".join(rel_changes), "WARN")
90
+ else:
91
+ for p in sorted(changed_sandbox_files):
92
+ rel = os.path.relpath(p, sandbox_root)
93
+ dst = os.path.join(project_root, rel)
94
+ if os.path.exists(dst):
95
+ bak = backup_file(dst)
96
+ log(f"Backup created: {bak}", "DEBUG")
97
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
98
+ shutil.copy(p, dst)
99
+ log(f"File updated: {dst}")
100
+
101
+ shutil.rmtree(sandbox_root)
102
+ log(f"Fix completed in {attempt + 1} attempt(s) 🎉")
103
+ return True
104
+
105
+ log("Error detected ❌", "ERROR")
106
+ print(stderr)
107
+
108
+ if check_and_install_missing_lib(stderr, python_exe=venv_python, project_root=sandbox_root):
109
+ log("Missing dependency installed (venv), retrying…")
110
+ time.sleep(1)
111
+ continue
112
+
113
+ # Pick the most relevant local file from the traceback (entry or imported file)
114
+ from auto_code_fixer.traceback_utils import pick_relevant_file
115
+
116
+ target_file = pick_relevant_file(stderr, sandbox_root=sandbox_root) or sandbox_entry
117
+
118
+ # Optional AI fix plan can override file selection
119
+ try:
120
+ from auto_code_fixer.plan import ask_ai_for_fix_plan
121
+
122
+ plan = ask_ai_for_fix_plan(
123
+ sandbox_root=sandbox_root,
124
+ stderr=stderr,
125
+ api_key=api_key,
126
+ model=model,
127
+ )
128
+ if plan and plan.target_files:
129
+ # Use the first suggested file that exists
130
+ for rel in plan.target_files:
131
+ cand = os.path.abspath(os.path.join(sandbox_root, rel))
132
+ if cand.startswith(os.path.abspath(sandbox_root)) and os.path.exists(cand):
133
+ target_file = cand
134
+ break
135
+ except Exception:
136
+ pass
137
+
138
+ log(f"Sending {os.path.relpath(target_file, sandbox_root)} + error to GPT 🧠", "DEBUG")
139
+
140
+ fixed_code = fix_code_with_gpt(
141
+ original_code=open(target_file, encoding="utf-8").read(),
142
+ error_log=stderr,
143
+ api_key=api_key,
144
+ model=model,
145
+ )
146
+
147
+ if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
148
+ log("GPT returned no changes. Stopping.", "WARN")
149
+ break
150
+
151
+ with open(target_file, "w", encoding="utf-8") as f:
152
+ f.write(fixed_code)
153
+
154
+ changed_sandbox_files.add(os.path.abspath(target_file))
155
+
156
+ log("Code updated by GPT ✏️")
157
+ time.sleep(1)
158
+
159
+ log("Failed to auto-fix file after max retries ❌", "ERROR")
160
+ shutil.rmtree(sandbox_root)
161
+ return False
162
+
163
+
164
+ def main():
165
+ parser = argparse.ArgumentParser(
166
+ description="Auto-fix Python code using OpenAI (advanced sandbox + retry loop)"
167
+ )
168
+
169
+ parser.add_argument(
170
+ "--version",
171
+ action="version",
172
+ version=f"%(prog)s {__version__}",
173
+ )
174
+
175
+ parser.add_argument(
176
+ "entry_file",
177
+ nargs="?",
178
+ help="Path to the main Python file",
179
+ )
180
+
181
+ parser.add_argument("--project-root", default=".")
182
+ parser.add_argument("--api-key")
183
+ parser.add_argument("--model", default=None, help="OpenAI model (default: AUTO_CODE_FIXER_MODEL or gpt-4.1-mini)")
184
+ parser.add_argument("--timeout", type=int, default=30, help="Execution timeout seconds (default: 30)")
185
+ parser.add_argument("--dry-run", action="store_true", help="Do not overwrite files; just report what would change")
186
+ parser.add_argument("--max-retries", type=int, default=DEFAULT_MAX_RETRIES, help="Max fix attempts (default: 8)")
187
+ parser.add_argument(
188
+ "--run",
189
+ default=None,
190
+ help=(
191
+ "Optional command to run instead of `python entry.py`. Examples: 'pytest -q', 'python -m module'. "
192
+ "If set, it runs inside the sandbox venv."
193
+ ),
194
+ )
195
+ parser.add_argument(
196
+ "--ai-plan",
197
+ action="store_true",
198
+ help="Optional: use AI to suggest which file to edit (AUTO_CODE_FIXER_AI_PLAN=1)",
199
+ )
200
+
201
+ # ✅ Proper boolean flags
202
+ ask_group = parser.add_mutually_exclusive_group()
203
+ ask_group.add_argument(
204
+ "--ask",
205
+ action="store_true",
206
+ help="Ask before overwriting files",
207
+ )
208
+ ask_group.add_argument(
209
+ "--no-ask",
210
+ action="store_true",
211
+ help="Do not ask before overwriting files",
212
+ )
213
+
214
+ parser.add_argument(
215
+ "--verbose",
216
+ action="store_true",
217
+ help="Enable verbose/debug output",
218
+ )
219
+
220
+ args = parser.parse_args()
221
+
222
+ if not args.entry_file:
223
+ parser.error("the following arguments are required: entry_file")
224
+
225
+ # ENV defaults
226
+ env_ask = config("AUTO_CODE_FIXER_ASK", default=False, cast=bool)
227
+ env_verbose = config("AUTO_CODE_FIXER_VERBOSE", default=False, cast=bool)
228
+
229
+ # Final ask resolution (CLI overrides ENV)
230
+ if args.ask:
231
+ ask = True
232
+ elif args.no_ask:
233
+ ask = False
234
+ else:
235
+ ask = env_ask
236
+
237
+ verbose = args.verbose or env_verbose
238
+ set_verbose(verbose)
239
+
240
+ # Optional: enable AI planning helper
241
+ if args.ai_plan:
242
+ os.environ["AUTO_CODE_FIXER_AI_PLAN"] = "1"
243
+
244
+ ok = fix_file(
245
+ args.entry_file,
246
+ args.project_root,
247
+ args.api_key,
248
+ ask,
249
+ verbose,
250
+ dry_run=args.dry_run,
251
+ model=args.model,
252
+ timeout_s=args.timeout,
253
+ max_retries=args.max_retries,
254
+ run_cmd=args.run,
255
+ )
256
+
257
+ raise SystemExit(0 if ok else 2)
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
@@ -0,0 +1,45 @@
1
+ import os
2
+ import shlex
3
+ import subprocess
4
+
5
+
6
+ def run_command(
7
+ cmd: str,
8
+ *,
9
+ timeout_s: int,
10
+ python_exe: str,
11
+ cwd: str,
12
+ extra_env: dict | None = None,
13
+ ):
14
+ """Run an arbitrary command inside the sandbox.
15
+
16
+ If cmd starts with 'python', replace with the sandbox venv python.
17
+ If cmd starts with 'pytest', run as `python -m pytest ...`.
18
+ """
19
+
20
+ parts = shlex.split(cmd)
21
+ if not parts:
22
+ return -1, "", "Empty command"
23
+
24
+ if parts[0] == "python" or parts[0] == "python3":
25
+ parts[0] = python_exe
26
+
27
+ if parts[0] == "pytest":
28
+ parts = [python_exe, "-m", "pytest", *parts[1:]]
29
+
30
+ env = {**os.environ}
31
+ if extra_env:
32
+ env.update({k: str(v) for k, v in extra_env.items()})
33
+
34
+ try:
35
+ r = subprocess.run(
36
+ parts,
37
+ capture_output=True,
38
+ text=True,
39
+ timeout=timeout_s,
40
+ cwd=cwd,
41
+ env=env,
42
+ )
43
+ return r.returncode, r.stdout, r.stderr
44
+ except subprocess.TimeoutExpired:
45
+ return -1, "", f"TimeoutExpired: Command took too long to run ({timeout_s}s)."
@@ -0,0 +1,79 @@
1
+ import json
2
+ import os
3
+ from decouple import config
4
+ from openai import OpenAI
5
+
6
+
7
+ def get_openai_client(api_key: str | None = None) -> OpenAI:
8
+ key = api_key or os.getenv("OPENAI_API_KEY") or config("OPENAI_API_KEY", default=None)
9
+ if not key:
10
+ raise RuntimeError(
11
+ "OpenAI API key not found. Set OPENAI_API_KEY env var or .env file or pass --api-key"
12
+ )
13
+ return OpenAI(api_key=key)
14
+
15
+
16
+ def _strip_code_fences(text: str) -> str:
17
+ text = (text or "").strip()
18
+ if text.startswith("```"):
19
+ text = "\n".join(text.split("\n")[1:])
20
+ if text.endswith("```"):
21
+ text = "\n".join(text.split("\n")[:-1])
22
+ return text.strip()
23
+
24
+
25
+ def fix_code_with_gpt(
26
+ *,
27
+ original_code: str,
28
+ error_log: str,
29
+ api_key: str | None = None,
30
+ model: str | None = None,
31
+ ) -> str:
32
+ """Fix code using OpenAI. Returns full corrected code.
33
+
34
+ Model can be set via:
35
+ - --model
36
+ - AUTO_CODE_FIXER_MODEL env
37
+ - default fallback
38
+ """
39
+
40
+ client = get_openai_client(api_key)
41
+ model = model or os.getenv("AUTO_CODE_FIXER_MODEL") or "gpt-4.1-mini"
42
+
43
+ instructions = {
44
+ "task": "Fix Python code to run without errors.",
45
+ "requirements": [
46
+ "Preserve existing behavior unless required to fix the crash.",
47
+ "Do not remove functionality.",
48
+ "Return ONLY the full corrected Python source code (no markdown).",
49
+ ],
50
+ }
51
+
52
+ prompt = (
53
+ "You are a senior Python engineer. Fix the following Python code so it runs without errors.\n\n"
54
+ "INSTRUCTIONS (JSON):\n"
55
+ + json.dumps(instructions)
56
+ + "\n\nCODE:\n"
57
+ + original_code
58
+ + "\n\nERROR LOG:\n"
59
+ + error_log
60
+ )
61
+
62
+ # Use Responses API (openai>=1) for forward compatibility
63
+ resp = client.responses.create(
64
+ model=model,
65
+ input=[
66
+ {"role": "system", "content": "You fix broken Python code."},
67
+ {"role": "user", "content": prompt},
68
+ ],
69
+ temperature=0.2,
70
+ max_output_tokens=2000,
71
+ )
72
+
73
+ text = ""
74
+ for item in resp.output or []:
75
+ for c in item.content or []:
76
+ if getattr(c, "type", None) in ("output_text", "text"):
77
+ text += getattr(c, "text", "") or ""
78
+
79
+ return _strip_code_fences(text)
@@ -0,0 +1,47 @@
1
+ import re
2
+ import subprocess
3
+
4
+
5
+ _MISSING_RE = re.compile(r"No module named '([^']+)'")
6
+
7
+
8
+ def parse_missing_module(stderr: str) -> str | None:
9
+ m = _MISSING_RE.search(stderr or "")
10
+ if not m:
11
+ return None
12
+ return m.group(1)
13
+
14
+
15
+ def install_package(package: str, *, python_exe: str) -> bool:
16
+ try:
17
+ subprocess.check_call([python_exe, "-m", "pip", "install", package])
18
+ return True
19
+ except subprocess.CalledProcessError:
20
+ return False
21
+
22
+
23
+ def is_local_module(module: str, *, project_root: str) -> bool:
24
+ """Return True if `module` can be resolved to a local file/package under project_root."""
25
+ import os
26
+ from pathlib import Path
27
+
28
+ base = Path(project_root)
29
+ parts = module.split(".")
30
+ candidates = [
31
+ base.joinpath(*parts).with_suffix(".py"),
32
+ base.joinpath(*parts, "__init__.py"),
33
+ ]
34
+ return any(os.path.isfile(str(p)) for p in candidates)
35
+
36
+
37
+ def check_and_install_missing_lib(stderr: str, *, python_exe: str, project_root: str) -> bool:
38
+ lib = parse_missing_module(stderr)
39
+ if not lib:
40
+ return False
41
+
42
+ # ✅ Important: do NOT pip install if it's a local module.
43
+ if is_local_module(lib, project_root=project_root):
44
+ return False
45
+
46
+ # naive mapping; user can always fix the requirement name manually.
47
+ return install_package(lib, python_exe=python_exe)
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class RunResult:
8
+ returncode: int
9
+ stdout: str
10
+ stderr: str
11
+
12
+
13
+ @dataclass
14
+ class FixResult:
15
+ success: bool
16
+ attempts: int
17
+ changed_files: list[str]
18
+ last_error: str | None = None
19
+ sandbox_root: str | None = None