# /opt/docker/dev/service_finder/backend/app/services/ai_service.py import os import json import logging import asyncio import httpx from typing import Dict, Any, Optional, List from sqlalchemy import select from app.db.session import AsyncSessionLocal from app.models.system import SystemParameter from app.services.config_service import config logger = logging.getLogger("AI-Service-2.2-Gateway") class AIService: """ Sentinel Master AI Service 2.2 - Multi-Agent & Fallback Gateway. Felelős az LLM hívásokért. Ha a helyi GPU (Ollama) túlterhelt, automatikusan áttér a felhős (Groq/Gemini) megoldásokra. """ @classmethod async def _execute_ai_call(cls, db, prompt: str, model_key: str = "text", images: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: """ Központi AI végrehajtó intelligens teherelosztással (Load Balancing). """ # 1. BEÁLLÍTÁSOK LEKÉRÉSE base_url = await config.get_setting(db, "ai_ollama_url", default="http://ollama:11434/api/generate") delay = await config.get_setting(db, "AI_REQUEST_DELAY", default=0.1) temp = await config.get_setting(db, "ai_temperature", default=0.1) # A helyi timeout-ot levesszük 25 mp-re. Ha az RTX 3090 eddig nem végez, # akkor biztosan tele van a várólistája, és azonnal átváltunk felhőbe. local_timeout = await config.get_setting(db, "ai_timeout_local", default=25.0) # Fallback engedélyezése az .env fájlból enable_fallback = os.getenv("ENABLE_AI_FALLBACK", "false").lower() == "true" # Helyi modellek definiálása default_model = "llama3.2-vision:latest" if model_key == "vision" else "qwen2.5-coder:14b" model_name = await config.get_setting(db, f"ai_model_{model_key}", default=default_model) await asyncio.sleep(float(delay)) # 2. ELSŐDLEGES PRÓBA: Helyi Ollama szerver (Ingyenes, VRAM alapú) try: payload = { "model": model_name, "prompt": prompt, "stream": False, "format": "json", "options": {"temperature": float(temp)} } if images: payload["images"] = images logger.info(f"🧠 Helyi AI ({model_name}) hívása indult...") async with httpx.AsyncClient(timeout=float(local_timeout)) as client: response = await client.post(base_url, json=payload) response.raise_for_status() raw_res = response.json().get("response", "{}") return json.loads(raw_res) except (httpx.ReadTimeout, httpx.ConnectError) as e: logger.warning(f"⚠️ Helyi GPU túlterhelt vagy lassú (Timeout/ConnectError). Váltás Fallback módba!") except json.JSONDecodeError as je: logger.error(f"❌ Helyi AI JSON parszolási hiba: {je}. Váltás Fallback módba!") except Exception as e: logger.error(f"❌ Helyi AI váratlan hiba: {e}. Váltás Fallback módba!") # 3. MÁSODLAGOS PRÓBA: Hibrid Fallback (Csak ha engedélyezve van és a helyi elbukott) if enable_fallback: return await cls._fallback_ai_call(prompt, model_key, images, float(temp)) else: logger.error("❌ A Fallback ki van kapcsolva (.env), a feladat feldolgozása sikertelen.") return None @classmethod async def _fallback_ai_call(cls, prompt: str, model_key: str, images: Optional[List[str]], temp: float) -> Optional[Dict[str, Any]]: """ Külső API hívások (Groq és Gemini) vészhelyzet esetére. """ if model_key == "vision" and images: # --------------------------------------------------------- # VISION FALLBACK: GOOGLE GEMINI 1.5 FLASH (Képelemzés/OCR) # --------------------------------------------------------- gemini_api_key = os.getenv("GEMINI_API_KEY") if not gemini_api_key: logger.error("❌ Hiányzik a GEMINI_API_KEY! Képtelen Fallback módban OCR-t végezni.") return None logger.info("☁️ Google Gemini 1.5 API (Vision) hívása...") url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={gemini_api_key}" payload = { "contents": [{ "parts": [ {"text": prompt + "\nKérlek, SZIGORÚAN csak érvényes JSON objektummal válaszolj, Markdown formázás (```json) nélkül!"}, {"inline_data": { "mime_type": "image/jpeg", # A korábbi kódban JPEG-be konvertáljuk "data": images[0] }} ] }], "generationConfig": { "temperature": temp, "response_mime_type": "application/json" # Gemini specifikus JSON kényszerítés } } try: async with httpx.AsyncClient(timeout=30.0) as client: resp = await client.post(url, json=payload) resp.raise_for_status() data = resp.json() text_res = data["candidates"][0]["content"]["parts"][0]["text"] return json.loads(text_res) except Exception as e: logger.error(f"❌ Gemini Fallback kritikus hiba: {e}") return None else: # --------------------------------------------------------- # TEXT FALLBACK: GROQ CLOUD (Szövegelemzés / Web Scraping) # --------------------------------------------------------- groq_api_key = os.getenv("GROQ_API_KEY") if not groq_api_key: logger.error("❌ Hiányzik a GROQ_API_KEY! Képtelen Fallback módban szöveget elemezni.") return None logger.info("☁️ Groq Cloud (Llama 3 8B) hívása...") url = "[https://api.groq.com/openai/v1/chat/completions](https://api.groq.com/openai/v1/chat/completions)" headers = { "Authorization": f"Bearer {groq_api_key}", "Content-Type": "application/json" } payload = { "model": "llama3-8b-8192", # Villámgyors szöveges modell "messages": [{"role": "user", "content": prompt}], "temperature": temp, "response_format": {"type": "json_object"} # Groq specifikus JSON kényszerítés } try: async with httpx.AsyncClient(timeout=20.0) as client: resp = await client.post(url, headers=headers, json=payload) resp.raise_for_status() data = resp.json() text_res = data["choices"][0]["message"]["content"] return json.loads(text_res) except Exception as e: logger.error(f"❌ Groq Fallback kritikus hiba: {e}") return None # --- A TÖBBI METÓDUS VÁLTOZATLAN MARAD --- @classmethod async def get_gold_data_from_research(cls, make: str, model: str, raw_context: str) -> Optional[Dict[str, Any]]: async with AsyncSessionLocal() as db: template = await config.get_setting(db, "ai_prompt_gold_data", default="Extract technical car data for {make} {model} from: {context}") full_prompt = template.format(make=make, model=model, context=raw_context) return await cls._execute_ai_call(db, full_prompt, model_key="text") @classmethod async def get_clean_vehicle_data(cls, make: str, raw_model: str, sources: Dict[str, Any]) -> Optional[Dict[str, Any]]: async with AsyncSessionLocal() as db: template = await config.get_setting(db, "ai_prompt_normalization", default="Normalize car model names: {make} {model}. Sources: {sources}") full_prompt = template.format(make=make, model=raw_model, sources=json.dumps(sources)) return await cls._execute_ai_call(db, full_prompt, model_key="text") @classmethod async def process_ocr_document(cls, doc_type: str, base64_image: str) -> Optional[Dict[str, Any]]: async with AsyncSessionLocal() as db: template = await config.get_setting(db, f"ai_prompt_ocr_{doc_type}", default="Analyze this {doc_type} image and return structured JSON data.") full_prompt = template.format(doc_type=doc_type) # Itt mondjuk meg a diszpécsernek, hogy ez egy Vision feladat! return await cls._execute_ai_call(db, full_prompt, model_key="vision", images=[base64_image])