ZuperAI-14B / toolkit /verify.py
Tyesee's picture
toolkit: verify.py
f69f6f6 verified
Raw History Blame Contribute Delete
9.48 kB
# -*- coding: utf-8 -*-
"""ZuperAI с исполняемой самопроверкой (test-driven): тесты → код → прогон → чинка.
Поток:
1. «судья» пишет юнит-тесты ПО ЗАДАЧЕ (контракт, без видения кода);
2. локальная модель пишет код, который должен пройти эти тесты;
3. тесты гоняются в песочнице (отдельный subprocess, таймаут);
4. при провале судья решает, где ошибка — в коде или в тесте, и чинит её;
5. повтор, пока тесты не пройдут (или не выйдет лимит попыток).
Кто есть кто:
--code local|lmstudio кто пишет КОД. local = transformers+LoRA (грузит 3B в память),
lmstudio = LM Studio (GGUF, быстрее, без torch). По умолчанию local.
--judge deepseek|local|lmstudio кто пишет ТЕСТЫ и судит. По умолчанию deepseek, если ключ есть.
Практичные связки:
--code lmstudio --judge deepseek → код локально (быстро), тесты/судья сильные
--code lmstudio --judge lmstudio → полностью офлайн, без DeepSeek и без torch
Запуск:
venv/Scripts/python.exe verify.py "Напиши функцию, которая ..."
Ключи:
--attempts N число попыток (по умолчанию 5)
--timeout N таймаут прогона тестов, сек (по умолчанию 30)
--keep не удалять песочницу (покажет путь к solution.py/test_*.py)
"""
import sys
import tempfile
from webui import (deepseek_available, deepseek_generate,
lmstudio_available, lmstudio_generate)
from verify_util import (TEST_SYSTEM, VERIFY_ATTEMPTS, VERIFY_TIMEOUT,
run_tests, looks_like_tests, tail, ensure_unittest_header)
SYSTEM = ("Ты — опытный программист. Пиши рабочий, чистый код без лишних пояснений. "
"Комментарии и докстринги — на русском.")
def _cli_args():
args = sys.argv[1:]
attempts, timeout, keep = VERIFY_ATTEMPTS, VERIFY_TIMEOUT, False
code = "local"
judge = "deepseek" if deepseek_available() else "local"
prompt_parts = []
i = 0
while i < len(args):
a = args[i]
if a == "--attempts" and i + 1 < len(args):
attempts = int(args[i + 1]); i += 2; continue
if a == "--timeout" and i + 1 < len(args):
timeout = int(args[i + 1]); i += 2; continue
if a == "--judge" and i + 1 < len(args):
judge = args[i + 1].lower(); i += 2; continue
if a == "--code" and i + 1 < len(args):
code = args[i + 1].lower(); i += 2; continue
if a == "--keep":
keep = True; i += 1; continue
prompt_parts.append(a); i += 1
if judge not in ("deepseek", "local", "lmstudio"):
judge = "local"
if code not in ("local", "lmstudio"):
code = "local"
if judge == "deepseek" and not deepseek_available():
print("[!] DeepSeek недоступен — судья переключён на local", file=sys.stderr)
judge = "local"
if judge == "lmstudio" and not lmstudio_available():
print("[!] LM Studio не запущен (порт 1234) — судья переключён на local", file=sys.stderr)
judge = "local"
if code == "lmstudio" and not lmstudio_available():
print("[!] LM Studio не запущен (порт 1234) — код переключён на local", file=sys.stderr)
code = "local"
return (" ".join(prompt_parts) or
"Напиши функцию, которая принимает список чисел и возвращает их среднее",
attempts, timeout, keep, judge, code)
def main():
prompt, max_attempts, timeout, keep, judge, code_backend = _cli_args()
# грузим torch-модель только если кто-то (код или судья) работает через transformers
tok = model = None
if code_backend == "local" or judge == "local":
from infer_smart import load, gen, _strip_fences as _sf
tok, model = load()
else:
from infer_smart import _strip_fences as _sf
def code_gen(msg, max_tokens=768):
if code_backend == "lmstudio":
return _sf(lmstudio_generate(msg, max_tokens=max_tokens))
return _sf(gen(tok, model, msg, max_tokens=max_tokens))
def judge_gen(msg, max_tokens=768):
if judge == "deepseek":
return _sf(deepseek_generate(msg, max_tokens=max_tokens))
if judge == "lmstudio":
return _sf(lmstudio_generate(msg, max_tokens=max_tokens))
return _sf(gen(tok, model, msg, max_tokens=max_tokens))
print(f"[*] код: {code_backend}, судья: {judge}", file=sys.stderr)
if judge == "local":
print("[!] честно: локальная 3B может ошибаться в ТЕСТАХ (неверные ожидания). "
"Для надёжности используйте --judge deepseek.", file=sys.stderr)
sandbox = tempfile.mkdtemp(prefix="zuperai_verify_")
# --- шаг 1: судья пишет тесты ПО ЗАДАЧЕ (TDD) ---
t_messages = [{"role": "system", "content": TEST_SYSTEM},
{"role": "user",
"content": f"Задача: {prompt}\n\nНапиши unittest-тесты, которые "
f"импортируют решение из solution и проверяют корректность. "
f"Верни ТОЛЬКО код тестов, без пояснений и без ```."}]
tests = ensure_unittest_header(judge_gen(t_messages))
# --- шаг 2+: модель пишет код, гоняем, чиним ---
messages = [{"role": "system", "content": SYSTEM},
{"role": "user",
"content": f"Задача: {prompt}\n\nТесты (unittest):\n{tests}\n\n"
f"Напиши код (solution.py), который проходит все эти тесты. "
f"Верни ТОЛЬКО код, без пояснений и без ```."}]
code = ""
for attempt in range(1, max_attempts + 1):
if not code:
code = code_gen(messages)
print(f"[попытка {attempt}/{max_attempts}] генерирую код…", file=sys.stderr)
else:
print(f"[попытка {attempt}/{max_attempts}] тестирую исправленный код…",
file=sys.stderr)
ok, out = run_tests(code, tests, sandbox, timeout)
if ok:
print(code)
print(f"\n[OK] тесты прошли, попытка {attempt}")
print("----- сгенерированные тесты -----")
print(tests)
print("----- песочница -----")
print(sandbox if keep else "(временная, удалена)")
return
print(f"[попытка {attempt}] тесты упали:\n{tail(out)}", file=sys.stderr)
messages.append({"role": "assistant", "content": code})
messages.append({"role": "user",
"content": f"Решение не прошло тесты. Вывод тестов:\n"
f"{tail(out)}\n\n"
f"Определи, где ошибка: в коде (неверная реализация) "
f"или в тесте (неверное ожидаемое значение). "
f"Верни исправленный КОД, либо исправленный ТЕСТ — "
f"только одно, без пояснений и без ```."})
fix = judge_gen(messages)
if looks_like_tests(fix):
tests = ensure_unittest_header(fix)
code = ""
messages = messages[:2]
messages[1] = {"role": "user",
"content": f"Задача: {prompt}\n\nТесты (unittest):\n{tests}\n\n"
f"Напиши код (solution.py), который проходит все эти "
f"тесты. Верни ТОЛЬКО код, без пояснений и без ```."}
print("[*] судья решил, что ошибка в ТЕСТЕ — тест обновлён", file=sys.stderr)
else:
code = fix
messages[-2] = {"role": "assistant", "content": code}
print(code)
print(f"\n[!] за {max_attempts} попыток тесты не прошли — выше последний вариант")
print("----- последние тесты -----")
print(tests)
print("----- песочница -----")
print(sandbox if keep else "(временная, удалена)")
if __name__ == "__main__":
main()