Files
aiform_prod/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

14 KiB
Raw Blame History

Исправление узла 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):

const sessionToken = $('Redis Trigger').first().json.message.claim_id

Проблема: Используется claim_id вместо session_token для Redis ключа. claim_id может быть недоступен или генерируется позже.

Исправленный код:

// Получаем 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 запрос:

-- $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:

-- Вместо:
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 Agent1output (факты полный и короткий):

    • facts_short - краткая суть проблемы
    • facts_full - полный текст/саммари
    • problem - классификатор проблемы
    • Сохраняется в payload.ai_agent1_facts
  • AI Agent13output (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), т.к. они могут пригодиться для дальнейшей обработки.