Files
aiform_dev/docs/CLAIMSAVE_PRIMARY_DRAFT_FIX.md
AI Assistant 3621ae6021 feat: Session persistence with Redis + Draft management fixes
- Implement session management API (/api/v1/session/create, verify, logout)
- Add session restoration from localStorage on page reload
- Fix session_id priority when loading drafts (use current, not old from DB)
- Add unified_id and claim_id to wizard payload sent to n8n
- Add Docker volume for frontend HMR (Hot Module Replacement)
- Add comprehensive session logging for debugging

Components updated:
- backend/app/api/session.py (NEW) - Session management endpoints
- backend/app/main.py - Include session router
- frontend/src/components/form/Step1Phone.tsx v2.0 - Create session after SMS
- frontend/src/pages/ClaimForm.tsx v3.8 - Session restoration & priority fix
- frontend/src/components/form/StepWizardPlan.tsx v1.4 - Add unified_id/claim_id
- docker-compose.yml - Add frontend volume for live reload

Session flow:
1. User verifies phone -> session created in Redis (24h TTL)
2. session_token saved to localStorage
3. Page reload -> session restored automatically
4. Draft selected -> current session_id used (not old from DB)
5. Wizard submit -> unified_id, claim_id, session_id sent to n8n
6. Logout -> session removed from Redis & localStorage

Fixes:
- Session token not persisting after page reload
- unified_id missing in n8n webhook payload
- Old session_id from draft overwriting current session
- Frontend changes requiring container rebuild
2025-11-20 18:31:42 +03:00

336 lines
14 KiB
Markdown
Raw 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.

