diff --git a/rag/functions/soma_evaluate.py b/rag/functions/soma_evaluate.py new file mode 100644 index 0000000..94b99ed --- /dev/null +++ b/rag/functions/soma_evaluate.py @@ -0,0 +1,117 @@ +# rag/functions/soma_evaluate.py +""" +SOMA-анализ (LLM-as-judge) для оценки качества ответа. +Оценивает ответ по критериям: релевантность, полнота, отсутствие галлюцинаций, +стиль, полезность. Возвращает оценки и вердикт. +""" + +import json +import logging +from typing import Dict, Any, Optional + +from rag.services.giga_client import GigaClient +from rag.config_models import AppConfig + +logger = logging.getLogger(__name__) + + +async def soma_evaluate( + query: str, + context: str, + answer: str, + config: AppConfig, + giga: GigaClient, + prompts: Dict[str, str], +) -> Dict[str, Any]: + """ + Оценивает ответ LLM с помощью другого вызова GigaChat. + + Аргументы: + query: исходный запрос пользователя. + context: контекст из базы знаний (может быть пустым). + answer: сгенерированный ответ. + config: объект конфигурации. + giga: клиент GigaChat для вызова. + prompts: словарь с содержимым промптов (ключ 'soma_evaluate'). + + Возвращает: + dict с полями: + scores: dict с оценками по критериям (числа от 1 до 5). + overall_score: средняя оценка (float). + verdict: 'pass' или 'fail' (в зависимости от порога). + feedback: текстовое обоснование. + error: строка ошибки (если есть). + """ + default_result = { + "scores": { + "relevance": 3.0, + "completeness": 3.0, + "no_hallucination": 3.0, + "style": 3.0, + "usefulness": 3.0, + }, + "overall_score": 3.0, + "verdict": "fail", + "feedback": "Оценка не удалась, возвращены значения по умолчанию.", + "error": None, + } + + prompt_template = prompts.get("soma_evaluate") + if not prompt_template: + logger.error("Промпт для SOMA-оценки не найден") + default_result["error"] = "Отсутствует промпт soma_evaluate" + return default_result + + # Формируем полный промпт + full_prompt = prompt_template.format( + query=query, + context=context[:3000] if context else "Нет контекста", + answer=answer, + ) + + # Параметры из конфига (можно сделать отдельную секцию) + temperature = getattr(config, 'soma_temperature', 0.1) # низкая для детерминизма + timeout = getattr(config, 'soma_timeout', 30) + + try: + response = await giga.chat( + history=[], + query=full_prompt, + system_prompt=None, + file_id=None, + temperature=temperature, + timeout=timeout, # нужно добавить timeout в GigaClient.chat (если нет, игнорировать) + ) + # Ожидаем JSON + data = json.loads(response.strip()) + scores = data.get("scores", {}) + feedback = data.get("feedback", "") + + # Приводим оценки к числам + for key in scores: + try: + scores[key] = float(scores[key]) + except (ValueError, TypeError): + scores[key] = 3.0 + overall = sum(scores.values()) / len(scores) if scores else 3.0 + + # Порог можно задать в конфиге + threshold = getattr(config, 'soma_threshold', 3.5) + verdict = "pass" if overall >= threshold else "fail" + + return { + "scores": scores, + "overall_score": overall, + "verdict": verdict, + "feedback": feedback, + "error": None, + } + + except json.JSONDecodeError as e: + logger.error(f"Ошибка парсинга JSON от GigaChat: {e}, ответ: {response[:200]}") + default_result["error"] = f"Ошибка парсинга JSON: {e}" + return default_result + except Exception as e: + logger.exception(f"Ошибка при вызове SOMA-оценки: {e}") + default_result["error"] = str(e) + return default_result \ No newline at end of file