# -*- coding: utf-8 -*- """ZuperAI — агент «почини мой проект». Сканирует папку на .py-файлы, прогоняет статический анализ (синтаксис + undefined names), а при наличии ошибок — чинит каждый файл через модель, затем перепроверяет. Опционально запускает тесты (pytest/unittest), если они есть в проекте. Запуск: venv/Scripts/python.exe fix_project.py <путь_к_проекту> [--code deepseek|lmstudio|local] Ключи: --code кто чинит код (по умолчанию deepseek, если ключ есть) --tests запускать ли тесты после статического анализа (по умолчанию нет) --dry не перезаписывать файлы, только показать найденные ошибки """ import os import subprocess import sys from selfcheck import check_python from webui import deepseek_available, deepseek_generate, lmstudio_generate, _strip_fences SKIP_DIRS = {"venv", ".venv", "__pycache__", ".git", "node_modules", ".idea"} def scan_py(path): out = [] for root, dirs, files in os.walk(path): dirs[:] = [d for d in dirs if d not in SKIP_DIRS] for f in files: if f.endswith(".py"): out.append(os.path.join(root, f)) return sorted(out) def _gen(backend, msgs, max_tokens): if backend == "deepseek": return _strip_fences(deepseek_generate(msgs, max_tokens=max_tokens)) if backend == "lmstudio": return _strip_fences(lmstudio_generate(msgs, max_tokens=max_tokens)) from infer_smart import load, gen if not hasattr(_gen, "_m"): _gen._m = load() tok, model = _gen._m return _strip_fences(gen(tok, model, msgs, max_tokens=max_tokens)) def fix_code(code, error, backend): msgs = [{"role": "system", "content": "Ты — senior-разработчик. Исправляй код по описанию ошибки. " "Верни ТОЛЬКО полный исправленный файл, без пояснений и без ```."}, {"role": "user", "content": f"Вот код:\n{code}\n\nОшибка: {error}\n\nИсправь код."}] return _gen(backend, msgs, 2048) def run_tests(path): """Пробует запустить тесты проекта, возвращает (ok, output).""" try: r = subprocess.run([sys.executable, "-m", "pytest", "-q"], cwd=path, capture_output=True, text=True, timeout=120) return r.returncode == 0, (r.stdout or "") + (r.stderr or "") except subprocess.TimeoutExpired: return False, "тесты зависли (таймаут 120с)" except Exception as e: # pytest может не быть установлен — пробуем unittest try: r = subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", path], capture_output=True, text=True, timeout=120) return r.returncode == 0, (r.stdout or "") + (r.stderr or "") except Exception as e2: return None, f"тесты не найдены/не запустились: {e2}" def main(): args = sys.argv[1:] if not args: print("usage: fix_project.py <путь> [--code deepseek|lmstudio|local] [--tests] [--dry]") sys.exit(2) path = args[0] backend = "deepseek" if deepseek_available() else "local" run_tests_flag = False dry = False for i, a in enumerate(args[1:], 1): if a == "--code" and i + 1 < len(args): backend = args[i + 1] elif a == "--tests": run_tests_flag = True elif a == "--dry": dry = True files = scan_py(path) print(f"найдено .py файлов: {len(files)}") fixed_any = False for f in files: try: code = open(f, encoding="utf-8").read() except Exception as e: print(f"! {f}: не прочитан ({e})") continue ok, msg = check_python(code) if ok: continue print(f"[!] {f}: {msg}") fixed_any = True if dry: continue fixed = fix_code(code, msg, backend) if fixed and fixed.strip() != code.strip(): ok2, msg2 = check_python(fixed) if ok2: open(f, "w", encoding="utf-8").write(fixed) print(f" -> исправлено") else: print(f" -> модель не починила: {msg2}") if run_tests_flag and not dry: print("--- запуск тестов ---") ok, out = run_tests(path) if ok is None: print(out) elif ok: print("[OK] тесты прошли") else: print(f"[!] тесты падают:\n{out[-800:]}") if not fixed_any: print("чисто — статических ошибок не найдено") if __name__ == "__main__": main()