Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
66 lines
4.4 KiB
C#
66 lines
4.4 KiB
C#
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)
|
||
}
|
||
}
|