Frontend: - Динамическая загрузка 226 банков из NSPK API - Выбор банка добавлен в Step3Payment (новая заявка) - Выбор банка добавлен в generateConfirmationFormHTML (редактирование) - Поля: bank_id (ID из NSPK) и bank_name (название для отображения) Backend: - Добавлено поле bank_id в ClaimCreateRequest API: - http://212.193.27.93/api/payouts/dictionaries/nspk-banks Изменения: - ticket_form/frontend/src/components/form/Step3Payment.tsx - ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts - ticket_form/frontend/src/pages/ClaimForm.tsx - ticket_form/backend/app/api/models.py
15 KiB
15 KiB
Лог сессии: Настройка 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
// 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
// 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 (НУЖНО ДОБАВИТЬ!)
// 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 (следующие шаги)
- Добавить ноду
prepare_rag_itemsмежду Code1 и Loop Over Items - Добавить постобработку данных (валидация, исправление ошибок AI)
- Сохранение результата в Redis для формы
- Подключить к генерации формы заявления
📁 Связанные файлы
ticket_form/docs/SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED_FIXED.sql— SQL для сохранения документовticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts— шаблон формы заявления