337 lines
21 KiB
Markdown
337 lines
21 KiB
Markdown
# AI-RPG — Text RPG with an LLM Game Master
|
||
|
||
> **Version:** 1.0.0
|
||
> **Stack:** Python 3.12 · FastAPI · SQLAlchemy 2.x async · PostgreSQL 15 · Qdrant 1.9 · React 18 · Vite 5 · TypeScript 5 · Tailwind CSS 3 · zustand 4 · react-i18next 14
|
||
|
||
AI-RPG — это веб-приложение, в котором игрок ведёт текстовую ролевую игру с ИИ-мастером (GM). Игрок создаёт мир (или выбирает готовый пресет), настраивает персонажа, и далее вступает в пошаговое взаимодействие: каждое действие игрока обрабатывается трёхфазным orchestrator-ом, который генерирует нарратив, обновляет состояние мира через tool calls, и предлагает 1-3 следующих действия.
|
||
|
||
Полное ТЗ — в `docs/AI-RPG_TZ_TDD.md`.
|
||
|
||
---
|
||
|
||
## Возможности (v1.0.0)
|
||
|
||
- ✅ **JWT-аутентификация** — регистрация обычных пользователей и первого админа по токену.
|
||
- ✅ **Админ-панель** — настройки (LLM, embeddings, Qdrant, UI, game), логи LLM-вызовов с фильтрами, список пользователей, статистика, диагностические кнопки (test LLM / test LLM tools / test embeddings / probe dimension / recreate collections), загрузка favicon/logo/OG-image.
|
||
- ✅ **Пресеты миров** — 2 встроенных (Classic Fantasy, Deep Space Outpost) + создание/редактирование своих.
|
||
- ✅ **World Builder** — генерация нового мира из пресета или формы через SSE-стрим: schemas → environment → entities → intro scene.
|
||
- ✅ **World Editor** — чат-инструция для LLM, `propose_changes` с diff, `ask_user` для уточнений, optimistic locking.
|
||
- ✅ **Orchestrator (3 фазы)**:
|
||
- **Phase 1**: planner+executor — цикл tool-calls до `submit_plan`.
|
||
- **Phase 2**: writer — single LLM call с `submit_step`, стриминг `scene_chunk`.
|
||
- **Phase 3**: persist + deferred triggers + summary (если история длинная) + suggest actions.
|
||
- ✅ **RAG через Qdrant** — `rag_query` / `rag_add`, двухстадийный retrieval (Qdrant → PostgreSQL), изоляция миров через payload-фильтр, фоновая индексация (через `embedding_status`).
|
||
- ✅ **Embeddings** — `HashEmbedder` (offline, для dev) и `OpenAIEmbedder` (OpenAI-compatible API), авто-fallback `embeddings.api_url` → `llm.api_url`.
|
||
- ✅ **Игровые инструменты** (17 шт.): `entity_create/get/list/update/delete`, `env_update/get`, `update_plot_rails`, `advance_time`, `schedule_trigger`, `calc` (с кубиками), `random_choice` (детерминированный), `rag_query/add`, `run_subagent`, `submit_plan/step`, `suggest_actions`.
|
||
- ✅ **Schema tools** — `schema_add_type/add_field/remove_field/modify_field` для world_editor.
|
||
- ✅ **Контекстный менеджер** — последние N сообщений + summary при превышении порога, деградация recent → rag → summary.
|
||
- ✅ **SSE-стриминг** — все долгие операции (world_builder, world_editor, orchestrator) отдают прогресс через SSE с `event:`/`data:`/`id:`, heartbeat, reconnect через `Last-Event-ID`.
|
||
- ✅ **Фронтенд** — React+TS+Vite+Tailwind, тёмная тема, i18n (en/ru), мобильный responsive, SSE-клиент с автопереподключением.
|
||
|
||
---
|
||
|
||
## Архитектура
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Браузер (React 18 + Vite + TS + Tailwind + zustand) │
|
||
└──────────────────────────┬───────────────────────────────────┘
|
||
│ HTTP / SSE
|
||
┌──────────────────────────▼───────────────────────────────────┐
|
||
│ FastAPI Backend (uvicorn) │
|
||
│ ├─ app/api/ — роутеры (auth, worlds, sessions, ...) │
|
||
│ ├─ app/engine/ — game_master, world_builder, editor │
|
||
│ │ └─ tools/ — ToolRegistry + 17 game tools │
|
||
│ ├─ app/core/ — llm, rag, embeddings, security, ... │
|
||
│ ├─ app/models/ — SQLAlchemy ORM │
|
||
│ ├─ app/prompts/ — системные промпты (en) │
|
||
│ └─ app/schemas/ — Pydantic request/response │
|
||
└──────┬─────────────────────────────────┬─────────────────────┘
|
||
│ async SQLAlchemy │ httpx + qdrant-client
|
||
┌──────▼──────────────┐ ┌───────▼──────────────────────┐
|
||
│ PostgreSQL 15 │ │ Qdrant 1.9 (векторный индекс)│
|
||
│ (users, worlds, │ │ collections: entities, │
|
||
│ entities, steps, │ │ story_entries │
|
||
│ logs, ...) │ └──────────────────────────────┘
|
||
└─────────────────────┘ ▲
|
||
│ httpx (embeddings API)
|
||
┌────────┴─────────────┐
|
||
│ LLM Provider (any │
|
||
│ OpenAI-compatible) │
|
||
└──────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Быстрый старт
|
||
|
||
### Опция 1: docker-compose (рекомендуется)
|
||
|
||
```bash
|
||
# 1. Скопировать .env.example в .env и отредактировать
|
||
cp .env.example .env
|
||
# Отредактируйте SECRET_KEY, ADMIN_SETUP_TOKEN, LLM_API_URL, LLM_API_KEY
|
||
|
||
# 2. Поднять всё
|
||
docker compose up -d --build
|
||
|
||
# 3. Открыть в браузере:
|
||
# - Frontend (nginx + React build): http://localhost:8080
|
||
# - Backend Swagger docs: http://localhost:8000/api/docs
|
||
# - Health-check: http://localhost:8000/api/health
|
||
```
|
||
|
||
При первом старте backend напечатает в лог:
|
||
```
|
||
=== AI-RPG Admin Setup ===
|
||
No admin user yet. Open this URL in your browser:
|
||
/register/admin?token=<ADMIN_SETUP_TOKEN>
|
||
===========================
|
||
```
|
||
|
||
Посмотреть лог: `docker compose logs backend | head -20`.
|
||
|
||
Откройте `http://localhost:8080/register/admin?token=<...>` и создайте первого админа.
|
||
|
||
> **Примечание про LLM:** если у вас Ollama на хосте, используйте `LLM_API_URL=http://host.docker.internal:11434/v1` — backend-контейнер автоматически резолвит `host.docker.internal` через `extra_hosts: host-gateway` (работает на Linux/macOS/Windows).
|
||
|
||
### Опция 2: локальный dev (backend + frontend раздельно)
|
||
|
||
```bash
|
||
# Backend
|
||
cd /path/to/ai-rpg
|
||
python -m venv .venv && source .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
|
||
# Запустить PostgreSQL и Qdrant (через docker compose up db qdrant)
|
||
docker compose up -d db qdrant
|
||
|
||
# Применить миграции (создаёт все таблицы)
|
||
python -c "import asyncio; from app.db import get_engine; from app.migrations.versions._001_initial_schema import create_all_tables; asyncio.run(create_all_tables(get_engine()))"
|
||
|
||
# Запустить backend
|
||
uvicorn app.main:app --reload --port 8000
|
||
|
||
# В другом терминале — frontend
|
||
cd frontend
|
||
npm install
|
||
npm run dev
|
||
# Откроется http://localhost:5173
|
||
```
|
||
|
||
---
|
||
|
||
## Конфигурация LLM
|
||
|
||
AI-RPG работает с **любым OpenAI-compatible API**:
|
||
|
||
| Provider | `LLM_API_URL` | Пример `LLM_MODEL` |
|
||
|-----------------|-------------------------------------|------------------------------|
|
||
| OpenAI | `https://api.openai.com/v1` | `gpt-4o-mini` |
|
||
| Ollama (local) | `http://localhost:11434/v1` | `qwen2.5:7b-instruct` |
|
||
| LM Studio | `http://localhost:1234/v1` | `local-model` |
|
||
| vLLM | `http://localhost:8000/v1` | `Qwen/Qwen2.5-7B-Instruct` |
|
||
| OpenRouter | `https://openrouter.ai/api/v1` | `qwen/qwen-2.5-7b-instruct` |
|
||
|
||
Если `LLM_API_URL` пуст — backend использует `MockLlmClient` (возвращает записанные replay-ответы). Это удобно для dev и тестов.
|
||
|
||
### Embeddings
|
||
|
||
Два провайдера:
|
||
- `offline_hash` (по умолчанию) — `HashEmbedder`, детерминированный bag-of-words + hash projection. Не делает HTTP-запросов, работает offline. Размерность 256.
|
||
- `openai` — OpenAI-compatible embeddings API. URL/key fallback на `llm.api_url`/`llm.api_key`, если `embeddings.api_url`/`embeddings.api_key` пустые.
|
||
|
||
Кнопка «Авто-проба размерности» в админке (`POST /api/admin/test/embeddings/probe-dimension`) определяет реальную размерность модели и предлагает сохранить её в `embeddings.dimension`.
|
||
|
||
---
|
||
|
||
## Структура проекта
|
||
|
||
```
|
||
ai-rpg/
|
||
├── app/ # Backend (Python 3.12)
|
||
│ ├── api/ # FastAPI роутеры
|
||
│ │ ├── auth.py # /api/register, /api/auth/*
|
||
│ │ ├── worlds.py # /api/worlds
|
||
│ │ ├── sessions.py # /api/sessions/* (SSE streams)
|
||
│ │ ├── presets.py # /api/presets
|
||
│ │ ├── admin.py # /api/admin/* (settings, logs, test, upload)
|
||
│ │ └── misc.py # /api/health, /api/i18n
|
||
│ ├── core/ # Сквозные сервисы
|
||
│ │ ├── llm.py # LlmClient + MockLlmClient
|
||
│ │ ├── rag.py # RAG через Qdrant + PostgreSQL
|
||
│ │ ├── embeddings.py # HashEmbedder, OpenAIEmbedder
|
||
│ │ ├── qdrant_client.py # singleton AsyncQdrantClient
|
||
│ │ ├── security.py # JWT + bcrypt
|
||
│ │ ├── settings_service.py # settings table CRUD + seed
|
||
│ │ ├── state_validator.py # validate_state, apply_patch
|
||
│ │ ├── time_utils.py # GameTime, parse_delta, advance_time
|
||
│ │ └── logging.py # structlog setup
|
||
│ ├── engine/ # Игровой движок
|
||
│ │ ├── game_master.py # orchestrator (3 фазы)
|
||
│ │ ├── world_builder.py # flow создания мира
|
||
│ │ ├── world_editor.py # flow редактирования мира
|
||
│ │ ├── context.py # контекстный менеджер
|
||
│ │ ├── sse.py # SseEmitter
|
||
│ │ └── tools/
|
||
│ │ ├── base.py # Tool, ToolRegistry, ToolContext, ToolResult
|
||
│ │ ├── game.py # 17 игровых инструментов
|
||
│ │ ├── schema_tools.py # 4 schema-инструмента
|
||
│ │ └── register_all.py # build_default_registry()
|
||
│ ├── models/ # SQLAlchemy ORM (10 таблиц)
|
||
│ ├── prompts/
|
||
│ │ ├── registry.py # get_prompt(stage, language)
|
||
│ │ └── stages/ # 11 stage-промптов (en)
|
||
│ ├── schemas/ # Pydantic request/response
|
||
│ ├── migrations/
|
||
│ │ ├── init_db.py # init_db(session)
|
||
│ │ ├── init_qdrant.py # re-export init_qdrant_collections
|
||
│ │ ├── seed.py # builtin presets (fantasy, sci-fi)
|
||
│ │ └── versions/_001_initial_schema.py
|
||
│ ├── config.py # Settings (pydantic-settings)
|
||
│ ├── db.py # async engine + session factory
|
||
│ └── main.py # FastAPI app + lifespan
|
||
├── frontend/ # Frontend (React 18 + Vite + TS)
|
||
│ ├── src/
|
||
│ │ ├── pages/ # 8 pages
|
||
│ │ ├── components/
|
||
│ │ │ ├── ui/ # 9 primitives (Button, Card, Modal, ...)
|
||
│ │ │ ├── sessions/ # Chat, ToolCallBubble, ActionInput, ...
|
||
│ │ │ ├── worlds/ # WorldCard, WorldBuilder, WorldEditor
|
||
│ │ │ └── admin/ # SettingsPanel, LlmLogsTable, ...
|
||
│ │ ├── stores/ # zustand (auth, ui, worlds, session, toast)
|
||
│ │ ├── lib/ # api.ts, sse.ts, cn.ts
|
||
│ │ ├── i18n/ # en.json, ru.json
|
||
│ │ └── types/index.ts
|
||
│ ├── package.json
|
||
│ ├── vite.config.ts
|
||
│ ├── tsconfig.json
|
||
│ └── tailwind.config.js
|
||
├── tests/ # pytest
|
||
│ ├── unit/ # 65+ unit-тестов
|
||
│ └── integration/ # (placeholder)
|
||
├── deploy/
|
||
│ ├── Dockerfile.backend
|
||
│ ├── Dockerfile.frontend
|
||
│ └── nginx.conf
|
||
├── docs/
|
||
│ └── AI-RPG_TZ_TDD.md # оригинальное ТЗ
|
||
├── docker-compose.yml
|
||
├── requirements.txt
|
||
├── pytest.ini
|
||
├── .env.example
|
||
└── README.md
|
||
```
|
||
|
||
---
|
||
|
||
## API обзор
|
||
|
||
Полная OpenAPI-схема — на `http://localhost:8000/api/docs` (Swagger UI).
|
||
|
||
### Ключевые эндпоинты
|
||
|
||
| Метод | Путь | Назначение |
|
||
|---------|---------------------------------------------------|-----------------------------------------|
|
||
| `POST` | `/api/register` | Регистрация пользователя |
|
||
| `POST` | `/api/register/admin?token=` | Регистрация первого админа |
|
||
| `POST` | `/api/auth/login` | Логин по email/username → JWT |
|
||
| `GET` | `/api/auth/me` | Текущий профиль |
|
||
| `GET` | `/api/worlds` | Список миров пользователя |
|
||
| `POST` | `/api/worlds` | Создать мир → SSE URL для world_builder |
|
||
| `GET` | `/api/sessions/worlds/{id}/state` | Текущее состояние для play-страницы |
|
||
| `POST` | `/api/sessions/worlds/{id}/iterate` | Запустить orchestrator → SSE URL |
|
||
| `GET` | `/api/sessions/worlds/{id}/iterate/stream` | SSE-стрим итерации |
|
||
| `POST` | `/api/sessions/worlds/{id}/retry` | Повторить последний шаг |
|
||
| `POST` | `/api/sessions/worlds/{id}/rollback` | Откатить последний шаг |
|
||
| `GET` | `/api/admin/settings` | Все настройки (секреты замаскированы) |
|
||
| `PATCH` | `/api/admin/settings` | Обновить настройки |
|
||
| `GET` | `/api/admin/llm-logs?stage=&status_filter=&...` | Логи LLM-вызовов с фильтрами |
|
||
| `POST` | `/api/admin/test/llm?api_url=&api_key=&model=` | Проверка связности LLM |
|
||
| `POST` | `/api/admin/test/embeddings/probe-dimension` | Авто-проба размерности эмбеддингов |
|
||
| `POST` | `/api/admin/upload-icon` | Загрузить favicon/logo/og_image |
|
||
| `GET` | `/api/health` | Health-check (db, qdrant, llm, emb) |
|
||
|
||
### SSE-события (orchestrator)
|
||
|
||
| Event | Когда |
|
||
|----------------------|--------------------------------------------------|
|
||
| `phase_start` | Начало Phase 1/2/3 |
|
||
| `phase_end` | Конец фазы |
|
||
| `tool_call` | LLM вызвала инструмент |
|
||
| `llm_call_start/end` | Начало/конец LLM-вызова |
|
||
| `scene_chunk` | Streaming-чанк текста из Phase 2 |
|
||
| `scene_complete` | Полный текст сцены + delta_time |
|
||
| `suggested_actions` | 1-3 следующих действия |
|
||
| `trigger_fired` | Сработал отложенный триггер |
|
||
| `summary_generated` | Сгенерирован summary |
|
||
| `iteration_complete` | Полное завершение итерации |
|
||
| `done` | Успешное завершение стрима |
|
||
| `error` | Фатальная ошибка, стрим закрывается |
|
||
|
||
---
|
||
|
||
## Тестирование
|
||
|
||
```bash
|
||
# Backend unit-тесты
|
||
pytest tests/unit/ -v
|
||
|
||
# С покрытием
|
||
pytest --cov=app --cov-report=term-missing tests/
|
||
|
||
# Frontend
|
||
cd frontend
|
||
npm run typecheck # tsc --noEmit
|
||
npm run lint # ESLint
|
||
npm run test # vitest
|
||
npm run build # production build
|
||
```
|
||
|
||
Покрытие unit-тестами:
|
||
- `app.core.time_utils` — парсинг/advance времени, дельты
|
||
- `app.core.security` — JWT, bcrypt, валидация пароля
|
||
- `app.core.state_validator` — валидация state/world, apply_patch (set/inc/dec/append/remove)
|
||
- `app.core.embeddings.HashEmbedder` — детерминизм, нормализация, размерность
|
||
- `app.core.llm.MockLlmClient` — replay, исчерпание, запись вызовов, стриминг
|
||
- `app.engine.tools.base.ToolRegistry` — регистрация, диспетч, unknown tool, исключения
|
||
- `app.engine.tools.game.CalcTool` — арифметика, кубики, переменные, ошибки
|
||
- `app.engine.tools.game.RandomChoiceTool` — детерминизм, веса
|
||
- `app.engine.sse.SseEmitter` — emit/done/error, ID, сериализация
|
||
- `app.prompts.registry` — все 11 stage-промптов валидны
|
||
|
||
---
|
||
|
||
## Production-деплой
|
||
|
||
См. `docs/AI-RPG_TZ_TDD.md` §13 (DevOps / Deployment) и §18.3 (Pre-deploy checklist).
|
||
|
||
Ключевые моменты:
|
||
1. **SECRET_KEY** — сгенерировать через `python -c "import secrets; print(secrets.token_urlsafe(48))"`.
|
||
2. **ADMIN_SETUP_TOKEN** — задать в `.env` ИЛИ оставить пустым (тогда сгенерируется случайно при первом старте, напечатается в логах).
|
||
3. **HTTPS** — terminate TLS на nginx или на внешнем reverse-proxy.
|
||
4. **Backups** — ежедневно `pg_dump` + Qdrant snapshot в S3.
|
||
5. **Health-check** — `GET /api/health` должен вернуть 200 с `db:true, qdrant:true`.
|
||
6. **Мониторинг** — structlog пишет JSON в stdout, забирается любой log-агрегатор.
|
||
|
||
---
|
||
|
||
## Лицензия
|
||
|
||
MIT — см. `LICENSE` (если отсутствует, предполагается MIT).
|
||
|
||
---
|
||
|
||
## Changelog
|
||
|
||
### v1.0.0 (2026-06-20)
|
||
- Первый release. Реализованы все 8 спринтов из ТЗ:
|
||
- Sprint 1: Foundation (docker-compose, БД, Qdrant, миграции, seed, health-check)
|
||
- Sprint 2: Auth + Admin (JWT, регистрация, админ-панель настроек, тесты LLM/embeddings)
|
||
- Sprint 3: World Builder (LLM-клиент, промпты, SSE-стрим генерации мира)
|
||
- Sprint 4: World Editor (чат-редактор, propose_changes, ask_user, optimistic lock)
|
||
- Sprint 5: Orchestrator (3 фазы, retry/rollback)
|
||
- Sprint 6: Frontend polish (i18n en/ru, тёмная тема, responsive, SSE reconnect)
|
||
- Sprint 7: RAG + Triggers + Summary + Context Manager
|
||
- Sprint 8: Production (README, метрики, тесты, сборка ZIP)
|