370 lines
17 KiB
Markdown
370 lines
17 KiB
Markdown
# RPG-режим: полный алгоритм запросов (шпаргалка)
|
||
|
||
Документ описывает, **какой HTTP-запрос что получает и что делает**, когда RPG включён полностью (`rpg_enabled = 1`) и в настройках активны все опции мастера:
|
||
|
||
```json
|
||
{
|
||
"dice": true,
|
||
"narrator": true,
|
||
"quests": true,
|
||
"affinity": true,
|
||
"choices": true,
|
||
"stats": false
|
||
}
|
||
```
|
||
|
||
> **Шкалы** (`lust` / `stamina` / `tension`): по умолчанию `stats: false`. Если включить чекбокс «Шкалы», после каждого хода добавляется ветка `stats_delta` (см. § «Вариант: stats включены»).
|
||
|
||
Пример проходит **от создания чата до 3-го сообщения игрока**, с перекрытием основных веток (диалог / d20 / кнопка выбора / сдвиг сюжета / факты / SD).
|
||
|
||
---
|
||
|
||
## Модели и роли
|
||
|
||
| Роль | Env / модель по умолчанию | Где вызывается |
|
||
|------|---------------------------|----------------|
|
||
| **Персонаж (чат)** | `CHAT_MODEL` → `mistralai/mistral-nemo` | `POST /chat/stream` → `stream_message()` |
|
||
| **Нарратор pre/post** | `RPG_NARRATOR_MODEL` → `deepseek/deepseek-chat-v3` | Перед/после ответа персонажа |
|
||
| **Сюжетная арка** | `RPG_PLOT_MODEL` → DeepSeek v3 | Opening, fallback если арки нет |
|
||
| **Факты** | `RPG_FACTS_MODEL` → DeepSeek v3 | После каждого ответа персонажа |
|
||
| **SD-сцена** | `SD_PROMPT_MODEL` или `SYSTEM_MODEL` | Opening + конец каждого stream |
|
||
| **Появление (prose)** | `SYSTEM_MODEL` | Только при сохранении тегов персоны (не в ходе RPG) |
|
||
|
||
Персонаж **не пишет** affinity/stats в БД — только читает их из `runtime_suffix`. Пишет **только narrator post** (`affinity_delta`, `stats_delta`, `scene_update`, …).
|
||
|
||
---
|
||
|
||
## Слои промпта (что видит CHAT_MODEL)
|
||
|
||
```
|
||
llm_system = static_prompt + runtime_suffix [+ context_warning]
|
||
```
|
||
|
||
| Слой | Источник | Когда меняется |
|
||
|------|----------|----------------|
|
||
| **static_prompt** | `get_system_prompt()` — persona + lorebook (последние 5 реплик + текущий user) + `ROLEPLAY_GUARDRAILS` | Каждый stream; в `messages` хранится **только** static (без RPG) |
|
||
| **facts_block** | До 20 фактов из `facts_json` | После `extract_facts` на прошлом ходу |
|
||
| **PlotArc** | `title`, `phase`, `next_beat_hint` | При генерации/сдвиге арки |
|
||
| **Status quo** | `sessions.status_quo` | narrator pre/post, opening |
|
||
| **Scene** | `sessions.scene_json` | narrator pre/post, opening |
|
||
| **Relationship** | `affinity` + тон | narrator post (`affinity_delta`) |
|
||
| **Character state** | lust/stamina/tension | Только если `stats: true` |
|
||
| **Narrator directives** | narrator **pre** (текущий ход) | Эфемерно, не в БД |
|
||
| **Mechanics** | d20 + outcome (текущий ход) | Только если была проверка |
|
||
| **Context warning** | `~N% of CHAT_CONTEXT_MAX` | Если оценка > 85% |
|
||
|
||
История в LLM: **все** `user` / `assistant` из БД + один объединённый `system`.
|
||
|
||
---
|
||
|
||
## Фаза 0: создание RPG-чата (UI → API)
|
||
|
||
### 0.1 `PATCH /sessions/{id}`
|
||
|
||
**Тело (пример):**
|
||
```json
|
||
{
|
||
"persona_id": "card_abc",
|
||
"rpg_enabled": true,
|
||
"genre": "fantasy",
|
||
"rpg_settings_json": "{\"dice\":true,\"narrator\":true,...}"
|
||
}
|
||
```
|
||
|
||
**Действия:** запись в `sessions` — флаги RPG, жанр, настройки. Сообщений ещё нет.
|
||
|
||
### 0.2 `POST /chat/init`
|
||
|
||
**Тело:** `{ "session_id", "persona_id", "first_mes_override"? }`
|
||
|
||
**Действия:**
|
||
1. `get_system_prompt(persona, [], "")` → static (персона, lorebook, guardrails).
|
||
2. `upsert_static_system_message` — одна строка `role=system` в `messages`.
|
||
3. Если истории нет — `add_message(assistant, first_mes)` из карты / override.
|
||
|
||
**LLM:** не вызывается.
|
||
|
||
**БД после:** `messages = [system, assistant(first_mes)]`.
|
||
|
||
### 0.3 `POST /chat/opening/process`
|
||
|
||
**Тело:** `{ "session_id", "persona_id", "rpg": true }`
|
||
|
||
Последовательность **только на opening** (без `narrator_pre`, без stream персонажа):
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant UI
|
||
participant API as opening/process
|
||
participant Plot as RPG_PLOT_MODEL
|
||
participant Nar as RPG_NARRATOR_MODEL
|
||
participant SD as SD_PROMPT_MODEL
|
||
|
||
UI->>API: POST opening/process
|
||
API->>Plot: generate_plot_arc(first_mes, persona, genre)
|
||
Plot-->>API: plot_arc_json
|
||
API->>API: seed rpg_quests from beats
|
||
API->>Nar: narrator_post OPENING
|
||
Nar-->>API: status_quo, scene, affinity_delta, outfit, choices, quests
|
||
API->>API: apply_narrator_post → sessions
|
||
API->>SD: generate_sd_prompt + run_sd_for_message
|
||
API-->>UI: plot_arc, quests, affinity, choices, image_*
|
||
```
|
||
|
||
| Шаг | Модель | Вход | Выход в БД / UI |
|
||
|-----|--------|------|-----------------|
|
||
| 1 | `generate_plot_arc` | имя, description, scenario, **first_mes**, facts, genre | `plot_arc_json`; квесты из `beats[].title` |
|
||
| 2 | `narrator_post(is_opening=true)` | контекст: `assistant: {first_mes}`; arc; facts | `status_quo`, `scene_json`, `outfit_json`, `affinity += delta`, квесты, **choices** (в ответ API, не в messages) |
|
||
| 3 | `generate_sd_prompt` | история, outfit, **scene_json** | картинка на last assistant id |
|
||
|
||
**Особенности opening для narrator post:**
|
||
- Просит заполнить `scene_update` из greeting + scenario.
|
||
- Просит **non-zero `affinity_delta`**, если в first_mes явный тёплый/враждебный тон.
|
||
- `outfit_update` — начальная одежда из greeting (danbooru-теги).
|
||
|
||
**UI после:** `reloadChatFromServer`, панель квестов, 💖 affinity, кнопки `choices`, картинка SD.
|
||
|
||
---
|
||
|
||
## Состояние сессии перед 1-м сообщением игрока
|
||
|
||
| Поле | Пример |
|
||
|------|--------|
|
||
| `messages` | system, assistant(first_mes) |
|
||
| `plot_arc_json` | сгенерированная арка |
|
||
| `status_quo`, `scene_json`, `outfit_json` | из narrator opening |
|
||
| `affinity` | 0 или ±1..2 после opening |
|
||
| `facts_json` | `[]` |
|
||
| `narrative_stats_json` | `{"lust":0,"stamina":10,"tension":0}` (если stats выкл. — всё равно дефолт в БД) |
|
||
|
||
---
|
||
|
||
## Сообщение игрока №1 — обычная реплика (без d20)
|
||
|
||
**Пример:** игрок пишет: «Привет, как тебя зовут?»
|
||
|
||
**UI:** `POST /chat/stream`
|
||
```json
|
||
{
|
||
"session_id": "...",
|
||
"persona_id": "...",
|
||
"message": "Привет, как тебя зовут?",
|
||
"is_narrator_choice": false
|
||
}
|
||
```
|
||
|
||
### Этап A — подготовка (до stream)
|
||
|
||
1. **static_prompt** = `get_system_prompt(persona, history, message)` — lorebook по последним 5 репликам + guardrails.
|
||
2. **RPG pre (narrator):**
|
||
- **Вход user:** persona, user action, Global plot (= full arc JSON), Facts, Recent (до 8 реплик).
|
||
- **Модель:** `RPG_NARRATOR_MODEL`.
|
||
- **Ожидание:** `needs_check: false` для чистого диалога.
|
||
- **Эффекты:**
|
||
- `directives` → попадут в `narrator_extra`.
|
||
- `status_quo_update` → `UPDATE sessions.status_quo`.
|
||
- `scene_update` (partial) → merge в `scene_json`.
|
||
- **Нет** d20, **нет** bubble «Рассказчик».
|
||
3. **runtime_suffix** = `build_rpg_runtime_suffix(session)` + `narrator_extra` (directives только).
|
||
4. `upsert_static_system_message(static)` — в БД system без RPG-блоков.
|
||
5. `add_message(user, message)`.
|
||
6. **context_usage** — если > 85%, в конец system добавляется `[Context: ~N% …]`.
|
||
7. **llm_messages** = system(static+runtime) + вся история user/assistant.
|
||
|
||
### Этап B — SSE stream
|
||
|
||
| Событие SSE | Когда |
|
||
|-------------|--------|
|
||
| `{ "chunk": "..." }` | Поток `CHAT_MODEL` — ответ персонажа |
|
||
| `{ "done": true, ... }` | После сохранения assistant в БД |
|
||
|
||
**Персонаж:** один запрос stream, читает affinity/scene/status/arc/facts/directives, **не меняет** счётчики.
|
||
|
||
### Этап C — post-process (после ответа персонажа, тот же HTTP stream)
|
||
|
||
Порядок внутри `generate()`:
|
||
|
||
| # | Действие | Модель | Условие |
|
||
|---|----------|--------|---------|
|
||
| C1 | `generate_plot_arc` | PLOT | Только если `plot_arc_json` пуст (редко после opening) |
|
||
| C2 | `should_advance_arc(user_message)` | код | Ключевые слова: отдых → `event_driven:rest`, путь → `travel`, помощь → `help_request` |
|
||
| C3 | `pop_matching_beats` + injection | — | Если trig совпал с beat; choices из beat |
|
||
| C4 | `advance_phase` | — | Если beats пусты — фаза opening→hook→… |
|
||
| C5 | `extract_facts` | FACTS | Последние 10 реплик; merge до 80, в промпт 20 |
|
||
| C6 | **`narrator_post`** | NARRATOR | Контекст: последние 8 реплик **включая новый ответ** |
|
||
| C7 | **`apply_narrator_post`** | — | status_quo, affinity_delta, scene, outfit, quests, stats_delta* |
|
||
| C8 | `generate_sd_prompt` + Comfy | SD | outfit + scene_json + последние 6 реплик |
|
||
| C9 | SSE `done` | — | choices, affinity, quests, image_*, debug |
|
||
|
||
\* `stats_delta` только при `rpg_settings.stats === true`.
|
||
|
||
**Для реплики №1 (диалог):** narrator pre → no check; post → facts, возможно choices, affinity ±, scene/status.
|
||
|
||
---
|
||
|
||
## Сообщение игрока №2 — действие с проверкой d20
|
||
|
||
**Пример:** «Пытаюсь перепрыгнуть через пропасть»
|
||
|
||
Тот же `POST /chat/stream`. Отличия в **этапе A**:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
pre1[narrator_pre без броска]
|
||
need{needs_check AND dice?}
|
||
roll[d20 1-20]
|
||
pre2[narrator_pre с roll+outcome]
|
||
res[resolution_text + action_resolutions]
|
||
chat[CHAT_MODEL stream]
|
||
post[narrator_post + facts + SD]
|
||
|
||
pre1 --> need
|
||
need -->|да| roll --> pre2 --> res
|
||
need -->|нет| chat
|
||
res --> chat
|
||
chat --> post
|
||
```
|
||
|
||
| Шаг | Что происходит |
|
||
|-----|----------------|
|
||
| pre (фаза 1) | `needs_check: true` |
|
||
| Бросок | `random 1..20` → outcome: crit fail / fail / success / crit success |
|
||
| pre2 | Тот же user + `Roll d20=N`, `Outcome=...` → **`resolution_text` обязателен** |
|
||
| UI | SSE **`{ "narrator": { roll, outcome, text } }`** **до** chunks — bubble «📖 Рассказчик» |
|
||
| runtime | + `--- Mechanics ---` (d20, outcome, «не противоречь») |
|
||
| БД | `action_resolutions` — intent, roll, outcome, resolution_text |
|
||
| Персонаж | Должен согласовать ответ с resolution + mechanics |
|
||
|
||
Post-process (C) — **тот же**, что на ходу 1. Facts могут записать «игрок не перепрыгнул», quest может обновиться.
|
||
|
||
---
|
||
|
||
## Сообщение игрока №3 — кнопка выбора + сдвиг арки
|
||
|
||
**Пример:** игрок жмёт choice «Отправиться в лес» (label уходит как user message)
|
||
или пишет: «Идём дальше в лес»
|
||
|
||
**UI:** `sendMessage(label, true)` → `is_narrator_choice: true`
|
||
|
||
| Отличие | Поведение |
|
||
|---------|-----------|
|
||
| Текст в БД | `"[Player chose: Отправиться в лес]"` вместо сырого label |
|
||
| pre/post | Как обычный ход; narrator видит метку выбора |
|
||
| **should_advance_arc** | Если в тексте есть «идем дальше», «в путь», … → `event_driven:travel` |
|
||
| beats | `pop_matching_beats(arc, trig)` — до 1 beat; **injection** в debug; **choices** из beat + post |
|
||
| phase | Если beats опустели → `advance_phase` (например hook → complication) |
|
||
|
||
Остальной pipeline = stream №1 или №2 (в зависимости от needs_check).
|
||
|
||
---
|
||
|
||
## Сводная таблица: кто что обновляет (ход 1–3)
|
||
|
||
| Данные | Opening | narrator pre | CHAT_MODEL | narrator post | extract_facts | arc engine |
|
||
|--------|---------|--------------|------------|---------------|---------------|------------|
|
||
| messages | first_mes | — | user + assistant | — | — | — |
|
||
| plot_arc_json | create | — | read | read | — | advance/beats |
|
||
| status_quo | post | pre/post | read | post | — | — |
|
||
| scene_json | post | pre/post | read | post | — | — |
|
||
| outfit_json | post | — | read | post | — | — |
|
||
| affinity | post | — | read | delta | — | — |
|
||
| facts_json | — | read | read | post.facts* | merge | — |
|
||
| rpg_quests | seed | — | — | post | — | — |
|
||
| action_resolutions | — | d20 path | — | — | — | — |
|
||
| narrative_stats | default | — | read** | stats_delta** | — | — |
|
||
|
||
\* facts из post в JSON схеме narrator — сейчас основной путь facts = `extract_facts`.
|
||
\** только при `stats: true`.
|
||
|
||
---
|
||
|
||
## Варианты настроек (перекрытие)
|
||
|
||
### `narrator: false`
|
||
|
||
- Нет narrator pre/post, нет directives/mechanics bubble.
|
||
- `runtime_suffix` = только `build_rpg_runtime_suffix` (facts, arc, status, scene, affinity).
|
||
- Post-process: нет C6–C7; facts/arc/SD могут остаться (arc/SD/facts всё ещё в коде stream).
|
||
|
||
### `dice: false`
|
||
|
||
- `needs_check` от pre **игнорируется** — всегда ветка без броска (как диалог).
|
||
- pre2 и mechanics не вызываются.
|
||
|
||
### `affinity: false`
|
||
|
||
- Нет блока Relationship в runtime; `affinity_delta` не применяется.
|
||
|
||
### `quests: false`
|
||
|
||
- Opening не сидирует квесты; post не вызывает `upsert_quest`.
|
||
|
||
### `choices: false`
|
||
|
||
- UI не рисует кнопки; post/beat choices не добавляются в SSE.
|
||
|
||
### `stats: true`
|
||
|
||
- В runtime: блок lust/stamina/tension (0–10).
|
||
- post: `stats_delta` каждый ключ −2..+2, clamp 0–10.
|
||
- Дефолт stamina = 10.
|
||
|
||
### `is_narrator_choice: true`
|
||
|
||
- User content = `[Player chose: …]`.
|
||
|
||
### Контекст > 85%
|
||
|
||
- В system добавляется одна строка-предупреждение (не в БД).
|
||
|
||
---
|
||
|
||
## Вспомогательные запросы (не в примере 1–3, но в RPG)
|
||
|
||
| Запрос | Когда |
|
||
|--------|-------|
|
||
| `GET /chat/system/{id}` | Панель system blob: static, runtime-превью, scene, stats, **context_usage** |
|
||
| `GET /chat/history/{id}` | Перезагрузка чата |
|
||
| `POST /chat/rpg/bootstrap` | RPG на **старом** чате: arc + **narrator_post opening** + apply (как opening, без нового first_mes) |
|
||
| `POST /chat/` (не stream) | Упрощённый ход: static + runtime, один ответ, без narrator pre/post |
|
||
|
||
---
|
||
|
||
## Пример хронологии (3 сообщения, все опции ON)
|
||
|
||
| # | HTTP | LLM-вызовы | Заметки |
|
||
|---|------|------------|---------|
|
||
| 0 | PATCH session | — | rpg_enabled |
|
||
| 0 | POST init | — | system + first_mes |
|
||
| 0 | POST opening/process | Plot, Narrator post, SD | arc, scene, affinity, quests, choices |
|
||
| 1 | POST stream | Pre (no check), **Chat**, Facts, Post, SD | Диалог; +facts; choices в done |
|
||
| 2 | POST stream | Pre (check), **Pre2+d20**, **Chat**, Facts, Post, SD | Bubble рассказчика; action_resolutions |
|
||
| 3 | POST stream | Pre, Chat, Facts, Post, SD; arc travel? | `[Player chose:…]` или текст; beat injection |
|
||
|
||
**Порядок SSE на ходу 2:**
|
||
1. `narrator` (d20)
|
||
2. `chunk` × N
|
||
3. `image_generating` (если SD)
|
||
4. `done` (choices, affinity, quests, debug)
|
||
|
||
---
|
||
|
||
## Файлы для углубления
|
||
|
||
| Тема | Файл |
|
||
|------|------|
|
||
| Stream + RPG | `routers/chat.py` → `chat_stream` |
|
||
| Opening | `services/opening.py` |
|
||
| Narrator JSON | `services/rpg_narrator.py` |
|
||
| Runtime blocks | `routers/chat.py` → `build_rpg_runtime_suffix`, `services/rpg_state.py` |
|
||
| Persist post | `services/rpg_state.py` → `apply_narrator_post` |
|
||
| Arc / beats | `services/rpg_plot.py` |
|
||
| Facts | `services/rpg_facts.py` |
|
||
| UI stream | `static/js/chat.js` → `sendMessage`, `consumeStream` |
|
||
| New chat | `static/js/newChatWizard.js` |
|
||
| Context % | `services/context_budget.py` |
|
||
|
||
---
|
||
|
||
*Обновлено под текущую реализацию RP UX (anti-OOC, scene_json, context blob, rpFormat, bootstrap narrator).*
|