# LLM Robot Control Генератор синтетических датасетов для обучения моделей высокоуровневому планированию промышленной сборки. Создаёт структурированные сцены, строит планы, проверяет предусловия действий и достижение цели, экспортирует обучающие записи в JSONL. Для каждого примера сохраняются два варианта ответа: русский текст и JSON с навыками. Управление физическим роботом и расчёт траекторий не реализованы. ## Установка Требуются Python 3.13+, uv и Linux (для блокировки файлов используется `fcntl`). ```bash uv sync ``` ## Запуск Команды выполняются из корня репозитория: ```bash # Проверить конфигурацию без запросов к модели и создания файлов uv run python -m assembly_dataset --dry-run # Создать 100 обучающих записей uv run python -m assembly_dataset generate --count 100 # Продолжить корпус до 500 000 записей uv run python -m assembly_dataset generate --count 500000 # Проверить сохранённые эпизоды и экспорт uv run python -m assembly_dataset validate ``` `--count` задаёт точное суммарное число примеров по выбранным режимам. Каждый пример экспортируется в двух вариантах: `--count 100` даёт 100 текстовых и 100 структурированных записей с одинаковыми ID. `examples` и прогресс считают пары один раз; `examples_per_variant` в manifest показывает размеры обоих вариантов. Без этого аргумента используется `target_examples` из конфигурации. Одна сцена может давать несколько записей; количество сцен выводится отдельно. Ноль допустим; отрицательные и дробные значения запрещены. `step_sampling: one` выбирает один воспроизводимый шаг из каждого эпизода. `all` экспортирует каждый выполненный шаг. Общий лимит действует и в этом случае: последний эпизод может быть экспортирован частично. Если нужен корпус из полных планов для 500 000 исходных сцен, выберите `modes: [full]` и `--count 500000`. При трёх форматах число сцен отображается отдельно как `scenarios`. ## Прогресс выполнения При запуске CLI автоматически отображаются два индикатора: генерация записей и экспорт в JSONL. Каждый показывает количество записей относительно цели, процент выполнения, скорость, прошедшее и приблизительное оставшееся время. При продолжении корпуса генерация начинается с уже сохранённого количества. Счётчик генерации обновляется после сохранения каждого проверенного эпизода, а не после каждого API-запроса. Во время вызовов модели отображается «ожидание LLM». Экспорт отдельно показывает запись строк и подготовку контрольных сумм. Оценка времени появляется после первых обновлений и может меняться из-за скорости модели, повторных попыток и длины планов. В терминале полосы обновляются на месте; при перенаправлении вывода сохраняется итоговая строка каждого этапа. Индикаторы выводятся в stderr, итоговая статистика JSON — в stdout. Для отключения используйте `--no-progress`: ```bash uv run python -m assembly_dataset generate --count 100 --no-progress ``` ## Конфигурация Настройки находятся в [config.yaml](config.yaml): - `target_examples` — размер итогового корпуса. - `modes` — форматы `full`, `step`, `branch`. - `step_sampling` — один шаг или все шаги эпизода. - `seed`, `domains`, `min_components`, `max_components`, `blocked_fraction` — параметры создания сцен. - `split` — доли train, validation и test; записи одной сцены не разделяются. - `shard_size` — максимальное число строк в одном файле JSONL. - `output_dir` — папка результатов относительно файла конфигурации. - `teacher` — провайдер, модель, доля сцен для LLM, параллельность и повторные попытки. В поставляемом конфиге включён Codex для всех сцен. Для работы требуется доступ к указанной модели. При отсутствии файла учётных данных запускается авторизация через браузер. OpenAI-совместимые серверы настраиваются через `teacher.llm`; ключ и адрес сервера берутся из переменных окружения или корневого `.env`. Для генерации без API задайте `teacher.enabled: false`. Другой конфиг передаётся через `--config PATH`, другая папка результатов — через `--output-dir PATH`. Изменение параметров сцен или модели требует отдельной папки корпуса. Лимит записей и размер шардов можно менять при продолжении запуска. ## Где что менять | Задача | Файл и параметр | |---|---| | Размер датасета | `config.yaml` → `target_examples` или CLI `--count` | | Модель и провайдер | `config.yaml` → `teacher.llm` | | Сложность и типы сцен | `config.yaml` → `domains`, `min_components`, `max_components`, `blocked_fraction` | | Инструкции генератору планов | `prompts.yaml` → `teacher_system` | | Текст запроса со сценой | `prompts.yaml` → `teacher_user` | | Запрос исправления ошибочного плана | `prompts.yaml` → `teacher_retry` | | Инструкции обучаемой модели для JSON | `prompts.yaml` → `structured_system.full`, `.step`, `.branch` | | Инструкции обучаемой модели для русского текста | `prompts.yaml` → `training_system.full`, `.step`, `.branch` | | Текст входа обучающей записи | `prompts.yaml` → `training_user` | | Формулировки заданий по типам сборки | `prompts.yaml` → `task_instructions` | | Названия объектов | `prompts.yaml` → `object_names` | | Русские описания для локального режима и просмотра сцены | `prompts.yaml` → `action_templates` | В шаблонах `teacher_user`, `teacher_retry` и `training_user` нужно оставить `${payload}`: сюда подставляется JSON с заданием и сценой. В запросе исправления JSON также содержит `validation_error`. Остальной текст можно менять свободно. После изменения промптов используйте новую папку, например `--output-dir dataset/prompt-experiment`. Содержимое промптов сохраняется в настройках запуска и проверяется при возобновлении, чтобы не смешивать разные версии корпуса. Промпты меняют инструкции и формулировки, но не правила логической проверки. Пример изменения запроса: ```yaml teacher_user: | Составь план выполнения следующего задания. Учитывай доступность деталей и зависимости операций. ${payload} ``` `${payload}` обязателен в трёх шаблонах user/retry. Он заменяется JSON с полями `instruction` и `scene`; при исправлении добавляется `validation_error`. В обучающем запросе JSON также содержит `mode`, а для шага — `history`. Обычные фигурные скобки разрешены; литеральный знак доллара записывается как `$$`. Системные инструкции и `task_instructions` — обычный текст без подстановок. Пустые инструкции и неизвестные переменные шаблона отклоняются до API-запроса. Изменённые промпты генерации требуют отдельного `output_dir`. Исключение — `structured_system`: эти инструкции используются только при экспорте, поэтому их можно изменить и выполнить `export` в той же папке. Их полное содержимое сохраняется в настройках корпуса. Формат ответа `Plan` и правила проверки остаются заданными кодом: смена текста не добавляет новые навыки или поля JSON. ## Форматы и результаты ```text dataset/assembly-ru/ episodes.sqlite3 # сцены, планы, история состояний, графы, provenance sft/ manifest.json # точные размеры, распределения, SHA-256 файлов train/full/000000.jsonl # ответ русским текстом train/full/000000.structured.jsonl # ответ status/plan train/step/000000.jsonl train/branch/000000.jsonl validation/... test/... ``` Каждая строка содержит `messages` (system/user/assistant), `record_id`, `scenario_id`, `mode`, `split`, `source`, `validation_level`. В `000000.jsonl` ответ `assistant.content` — обычный русский текст: - **full** — нумерованная последовательность действий. - **step** — одна русская фраза с названием объекта. Вход содержит состояние перед действием и историю выполненных действий. - **branch** — нумерованный план с условиями «если выполнено успешно» и «если произошла ошибка». После ошибки требуется уточнить сцену и перепланировать. В `000000.structured.jsonl` поле `assistant.content` содержит JSON в виде строки (стандартный формат `messages` для обучения): ```json {"status":"ready","plan":[{"skill":"pick","object":"деталь №1"},{"skill":"align","object":"деталь №1","target":"сборочная позиция №1"}]} ``` Это фрагмент примера; полный план включает остальные действия. Названия объектов и мест берутся из сцены. `target` есть у действий с местом назначения; у `pick`, `release`, `inspect` его нет. Навыки остаются на английском. `return_home` относится к объекту «робот» с целью «исходное положение». В `step` массив `plan` содержит одно следующее действие со статусом `ready`. Без доступных действий он пуст; статус — `ready` при достигнутой цели или `blocked` при блокировке. В `full` и `branch` статус характеризует весь сценарий; при `blocked` массив может содержать выполнимую часть плана. В `branch` у каждого действия дополнительно есть `id`, `on_success`, `on_failure`: ```json {"id":"step_1","skill":"pick","object":"деталь №1","on_success":"step_2","on_failure":"replan_required"} ``` Первым выполняется `step_1`. Успех ведёт к следующему ID, а у последнего действия — к терминальному исходу `ready` или `blocked`. Ошибка ведёт к `replan_required`: остановиться, обновить сцену и построить новый план. Для пустого плана итог задан полем `status`. Две версии имеют одинаковые ID, порядок, разбиение и входные сцены, но разные системные инструкции. Для обучения одному формату выбирайте только соответствующие файлы; совместное обучение двум форматам требует сохранять их системные инструкции. Пример для помощи оператору: ```text 1. Определить положение объекта «распределительная коробка №1». 2. Переместиться к объекту «распределительная коробка №1». 3. Захватить объект «распределительная коробка №1». 4. Переместить объект «распределительная коробка №1» к оператору. 5. Удерживать объект «распределительная коробка №1» в неподвижном состоянии возле оператора. 6. Дождаться подтверждения завершения сборки объекта «распределительная коробка №1», продолжая удерживать его. 7. Переместить объект «распределительная коробка №1» в место «зона хранения №1». 8. Отпустить объект «распределительная коробка №1». 9. Вернуться в исходное положение. ``` Домен `operator_assistance` описывает удержание объекта у оператора. `collaborative` описывает передачу объекта оператору: это другая задача, с другим порядком отпускания. Чтобы получить только сценарии удержания, задайте `domains: [operator_assistance]`. Технические навыки, предусловия, эффекты и графы сохраняются внутри канонического эпизода для проверки. Обучаемая модель получает сцену с русскими названиями объектов, их положением, доступностью и зависимостями. Статусы находятся в метаданных записи; при невыполнимости текст ответа явно сообщает о блокировке. Версия экспорта — 3, версия хранилища эпизодов — 2. Сохранённые эпизоды версии 2 можно переэкспортировать в оба варианта без запросов к LLM, сохранив настройки генерации. Корпуса версии 1 несовместимы: используйте новую папку. В конфигурации задана `dataset/assembly-ru`, чтобы не смешивать русские ответы с прежним техническим JSON. SQLite служит транзакционным checkpoint и индексом уникальности. Он хранит полные эпизоды, включая trace и teacher-ошибки. Пакет в памяти ограничен размером очереди; Каждый готовый ответ сохраняется сразу, не ожидая остальных запросов пакета. При Ctrl+C, SIGTERM или ошибке генерации CLI экспортирует накопленные данные в JSONL/CSV; неполный экспорт отмечается `partial: true` в manifest. После исчерпания повторных попыток ошибки API останавливают генерацию. Невалидные планы, как прежде, заменяются проверенным символическим планом с отметкой ошибки. Для корпуса только из ответов LLM запускайте `generate --teacher-only`: при невалидном ответе генерация остановится с экспортом накопленных записей, а символическая замена не попадёт в базу. Флаг действует на новые записи; ранее сохранённые эпизоды он не удаляет. Для продолжения повторите команду с теми же настройками: сохранённые эпизоды не запрашиваются повторно, незавершённые запросы выполняются заново. После принудительного завершения (SIGKILL) данные остаются в SQLite; экспортировать их без запросов к LLM можно командой `uv run python -m assembly_dataset export --allow-partial`. Одновременные писатели в одну папку блокируются. Неизменённый процедурный запуск воспроизводим побайтно. Полный экспорт пересобирается во временную папку и затем заменяет предыдущий. На время переэкспорта нужно место для обеих копий. Старый экспорт до завершения нового остаётся доступен. SQLite и SFT для 500 тысяч строк могут занимать десятки гигабайт: размер зависит от числа компонентов, режимов и `step_sampling`. ## CSV для просмотра Рядом с каждым шардом JSONL автоматически создаётся одноимённый CSV: ```text sft/train/full/000000.jsonl sft/train/full/000000.csv sft/train/full/000000.structured.jsonl sft/train/full/000000.structured.csv ``` Одна строка CSV соответствует одной обучающей записи. Первые колонки: «Задание», «Сцена», «План», «Статус», «Формат», «Источник текста». В колонке «План» находится содержимое `assistant.content`: русский текст в обычном CSV и JSON в `.structured.csv`. Метаданные и полный вход находятся после основных колонок. Кодировка — UTF-8 с BOM, разделитель — точка с запятой (`;`). При импорте в Excel или LibreOffice выберите этот разделитель и включите перенос текста в ячейках. Поля с кавычками, разделителями и переносами строк экранируются. CSV включены в контрольные суммы manifest и проверяются командой `validate`. CSV не увеличивают `examples`: это представления тех же записей. Для добавления CSV к существующему корпусу выполните `export` с нужным лимитом; запросов к LLM при этом нет. ## Продолжение и переэкспорт После остановки повторите команду `generate` с тем же конфигом и папкой результатов. Для изменения количества строк используйте `--count`. Меньший лимит уменьшает экспорт, но сохраняет канонические эпизоды в SQLite. ```bash # Повторно экспортировать 100 записей из сохранённых эпизодов uv run python -m assembly_dataset export --count 100 ``` Без `--count` команда `export` использует лимит из YAML. Если столько записей ещё не подготовлено, сначала выполните `generate` или укажите меньший лимит. ## Модель мира и ограничения В `world.py` находятся одиннадцать расширяемых рецептов: перемещение, механосборка, крепёж, прессовая посадка, электромонтаж, электроника, клей, сварка, совместная сборка, удержание объекта у оператора и контроль. Сцены различаются количеством компонентов, начальными положениями, уже завершёнными стадиями, отсутствующими деталями и DAG-зависимостями. Текущее положение задают предикаты `at`/`holding` в состоянии; поле `components[].source_location` обозначает исходное место подачи детали. Это каталог базовых абстрактных процессов, **не исчерпывающая модель всей промышленности**. Новый процесс требует рецепта, корректных предусловий/эффектов и тестов. У каждой операции есть `requires`, `forbids`, `add`, `delete`. Полный план проверяется повторным исполнением; ветвящийся — обходом всех доступных путей. Состояния и эффекты задаёт генератор, teacher не может их переписать. `blocked` проверяется эталонным решателем для этого ограниченного ациклического домена. Это не универсальный PDDL-планировщик. Структурная сигнатура строится из сцены с фиксированными каноническими ID. Номер кандидата, формулировка задания и ответ LLM не увеличивают разнообразие. Дедупликация выполняется уникальным индексом SQLite; при исчерпании малого пространства параметров генератор завершится с ошибкой. Варианты одного сценария получают один split по сигнатуре. Родственные, но различные сцены могут попасть в разные split — это не строгий тест переноса на неизвестные семейства изделий. Для маленьких корпусов присутствие validation/test не гарантируется. `validation_level: logical_only`: нет проверки геометрии, столкновений, усилий, качества сварки/клея и реальной выполнимости навыков. VLM/VLA и разметка изображений не поддерживаются. ## Teacher-модели В поставляемом `config.yaml` включён Codex для всех сцен (`enabled: true`, `fraction: 1.0`). При `enabled: false` планы строятся локально. Для подключения OpenAI-совместимого сервера настройте блок: ```yaml teacher: enabled: true fraction: 0.15 # доля сцен; 1.0 — все сцены concurrency: 4 timeout_seconds: 180 retries: 2 llm: provider: openai model: your-provider/your-model api_key_env: API_KEY base_url_env: BASE_URL generation: temperature: null max_tokens: 8192 ``` Ключ и адрес сервера берутся из окружения или корневого `.env`. Для Codex замените `llm`: ```yaml llm: provider: codex model: <доступная вам модель> credential_path: .config/codex-credentials.json ``` Подключение выполняет `llm.factory.LLMModelProvider`. Teacher возвращает полный план: для каждого действия — внутренние `skill/object/target` и русское описание `text`. Код проверяет последовательность внутренних действий и сохраняет текст модели без замены шаблонной формулировкой. Затем из полного плана создаются пошаговые записи и условия ветвления; сами условия ошибки формирует код. Логическая проверка подтверждает внутренние действия. Для текста проверяются наличие русского описания, количество шагов, названия объектов и отсутствие технических ID. Текст также должен совпадать с `action_templates` после нормализации пунктуации и ограниченного набора синонимов. Допускаются «Сначала/Затем/Далее», «взять/захватить», «перенести/переместить», «проконтролировать/проверить», «возвратиться/вернуться», «местоположение/положение». Другие формулировки отклоняются и запрашиваются заново: свободный текст не считается доказательством корректности. Для изменения основного стиля редактируйте `action_templates` в `prompts.yaml`. После неудачных попыток используется символьный план и русские шаблоны из `action_templates`. Это помечается как `source: symbolic`, `text_source: template` и `teacher_error`. У успешного ответа модели `text_source: teacher`. В CSV это видно в колонке «Источник текста». Смена `action_templates` не переписывает сохранённые формулировки teacher. Реальные API-вызовы не входят в локальные тесты. Для сравнения моделей используйте отдельные папки и проверяйте `sources` и `teacher_failures` в manifest. ## Структура проекта ```text assembly_dataset/ # генерация сцен, валидация, teacher, хранение и CLI config.yaml # модель, размер, сцены и параметры генерации prompts.yaml # инструкции LLM и тексты обучающих записей llm/ # провайдеры моделей, авторизация и общие схемы настроек tests/ # тесты генератора и конвейера ``` Промпты редактируются в [prompts.yaml](prompts.yaml), без изменения Python-кода. Параметр `prompts_file` в `config.yaml` позволяет выбрать другой файл промптов. ## Проверки ```bash uv run python -m unittest discover -s tests -v ``` Тесты используют локальную генерацию и тестовый агент без внешних API-запросов.