336 lines
14 KiB
Markdown
336 lines
14 KiB
Markdown
|
|
# Исправление узла `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`), т.к. они могут пригодиться для дальнейшей обработки.
|
|||
|
|
|