docs(plan): record the implemented LLM graph execution stage
This commit is contained in:
@@ -0,0 +1,173 @@
|
|||||||
|
# План реализации исполнения графов Qwen3 в Webots
|
||||||
|
|
||||||
|
> **Для агента-разработчика:** выполнять задачи по порядку и отмечать выполненные шаги чекбоксами. Этот документ — план разработки, а [спецификация](../specs/2026-11-10-llm-graph-execution-design.md) задаёт поведение.
|
||||||
|
|
||||||
|
**Цель:** получать актуальный `scene_json`, принимать готовый JSON-граф Qwen3 и безопасно исполнять его в Webots независимо от эталонного сценария выбранного эксперимента.
|
||||||
|
|
||||||
|
**Архитектура:** пакет `iiwa_scene_context` формирует снимок из живого supervisor и публикует контракт для модели. Пакет `iiwa_llm_plan_executor` проверяет граф, исполняет узлы через реестр навыков и ограниченный адаптер MoveItPy. Существующий `iiwa_web` даёт тонкие HTTP/MCP маршруты; `iiwa_experement` только создаёт исходную сцену и управляет виртуальными захватами.
|
||||||
|
|
||||||
|
**Стек:** Ubuntu 24.04, ROS 2 Jazzy, Python 3, `rclpy`, MoveItPy 2.12, Webots, `webots_ros2_driver`, FastAPI, FastMCP, pytest.
|
||||||
|
|
||||||
|
**Спецификация:** [Согласованная архитектура](../specs/2026-11-10-llm-graph-execution-design.md).
|
||||||
|
|
||||||
|
## Общие ограничения
|
||||||
|
|
||||||
|
- Работать в ветке `experement`. До первой правки выполнить `git status --short --branch`. Не сбрасывать чужие изменения. Код этого этапа только для Webots, запуск `simulate:=true`; физический FRI не подключать.
|
||||||
|
- Создать ровно два дополнительных ROS 2 пакета: `src/iiwa_scene_context` и `src/iiwa_llm_plan_executor`. Изменения `iiwa_experement` и `iiwa_web` ограничить интеграцией. Не делать систему оценки Qwen3, её обучение или вызов модели: внешний клиент передаёт **готовый** граф.
|
||||||
|
- Номер эксперимента выбирает исходную сцену, но не задаёт робототехническое задание. Исполнять только переданный граф. Не вставлять операции взятия инструмента, герметизации, установки люка или передачи винтов на основании номера эксперимента.
|
||||||
|
- Внутри *одного* навыка допустимы обязательные геометрические фазы: безопасный подход сверху, осевое опускание, действие, осевой подъём к сохранённой высоте. Явные узлы этих фаз не должны вызывать повторное действие. Неизвестный объект, навык, инструмент, цель, положение или неоднозначная зависимость дают отказ до движения либо `replan_required`.
|
||||||
|
- `release(vacuum)` не перемещает инструмент к стойке. Чтобы вернуть присоску, граф должен явно содержать `transfer(vacuum,vacuum_socket)` и `release(vacuum,vacuum_socket)` либо `dock_tool(vacuum,vacuum_socket)` в достигнутом гнезде. `release(vacuum)` из `work` без безопасной цели отклоняется; присоска остаётся в `patron`. Для `grab(cap)` присоска в `patron` обязательна; если из исходного состояния она находится в стойке, граф должен явно предписать её взятие. Винт подносится к отверстию и передаётся виртуальному человеку: без вставки и затяжки. Навык `tighten` в профиле Grant первого этапа не поддерживается.
|
||||||
|
- `detect`: перейти в `work`, дождаться свежего кадра верхней камеры, получить точную позу из supervisor. При настроенном близком осмотре получить также новый кадр камеры на роботе; RGB/D используется для контроля, а не для вычисления позы.
|
||||||
|
- Рабочий кончик направлен вниз; при рабочих движениях наклон не более 10° и J7 в жёстком коридоре ±90° относительно `work` (сейчас 0 рад). Применять ограничения к *траектории*, не только к конечной позе. Если траектория недоступна, остановиться; не переходить на ветвь с разворотом J7 около −180°. Поворот кисти и переход в `work` только после осевого отвода от контакта.
|
||||||
|
- Один активный граф на робота. Внешний клиент передаёт ответ Qwen3 через `/iiwa_llm_plan_executor/submit`; принятый граф можно прочитать из ROS 2 топика `/iiwa_llm_plan_executor/plan`, а события и итог — из отдельного `/iiwa_llm_plan_executor/status`. `status=blocked` не запускает движение. `replan_required` выдаёт новый снимок сцены, но не вызывает Qwen3. `completed` означает выполненный граф и проверенные постусловия, а не оценку соответствия произвольному текстовому запросу.
|
||||||
|
- Движение запускается после готовности сцены, supervisor, контроллера, MoveIt, свежего состояния робота и задержки минимум 3,0 с симуляционного времени. `mode=view` и `mode=execute` сохраняют свою нынешнюю семантику.
|
||||||
|
|
||||||
|
Поток данных: `/iiwa_scene_context/scene_json` → внешний вызов Qwen3 → `/iiwa_llm_plan_executor/submit` → `/iiwa_llm_plan_executor/plan` (сам принятый граф) → `/iiwa_llm_plan_executor/status` (ход и итог). После реализации граф можно посмотреть командой `ros2 topic echo /iiwa_llm_plan_executor/plan`; пока модель вызывается отдельно, в этом топике ничего не появится без передачи её ответа в `submit`.
|
||||||
|
|
||||||
|
## На что смотреть перед изменениями
|
||||||
|
|
||||||
|
| Назначение | Файлы и каталоги |
|
||||||
|
| --- | --- |
|
||||||
|
| Согласованная архитектура | `docs/superpowers/specs/2026-11-10-llm-graph-execution-design.md` |
|
||||||
|
| Исходные сцены и объекты | `src/iiwa_experement/config/scenes.yaml`, `src/iiwa_experement/iiwa_experement/scenario.py`, `src/iiwa_experement/worlds/grant/iiwa-grant.wbt`, `src/iiwa_description/objects/grant/` |
|
||||||
|
| Геометрия и захват | `src/iiwa_experement/config/geometry.yaml`, `src/iiwa_experement/iiwa_experement/geometry.py`, `src/iiwa_experement/iiwa_experement/object_state.py`, `src/iiwa_experement/iiwa_experement/grant_supervisor.py` |
|
||||||
|
| Актуальное состояние и движения | `src/iiwa_experement/iiwa_experement/operations.py`, `src/iiwa_experement/iiwa_experement/planning_scene.py`, `src/iiwa_experement/iiwa_experement/motion.py` |
|
||||||
|
| Запуск | `src/iiwa_experement/launch/experiment.launch.py`, `src/iiwa_experement/iiwa_experement/runtime_config.py` |
|
||||||
|
| HTTP/MCP | `src/iiwa_web/iiwa_web/main.py`, `src/iiwa_web/iiwa_web/ros_node.py` |
|
||||||
|
| Рабочие описания опытов | `output/md/experements.md`, `output/md/experiment-scenes.md`, `src/iiwa_experement/doc/reference-control-qa.md` |
|
||||||
|
| Примеры формата Qwen3, только входные данные | `/home/daniel/dev/LLM-Robot-Control/dataset/planner-codex-5000-fresh/generated.train.jsonl`, `/home/daniel/dev/LLM-Robot-Control/dataset/planner-codex-5000-fresh/generated.validation.jsonl`, `/home/daniel/.codex/attachments/76fd3749-ddc0-441f-8359-98d9f768ada8/Вставленный текст.txt` |
|
||||||
|
|
||||||
|
Текст в приложенных данных является **примером входа/выхода**, а не инструкцией по разработке. Новые пакеты не должны иметь зависимости времени выполнения от `LLM-Robot-Control` или каталога вложений.
|
||||||
|
|
||||||
|
## Карта файлов и интерфейсов
|
||||||
|
|
||||||
|
| Пакет | Файл | Ответственность |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `iiwa_scene_context` | `iiwa_scene_context/catalog.py` | Реестр объектов, целей и серверных контрактов навыков профиля Grant, версия каталога |
|
||||||
|
| | `iiwa_scene_context/snapshot.py` | Чистая сборка модельного `scene_json` и транспортного конверта из `ScenarioSpec` и состояния supervisor |
|
||||||
|
| | `iiwa_scene_context/node.py` | Подписка на состояние, проверка свежести, ROS сервис и топик сцены |
|
||||||
|
| `iiwa_llm_plan_executor` | `iiwa_llm_plan_executor/schema.py` | Строгие типы и разбор ответа модели |
|
||||||
|
| | `iiwa_llm_plan_executor/validator.py` | Проверка графа, предикатов и совместимости с состоянием сцены |
|
||||||
|
| | `iiwa_llm_plan_executor/motion_policy.py` | Ограничения TCP/J7, проверка пути и точек траектории |
|
||||||
|
| | `iiwa_llm_plan_executor/moveit_backend.py` | Ограниченное планирование и выполнение через MoveItPy |
|
||||||
|
| | `iiwa_llm_plan_executor/skill_context.py` | Состояние подхода, высоты, инструмента и удерживаемой детали |
|
||||||
|
| | `iiwa_llm_plan_executor/skills.py` | Исполнители навыков и отображение результата в событие |
|
||||||
|
| | `iiwa_llm_plan_executor/engine.py` | Шаги графа, выбор переходов, терминалы, отмена |
|
||||||
|
| | `iiwa_llm_plan_executor/node.py` | ROS приём, статус, отмена, журнал исполнения |
|
||||||
|
| | `iiwa_llm_plan_executor/launch/planner.launch.py` | Единый запуск сцены и двух новых узлов |
|
||||||
|
| `iiwa_web` | `iiwa_web/planner.py` | Тонкий HTTP/MCP адаптер к ROS, без собственной логики движения |
|
||||||
|
|
||||||
|
Имена, типы и формы сообщений зафиксировать в `docs/interfaces.md` каждого нового пакета до интеграции: `scene_json`, конверт снимка, вход `submit`, принятый граф, ответ, статус, коды ошибок. Для JSON в ROS использовать `std_srvs/Trigger` (снимок), `std_msgs/String` (снимок, принятый граф и статус) и `webots_ros2_msgs/SetString` (`submit`/`cancel`). Для запроса статуса по ID добавить `GetRunStatus.srv` в **существующий** `iiwa_msgs`: запрос `string run_id`, ответ `bool found` и `string status_json`. Третьего нового пакета нет. `SetString` возвращает только признак принятия; `run_id` задаёт клиент.
|
||||||
|
|
||||||
|
## Вопросы для проверки при рецензии
|
||||||
|
|
||||||
|
- Повторный запрос `submit` с тем же `run_id` не должен запускать второй экземпляр движения (задача 7, тест повторной отправки).
|
||||||
|
- Если supervisor устарел или пропал между двумя узлами, результат не должен трактоваться как `found`/`success` (задачи 2 и 7, тест потери свежести).
|
||||||
|
- Граф с циклом или недостижимыми узлами не должен зависнуть или обойти предел шагов (задача 4, тест циклов и лимита).
|
||||||
|
- После неудачного захвата возможны два состояния — пустой `patron` либо удерживаемый объект; продолжать без `inspect_state` нельзя (задачи 4 и 6, тест неопределённого владения).
|
||||||
|
- Даже при допустимой конечной позе промежуточный J7 или TCP может выйти за пределы; траектория должна быть отклонена до исполнения (задача 5, тест промежуточной точки).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Задача 1. Режим сцены для внешнего графа
|
||||||
|
|
||||||
|
**Файлы:** изменить `src/iiwa_experement/iiwa_experement/runtime_config.py`, `src/iiwa_experement/launch/experiment.launch.py`; тесты `src/iiwa_experement/test/test_runtime_config.py`, `src/iiwa_experement/test/test_launch.py`.
|
||||||
|
|
||||||
|
**Интерфейс:** `mode:=planner` создаёт тот же мир, спавнер, supervisor и bringup, что `execute`, но **не** запускает `reference_runner`. Supervisor в этом режиме доступен для команд захвата (`read_only=False`). TCP gizmo остаётся выключенным.
|
||||||
|
|
||||||
|
- [x] Написать тесты: `validate_mode("planner")` проходит; `runtime_actions(..., mode="planner")` создаёт writable supervisor и не создаёт `reference_runner`; `view` и `execute` сохраняют прежние действия.
|
||||||
|
- [x] Запустить `pytest -q src/iiwa_experement/test/test_runtime_config.py src/iiwa_experement/test/test_launch.py` и убедиться в ожидаемом падении новых тестов.
|
||||||
|
- [x] Добавить режим в валидатор и изменить вычисление `read_only` на `mode == "view"`; проверку добавления эталонного runner оставить только для `execute`.
|
||||||
|
- [x] Повторить тесты; отдельным коммитом зафиксировать этот режим.
|
||||||
|
|
||||||
|
### Задача 2. Пакет снимка сцены
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_scene_context/package.xml`, `setup.py`, `setup.cfg`, `resource/iiwa_scene_context`, `iiwa_scene_context/{__init__,catalog,snapshot,node}.py`, `test/test_snapshot.py`, `test/test_node.py`, `docs/interfaces.md`.
|
||||||
|
|
||||||
|
**Интерфейсы:** `build_scene(spec: ScenarioSpec, supervisor_state: Mapping, catalog: SkillCatalog) -> dict` создаёт модельный JSON. `build_envelope(..., scene_revision: int, stamp: float) -> dict` добавляет `experiment_id`, `state_version`, `scene_revision`, `timestamp`, `scene_json`. ROS: `/iiwa_scene_context/get_scene` (`Trigger`, `message` — JSON-конверт), `/iiwa_scene_context/scene_json` (`String`, transient-local).
|
||||||
|
|
||||||
|
- [x] Написать тесты для сцен 1 и 20: выдаются только присутствующие объекты и цели, в сцене с лотком не менее 10 отдельных винтов; `cap`, `vacuum`, `vacuum_socket` и отверстия имеют стабильные ID; у каждого присутствующего инструмента есть отдельная именованная цель гнезда; в `scene_json` нет координат и внутренних полей supervisor; логическое состояние инструмента/детали отражает живой snapshot.
|
||||||
|
- [x] Написать тесты свежести: до `scene_ready` либо при устаревшем `object_state` сервис отвечает `success=False`; `scene_revision` растёт при изменении `state.version`, но не от каждого обновления позы удерживаемого объекта; поздний подписчик получает текущий снимок.
|
||||||
|
- [x] Убедиться в ожидаемом падении `pytest -q src/iiwa_scene_context/test`; реализовать каталог и чистую сборку, затем узел с подпиской на `/iiwa_experement/object_state` и `/iiwa_experement/scene_ready`.
|
||||||
|
- [x] Повторить тесты. Записать версию контракта, точную форму полей и правило `scene_revision` в `docs/interfaces.md`; зафиксировать пакет отдельным коммитом.
|
||||||
|
|
||||||
|
### Задача 3. Разбор и валидация графа
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/package.xml`, `setup.py`, `setup.cfg`, `resource/iiwa_llm_plan_executor`, `iiwa_llm_plan_executor/{__init__,schema,validator}.py`, `test/test_schema.py`, `test/test_validator.py`, `docs/interfaces.md`. Зависимость пакета от `iiwa_scene_context` и `iiwa_experement` объявить в `package.xml`.
|
||||||
|
|
||||||
|
**Интерфейсы:** `parse_response(raw: str | Mapping) -> PlanGraph`; `validate_graph(graph: PlanGraph, scene: SceneEnvelope, catalog: SkillCatalog) -> ValidationResult`. Каталог и допустимые исходы приходят из серверного `iiwa_scene_context/catalog.py`, а не из присланного графом `skill_contracts`.
|
||||||
|
|
||||||
|
- [x] Написать тесты: показанный пользователем формат `status/entry/plan/transitions` разбирается; `status=blocked` не допускается к исполнению; дубликаты ID, неизвестные навыки/объекты/цели/события, пропущенный `entry`, несуществующие `next`, недостижимые узлы, пустые обязательные переходы и превышение лимитов отвергаются.
|
||||||
|
- [x] Написать тесты ветвей отказа: `collision` не ведёт к продолжению операции; `grasp_failed` не превращается в `success`; цикл с допустимым восстановлением имеет ограничение 256 узлов и отдельный предел выполненных шагов; неверный `scene_revision` отклоняется.
|
||||||
|
- [x] Написать тест на исходную присоску в стойке: граф `grab(cap)` без явного `grab(vacuum)` получает `replan_required` **до первого движения**. Граф `grab(vacuum) → transfer(vacuum,work) → completed` принимается и оставляет инструмент в `patron`; граф `grab(vacuum) → transfer(vacuum,work) → release(vacuum)` отклоняется. Граф с явным `transfer(vacuum,vacuum_socket) → dock_tool(vacuum,vacuum_socket)` допускает возврат. Не выводить задачу из `experiment_id`.
|
||||||
|
- [x] Прочитать несовпадение имён в приложенных наборах. Ввести версию контракта и только явно проверенные алиасы `pick`, `move_to_storage`, `inspect`; `align` и Grant-расширения — самостоятельные записи каталога. Неизвестный `tighten` отклоняется до движения. Проверить тестами и зафиксировать.
|
||||||
|
- [x] Запустить `pytest -q src/iiwa_llm_plan_executor/test/test_schema.py src/iiwa_llm_plan_executor/test/test_validator.py` сначала до, затем после реализации; сохранить контракт и отдельный коммит.
|
||||||
|
|
||||||
|
### Задача 4. Проверка достижимости ограниченных движений
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/iiwa_llm_plan_executor/{motion_policy,moveit_backend}.py`, `config/motion_limits.yaml`, `test/test_motion_policy.py`, `test/test_moveit_backend.py`.
|
||||||
|
|
||||||
|
**Интерфейсы:** `MotionPolicy.validate_trajectory(points, tcp_poses, current_joints) -> MotionCheck`; `MoveItBackend.plan_and_execute(goal: Transform, phase: str, held_object: str | None) -> MotionOutcome`; `MoveItBackend.stop() -> None`. Все планируемые траектории проходят один и тот же проверяющий слой.
|
||||||
|
|
||||||
|
- [x] Сначала проверить в установленном Jazzy/MoveItPy возможность задать `set_path_constraints`, семя IK из текущих суставов и исполнить траекторию через действующий контроллер Webots. Описать проверенные вызовы и ограничения в `docs/interfaces.md`. Если связка не исполняет план, не заменять её обычным `MoveToPose.action` без ограничений: зафиксировать блокирующую несовместимость и безопасный вариант реализации.
|
||||||
|
- [x] Написать тесты: J7 в `work` равен 0; границы −π/2 и +π/2 включительны; промежуточная точка за границей, скачок почти на π, наклон TCP >10°, несвежие `joint_states` и неактуальная планировочная сцена отвергаются до отправки контроллеру.
|
||||||
|
- [x] Реализовать проверку в `motion_policy.py`, затем MoveItPy backend с `current-state` IK seed, ограничением `joint7` и рабочего наклона, проверкой всей траектории, синхронизацией с `PlanningSceneBridge` и подтверждением достигнутой позы. Путь с удерживаемым объектом проверять вместе с прикреплённой геометрией.
|
||||||
|
- [x] Прогнать `pytest -q src/iiwa_llm_plan_executor/test/test_motion_policy.py src/iiwa_llm_plan_executor/test/test_moveit_backend.py`. В Webots выполнить один безопасный переход `work → над инструментом → work` и сохранить в QA журнале J7 по всем точкам и минимальный зазор; зафиксировать коммит.
|
||||||
|
|
||||||
|
### Задача 5. Контекст исполнения и навыки
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/iiwa_llm_plan_executor/{skill_context,skills}.py`, `test/test_skills.py`. Использовать `iiwa_experement/geometry.py`, `planning_scene.py` и протокол `/iiwa_experement/object_command`; не вызывать `reference_scenarios.py`.
|
||||||
|
|
||||||
|
**Интерфейсы:** `SkillContext` хранит текущий `scene_revision`, удерживаемый объект/инструмент, сохранённую безопасную позу подхода, фазу контакта и последнюю проверенную позу. `SkillRegistry.execute(node: PlanNode, context: SkillContext) -> SkillOutcome(event, details)`. Команды supervisor отправлять с `request_id` и подтверждать по следующему `object_state`.
|
||||||
|
|
||||||
|
- [x] Написать тесты фаз на фиктивном backend: `approach` идёт над целью и сохраняет высоту; `grab` без предварительного подхода выполняет подход, осевое опускание, захват и подъём; явные `approach/lower/lift` не дублируют движение. После `release` сначала осевой подъём, только затем горизонтальный уход или поворот.
|
||||||
|
- [x] Написать тесты инструментов: `grab(vacuum)` стыкуется кончиком `patron` к нужной точке; `release(vacuum)` из `work` **не двигает робот и не отделяет инструмент**; только после явного `transfer(vacuum,vacuum_socket)` навыки `release(vacuum,vacuum_socket)` и `dock_tool(vacuum,vacuum_socket)` опускаются в гнездо, отделяют и поднимаются. Отсутствие/занятость гнезда даёт отказ. `grab(cap)` без присоски не начинается.
|
||||||
|
- [x] Написать тесты объектов: `transfer/align/release(cap,hatch_seat)` реально переносит и сажает люк; `dispense` движется вдоль заданного участка; `handoff_screw(screw_1,hole_1)` подносит винт и отпускает виртуальному человеку, не занимает отверстие установленным винтом. Неподдерживаемый тип цели отвергается.
|
||||||
|
- [x] Написать тесты `detect`: сначала осевой отвод и `work`, затем свежий верхний кадр + поза из supervisor; близкий кадр по настройке; просроченный кадр возвращает `failed`. `inspect_state` различает `empty`, `holding`, `placed`, `unknown`.
|
||||||
|
- [x] Реализовать навыки реестра из спецификации, используя общие геометрические примитивы. Каждый результат отображать в одно событие контракта; неизвестный исход не подменять успехом. Прогнать `pytest -q src/iiwa_llm_plan_executor/test/test_skills.py` и зафиксировать коммит.
|
||||||
|
|
||||||
|
### Задача 6. Движок графа и ROS узел
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/iiwa_llm_plan_executor/{engine,node}.py`, `test/test_engine.py`, `test/test_node.py`, `src/iiwa_msgs/srv/GetRunStatus.srv`; изменить `src/iiwa_msgs/CMakeLists.txt`; дополнить `docs/interfaces.md`.
|
||||||
|
|
||||||
|
**Интерфейсы:** `GraphEngine.submit(run_id: str, response_json: str, scene_revision: int) -> AdmissionResult`; `get_status(run_id: str) -> RunStatus`; `cancel(run_id: str) -> CancelResult`. ROS: `/iiwa_llm_plan_executor/submit` (`SetString`; JSON `{run_id, scene_revision, response}`), `/iiwa_llm_plan_executor/plan` (`String`; JSON `{run_id, scene_revision, response}`, исходный принятый граф без изменений, transient-local QoS, глубина 1), `/iiwa_llm_plan_executor/status` (`String`; события и итог), `/iiwa_llm_plan_executor/get_status` (`iiwa_msgs/srv/GetRunStatus`), `/iiwa_llm_plan_executor/cancel` (`SetString` с `run_id`).
|
||||||
|
|
||||||
|
- [x] Написать тесты: до готовности сцены запрос отклоняется; после задержки 3,0 с принимается; одновременно активен один `run_id`; повтор того же ID идемпотентен; новый ID при активном запуске отклоняется без второго движения. Принятый граф публикуется в `/iiwa_llm_plan_executor/plan` до первого движения, ровно один раз на `run_id`; отклонённый не публикуется; поздний подписчик получает последний принятый граф.
|
||||||
|
- [x] Написать тесты исполнения: каждое событие выбирает ровно одно ребро; `completed` завершается, `replan_required` даёт новый снимок, `operator_required` останавливается и уведомляет; `status=blocked` не отправляет движений; переходы и причина остановки видны в статусе.
|
||||||
|
- [x] Написать тесты отказов: потеря свежести сцены/суставов, неизвестный исход навыка, ошибка supervisor, столкновение, отмена и превышение предела шагов вызывают `stop`; удерживаемый инструмент/деталь не «исчезает» из статуса. Не продолжать по старой сцене после смены `scene_revision`, кроме изменений, сделанных и подтверждённых текущим графом.
|
||||||
|
- [x] Реализовать `GraphEngine` без ROS, затем сервис `GetRunStatus.srv`, ROS обёртку и машинно-читаемый журнал с `run_id`, ID узла, навыком, событием, следующей вершиной, снимком состояния и причиной терминала. Выполнение в отдельном рабочем потоке; колбэки статуса и отмены остаются доступными. Прогнать `pytest -q src/iiwa_llm_plan_executor/test/test_engine.py src/iiwa_llm_plan_executor/test/test_node.py`; зафиксировать коммит.
|
||||||
|
|
||||||
|
### Задача 7. HTTP и MCP через существующий сервер
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_web/iiwa_web/planner.py`, `src/iiwa_web/test/test_planner.py`; изменить `src/iiwa_web/iiwa_web/main.py`.
|
||||||
|
|
||||||
|
**Интерфейс:** `GET /planner/scene`, `POST /planner/runs` с `{run_id, scene_revision, response}`, `GET /planner/runs/{run_id}`, `POST /planner/runs/{run_id}/cancel`. Тот же маршрутизатор включить в `_schema_app` до `FastMCP.from_fastapi` и в основной `app`; MCP получает соответствующие инструменты без второго сервера.
|
||||||
|
|
||||||
|
- [x] Написать тесты HTTP: корректный JSON снимка, принятие графа без ожидания его завершения, чтение статуса, отмена, ответ при недоступном ROS сервисе, конфликт второго запуска. Проверить, что MCP строится из того же маршрутизатора.
|
||||||
|
- [x] Реализовать адаптер через существующий `get_bridge().call_service`; он не интерпретирует узлы и не держит собственный `scene_revision`. В `POST /stop` добавить отмену активного графа до штатной команды `cobot/stop`.
|
||||||
|
- [x] Прогнать `pytest -q src/iiwa_web/test/test_planner.py src/iiwa_web/test/test_trajectory_admission.py`; зафиксировать коммит.
|
||||||
|
|
||||||
|
### Задача 8. Совместный запуск и документация пользователя
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/launch/planner.launch.py`, `src/iiwa_llm_plan_executor/config/planner.yaml`, `src/iiwa_llm_plan_executor/test/test_launch.py`, `src/iiwa_llm_plan_executor/doc/README.md`; обновить `setup.py` пакета для установки launch/config/doc и при необходимости `src/iiwa_experement/doc/README.md`.
|
||||||
|
|
||||||
|
**Интерфейс запуска:** `ros2 launch iiwa_llm_plan_executor planner.launch.py experiment_id:=20`. Launch включает `iiwa_experement/experiment.launch.py mode:=planner experiment_id:=20` и узлы сцены/графа. Уже включённый в `iiwa_bringup` сервер `iiwa_web` запускается **один раз** по `web.enabled` из `base_setting` (в текущем `cobot-setting.yaml` это `true`); новый launch не создаёт второй сервер. Для запуска только с ROS передать настройки с `web.enabled: false`. Значения задержки, таймаутов и J7/наклона берутся из конфигурации.
|
||||||
|
|
||||||
|
- [x] Написать тесты launch: в новом режиме есть scene context и executor, нет `reference_runner` и второго экземпляра `iiwa_web`, `simulate:=true`; `experiment_id` 1..20, недопустимый ID и неизвестный режим проверяются до запуска Webots.
|
||||||
|
- [x] Реализовать launch с готовностью `scene_ready` и задержкой; описать команды запуска, получение `scene_json`, передачу графа через ROS и HTTP, чтение `/iiwa_llm_plan_executor/plan` и `/iiwa_llm_plan_executor/status`, отмену, события/терминалы и то, что Qwen3 вызывается снаружи.
|
||||||
|
- [x] Прогнать `pytest -q src/iiwa_llm_plan_executor/test/test_launch.py` и `colcon build --base-paths src --symlink-install --packages-up-to iiwa_experement iiwa_scene_context iiwa_llm_plan_executor iiwa_web`; зафиксировать коммит.
|
||||||
|
|
||||||
|
### Задача 9. Проверки всей цепочки в Webots
|
||||||
|
|
||||||
|
**Файлы:** создать `src/iiwa_llm_plan_executor/doc/webots-qa.md`; по результатам исправить только затронутые файлы и тесты задач 1–8.
|
||||||
|
|
||||||
|
- [x] `source /opt/ros/jazzy/setup.bash`, затем `colcon build --base-paths src --symlink-install`, `source install/setup.bash`, `colcon test --base-paths src --packages-select iiwa_experement iiwa_scene_context iiwa_llm_plan_executor iiwa_web`, `colcon test-result --verbose`. Сохранить конкретные результаты.
|
||||||
|
- [ ] Запустить **одну и ту же** сцену 20 и последовательно передавать разные короткие графы: только `detect(cap)`; `grab(vacuum) → transfer(vacuum,work) → completed` с инструментом в `patron`; отдельным следующим графом `transfer(vacuum,vacuum_socket) → dock_tool(vacuum,vacuum_socket)`; после восстановления сцены — захват и установка люка; отдельно один винт к `hole_1`. Никакой опыт не должен автоматически выполнять полный эталон.
|
||||||
|
- [x] Проверить отказ: показанный граф люка без явного взятия присоски отклоняется **до движения**; ошибка захвата ведёт к `inspect_state` или `replan_required`; ложный/несвежий `scene_revision`, отмена и столкновение останавливают дальнейшие движения.
|
||||||
|
- [ ] В журнале подтвердить верхний подход, осевой спуск и подъём, фактическое перемещение люка/винта, сохранение инструмента в `patron` после завершения первого графа, возврат в гнездо только по второму явному графу, J7 внутри коридора на **всех** точках, отсутствие столкновений. Записать границы проверки: это Webots, а не проверка физического робота или качества Qwen3.
|
||||||
|
- [ ] После исправлений повторить затронутые тесты и `git diff --check`. Зафиксировать итоговый отчёт и передать пользователю команды запуска и ссылки на интерфейсы.
|
||||||
|
|
||||||
|
## Критерий готовности
|
||||||
|
|
||||||
|
Другой процесс может запросить актуальный `scene_json` через ROS/HTTP/MCP, получить граф от Qwen3 вне этой системы, передать его в ROS/HTTP/MCP, прочитать принятый граф из `/iiwa_llm_plan_executor/plan` и наблюдать каждый узел, событие, состояние и терминал в `/iiwa_llm_plan_executor/status`. Робот в Webots исполняет только данный граф с ограничениями TCP/J7 и подтверждённым захватом; при невозможности безопасного выполнения останавливается и сообщает причину. Система сравнения модели с эталоном остаётся отдельным следующим этапом.
|
||||||
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Исполнение графов Qwen3 в Webots: согласованная архитектура
|
||||||
|
|
||||||
|
## Цель и границы
|
||||||
|
|
||||||
|
Новая система получает текущую сцену Webots, выдаёт `scene_json` для внешнего вызова Qwen3, принимает готовый JSON-граф модели и выполняет только его. Номер одного из 20 опытов задаёт **исходный набор объектов**, но не диктует цель или эталонную последовательность. Например, в сцене опыта 20 допустим отдельный запрос «возьми присоску, переместись в рабочую позицию, верни присоску»: исполнитель не должен самостоятельно наносить герметик или переносить люк. Вызов Qwen3, оценивание качества модели и инфраструктура сравнения с эталоном входят в последующий проект, а не в этот.
|
||||||
|
|
||||||
|
Создать два ROS 2 пакета: `iiwa_scene_context` (снимок сцены и `scene_json`) и `iiwa_llm_plan_executor` (проверка и исполнение графа). Минимальные изменения существующих `iiwa_experement` и `iiwa_web` допустимы для запуска мира и для API/MCP. Реальный робот пока не подключать. Будущая замена источника сцены и физических навыков должна быть возможна через адаптеры, без переписывания валидатора графа.
|
||||||
|
|
||||||
|
## Что обнаружено в проекте
|
||||||
|
|
||||||
|
`iiwa_experement` уже создаёт сцены, предоставляет `/iiwa_experement/scene_ready`, состояние объектов `/iiwa_experement/object_state`, команды захвата `/iiwa_experement/object_command`, измеренную геометрию, мост сцены MoveIt и эталонные операции. В `mode=view` supervisor работает только на чтение; `mode=execute` сразу запускает эталон. Для внешнего графа требуется третий `mode=planner`: supervisor с разрешёнными действиями, без эталонного runner, без TCP gizmo. Старые два режима сохраняют прежнюю семантику.
|
||||||
|
|
||||||
|
`iiwa_web` уже публикует FastAPI и получает MCP через `FastMCP.from_fastapi`. Для новых функций следует добавить тонкие маршруты в этот сервер, а не запускать второй HTTP/MCP сервер.
|
||||||
|
|
||||||
|
Приложенные `generated.train.jsonl` (4499 строк) и `generated.validation.jsonl` (500 строк) содержат общий процесс переноса в хранилище, без Grant-объектов и инструментов. Они используют в ответах `pick`, `move_to_storage`, `inspect`. Текущий `planner_dataset/assets/response-format.json` описывает другой список навыков, а показанный пользователем граф добавляет `align`. Поэтому исполнитель не должен слепо импортировать какую-либо одну из этих схем; нужна собственная версия контракта, совместимая с опубликованными `available_skills` и с оговорёнными алиасами.
|
||||||
|
|
||||||
|
## Подход к исполнению
|
||||||
|
|
||||||
|
Выбран **граф + реестр навыков**. Движения, неизбежные внутри одного навыка, задаются его проверяемым контрактом: например, `grab` может сам подойти сверху, опуститься, захватить и подняться до сохранённой высоты, если явных узлов `approach`/`lower`/`lift` не было. Если они уже выполнялись, повторного движения нет. Это геометрические фазы **того же** действия, а не добавление нового технологического задания.
|
||||||
|
|
||||||
|
Исполнитель не придумывает следующую деталь, нужный инструмент или цель из номера эксперимента. Если `grab(cap)` требует вакуумный инструмент, а в `patron` его нет, граф должен явно содержать взятие инструмента; иначе предзапусковая проверка возвращает `replan_required` без движения. `release` инструмента **не возвращает его автоматически**. Для возврата граф должен явно содержать перемещение к гнезду `vacuum_socket` и затем `release(vacuum, vacuum_socket)` либо `dock_tool(vacuum, vacuum_socket)`; оба последних навыка только выполняют стыковку и отделение в уже достигнутом гнезде. `release(vacuum)` из `work` без подходящего места для инструмента отклоняется, присоска остаётся в `patron`. Если граф заканчивается в `work` с присоской в `patron`, она остаётся там до следующего явного задания. Новый процесс может подключить другой адаптер и свои контракты; неизвестный навык или объект не исполняется.
|
||||||
|
|
||||||
|
Рассмотренный, но отвергнутый подход — переписывать граф модели и автоматически вставлять недостающие технологические действия. Это могло бы выполнить лишние операции при произвольных запросах. Второй отвергнутый подход — напрямую переводить ID навыка в фиксированную траекторию без проверки текущей позы и состояния; он не учитывает смещения объектов и ветви событий.
|
||||||
|
|
||||||
|
## Снимок сцены и интерфейсы
|
||||||
|
|
||||||
|
`iiwa_scene_context` собирает **текущий** состав и логическое состояние из `ScenarioSpec` и живого supervisor, а не из эталонного маршрута. Внешний `scene_json` сохраняет поля, ожидаемые Qwen3: `domain`, `components`, `destinations`, `tools`, `initial_state`, `cameras`, `available_skills`, `skill_contracts`, `service_objects`, `recovery_attempts_per_component`. ID соответствуют Webots: `cap`, `vacuum`, `vacuum_socket`, `screw_1`…`screw_10`, `hatch_seat`, `hole_1`…`hole_4`, `work` и т. д. Гнездо каждого присутствующего инструмента публикуется как отдельная именованная цель. Список зависит от реально присутствующих объектов. Абсолютные координаты в модельный `scene_json` не передаются: навык `detect` получает актуальную позу при исполнении.
|
||||||
|
|
||||||
|
Транспортный конверт содержит `experiment_id`, `scene_revision`, время снимка, версию состояния supervisor и сам объект `scene_json`. `scene_revision` меняется при дискретном изменении сцены или владения объектом, но не на каждом кадре движения удерживаемой детали. Версия графа передаётся отдельно; исполнитель отвергает граф, построенный по устаревшей сцене. Снимок доступен как ROS 2 сервис `/iiwa_scene_context/get_scene` (ответ с JSON), ROS 2 топик `/iiwa_scene_context/scene_json` с transient-local QoS, HTTP `GET /planner/scene` и одноимённый инструмент MCP через существующий `iiwa_web`. Контракты навыков берутся из серверного реестра, а не из текста, который прислала модель.
|
||||||
|
|
||||||
|
## Приём и переходы графа
|
||||||
|
|
||||||
|
`iiwa_llm_plan_executor` получает готовый ответ Qwen3, `scene_revision` и `run_id` через ROS 2 сервис `/iiwa_llm_plan_executor/submit`; HTTP `POST /planner/runs` и MCP передают то же через адаптер `iiwa_web`. Модель в этой версии вызывается **вне** проекта. После успешной проверки исполнитель публикует принятый граф без изменений в ROS 2 топик `/iiwa_llm_plan_executor/plan` (`std_msgs/String`, JSON-конверт с `run_id`, `scene_revision` и исходным `response`, QoS transient-local, глубина 1), прежде чем выполнять первый узел. Отклонённый граф в этом топике не появляется; причина отказа доступна в ответе приёма и статусе. Выполнение асинхронно, не более одного активного графа на робота. Отдельный топик `/iiwa_llm_plan_executor/status` сообщает текущий узел, полученное событие, выбранное ребро и итоговый терминал; HTTP и MCP дают те же сведения. Отмена останавливает движение и сохраняет состояние захвата. Для запроса статуса **по `run_id`** добавить сервис `GetRunStatus.srv` в существующий `iiwa_msgs` (`string run_id` в запросе, `bool found` и `string status_json` в ответе); стандартный `Trigger` не имеет поля запроса.
|
||||||
|
|
||||||
|
Предзапусковая проверка: строгий JSON без лишних исполняемых полей; `status=blocked` не запускает робот; уникальные ID, известный `entry`, только разрешённые навыки/объекты/цели/события/терминалы, все ссылки существуют, достижимость узлов, предел размера/числа шагов, безопасные ветви для отказа и столкновения, допустимые условия владения инструментом и деталью. Каждый результат навыка выбирает **ровно одно** ребро графа. Терминалы: `completed`, `replan_required`, `operator_required`. На `replan_required` система останавливается и отдаёт свежий `scene_json`; повторный вызов Qwen3 делает внешний клиент. `completed` означает выполнение переданного графа и его зарегистрированных постусловий, а не доказательство, что модель правильно поняла произвольный русский запрос.
|
||||||
|
|
||||||
|
## Семантика движения и наблюдения
|
||||||
|
|
||||||
|
При `detect` робот сначала отходит от контакта по оси к сохранённой высоте, затем переходит в `work`. Ждёт свежий кадр `/d455_top/image_raw`, берёт точную позу из supervisor и возвращает `found`/`not_found`/`failed`. Для объекта, требующего близкого контроля, конфигурация навыка задаёт безопасную точку обзора и повторный кадр с `/camera/color` или `/camera/depth`; поза всё ещё берётся из supervisor. Задача компьютерного зрения по RGB/D в этот этап не входит.
|
||||||
|
|
||||||
|
`approach` ставит рабочий кончик над объектом и сохраняет позу/высоту подхода. `lower` движется вдоль оси к контакту. `grab` при необходимости выполняет недостающие фазы подхода и опускания, подтверждает захват через supervisor и поднимается **к сохранённой позе подхода**. Явный `lift` после этого подтверждает достигнутую высоту и не дублирует подъём. `transfer` идёт к указанной цели на безопасной высоте, `align` меняет ориентацию только на высоте. `release` опускается в указанном безопасном месте установки/передачи, отпускает и поднимается к сохранённой высоте; для инструмента разрешено только его именованное гнездо. `dock_tool` выполняет ту же локальную стыковку инструмента в гнезде без скрытого переноса к стойке. `retreat` не выполняет горизонтальное перемещение до осевого отвода. Для винта отпускание у отверстия означает передачу виртуальному человеку, без вкручивания.
|
||||||
|
|
||||||
|
Рабочий кончик направлен вниз. Предпочтительная ось 7 — значение в `work` (сейчас 0 рад); начальный жёсткий коридор ±90° и максимальный наклон рабочего инструмента 10° задаются в конфигурации и уточняются проверкой достижимости. Перед каждым планом используются текущие суставы как IK seed, ограничения MoveIt на `joint7` и ориентацию, а перед исполнением — проверка каждой точки траектории, столкновений и скачков суставов. При недостижимости в этом коридоре исполнитель сообщает отказ; не выбирает ветвь IK с поворотом примерно на −180°. Поворот кисти или возврат в `work` допустим только после осевого подъёма. На каждом переходе между узлами проверяется свежесть состояния сцены и робота.
|
||||||
|
|
||||||
|
Для этого `iiwa_llm_plan_executor` нужен собственный адаптер планирования на установленном MoveItPy: текущий `MoveToPose.action` не передаёт ограничение `joint7`. Он повторно использует геометрию и обновление сцены из `iiwa_experement`, но не запускает эталонную последовательность. Физический слой остаётся сменным для будущей адаптации к реальному роботу.
|
||||||
|
|
||||||
|
## Поддерживаемые навыки первого этапа
|
||||||
|
|
||||||
|
Реестр поддерживает имена из показанного графа и необходимые действия с текущими объектами Grant: `detect`, `approach`, `lower`, `grab`, `lift`, `transfer`, `align`, `release`, `retreat`, `capture`, `verify`, `stop`, `wait_scene_clear`, `inspect_state`, `notify_operator`, `dispense`, `dock_tool`, `handoff_screw`. Для графов из обучающего набора описать и проверить совместимые алиасы `pick`, `move_to_storage`, `inspect`; их запуск разрешать лишь при поддерживаемом типе объекта и места назначения в текущей сцене. Любой другой навык (включая `tighten`, если у отверстия нет установленного винта) получает отказ до движения. Каталог можно расширять новым обработчиком и контрактом, без изменения движка графа.
|
||||||
|
|
||||||
|
## Проверки и результат этапа
|
||||||
|
|
||||||
|
Пакетные тесты проверяют формат снимка, контракт навыков, валидацию графа и переходы, физические фазы на поддельном backend, ограничения суставов и API/MCP адаптеры. Минимальные пробы Webots должны подтвердить произвольные короткие запросы в одной и той же сцене: взять и вернуть присоску; обнаружить люк; взять люк и установить; выбрать один из десяти винтов и передать у указанного отверстия; остановка и перепланирование после отказа. Это проверка работоспособности двух пакетов, **не** система оценивания Qwen3 и не сравнение 20 экспериментов с эталоном.
|
||||||
Reference in New Issue
Block a user