Files
service-finder/backend/app/services/ai_service.py

179 lines
8.7 KiB
Python
Executable File

# /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])