# Исправление узла `claimsave` для сохранения первичного черновика
## Проблемы
1. **`claim_id` генерируется в другом workflow** - нужно использовать `session_id` для связи, `claim_id` генерировать позже
2. **Неправильный `sessionToken` в Code4** - используется `claim_id` вместо `session_token`
3. **Нет сохранения первичного черновика** - нужно сохранить сразу после генерации `wizard_plan`
4. **Данные из AI Agent1 и AI Agent13 не сохраняются** - они пригодятся, нужно их сохранить в черновик
## Решение
### 1. Исправить узел `Code4` (подготовка данных для Redis)
**Текущий код (строка 459):**
```javascript
const sessionToken = $('Redis Trigger').first().json.message.claim_id
```
**Проблема:** Используется `claim_id` вместо `session_token` для Redis ключа. `claim_id` может быть недоступен или генерируется позже.
**Исправленный код:**
```javascript
// Получаем session_token из разных источников (приоритет: Edit Fields11 > Redis Trigger)
const sessionToken = $('Edit Fields11').first().json.session_token
|| $('Redis Trigger').first().json.message.session_id
|| null;
// Если session_token недоступен, генерируем временный ключ
if (!sessionToken) {
console.warn('⚠️ session_token не найден, используем временный ключ');
}
// Используем session_token для Redis ключа (claim_id будет сгенерирован позже)
const redisKey = `ocr_events:${sessionToken || 'temp-' + Date.now()}`;
```
### 2. Создать новый узел `claimsave_primary` (сохранение первичного черновика)
**Позиция:** После узла `Code4`, перед `push_wizard1`
**Назначение:** Сохранить первичный черновик сразу после генерации `wizard_plan`
**SQL запрос:**
```sql
-- $1 = payload_json (jsonb) - полный payload с wizard_plan, problem_description, AI Agent1, AI Agent13 и т.д.
-- $2 = session_token (text) - сессия пользователя (используем для связи, claim_id генерируем позже)
-- $3 = unified_id (text, опционально) - unified_id пользователя
WITH partial AS (
SELECT
$1::jsonb AS p,
$2::text AS session_token_str,
NULLIF($3::text, '') AS unified_id_str
),
-- Находим существующую запись по session_token или создаем новую
claim_lookup AS (
SELECT
COALESCE(
(SELECT id FROM clpr_claims WHERE session_token = partial.session_token_str LIMIT 1),
gen_random_uuid()
) AS claim_uuid
FROM partial
),
-- Если записи нет, создаем её
claim_created AS (
INSERT INTO clpr_claims (
id,
session_token,
unified_id,
channel,
type_code,
status_code,
payload,
created_at,
updated_at,
expires_at
)
SELECT
claim_lookup.claim_uuid,
partial.session_token_str,
partial.unified_id_str,
'web_form',
COALESCE(partial.p->>'type_code', 'consumer'),
'draft',
jsonb_build_object(
-- claim_id будет сгенерирован позже, пока NULL
'claim_id', NULL,
'problem_description', partial.p->>'problem_description',
'wizard_plan',
CASE
WHEN partial.p->>'wizard_plan' IS NOT NULL
THEN (partial.p->>'wizard_plan')::jsonb
WHEN partial.p->'wizard_plan' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
THEN partial.p->'wizard_plan'
ELSE NULL
END,
'answers_prefill',
CASE
WHEN partial.p->>'answers_prefill' IS NOT NULL
THEN (partial.p->>'answers_prefill')::jsonb
WHEN partial.p->'answers_prefill' IS NOT NULL AND jsonb_typeof(partial.p->'answers_prefill') = 'array'
THEN partial.p->'answers_prefill'
ELSE '[]'::jsonb
END,
'coverage_report',
CASE
WHEN partial.p->>'coverage_report' IS NOT NULL
THEN (partial.p->>'coverage_report')::jsonb
WHEN partial.p->'coverage_report' IS NOT NULL AND jsonb_typeof(partial.p->'coverage_report') = 'object'
THEN partial.p->'coverage_report'
ELSE NULL
END,
-- Данные из AI Agent1 (факты)
'ai_agent1_facts',
CASE
WHEN partial.p->'ai_agent1_facts' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent1_facts') = 'object'
THEN partial.p->'ai_agent1_facts'
ELSE NULL
END,
-- Данные из AI Agent13 (RAG ответ)
'ai_agent13_rag',
CASE
WHEN partial.p->>'ai_agent13_rag' IS NOT NULL
THEN (partial.p->>'ai_agent13_rag')::jsonb
WHEN partial.p->'ai_agent13_rag' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent13_rag') = 'object'
THEN partial.p->'ai_agent13_rag'
ELSE NULL
END,
'phone', partial.p->>'phone',
'email', partial.p->>'email'
),
now(),
now(),
now() + interval '14 days'
FROM partial, claim_lookup
WHERE NOT EXISTS (
SELECT 1 FROM clpr_claims WHERE id = claim_lookup.claim_uuid
)
ON CONFLICT (id) DO NOTHING
RETURNING id
),
-- Получаем финальный UUID
claim_final AS (
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM claim_created)
THEN (SELECT id FROM claim_created LIMIT 1)
ELSE claim_lookup.claim_uuid
END AS claim_uuid
FROM claim_lookup
),
-- Обновляем существующую запись (если есть)
upd AS (
UPDATE clpr_claims c
SET
unified_id = COALESCE(partial.unified_id_str, c.unified_id),
payload = jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
COALESCE(c.payload, '{}'::jsonb),
'{wizard_plan}',
COALESCE(
CASE
WHEN partial.p->>'wizard_plan' IS NOT NULL
THEN (partial.p->>'wizard_plan')::jsonb
WHEN partial.p->'wizard_plan' IS NOT NULL AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
THEN partial.p->'wizard_plan'
ELSE NULL
END,
c.payload->'wizard_plan'
),
true
),
'{ai_agent1_facts}',
COALESCE(
CASE
WHEN partial.p->'ai_agent1_facts' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent1_facts') = 'object'
THEN partial.p->'ai_agent1_facts'
ELSE NULL
END,
c.payload->'ai_agent1_facts'
),
true
),
'{ai_agent13_rag}',
COALESCE(
CASE
WHEN partial.p->>'ai_agent13_rag' IS NOT NULL
THEN (partial.p->>'ai_agent13_rag')::jsonb
WHEN partial.p->'ai_agent13_rag' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent13_rag') = 'object'
THEN partial.p->'ai_agent13_rag'
ELSE NULL
END,
c.payload->'ai_agent13_rag'
),
true
),
'{problem_description}',
COALESCE(partial.p->>'problem_description', c.payload->>'problem_description'),
true
),
updated_at = now(),
expires_at = now() + interval '14 days'
FROM partial, claim_final
WHERE c.id = claim_final.claim_uuid
AND EXISTS (SELECT 1 FROM claim_lookup WHERE claim_uuid = c.id)
RETURNING c.id, c.payload
)
SELECT
(SELECT jsonb_build_object(
'claim_id', u.id::text,
'session_token', partial.session_token_str,
'status_code', 'draft',
'payload', COALESCE(u.payload, jsonb_build_object())
)
FROM claim_final cf, partial
LEFT JOIN upd u ON true
LIMIT 1) AS claim;
```
**Параметры в n8n:**
```
$1 = {{ JSON.stringify({
problem_description: $('Edit Fields16').first().json.chatInput,
wizard_plan: $('Code4').first().json.redis_value.wizard_plan,
answers_prefill: $('Code4').first().json.redis_value.answers_prefill,
coverage_report: $('Code4').first().json.redis_value.coverage_report,
// Данные из AI Agent1 (факты)
ai_agent1_facts: {
facts_short: $('пробрасываем факт фул и факт шорт1').first().json.facts_short,
facts_full: $('пробрасываем факт фул и факт шорт1').first().json.facts_full,
problem: $('пробрасываем факт фул и факт шорт1').first().json.problem
},
// Данные из AI Agent13 (RAG ответ)
ai_agent13_rag: $('AI Agent13').first().json.output,
phone: $('Redis Trigger').first().json.message.phone,
email: $('Redis Trigger').first().json.message.email || null,
type_code: $('Code4').first().json.redis_value.wizard_plan?.case_type || 'consumer'
}) }}
$2 = {{ $('Edit Fields11').first().json.session_token || $('Redis Trigger').first().json.message.session_id }}
$3 = {{ $('Edit Fields10').first().json.unified_id || $('Redis Trigger').first().json.message.unified_id || null }}
```
### 3. Исправить узел `claimsave` (для последующих обновлений)
**Текущий queryReplacement:**
```
={{ $json.payload_partial_json }}, {{ $('Redis Trigger').item.json.message.claim_id }}
```
**Проблема:** Используется `claim_id` из `Redis Trigger`, который может быть недоступен. Также SQL ищет запись по `claim_id`, но на этапе первичного черновика `claim_id` может быть NULL.
**Исправленный queryReplacement:**
```
={{ $json.payload_partial_json }}, {{ $('Edit Fields11').first().json.session_token || $('Redis Trigger').first().json.message.session_id }}
```
**Также нужно обновить SQL в узле `claimsave`** - искать запись по `session_token` вместо `claim_id`:
```sql
-- Вместо:
WHERE payload->>'claim_id' = partial.claim_id_str
-- Использовать:
WHERE session_token = partial.session_token_str
```
**Примечание:** Узел `claimsave` используется для последующих обновлений (после загрузки файлов, ответов пользователя и т.д.), поэтому он должен работать с уже существующим черновиком, найденным по `session_token`.
## Порядок узлов в workflow
1. `Redis Trigger` → получает событие
2. `get_claime_data1` → получает данные из Redis
3. `Edit Fields8` → извлекает поля из сообщения
4. `Merge2` → объединяет данные
5. `Get row(s) in sheet2` → получает шаги формы
6. `Edit Fields16` → подготавливает данные для AI
7. `AI Agent1` → извлекает факты (полный и короткий)
8. `пробрасываем факт фул и факт шорт1` → передает факты
9. `AI Agent13` → генерирует RAG ответ
10. `output_set1` → форматирует выход
11. `Edit Fields11` → подготавливает данные для wizard
12. `AI Agent12` → генерирует wizard_plan
13. `Code` → парсит JSON
14. `Code4` → форматирует для Redis
15. **`claimsave_primary`** → **СОХРАНЯЕТ ПЕРВИЧНЫЙ ЧЕРНОВИК**
16. `push_wizard1` → пушит wizard_plan в Redis для SSE
## Что сохраняется в первичный черновик
-`wizard_plan` - план вопросов от AI Agent12
-`problem_description` - описание проблемы от пользователя
-`answers_prefill` - предзаполненные ответы (если есть)
-`coverage_report` - отчёт о покрытии (если есть)
-`ai_agent1_facts` - данные из AI Agent1 (facts_short, facts_full, problem)
-`ai_agent13_rag` - RAG ответ от AI Agent13
-`session_token` - сессия пользователя (используется для связи, claim_id генерируется позже)
-`unified_id` - если есть (передается с фронта)
-`phone`, `email` - контакты пользователя
-`status_code = 'draft'` - статус черновика
- ⚠️ `claim_id` - пока NULL, будет сгенерирован позже
## Что НЕ сохраняется на этом этапе
-`wizard_answers` - ещё нет (пользователь не ответил)
-`documents_meta` - ещё нет (файлы не загружены)
## Данные из AI Agent1 и AI Agent13
Эти данные используются в `AI Agent12` для генерации `wizard_plan`, но **также сохраняются в черновик** для дальнейшего использования:
- **AI Agent1** → `output` (факты полный и короткий):
- `facts_short` - краткая суть проблемы
- `facts_full` - полный текст/саммари
- `problem` - классификатор проблемы
- Сохраняется в `payload.ai_agent1_facts`
- **AI Agent13** → `output` (RAG ответ):
- Аналитическая справка/правовой ответ из базы знаний
- Сохраняется в `payload.ai_agent13_rag`
Они передаются в `AI Agent12` через `Edit Fields11`:
- `chatInput` = описание проблемы
- `output` = RAG ответ от AI Agent13
- `questions_numbered_html` = шаги формы из Google Sheets
**Важно:** Сохраняем и промежуточные данные (AI Agent1, AI Agent13), и результат (`wizard_plan`), т.к. они могут пригодиться для дальнейшей обработки.