Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
// ml.proto — контракт между ядром Deal и ml-service (этап 6).
//
// Инкрементальная наивно-байесовская модель по терминам, 1:1 с python
// mlservice/model.py (predict L184293, status L325345, reset L348354,
// learn_batch L147173) и DTO ядра Deal.Contracts.Integrations.Models
// (MlPredictResultDto/MlServiceStatusDto/MlEvalDto/MlResetResultDto).
// Модель per-tenant: пул в ml-service, файл SQLite data/ml/<tenantId>.sqlite
// (Ruling 4). Обучение ядро шлёт батчами из очереди ml_outbox
// (MlOutboxFlushScheduler, Ruling 6).
//
// Каждый RPC обязан нести в gRPC-metadata два заголовка (Ruling 1):
// tenant-id — id тенанта (строка; модель тенанта — в пуле сервиса);
// service-token — общий токен сервисов (env DEAL_SERVICE_TOKEN); неверный/
// пустой → UNAUTHENTICATED.
//
// Ошибки домена — gRPC-статусы с detail = текст причины 1:1 (Ruling 1):
// INVALID_ARGUMENT — невалидный запрос (пустой text и т.п.);
// UNAVAILABLE — хранилище модели недоступно (ядро отвечает «не уверен»,
// Ruling 6 — кэш reachable 15 с).
//
// Семантика неготовой модели: Predict НЕ ошибка — модель без опыта отвечает
// фиксированным «не уверен»: take=false, label пуст, scores пуст, hits=0,
// ready=false, margin пуст, terms пуст, type пуст (Ruling 5 этапа 2, 1:1).
//
// Deadlines (клиент ядра): Predict — 5 с; Status/Reset — 10 с; TrainBatch — 30 с
// (батч ≤100 примеров, одна транзакция).
syntax = "proto3";
package deal.ml.v1;
option csharp_namespace = "Deal.Grpc.Ml";
service MlService {
// Предсказание по тексту сообщения (model.py predict L184293).
// take/label/scores/hits/margin/terms/type осмысленны только при take=true;
// scores — до 5 лучших «класс → вес» (round 3), margin — адаптивный порог
// (0.9/0.7/0.5/0.35 после 0/60/150/400 примеров), terms — узнанные термины
// класса-победителя, type — решение о типе заявки (t:hire/t:order).
rpc Predict(PredictRequest) returns (PredictReply);
// Статус модели тенанта (model.py status L325345): ready/classes/learned/eval.
// classes — «label → вес» (round 2, по убыванию); eval — самооценка по окну
// последних подтверждённых решений (EVAL_WINDOW). Модель создаётся лениво
// по первому обращению (Ruling 4) — отсутствие опыта это НЕ ошибка.
rpc Status(StatusRequest) returns (StatusReply);
// Полный сброс модели тенанта (model.py reset L348354): очистка классов,
// терминов и журнала самооценки + пересоздание файла. Ok=true; мягкая
// ошибка — Ok=false + error (ядро чистит свою ml_outbox только при успехе).
rpc Reset(ResetRequest) returns (ResetReply);
// Пакетное обучение (model.py learn_batch L147173): одна транзакция +
// пакетные вставки терминов; самооценка по действиям пользователя (delta=1,
// не t:*) до применения. Ответ — число применённых примеров.
rpc TrainBatch(TrainBatchRequest) returns (TrainBatchReply);
}
message PredictRequest {
// Текст сообщения (ядро передаёт уже обрезанный/нормализованный, как
// ml_routes.py L8690; пустой/пробельный — не ошибка: ответ «не уверен»).
string text = 1;
}
message PredictReply {
// True — модель уверена (take) и решение можно использовать без ИИ.
bool take = 1;
// Класс решения: id колонки канбана (b_…) или "spam"; пуст, если не уверена.
optional string label = 2;
// Веса классов: «label → вес» (до 5 лучших; пуст у неготовой модели).
map<string, double> scores = 3;
// Сколько терминов класса-победителя модель узнала в тексте.
int32 hits = 4;
// Модель обучена (набрала MIN_TOTAL/MIN_WINNER/MIN_WINNER_SPAM) и может
// принимать решения.
bool ready = 5;
// Порог уверенности решения (адаптивный margin, 2 знака); пуст — нет решения.
optional double margin = 6;
// Узнанные термины класса-победителя (подсказка структуры карточки, ≤8).
repeated string terms = 7;
// Решение о типе заявки (hire/order); пуст — модель тип не определила.
TypeDecision type = 8;
}
// Решение ML о типе заявки (predict L233238; MlTypeDecisionDto).
message TypeDecision {
// True — модель уверена в типе.
bool take = 1;
// Тип: "hire" | "order".
string label = 2;
// Внутренний класс ML: "t:hire" | "t:order" (не показывается UI).
string value = 3;
// Запас уверенности (margin, 2 знака).
double margin = 4;
}
message StatusRequest {}
message StatusReply {
// Модель готова принимать решения.
bool ready = 1;
// Классы модели: «label → вес» (round 2; пуст, пока нет обучения).
map<string, double> classes = 2;
// Всего примеров, на которых модель обучалась (сумма по классам).
int32 learned = 3;
// Самооценка модели по последним подтверждённым решениям.
ModelEval eval = 4;
}
// Окно самооценки модели (model.py status L329339; MlEvalDto).
message ModelEval {
// Решений в окне самооценки (последние EVAL_WINDOW).
int32 count = 1;
// Из них совпавших с действием пользователя.
int32 correct = 2;
// Доля верных (correct/count, 0..1; 0 при пустом окне).
double accuracy = 3;
}
message ResetRequest {}
message ResetReply {
// True — модель сброшена (и ядро очищает свою очередь обучения).
bool ok = 1;
// Текст ошибки при сбое сброса (пуст при успехе) — мягкая ошибка.
optional string error = 2;
}
message TrainBatchRequest {
// Примеры обучения (1 транзакция на батч; ядро шлёт ≤100 за цикл, Ruling 6).
repeated TrainExample items = 1;
}
// Один обучающий пример (строка ml_outbox ядра: text/label/delta).
message TrainExample {
// Текст примера (source_msg карточки или title).
string text = 1;
// Метка: id доски (b_…), "spam" либо тип "t:hire"/"t:order".
string label = 2;
// Вес сигнала: 1.0 — действие пользователя; −1.0 — снять метку;
// 0.4/0.6 — сигналы ИИ/правил (этапы 4/6).
double delta = 3;
}
message TrainBatchReply {
// Число применённых примеров (= len(items) при успехе).
int32 learned = 1;
}