141 lines
5.5 KiB
Python
141 lines
5.5 KiB
Python
import os
|
|
import json
|
|
import logging
|
|
import asyncio
|
|
import re
|
|
import base64
|
|
import httpx
|
|
from typing import Dict, Any, Optional, List
|
|
from sqlalchemy import select
|
|
from app.db.session import SessionLocal
|
|
from app.models import SystemParameter
|
|
|
|
logger = logging.getLogger("AI-Service")
|
|
|
|
class AIService:
|
|
"""
|
|
AI Service v1.3.5 - Private High-Performance Edition
|
|
- Engine: Local Ollama (GPU Accelerated)
|
|
- Features: DVLA Integration, 50-char Normalization, Private OCR
|
|
"""
|
|
|
|
# A Docker belső hálózatán a szerviznév 'ollama'
|
|
OLLAMA_BASE_URL = "http://ollama:11434/api/generate"
|
|
TEXT_MODEL = "vehicle-pro"
|
|
VISION_MODEL = "llava:7b"
|
|
DVLA_API_KEY = os.getenv("DVLA_API_KEY")
|
|
|
|
@classmethod
|
|
async def get_config_delay(cls) -> float:
|
|
"""Késleltetés lekérése az adatbázisból."""
|
|
try:
|
|
async with SessionLocal() as db:
|
|
stmt = select(SystemParameter).where(SystemParameter.key == "AI_REQUEST_DELAY")
|
|
res = await db.execute(stmt)
|
|
param = res.scalar_one_or_none()
|
|
return float(param.value) if param else 0.1
|
|
except Exception:
|
|
return 0.1
|
|
|
|
@classmethod
|
|
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str, sources: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
"""Robot 2: Adat-összefésülés és normalizálás."""
|
|
# Várjunk egy kicsit a GPU kímélése érdekében
|
|
await asyncio.sleep(await cls.get_config_delay())
|
|
|
|
prompt = f"""
|
|
FELADAT: Normalizáld a jármű adatait több forrás alapján.
|
|
GYÁRTÓ: {make}
|
|
NYERS MODELLNÉV: {raw_model}
|
|
FORRÁSOK NYERS ADATAI: {json.dumps(sources, ensure_ascii=False)}
|
|
|
|
SZIGORÚ SZABÁLYOK:
|
|
1. 'marketing_name': MAXIMUM 50 KARAKTER!
|
|
2. 'synonyms': Gyűjtsd ide az összes többi névváltozatot.
|
|
3. 'technical_code': Keresd meg a gyári kódokat.
|
|
|
|
VÁLASZ FORMÁTUM (Csak tiszta JSON):
|
|
{{
|
|
"marketing_name": "string (max 50)",
|
|
"synonyms": ["string"],
|
|
"technical_code": "string",
|
|
"ccm": int,
|
|
"kw": int,
|
|
"euro_class": int,
|
|
"year_from": int,
|
|
"year_to": int vagy null,
|
|
"maintenance": {{
|
|
"oil_type": "string",
|
|
"oil_qty": float,
|
|
"spark_plug": "string"
|
|
}},
|
|
"is_duplicate_potential": bool
|
|
}}
|
|
"""
|
|
|
|
payload = {
|
|
"model": cls.TEXT_MODEL,
|
|
"prompt": prompt,
|
|
"stream": False,
|
|
"format": "json",
|
|
"options": {"temperature": 0.1}
|
|
}
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=90.0) as client:
|
|
logger.info(f"📡 AI kérés küldése: {make} {raw_model}...")
|
|
response = await client.post(cls.OLLAMA_BASE_URL, json=payload)
|
|
response.raise_for_status()
|
|
res_json = response.json()
|
|
clean_data = json.loads(res_json.get("response", "{}"))
|
|
|
|
if clean_data.get("marketing_name"):
|
|
clean_data["marketing_name"] = clean_data["marketing_name"][:50].strip()
|
|
|
|
return clean_data
|
|
except Exception as e:
|
|
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
|
|
return None
|
|
|
|
@classmethod
|
|
async def get_dvla_data(cls, vrm: str) -> Optional[Dict[str, Any]]:
|
|
"""Brit rendszám alapú adatok lekérése."""
|
|
if not cls.DVLA_API_KEY: return None
|
|
url = "https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles"
|
|
headers = {"x-api-key": cls.DVLA_API_KEY, "Content-Type": "application/json"}
|
|
try:
|
|
async with httpx.AsyncClient() as client:
|
|
resp = await client.post(url, json={"registrationNumber": vrm}, headers=headers)
|
|
return resp.json() if resp.status_code == 200 else None
|
|
except Exception as e:
|
|
logger.error(f"❌ DVLA API hiba: {e}")
|
|
return None
|
|
|
|
@classmethod
|
|
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
|
|
"""Robot 3: Helyi OCR és dokumentum elemzés (Llava:7b)."""
|
|
await asyncio.sleep(await cls.get_config_delay())
|
|
prompts = {
|
|
"identity": "Extract ID card data (name, id_number, expiry) as JSON.",
|
|
"vehicle_reg": "Extract vehicle registration (plate, VIN, power_kw, engine_ccm) as JSON.",
|
|
"invoice": "Extract invoice details (vendor, total_amount, date) as JSON.",
|
|
"odometer": "Identify the number on the odometer and return as JSON: {'value': int}."
|
|
}
|
|
img_b64 = base64.b64encode(image_data).decode('utf-8')
|
|
payload = {
|
|
"model": cls.VISION_MODEL,
|
|
"prompt": prompts.get(doc_type, "Perform OCR and return JSON"),
|
|
"images": [img_b64],
|
|
"stream": False,
|
|
"format": "json"
|
|
}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
response = await client.post(cls.OLLAMA_BASE_URL, json=payload)
|
|
res_data = response.json()
|
|
clean_json = res_data.get("response", "{}")
|
|
match = re.search(r'\{.*\}', clean_json, re.DOTALL)
|
|
return json.loads(match.group()) if match else json.loads(clean_json)
|
|
except Exception as e:
|
|
logger.error(f"❌ Helyi OCR hiba: {e}")
|
|
return None |