"""Клиент для Ollama HTTP API — LLM анализ парафраза и суммаризация.""" import json import logging import httpx from app.config import settings logger = logging.getLogger(__name__) class OllamaClient: """HTTP клиент для Ollama LLM.""" def __init__(self) -> None: self.base_url = settings.OLLAMA_URL self.model = "llama3:8b" self.timeout = 60.0 # секунд def check_paraphrase(self, text_a: str, text_b: str) -> dict: """ Проверить является ли text_b парафразом text_a с помощью LLM. Args: text_a: Исходный текст из базы данных (до 500 символов) text_b: Проверяемый фрагмент (до 500 символов) Returns: dict с полями: - is_paraphrase: bool - confidence: float 0.0-1.0 - reason: str — краткое объяснение """ prompt = f"""Ты эксперт по академическому плагиату. Определи, является ли Текст B парафразом Текста A. Текст A (источник): {text_a[:500]} Текст B (проверяемый): {text_b[:500]} Критерии парафраза: передача тех же идей другими словами, перефразировка без ссылки на источник. Ответь ТОЛЬКО валидным JSON без пояснений и markdown: {{"is_paraphrase": true/false, "confidence": 0.0-1.0, "reason": "краткое объяснение на русском"}}""" try: response = httpx.post( f"{self.base_url}/api/generate", json={ "model": self.model, "prompt": prompt, "stream": False, "format": "json", "options": { "temperature": 0.1, # Детерминированный вывод "num_predict": 200, }, }, timeout=self.timeout, ) response.raise_for_status() result = response.json() llm_response = result.get("response", "{}") # Парсим JSON из ответа parsed = json.loads(llm_response) return { "is_paraphrase": bool(parsed.get("is_paraphrase", False)), "confidence": float(parsed.get("confidence", 0.0)), "reason": str(parsed.get("reason", "")), } except (httpx.TimeoutException, httpx.ConnectError) as e: logger.warning(f"Ollama недоступна: {e}") return {"is_paraphrase": False, "confidence": 0.0, "reason": "LLM недоступна"} except (json.JSONDecodeError, KeyError) as e: logger.warning(f"Ошибка парсинга ответа Ollama: {e}") return {"is_paraphrase": False, "confidence": 0.0, "reason": "Ошибка парсинга ответа"} except Exception as e: logger.error(f"Неожиданная ошибка при обращении к Ollama: {e}") return {"is_paraphrase": False, "confidence": 0.0, "reason": str(e)} def summarize(self, title: str, abstract: str, lang: str = "ru") -> str: """ Сгенерировать краткое академическое изложение статьи. Args: title: Название статьи abstract: Аннотация статьи lang: Язык ответа (ru/en) Returns: Краткое изложение на указанном языке """ lang_instruction = "на русском языке" if lang == "ru" else "in English" prompt = f"""Сделай краткое академическое изложение {lang_instruction}. Название: {title} Аннотация: {abstract[:1000]} Изложение должно: - Содержать 3-5 предложений - Передавать основную идею и результаты - Быть написано академическим стилем - НЕ копировать текст дословно Ответ:""" try: response = httpx.post( f"{self.base_url}/api/generate", json={ "model": self.model, "prompt": prompt, "stream": False, "options": { "temperature": 0.3, "num_predict": 300, }, }, timeout=self.timeout, ) response.raise_for_status() return response.json().get("response", "").strip() except Exception as e: logger.error(f"Ошибка суммаризации через Ollama: {e}") return abstract[:500] if abstract else title def is_available(self) -> bool: """Проверить доступность Ollama сервера.""" try: response = httpx.get(f"{self.base_url}/api/tags", timeout=5.0) return response.status_code == 200 except Exception: return False