import json import logging from typing import Optional, Dict, Any import httpx from app import config from app.database import get_cached_sentence_translation, save_sentence_translation logger = logging.getLogger(__name__) def build_translation_prompt(sentence: str, target_word: str = "") -> str: clean_sentence = sentence.strip() clean_word = target_word.strip() if clean_word: return ( "You are a professional English-to-Russian translator.\n" "Translate the following English sentence into clear, natural, and idiomatic Russian.\n" f"Also provide the precise in-context meaning and translation of the target word/phrase '{clean_word}'.\n\n" f'Sentence: "{clean_sentence}"\n' f'Target word: "{clean_word}"\n\n' "Respond ONLY with a valid JSON object in this format:\n" "{\n" ' "sentence_ru": "перевод предложения на русский язык",\n' ' "word_meaning": "значение слова в этом контексте"\n' "}" ) else: return ( "You are a professional English-to-Russian translator.\n" "Translate the following English sentence into clear, natural, and idiomatic Russian.\n\n" f'Sentence: "{clean_sentence}"\n\n' "Respond ONLY with a valid JSON object in this format:\n" "{\n" ' "sentence_ru": "перевод предложения на русский язык",\n' ' "word_meaning": ""\n' "}" ) async def translate_sentence_ai(sentence: str, target_word: str = "") -> Dict[str, Any]: """ Translates a sentence using local AI (Ollama), with SQLite instant-cache lookup. Returns: { "original_sentence": "...", "sentence_ru": "...", "target_word": "...", "word_meaning": "...", "source": "ollama:model" | "cache" | "fallback", "cached": bool } """ clean_sentence = sentence.strip() clean_word = target_word.strip() if not clean_sentence: return { "original_sentence": "", "sentence_ru": "", "target_word": clean_word, "word_meaning": "", "source": "none", "cached": False, } # 1. Fast SQLite Cache Lookup (0.01 ms) cached = get_cached_sentence_translation(clean_sentence, clean_word) if cached: return { "original_sentence": clean_sentence, "sentence_ru": cached["translated_text"], "target_word": clean_word, "word_meaning": cached.get("word_meaning") or "", "source": cached.get("source") or "cache", "cached": True, } # 2. Local AI Generation (Ollama) prompt = build_translation_prompt(clean_sentence, clean_word) payload = { "model": config.OLLAMA_MODEL, "prompt": prompt, "stream": False, "format": "json", "options": { "temperature": 0.2, "num_predict": 256, }, } sentence_ru = "" word_meaning = "" source = f"ollama:{config.OLLAMA_MODEL}" try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload) if resp.status_code == 200: data = resp.json() raw_response = data.get("response", "{}") try: parsed = json.loads(raw_response) sentence_ru = parsed.get("sentence_ru", "").strip() word_meaning = parsed.get("word_meaning", "").strip() except Exception: # In case LLM emitted unescaped markdown or wrapper clean_raw = raw_response.strip() if clean_raw.startswith("```json"): clean_raw = clean_raw[7:] if clean_raw.startswith("```"): clean_raw = clean_raw[3:] if clean_raw.endswith("```"): clean_raw = clean_raw[:-3] parsed = json.loads(clean_raw.strip()) sentence_ru = parsed.get("sentence_ru", "").strip() word_meaning = parsed.get("word_meaning", "").strip() else: logger.warning(f"Ollama returned HTTP {resp.status_code}: {resp.text}") except Exception as e: logger.warning(f"Ollama connection error: {e}") # 3. Graceful Fallback if Ollama is unreachable if not sentence_ru: sentence_ru = "(Локальный ИИ временно недоступен)" source = "fallback" # Save to SQLite Cache if valid translation obtained if sentence_ru and not sentence_ru.startswith("("): save_sentence_translation( sentence=clean_sentence, translated_text=sentence_ru, target_word=clean_word, word_meaning=word_meaning, source=source, ) return { "original_sentence": clean_sentence, "sentence_ru": sentence_ru, "target_word": clean_word, "word_meaning": word_meaning, "source": source, "cached": False, } async def get_ollama_status() -> Dict[str, Any]: """Checks Ollama server connectivity and lists installed models.""" try: async with httpx.AsyncClient(timeout=3.0) as client: resp = await client.get(f"{config.OLLAMA_BASE_URL}/api/tags") if resp.status_code == 200: data = resp.json() models = [m.get("name") for m in data.get("models", [])] return { "online": True, "models": models, "active_model": config.OLLAMA_MODEL, } except Exception: pass return { "online": False, "models": [], "active_model": config.OLLAMA_MODEL, }