ZuperAI-14B / toolkit /fix_project.py
Tyesee's picture
toolkit: fix_project.py
27a4cdc verified
Raw History Blame Contribute Delete
5.25 kB
# -*- 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()