Files
aiform_prod/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md
AI Assistant 080e7ec105 feat: Получение cf_2624 из MySQL и блокировка полей при подтверждении данных
- Добавлен сервис CrmMySQLService для прямого подключения к MySQL CRM
- Обновлён метод get_draft() для получения cf_2624 напрямую из БД
- Реализована блокировка полей (readonly) при contact_data_confirmed = true
- Добавлен выбор банка для СБП выплат с динамической загрузкой из API
- Обновлена документация по работе с cf_2624 и MySQL
- Добавлен network_mode: host в docker-compose для доступа к MySQL
- Обновлены компоненты формы для поддержки блокировки полей
2025-12-04 12:22:23 +03:00

15 KiB
Raw Blame History

Лог сессии: Настройка 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 (следующие шаги)

  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 — шаблон формы заявления