Агент пишет Lua-скрипт для LowCode-платформы MWS Octapi по описанию задачи на русском или английском. Всё крутится локально через Ollama. Ни prompt, ни код, ни wf.vars за пределы машины не уходят.
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/StulkovLD/hack_mts/master/install.sh | bashWindows (PowerShell):
iwr -useb https://raw.githubusercontent.com/StulkovLD/hack_mts/master/install.ps1 | iexОдин скрипт делает всё, что нужно на чистой машине: проверяет Python 3.10+, git и Ollama, дотягивает чего не хватает через штатный инсталлер ОС (brew / apt / curl|sh / winget), клонирует репо в ~/localscript и запускает launch.py. Первый запуск 3-5 минут, пока тянется модель ~5 ГБ. Повторный ~5 секунд, бутстрап скипает готовые шаги.
Ручной путь, если установщиком пользоваться не хочется:
git clone https://github.com/StulkovLD/hack_mts.git localscript
cd localscript
ollama pull qwen2.5-7b-instruct-q6_k:latest
pip install -r requirements.txt
python3 launch.pyПосле старта Chainlit UI открывается на http://localhost:8000. REST-сервер /generate поднимается отдельно командой python3 -m localscript.run_server на :8080.
Требования: Python 3.10+, Ollama, git, GPU с 8 GB VRAM.
ollama pull qwen2.5-7b-instruct-q6_k:latestПараметры зафиксированы в src/localscript/config.py и совпадают с ТЗ:
| Параметр | Значение |
|---|---|
num_ctx |
4096 |
num_predict |
256 |
batch |
1 |
parallel |
1 |
| GPU offload | 100%, без CPU offload |
num_ctx / num_predict / num_batch передаются в Ollama per-request из llm.py. parallel - это server-level env OLLAMA_NUM_PARALLEL, его нельзя задать per-request. launch.py при спавне своего ollama serve передаёт OLLAMA_NUM_PARALLEL=1 и OLLAMA_NUM_BATCH=1 в окружение (см. src/localscript/bootstrap.py). Если у тебя уже запущен свой ollama serve с другими значениями, bootstrap напечатает warning и попросит перезапустить его так:
export OLLAMA_NUM_PARALLEL=1
export OLLAMA_NUM_BATCH=1
ollama serve &Peak VRAM. Qwen2.5-7B в квантизации Q6_K даёт ~6 GB peak при заданных параметрах. Остаётся запас 2 GB до лимита 8 GB. Проверяется во время генерации:
nvidia-smi --query-gpu=memory.used --format=csvВыбор 7B Q6_K - компромисс: Q6_K работает практически без деградации относительно fp16, при этом комфортно влезает в 8 GB.
Все вызовы идут через локальный Ollama на localhost:11434. Внешних AI-API (OpenAI, Anthropic, Gemini, любые прокси) в runtime нет. Проверяется grep'ом по src/:
grep -r -i "openai\|anthropic\|api\.openai\|api\.anthropic" src/
# пустоОдин диалог с моделью, четыре роли переключаются внутри общего контекста:
router -> coder -> repair* -> reviewer
^ |
+-- pattern card (retrieval из локальной KB)
- router классифицирует задачу и возвращает JSON-план:
type,source,target,return(scalar / array / table),confidence, опциональныйclarify. Еслиconfidence < 0.3, агент задаёт уточняющий вопрос вместо генерации наугад. - coder пишет Lua по few-shot подсказке. Подсказка - pattern card из локальной KB, подобранная под
typeзадачи. - repair до двух итераций чинит код по структурированному фидбеку валидатора.
- reviewer выносит финальный verdict pass/fail. В eval-режиме выключается флагом
skip_reviewer=True.
Зачем один диалог на четыре роли. KV-cache переиспользуется между стадиями, экономит токены и время. Четыре независимых вызова тянули бы системный промпт заново каждый раз.
Если после двух попыток repair код всё ещё не пас, pipeline отдаёт лучший из полученных вариантов с флагом ok=false. Агент не молчит, не зацикливается, не маскирует провал.
Между стадиями coder и repair работает src/localscript/validator.py - четыре уровня проверки in-process, без вызовов наружу:
| Уровень | Инструмент | Что ловит |
|---|---|---|
| Синтаксис | lupa.load() (Lua 5.4 bundled) |
Parse-ошибки, незакрытые блоки, битые кавычки |
| Статанализ | luaparser AST walker |
Undefined globals, опечатки в wf и _utils, в том числе в мёртвых ветках |
| Домен | Встроенные линты Octapi | wf.vars paths, 1-based индексация массивов, ~= вместо !=, _utils.array.new() для новых массивов, запрет os.* / io.* |
| Семантика | Lua sandbox + _result assert'ы |
Shape результата (scalar/array/table), обязательные поля, реальное исполнение кода на контексте задачи |
Результат валидации уходит в repair как структурированный фидбек: какие линты упали, какие глобалы undefined, что ожидалось в _result. Repair не гадает - чинит конкретику.
lupa тащит bundled Lua 5.4 prebuilt wheel'ами под macOS, Linux, Windows. Системный lua ставить не нужно, brew / apt / choco не дёргаем.
src/localscript/knowledge.py - 14 PatternCard-ов, по одной на типовой паттерн Octapi:
direct_return, mutate_records, format_datetime, ensure_array, filter_new_array, augment_wf_vars, augment_existing_code, json_multi_field, multi_var_augment, epoch_convert, string_extract, sort_records, nested_collect, aggregate_reduce.
Каждая карточка содержит slug, task_types, keywords, why (зачем делать именно так), rules (правила Octapi), example_task, example_code - эталонный Lua на 10-80 строк.
Retrieval - функция retrieve_pattern_cards(task_text, task_type):
- Точное совпадение
task_typeиз router'а - плюс 10 к score. - Совпадение ключевых слов в тексте задачи - плюс 2 за каждое.
- Top-1 попадает в coder как few-shot пример.
- Fallback на
direct_return, если ничего не сматчилось.
Никаких внешних векторных баз, embedding-моделей, API. Вся подборка работает на JSON-карточках в репозитории, эталоны лежат в git и воспроизводятся байт-в-байт.
Контракт из docs/assets/localscript-openapi.yaml (SSOT организаторов):
python3 -m localscript.run_server
# слушает http://localhost:8080POST /generate, тело {"prompt": "<текст задачи>"}, ответ {"code": "<сырой Lua>"}.
Пример на первой публичной задаче:
curl -s -X POST http://localhost:8080/generate \
-H "Content-Type: application/json" \
-d '{
"prompt": "Из полученного списка email получи последний.\n```json\n{\"wf\":{\"vars\":{\"emails\":[\"a@x.ru\",\"b@x.ru\",\"c@x.ru\"]}}}\n```"
}'
# {"code":"return wf.vars.emails[#wf.vars.emails]"}Если в prompt вместе с текстом задачи приложен JSON-контекст (в ```json-блоке или инлайн {wf: ...}), pipeline его извлекает автоматически и прогоняет семантическую валидацию на реальных данных. Без контекста работают синтаксис, статанализ и доменные линты - семантический уровень деградирует, остальное остаётся.