288 lines
14 KiB
Markdown
288 lines
14 KiB
Markdown
---
|
||
title: Session Design — AI Psychologist App
|
||
aliases:
|
||
- Дизайн сессии
|
||
- Session design
|
||
- архитектура сессий
|
||
created: '2026-05-19'
|
||
updated: '2026-06-02'
|
||
tags:
|
||
- project
|
||
- app
|
||
- psychology
|
||
- ux
|
||
- architecture
|
||
related:
|
||
- '[[personal/projects/psychologist-app/overview]]'
|
||
- '[[personal/projects/psychologist-app/character-design]]'
|
||
- '[[personal/projects/psychologist-app/onboarding-ux]]'
|
||
---
|
||
# Session Design — AI Psychologist App
|
||
|
||
> Детальный дизайн сессии: UX-флоу, механика вопросов, открытие/закрытие.
|
||
> Связан с: [[overview]] (раздел 5-6 — базовая структура и архитектура ботов)
|
||
|
||
---
|
||
|
||
## 1. Механика вопросов (Question Delivery)
|
||
|
||
### Pipeline вопросов
|
||
|
||
```
|
||
Бот 2 (Analyst / "Психолог")
|
||
→ генерирует 1-2 вопроса в 3-м лице ("пользователь избегает...")
|
||
↓
|
||
Бот 1 (Narrator / "Посредник")
|
||
→ переформулирует в 1-е лицо ("ты избегаешь?")
|
||
→ на выходе: структурированный массив вопросов для UI
|
||
↓
|
||
UI алгоритм (не агент!)
|
||
→ показывает вопросы по одному в карточках
|
||
→ ждёт ответа → следующий
|
||
```
|
||
|
||
**Ключевой принцип**: раздача вопросов — детерминированный алгоритм, не LLM-вызов.
|
||
Агент генерирует батч вопросов один раз → посредник переформулирует → UI сам управляет порядком.
|
||
|
||
**Narrator детектирует 3-е лицо**: через prompt rule ("переформулируй в 1-е лицо") — регекс не нужен.
|
||
|
||
### Завершение батча вопросов → подбивка
|
||
|
||
Когда все вопросы в батче отвечены, UI передаёт агенту нумерованную подбивку ответов.
|
||
Это входные данные для следующей итерации (следующий батч или инсайт).
|
||
|
||
---
|
||
|
||
## 2. Открытие сессии
|
||
|
||
### Структура intro
|
||
|
||
1. **Персонаж-активация** (анимация, 1-2 сек)
|
||
2. **Короткое введение** — 1-2 предложения, сразу к делу, без "Давай начнём с..."
|
||
3. **"Крючки-активаторы"** — max 2 из profile.md / последних сессий. Если нет истории — пропустить.
|
||
4. **Переход к теме** — предложение агента или выбор пользователя
|
||
|
||
### Что получает Analyst при старте
|
||
|
||
- `profile.md` — паттерны пользователя
|
||
- последние 2 сессии из `observations/`
|
||
- задача: intro + max 2 крючка
|
||
|
||
---
|
||
|
||
## 3. Закрытие сессии
|
||
|
||
### Последовательность (3 этапа)
|
||
|
||
#### Этап 1 — Structured session close (backend, не видно пользователю)
|
||
|
||
Analyst генерирует `SessionCloseResult`:
|
||
- `insight` — ключевое наблюдение (1-2 предложения, 3-е лицо)
|
||
- `sessionSummary` — аналитическое резюме всей сессии (не первые реплики)
|
||
- `profileUpdates` — новые паттерны для добавления в `profile.md`
|
||
- `promptUpdates` — обновления для `prompts.md` (если применимо)
|
||
|
||
Приложение раскладывает результат по файлам через `ProfileManager`.
|
||
|
||
> **Статус**: частично реализован. `insight` генерируется (Sonnet + tools + полный диалог).
|
||
> `sessionSummary`, `profileUpdates`, `promptUpdates` — НЕ реализованы (см. §12.2.G).
|
||
|
||
#### Этап 2 — Посредник: человечный итог (видно пользователю)
|
||
|
||
- Тёплая живая фраза, без психолого-языка
|
||
- Не повторять аналитика, не хвалить избыточно
|
||
- Упомянуть дату следующей сессии если есть расписание
|
||
|
||
#### Этап 3 — "One last thing" + завершение
|
||
|
||
- Открытый вопрос: "Хочешь что-то добавить?"
|
||
- Маленький текстовый input, неакцентная кнопка "Завершить"
|
||
- Если написал → агент добавляет в заметки сессии, отвечает коротко (не перезапускает сессию)
|
||
- Если упомянул перенос → сценарий планировщика (EventKit)
|
||
|
||
---
|
||
|
||
## 4. Флоу сессии целиком
|
||
|
||
```
|
||
ОТКРЫТИЕ
|
||
├── Анимация персонажа (1-2 сек)
|
||
├── Intro: 1-2 предложения + max 2 крючка из истории
|
||
└── Выбор темы
|
||
|
||
ДИАЛОГ
|
||
├── Analyst генерирует 1-2 вопроса (3-е лицо)
|
||
├── Narrator переформулирует → карточки (1-е лицо)
|
||
├── UI раздаёт по одному (one-card UX)
|
||
├── Пользователь отвечает → нумерованная подбивка
|
||
└── Следующий батч / инсайт
|
||
└── session_complete: true → карточка "Завершить / Продолжить"
|
||
└── hard limit: 8 ходов → принудительный readyToClose
|
||
|
||
ЗАКРЫТИЕ
|
||
├── [backend] Analyst: insight + sessionSummary + profileUpdates
|
||
├── Посредник: тёплая фраза + дата следующей сессии
|
||
├── "One last thing"
|
||
│ ├── Написал что-то → в заметки, короткий ответ
|
||
│ ├── Упомянул перенос → сценарий планировщика
|
||
│ └── Завершить (кнопка) → сессия закрыта
|
||
```
|
||
|
||
---
|
||
|
||
## 5. State Persistence & Session Recovery
|
||
|
||
### Текущая реализация (актуально)
|
||
|
||
`resumeOrStart()` при открытии SessionView:
|
||
- Проверяет незавершённую сессию (`detectIncompleteSession()`)
|
||
- `.resumed` → восстанавливает карточки из `session.messages` (видимые `.analyst`)
|
||
- `.startedAfterPartialSummary` → была стale сессия >20h → показывает partial summary, затем чистый старт
|
||
- `.started` → обычный старт
|
||
|
||
`IncompleteSessionState.isFresh` — сессия считается свежей если < 20 часов.
|
||
Stale → `generatePartialSummary()` → `completed_partial`.
|
||
|
||
### Кнопка "Начать заново" при resume
|
||
|
||
При восстановлении сессии (`.resumed`) показывать кнопку **"Начать заново"** рядом с первой карточкой.
|
||
- Кнопка исчезает после первого отправленного ответа пользователя
|
||
- Нажатие → `cancelSession()` + `startNewSession()` (чистый старт)
|
||
- Реализовать как `SessionViewModel.showRestartButton: Bool`, сбрасывается в `send(text:)`
|
||
|
||
### Что не реализовано (Stage 2)
|
||
|
||
Retention protocol: при N пропущенных сессиях подряд → реактивационный флоу с push-уведомлением.
|
||
|
||
---
|
||
|
||
## 6. Что реализовано сейчас
|
||
|
||
> Актуально на 2026-06-02, коммит `53c1d2cb`.
|
||
|
||
### Пейсинг сессии
|
||
- `AnalystJSON.sessionComplete: Bool` (backward-compatible)
|
||
- `ProcessingResult.readyToClose([String]?, observation:)`
|
||
- `SessionManager.userTurnCount`, `consecutiveShortAnswerCount`
|
||
- Hard limit: 8 ходов → принудительный `.readyToClose`
|
||
- `continueSession()` — сброс флага после "Продолжить"
|
||
- Пейсинг-правила и директивы усталости в systemPrompt AnalystBot
|
||
|
||
### Session Recovery
|
||
- `detectIncompleteSession()`, `resumeOrStart()`, `generatePartialSummary()`
|
||
- `FatigueContext`, `IncompleteSessionState`, `SessionResumeResult`
|
||
- SessionViewModel: восстановление карточек при `.resumed`
|
||
|
||
### Завершение сессии
|
||
- `endSession()` → `generateSessionInsight()` (Sonnet + tools + полный диалог, без обрезки)
|
||
- Агент вызывает `search_sessions`, `read_session`, `read_profile` для сравнения с историей
|
||
- `finalInsight` → `ProfileManager.addInsight()` немедленно
|
||
- `suggestedInsight` по ходу диалога → `addInsight()` немедленно (не теряется при X)
|
||
|
||
### Агент / промпты
|
||
- NarratorBot: legit-маркер, injection guard, off-topic директива, детекция усталости/настроения
|
||
- AnalystBot: turnCount/duration/messageCount/fatigueCtx в user message; no-filepath директива
|
||
|
||
### UX
|
||
- One-card UX: `CardData.isActive`, `appendCard()`, `activeCard`, `historyCards`
|
||
- История — dimmed compact cards (read-only)
|
||
- `TextEditor` с placeholder, auto-grow (36→160pt), кнопка dismiss клавиатуры
|
||
- `CardData.isCompletion` → quickReply "Завершить" → `endSession()`, "Продолжить" → `continueAfterCompletion()`
|
||
|
||
### Тесты
|
||
- 202 теста, 0 фейлов
|
||
- Покрыто: пейсинг, усталость, injection guard, crisis, session recovery, one-card UX, insights, snapshot prompts
|
||
|
||
---
|
||
|
||
## 7. Что не реализовано (план)
|
||
|
||
### 7.1 Structured session close [ПРИОРИТЕТ]
|
||
|
||
**Что в плане** (§3, §4): Analyst возвращает структурированный объект — insight + sessionSummary + profileUpdates.
|
||
|
||
**Что в коде**: `generateSessionInsight()` возвращает только одну строку-инсайт.
|
||
- `profile.md` после сессии не обновляется новыми паттернами
|
||
- `prompts.md` не обновляется
|
||
- `sessions/YYYY-MM-DD.md` заполняется через `saveSessionToProfile()` где summary = **первые 3 реплики пользователя**, не аналитическое резюме
|
||
|
||
**Решение**: `SessionCloseResult { insight: String, sessionSummary: String, profileUpdates: [String], promptUpdates: [String]? }`.
|
||
Агент возвращает структуру → приложение раскладывает по файлам.
|
||
|
||
### 7.2 mood_delta → Orb visual feedback
|
||
|
||
`AnalystJSON` → добавить `moodDelta: "declining" | "stable" | "improving"`.
|
||
UI: declining = холодный синий/медленный пульс, stable = нейтральный, improving = amber/живой.
|
||
|
||
### 7.3 Планировщик как tool
|
||
|
||
EventKit-интеграция как tool у Narrator — аналогично `search_sessions` / `read_profile`.
|
||
Дизайн-решение отложено.
|
||
|
||
### 7.4 Retention protocol `[STAGE 2 — not in MVP]`
|
||
|
||
При N пропущенных сессиях подряд → реактивационный флоу (§5).
|
||
Отложено до после релиза MVP. Не входит в текущий спринт.
|
||
|
||
### 7.5 Android: TextEditor в Skip Fuse
|
||
|
||
Проверить поддержку `TextEditor` + `fixedSize(horizontal:vertical:)` в Skip Fuse на Android.
|
||
При необходимости заменить на `TextField(axis: .vertical)`.
|
||
`.scrollContentBackground(.hidden)` уже огорожен `#if os(iOS)`.
|
||
|
||
---
|
||
|
||
## 8. Открытые вопросы
|
||
|
||
- [ ] Планировщик — tool у Narrator (EventKit). Когда и как реализовывать?
|
||
- [ ] mood_delta — 3 значений достаточно? (declining/stable/improving)
|
||
- [ ] Android: `TextEditor` в Skip Fuse — нужна проверка на устройстве
|
||
|
||
---
|
||
|
||
## 9. Протокол агента — действующие правила
|
||
|
||
### NarratorBot systemPrompt содержит:
|
||
- Legit-маркер (защита от injection false positive)
|
||
- Injection guard директива
|
||
- Off-topic директива (мета-вопросы → вернуть фокус)
|
||
- Детекция усталости → передать в metadata для Analyst
|
||
- Детекция настроения → `sentimentObservation` в metadata
|
||
|
||
### AnalystBot systemPrompt содержит:
|
||
- Пейсинг-правила (turn_count, 4+ оборотов → оценка exhaustion, 7+ → принудительно)
|
||
- Директива при усталости: `session_complete: true`, `questions: []`
|
||
- Директива при мета-вопросах: не объяснять архитектуру, вернуть фокус
|
||
- No-filepath директива: не упоминать пути файлов
|
||
|
||
### AnalystBot tools (read-only):
|
||
- `search_sessions` — поиск по архиву
|
||
- `read_session` — читать конкретную сессию
|
||
- `get_index` — индекс всех сессий
|
||
- `read_profile` — профиль пользователя
|
||
- `list_files` / `read_file` — дополнительные файлы профиля
|
||
|
||
---
|
||
|
||
## 10. Хранение данных (on-device)
|
||
|
||
Всё хранится через `ProfileManager` в `Documents/UserProfile/`:
|
||
|
||
| Файл | Что содержит | Кто пишет |
|
||
|------|-------------|-----------|
|
||
| `profile.md` | Паттерны пользователя, наблюдения | `updateProfile()` — пока не вызывается автоматически после сессии |
|
||
| `insights.md` | Инсайты по сессиям | `addInsight()` — пишется немедленно |
|
||
| `prompts.md` | Промпты/темы пользователя | не обновляется автоматически |
|
||
| `observations/Www.md` | Запись сессии за неделю | `appendSession()` |
|
||
| `index.md` | Индекс сессий | `updateSessionIndex()` |
|
||
|
||
**Расписание**: через системный календарь (EventKit), не отдельный файл.
|
||
|
||
---
|
||
|
||
## Связанные заметки
|
||
|
||
- [[personal/projects/psychologist-app/overview]]
|
||
- [[personal/projects/psychologist-app/character-design]]
|
||
- [[personal/projects/psychologist-app/onboarding-ux]]
|