Files
2026-06-05 14:57:15 +03:00

373 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. **Linear story (pre-stream):** `reconcile_story_arc` → один active-квест = текущий `steps[i]`:
- `format_step_guidance_for_character` — цель шага в system CHAT.
- При первом входе в шаг: `injection` как мягкая подсказка (`format_step_hint_for_character`).
- Если арка завершена и игрок выбрал «новую арку» → `roll_next_arc`.
4. **runtime_suffix** = `build_rpg_runtime_suffix(session)` + `narrator_extra` (directives + step guidance/hint).
5. `upsert_static_system_message(static)` — в БД system без RPG-блоков.
6. `add_message(user, message)`.
7. **context_usage** — если > 85%, в конец system добавляется `[Context: ~N% …]`.
8. **llm_messages** = system(static+runtime) + история; язык сессии (`infer_rp_language`) в narrator/plot/injection.
### Этап 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 | **`narrator_post`** | NARRATOR | Контекст: шаг N/M, completion_criteria, последние 8 реплик |
| C3 | **`apply_narrator_post_with_story`** | — | facts, affinity, scene; `step_complete` → advance step, sync quest |
| C4 | step choices / new arc | — | choices из нового шага; при `arc_completed` — «Начать новую арку» |
| C5 | `extract_facts` | FACTS | Последние 10 реплик |
| C6 | SD + SSE `done` | — | **всегда** `quests` + `story_arc` в payload (live UI) |
| 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: нет C6C7; 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 (010).
- post: `stats_delta` каждый ключ 2..+2, clamp 010.
- Дефолт 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).*