# Лог сессии: Настройка RAG workflow для извлечения данных **Дата:** 2025-11-29 **Workflow ID:** `itX62h38faB51y9J` ("6 ocr_check:attempt") --- ## 🎯 Цель сессии Настроить workflow для автоматического извлечения данных из документов с использованием RAG (Retrieval-Augmented Generation) для заполнения формы заявления. --- ## 📊 Структура workflow ``` ocr_check:attempt (Redis Trigger) ↓ clime_id (Set) → извлекает claim_id, session_id ↓ analiz (Set) → добавляет prefix, session_token ↓ give_data1 (PostgreSQL) → большой SQL, собирает все данные ↓ Code1 (Code) → нормализует данные ↓ prepare_rag_items (Code) → создаёт 3 items: user, project, offenders ↓ Loop Over Items → итерация по типам ↓ Code6 (Code) → генерация промптов для AI ↓ AI Agent2 (LLM + RAG) → извлечение данных из документов ↓ Code5 (Code) → парсинг JSON из LLM ↓ Edit Fields4 → извлекает output ↓ Aggregate → собирает все результаты ↓ dataset (Set) → финальная сборка ``` --- ## 📝 Обновлённые ноды ### 1. Code1 — нормализация данных из give_data1 ```javascript // Code1 — нормализация данных из give_data1 // ИСПРАВЛЕНО: извлекаем payload.applicant, ai_analysis, wizard_plan, полные documents function toNullish(v) { if (v === undefined || v === null) return null; if (typeof v === 'string' && v.trim() === '') return null; return v; } function pick(...vals) { return vals.find(v => v !== undefined && v !== null && v !== '') ?? null; } function mapDocuments(docs = []) { if (!docs || !Array.isArray(docs)) return []; return docs.map(d => ({ id: toNullish(d.id), claim_document_id: toNullish(d.id), file_id: toNullish(d.file_id), file_url: toNullish(d.file_url), file_name: toNullish(d.file_name), original_file_name: toNullish(d.original_file_name), field_name: toNullish(d.field_name), uploaded_at: toNullish(d.uploaded_at), filename_for_upload: toNullish(d.filename_for_upload), // AI данные document_type: toNullish(d.document_type), document_label: toNullish(d.document_label), document_summary: toNullish(d.document_summary), ocr_status: toNullish(d.ocr_status), match_score: toNullish(d.match_score), match_status: toNullish(d.match_status), match_reason: toNullish(d.match_reason), })); } function mapCombinedDocs(cds = []) { if (!cds || !Array.isArray(cds)) return []; return cds.map(c => ({ claim_document_id: toNullish(c.claim_document_id), combined_document_id: toNullish(c.combined_document_id), pages: toNullish(c.pages), combined_text: toNullish(c.combined_text), })); } function normalizeOne(src) { const claim = src.claim ?? {}; const payload = claim.payload ?? {}; const userInfo = src.user_info ?? {}; // Извлекаем applicant из payload const applicant = payload.applicant ?? {}; const aiAnalysis = payload.ai_analysis ?? {}; const answersPrefill = payload.answers_prefill ?? []; const wizardPlan = payload.wizard_plan ?? null; // USER: приоритет payload.applicant const user = { firstname: pick(applicant.firstname, applicant.first_name), secondname: pick(applicant.middle_name, applicant.secondname), lastname: pick(applicant.lastname, applicant.last_name), mobile: pick(payload.phone), email: pick(payload.email), tgid: pick(claim.telegram_id, payload.tg_id), birthday: pick(applicant.birthday, applicant.birth_date), birthplace: pick(applicant.birthplace, applicant.birth_place), mailingstreet: pick(applicant.address), inn: pick(applicant.inn), zip: pick(applicant.zip), channel: pick(userInfo.channel, claim.channel), unified_id: pick(claim.unified_id), session_token: pick(claim.session_token), }; // CASE const caseData = { id: toNullish(claim.id), prefix: toNullish(claim.prefix), channel: toNullish(claim.channel), type_code: toNullish(claim.type_code), status_code: toNullish(claim.status_code), created_at: toNullish(claim.created_at), updated_at: toNullish(claim.updated_at), telegram_id: toNullish(claim.telegram_id), session_token: toNullish(claim.session_token), unified_id: toNullish(claim.unified_id), case_type: pick(wizardPlan?.case_type, claim.type_code), }; // ANSWERS const answers = {}; if (Array.isArray(answersPrefill)) { answersPrefill.forEach(a => { if (a?.name && a?.value !== undefined) { answers[a.name] = a.value; } }); } // AI_ANALYSIS const ai = { problem: toNullish(aiAnalysis.problem), facts_short: toNullish(aiAnalysis.facts_short), facts_full: toNullish(aiAnalysis.facts_full), recommendation: toNullish(aiAnalysis.recommendation), }; const problemDescription = toNullish(payload.problem_description); return { case: caseData, user, answers: Object.keys(answers).length ? answers : null, answers_prefill: answersPrefill.length ? answersPrefill : null, ai_analysis: ai, problem_description: problemDescription, documents: mapDocuments(src.documents), combined_docs: mapCombinedDocs(src.combined_docs), wizard_plan: wizardPlan, meta: { claim_id: caseData.id, session_token: caseData.session_token, unified_id: caseData.unified_id, } }; } const raw = items[0]?.json ?? {}; const arr = Array.isArray(raw) ? raw : [raw]; const results = arr.map(normalizeOne).map(obj => ({ json: obj })); return results.length ? results : [{ json: null }]; ``` --- ### 2. Code6 — генерация промптов для RAG ```javascript // n8n Code node: Генерация prompt'а под конкретный тип // ВХОД: { type: 'user'|'project'|'offenders', data: {...} } const type = $json.type; const data = $json.data; const code1Data = (() => { try { return $('Code1').first().json || {}; } catch(_) { return {}; } })(); const aiAnalysis = code1Data.ai_analysis || {}; const problemDescription = code1Data.problem_description || ''; const wizardPlan = code1Data.wizard_plan || {}; const caseType = wizardPlan.case_type || code1Data.case?.type_code || 'consumer'; let schema = ''; let searchHints = ''; let contextInfo = ''; contextInfo = ` КОНТЕКСТ ДЕЛА: - Тип: ${caseType} - Проблема: ${aiAnalysis.problem || 'не указана'} - Краткие факты: ${aiAnalysis.facts_short || 'не указаны'} `; if (type === 'user') { schema = `{ "user": { "firstname": string|null, "secondname": string|null, "lastname": string|null, "mobile": string|null, "email": string|null, "tgid": number|null, "birthday": "YYYY-MM-DD"|null, "birthplace": string|null, "mailingstreet": string|null, "inn": string|null (12 цифр для физлица) } }`; searchHints = `Ищи данные ПОКУПАТЕЛЯ/ЗАКАЗЧИКА: - ФИО: после "Покупатель:", "Заказчик:", "Потребитель:" - Адрес: "адрес регистрации", "адрес проживания", "место жительства" - ИНН физлица = 12 цифр - Телефон: в реквизитах, после "тел:", "моб:" - Email: в реквизитах`; } else if (type === 'project') { schema = `{ "project": { "category": string|null (тема обращения), "direction": string|null, "agrprice": number|null (сумма в рублях, только цифры!), "subject": string|null (предмет договора - что купили/заказали), "agrdate": "YYYY-MM-DD"|null (дата заключения договора), "startdate": "YYYY-MM-DD"|null (дата начала услуги/поездки), "finishdate": "YYYY-MM-DD"|null (дата окончания), "country": string|null (страна для турпутёвок), "hotel": string|null (название отеля), "transport": "да"|"нет"|null (включён ли трансфер), "insurance": "да"|"нет"|null (включена ли страховка), "description": string|null (краткое описание сделки) } }`; searchHints = `Ищи данные ДОГОВОРА/СДЕЛКИ: - Сумма: "Цена договора", "Стоимость", "Итого к оплате", "Сумма заказа" - Дата: "Дата заключения", "Договор № ... от ...", "Заказ от ..." - Предмет: что именно купили или заказали (товар, услуга, тур) - Для туров: страна, отель, даты заезда/выезда`; } else if (type === 'offenders') { schema = `{ "offenders": [ { "role": "seller"|"service_provider"|"tour_agent"|"tour_operator"|"delivery"|"installer"|"intermediary"|null, "accountname": string|null (название организации или ФИО ИП), "address": string|null (юридический адрес), "email": string|null, "website": string|null, "phone": string|null, "inn": string|null (10 цифр для юрлица, 12 для ИП), "ogrn": string|null (13 цифр ОГРН или 15 цифр ОГРНИП) } ] }`; searchHints = `Ищи данные ВСЕХ КОНТРАГЕНТОВ (может быть несколько!): ГДЕ ИСКАТЬ: - "Продавец:", "Исполнитель:", "Поставщик:" - После ООО, ИП, ЗАО, ОАО, ПАО, АО - В реквизитах договора, в шапке чека РЕКВИЗИТЫ: - ИНН юрлица = 10 цифр, ИП = 12 цифр - ОГРН = 13 цифр, ОГРНИП = 15 цифр РОЛИ (определи по контексту): - seller — продавец товара (магазин, салон) - service_provider — исполнитель услуги - tour_agent — турагент (кто продал путёвку) - tour_operator — туроператор (кто организует тур, указан в договоре отдельно) - delivery — служба доставки - installer — сборщик/установщик - intermediary — посредник, маркетплейс ВАЖНО: Если в документах несколько организаций — добавь всех!`; } const filledCount = Object.values(data || {}).filter(v => v !== null && v !== undefined && v !== '').length; const totalCount = Object.keys(data || {}).length; return [{ json: { systemMessage: `Ты — юридический помощник-экстрактор. У тебя есть инструмент vectorStore для поиска по документам. ${contextInfo} ${searchHints} ПРАВИЛА: 1. Ищи только поля из схемы ниже 2. Возвращай строго JSON в указанном формате 3. Если данные не найдены — ставь null 4. НЕ ПРИДУМЫВАЙ данные! 5. Дозаполняй только пустые/null поля`, userMessage: `Текущие данные (заполнено ${filledCount} из ${totalCount}, дозаполни остальное): ${JSON.stringify(data, null, 2)} Схема для ответа: ${schema}`, _meta: { type, filledCount, totalCount, caseType } } }]; ``` --- ### 3. prepare_rag_items — создание 3 items для RAG (НУЖНО ДОБАВИТЬ!) ```javascript // Code node: prepare_rag_items // Создаёт 3 items для RAG: user, project, offenders // Вставить между Code1 и Loop Over Items const src = $('Code1').first().json; // USER — из уже собранных данных Code1 const userData = src.user || {}; // PROJECT — пока пустой, RAG заполнит const projectData = { category: null, direction: null, agrprice: null, subject: null, agrdate: null, startdate: null, finishdate: null, country: null, hotel: null, transport: null, insurance: null, description: src.problem_description || null, }; // OFFENDERS — пока пустой массив, RAG найдёт const offendersData = []; // Выдаём 3 items для Loop Over Items return [ { json: { type: 'user', data: userData } }, { json: { type: 'project', data: projectData } }, { json: { type: 'offenders', data: offendersData } } ]; ``` --- ## ✅ Результат работы workflow Тестовый запуск на claim `509872e2-9666-4c5e-8ab7-2304dd6a5d18`: ### USER — полностью заполнен - firstname: Федор - secondname: Владимирович - lastname: Коробков - mobile: 79262306381 - email: help@clientright.ru - tgid: 295410106 - birthday: 1981-09-18 - birthplace: Москва - mailingstreet: МО, г. Балашиха, мкр. Железнодорожный, ул. Советская, д.20, кв. 52 ### PROJECT — основное заполнено - category: задержка ремонта/недоставка комплектующих и отказ в оказании услуги сборки - agrprice: **89620** (сумма договора) - subject: кровать-подиум Hemwood Base 180х200 и тумбы к ней - agrdate: **2025-08-09** (дата договора) - startdate: **2025-08-16** (дата доставки) - description: полное описание проблемы ### OFFENDERS — найдено 2 контрагента! **1. Продавец (seller):** - accountname: ИП Хациев Зелимхан Зелимханович - inn: 201471261963 (12 цифр — ИП ✅) - ogrn: 315774600000123 (15 цифр — ОГРНИП ✅) - website: raiton.ru **2. Исполнитель услуг (service_provider):** - accountname: АО «ОРМАТЕК» - inn: 7724890784 (10 цифр — юрлицо ✅) - email: kassa@ormatek.com --- ## 📋 TODO (следующие шаги) 1. [ ] Добавить ноду `prepare_rag_items` между Code1 и Loop Over Items 2. [ ] Добавить постобработку данных (валидация, исправление ошибок AI) 3. [ ] Сохранение результата в Redis для формы 4. [ ] Подключить к генерации формы заявления --- ## 📁 Связанные файлы - `ticket_form/docs/SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED_FIXED.sql` — SQL для сохранения документов - `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` — шаблон формы заявления