### Task 5: Оценка контента (язык, темы, fit по профилю задачи) **Files:** - Create: `backend/app/services/discovery_eval.py` **Interfaces:** - Consumes: `store`, `ml_client`, `ai_service` (chat_json), `pipeline.clean_short`. - Produces: - `def detect_lang_ru(texts: list[str]) -> bool | None` — доля кириллических букв от всех букв в сумме: `>=0.15 → True`; `<=0.03 → False`; между порогами → `None` (неопределённо). - `def group_by_topic(messages: list[dict]) -> list[dict]` — группировка по `topic_id` (None → "main"); возвращает `[{"topic_id", "title", "messages": [...]}]`, title = сниппет первого текста темы (≤60 симв.), сортировка по количеству сообщений (убыв.). - `async def evaluate_message(task: dict, text: str) -> dict` — `{"fit": bool, "reason": str, "source": "heuristic"|"ml"|"ai"}`: 1) текст пустой/длина <10 → fit False «слишком короткое»; 2) ML: если `ml_client.is_enabled()` и прогноз `take` и `label=='spam'` → fit False «ML: спам»; 3) ИИ: если `aiEnabled` → один JSON-вызов `ai_service.chat_json(промпт, user=text)` с промптом из описания задачи и ключей (`{fit, reason}`); ошибка → шаг 4; 4) эвристика: fit = любой ключ входит в `clean_short(text)` casefold; reason «совпал ключ "…"» / «нет совпадений с ключами». - `async def evaluate_sample(task: dict, messages: list[dict]) -> dict` — последовательно по каждому сообщению; вернуть `{"fit_count": int, "total": int, "fit_ratio": float, "per_message": [{"text": …, "fit", "reason", "topic_id"}]}`. - `def passed(ev: dict, task: dict) -> bool` — `ev["total"] >= 3 and ev["fit_ratio"]*100 >= task["threshold"]`. - [ ] **Step 1: Реализовать модуль**. Промпт ИИ (внутри модуля, константа): `Оцени, относится ли сообщение к сфере/задаче. Описание: {description}. Ключи: {keywords}. Верни JSON {"fit": 0|1, "reason": "краткая причина"}.` - [ ] **Step 2: Проверить на временной БД** (без сети): `detect_lang_ru(["Ищем python разработчика"]) is True`; `detect_lang_ru(["we need a python developer"]) is False`; `group_by_topic` объединяет по topic_id и сортирует; `evaluate_message` на задаче без ИИ/ML возвращает эвристический fit по ключу. ---