Files
Deal/src/core/Deal.Modules.Pipeline/Application/AiCardMapper.cs
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

66 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Parse;
namespace Deal.Modules.Pipeline.Application;
/// <summary>
/// Маппинг локального разбора в контрактный <see cref="AiParsedCardDto"/> (pipeline.py _local_fields L718–798 → raw-словарь карточки).
/// </summary>
/// <remarks>
/// Единый модульный способ превратить <see cref="LocalParsedFields"/> (LocalFieldsParser, Ruling 7) в форму
/// разбора, которую ест <see cref="CardComposer"/>: бюджет нормализуется <see cref="BudgetNormalizer.Normalize"/>
/// (ai.py clean_budget L316–326), контакты квалифицируются <see cref="ContactsQualifier.Build"/> (build_contacts
/// L389–421), блок «О заявке» — только legacy-суть (<see cref="LocalParsedFields.Summary"/>), тип — маркерная
/// гипотеза (<c>is_vacancy_known=false</c>), доска — inbox (<c>board=null</c>: «смысловые колонки до ИИ не
/// назначаем», python L954–958/L796–797). Маппинг используют локальные пути воркера (Ruling 8: aiEnabled=false,
/// сбой ИИ, ML-ветка с локальными полями) и адаптер <c>LocalAiClassifier</c> (Ruling 5 — эталон «ядро владельца +
/// тонкий адаптер»), поэтому он живёт в модуле, а не в Infrastructure: один источник истины для обеих сторон.
/// </remarks>
public static class AiCardMapper
{
/// <summary>
/// Строит разбор карточки из локальных полей и исходного текста (форма классификатора Ruling 5).
/// </summary>
/// <remarks>
/// Квалификация контактов идёт по строке <see cref="LocalParsedFields.Contacts"/> с fallback-поиском в тексте
/// (python build_contacts L389–421, ≤6, боты/сервисные ссылки отбрасываются) — как при сборке карточки
/// CardComposer'ом, чтобы разбор и карточка не расходились. Вызывающий при необходимости перекрывает
/// поля через <c>with</c> (ML-ветка: доска/тип ML, доклад terms в стек; локальные пути — нет).
/// </remarks>
/// <param name="fields">Локальные поля (заголовок/суть/стек/бюджет/контакты/признак найма).</param>
/// <param name="text">Исходный текст сообщения (fallback-источник кандидатов контактов).</param>
/// <returns>Контрактный разбор: бюджет нормализован, контакты квалифицированы, board=null, is_vacancy_known=false.</returns>
public static AiParsedCardDto FromLocal(LocalParsedFields fields, string text)
{
CardBudgetDto? normalized = BudgetNormalizer.Normalize(fields.Budget);
AiBudgetDto? budget = normalized is null ? null : new AiBudgetDto(normalized.From, normalized.To, normalized.Cur);
IReadOnlyList<CardContactDto> qualified = ContactsQualifier.Build(fields.Contacts, text);
var contacts = new List<AiContactDto>(qualified.Count);
foreach (CardContactDto contact in qualified)
{
contacts.Add(new AiContactDto(contact.Type, contact.Value));
}
return new AiParsedCardDto(
Title: fields.Title,
Company: null,
Format: null,
Task: null,
Requirements: null,
Plus: null,
Conditions: null,
Summary: fields.Summary,
Stack: fields.Stack,
Budget: budget,
Contacts: contacts,
IsVacancy: fields.IsVacancy,
IsVacancyKnown: false, // маркерная гипотеза — тип подтверждает только ИИ по контексту (python L796)
IsSpam: false,
Board: null); // смысловые колонки до ИИ не назначаем (python L797)
}
}