# 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.py` → `PY_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.py` → `DISCOVERY_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 на уровне воркера.