180 lines
8.8 KiB
Python
Executable File
180 lines
8.8 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 a konfigurációból
|
|
enable_fallback_setting = await config.get_setting(db, "ENABLE_AI_FALLBACK", default="false")
|
|
enable_fallback = str(enable_fallback_setting).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]) |