Files
Deal/.superpowers/sdd/channel-discovery/task-5-report.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

6.9 KiB
Raw Blame History

Task 5 — Отчёт: Оценка контента (язык, темы, fit по профилю задачи)

Статус

Реализовано и проверено (py_compile + офлайн-сценарий на временной БД в контейнере).

Файлы

  • Создан: backend/app/services/discovery_eval.py — чистая логика оценки (без карточек/очередей/обучения), поверх store, ml_client, ai_service.chat_json, pipeline.clean_short.

Что сделано

Интерфейсы брифа

  • detect_lang_ru(texts) — суммарная доля кириллицы (блок U+0400–U+04FF) среди всех str.isalpha()-букв выборки: >=0.15 → True, <=0.03 → False, между порогами или 0 букв → None.
  • group_by_topic(messages) — группировка по topic_id (None → "main"); [{"topic_id", "title", "messages": [...]}]; title — сниппет первого непустого текста темы (≤60 симв., whitespace схлопнут); группы отсортированы по числу сообщений (убыв.), порядок сообщений внутри группы — входной (хронологический из discovery_read).
  • evaluate_message(task, text) async — каскад:
    1. текст пустой/len(strip) < 10{"fit": False, "reason": "слишком короткое", "source": "heuristic"};
    2. ML: ml_client.is_enabled()predict(text); take и label=='spam'False, reason «ML: спам», source: "ml"; иначе ниже;
    3. ИИ: если aiEnabled и доступен ключ/локальный провайдер (см. Concerns) — один ai_service.chat_json(промпт, user="Сообщение:\n"+text[:4000]); {fit, reason} из ответа; любая ошибка → шаг 4;
    4. эвристика: любой ключ входит в clean_short(text).casefold(); reason совпал ключ "…" / нет совпадений с ключами, source: "heuristic".
  • evaluate_sample(task, messages) async — последовательно по сообщениям; {"fit_count", "total", "fit_ratio", "per_message": [{"text", "fit", "reason", "topic_id"}]}; topic_id в per_message нормализован None → "main" (стыкуется с ключами group_by_topic).
  • passed(ev, task)ev["total"] >= 3 и ev["fit_ratio"]*100 >= task["threshold"].
  • Промпт ИИ — константа _AI_PROMPT точно по тексту брифа (description/keywords подставляются через .format).

Вывод проверок

  1. cd /c/telbase && python -m py_compile backend/app/services/discovery_eval.pyPY_COMPILE OK (без ошибок).
  2. Сборка и офлайн-прогон на временной БД:
    • docker compose build app → образ собран;
    • MSYS_NO_PATHCONV=1 docker compose run --rm --no-deps -e PYTHONPATH=/srv -e LEADRADAR_DATA=/tmp/lr_eval --entrypoint python app /data/task5_check.pyDISCOVERY_EVAL OK:
      • detect_lang_ru(["Ищем python разработчика"]) is True; detect_lang_ru(["we need a python developer"]) is False; смесь 1/20 кириллицы → None; текст без букв → None;
      • group_by_topic: темы main/111/222, объединение 2 сообщений в 111, сортировка [111, main, 222], title «Топик A первый» (≤60), входной порядок внутри группы сохранён;
      • evaluate_message на задаче без ИИ/ML (aiEnabled=False, mlEnabled=False) → эвристика: {"fit": True, "reason": "совпал ключ "python"", "source": "heuristic"}; без ключа → False «нет совпадений с ключами»; «короче» → False «слишком короткое»;
      • aiEnabled=True без ключа провайдера → ИИ-ветка не вызывается (без сети), отвечает эвристика;
      • evaluate_sample: total=4/fit=2/ratio=0.5, per_message topic_id ["main","main",5,5]; passed: threshold 40 → True, 60 → False, выборка из 2 → False, пустая → ratio 0.0/False.
    • Временный файл data/task5_check.py удалён после прогона.
  3. Диагностика файла — без ошибок и предупреждений.

Concerns

  1. source для слишком коротких сообщений"heuristic": это первая ступень каскада (до ML/ИИ), но source по брифу — объединение heuristic|ml|ai, отдельного значения нет. Воркеру (Task 6) это не мешает; при необходимости подсчёта «отсевов по длине» лучше ориентироваться на reason.
  2. ИИ-ветка дополнительно проверяет наличие ключа (ai_service.provider_status(): keySet или local-провайдер), а не только aiEnabled — иначе chat_json при отсутствии ключа тратит ~6 c на ретраи и сыплет warning в лог на каждое сообщение. При любой ошибке/недоступности статуса — всё равно fallback на эвристику (как и требует бриф).
  3. topic_id в per_message нормализован None → "main", чтобы результат evaluate_sample стыковался с ключами group_by_topic. Task 6 при подсчёте «подходит X из N» по темам форума должен сравнивать ключ "main", а не None.
  4. title темы — сниппет первого непустого текста темы (первого во входном порядке), а не обязательно первого сообщения; topic_title из сообщений не используется (в discovery_read Task 4 его и нет). Если позже понадобится название темы из Telegram — добавить как fallback для пустых текстов.
  5. Лимиты на границе: тексту в ИИ обрезается до 4000 симв. (как в ai.filter_incoming), причина из ИИ — до 200 симв.; эвристика и длина считаются по полному тексту.
  6. Решения оценки нигде не логируются и не учитываются в счётчиках ml_client.track_decisions — модуль чистый; учёт/логирование при необходимости добавить в Task 6 на уровне воркера.