224 lines
10 KiB
Python
Executable File
224 lines
10 KiB
Python
Executable File
|
|
import asyncio
|
|
import logging
|
|
import datetime
|
|
import random
|
|
import sys
|
|
import json
|
|
from sqlalchemy import text, func, update, case
|
|
from app.database import AsyncSessionLocal
|
|
from app.models.vehicle_definitions import VehicleModelDefinition
|
|
from app.models.asset import AssetCatalog
|
|
from app.services.ai_service import AIService
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] Vehicle-Alchemist-Pro: %(message)s', stream=sys.stdout)
|
|
logger = logging.getLogger("Vehicle-Robot-3-Alchemist-Pro")
|
|
|
|
class TechEnricher:
|
|
"""
|
|
Vehicle Robot 3: Alchemist Pro (Atomi Zárolás Patch)
|
|
Tiszta GPU fókusz: Csak az AI elemzésre és adategyesítésre koncentrál.
|
|
Nincs felesleges webkeresés. Szigorú Sane-Check.
|
|
"""
|
|
def __init__(self):
|
|
self.max_attempts = 5
|
|
self.daily_ai_limit = int(os.getenv("AI_DAILY_LIMIT", "10000"))
|
|
self.ai_calls_today = 0
|
|
self.last_reset_date = datetime.date.today()
|
|
|
|
def check_budget(self) -> bool:
|
|
if datetime.date.today() > self.last_reset_date:
|
|
self.ai_calls_today = 0
|
|
self.last_reset_date = datetime.date.today()
|
|
return self.ai_calls_today < self.daily_ai_limit
|
|
|
|
ddef is_data_sane(self, data: dict, base_info: dict) -> bool:
|
|
""" Szigorított, de intelligens AI Hallucináció szűrő """
|
|
if not data:
|
|
logger.warning("Sane-check: Teljesen üres AI válasz.")
|
|
return False
|
|
|
|
try:
|
|
# 1. Alapvető fizikai korlátok vizsgálata (csak az AI adatokon)
|
|
ai_ccm = int(data.get("ccm", 0) or 0)
|
|
ai_kw = int(data.get("kw", 0) or 0)
|
|
v_class = base_info.get("v_type", "car")
|
|
|
|
if ai_ccm > 18000:
|
|
logger.warning(f"Sane-check bukás: Irreális CCM érték ({ai_ccm})")
|
|
return False
|
|
if ai_kw > 1500 and v_class != "truck":
|
|
logger.warning(f"Sane-check bukás: Irreális KW érték ({ai_kw})")
|
|
return False
|
|
|
|
# 2. KOMBINÁLT Adat teljesség vizsgálata (RDW + AI)
|
|
# Ha az RDW tudja, akkor nem baj, ha az AI nem találta meg!
|
|
merged_kw = base_info.get('rdw_kw') or ai_kw
|
|
merged_ccm = base_info.get('rdw_ccm') or ai_ccm
|
|
fuel = data.get("fuel_type", base_info.get("rdw_fuel", "")).lower()
|
|
|
|
# Ha még kombinálva sincs meg a KW
|
|
if merged_kw == 0:
|
|
logger.warning("Sane-check figyelmeztetés: Hiányzó KW (se RDW, se AI). Engedélyezve részleges adatként.")
|
|
# Nem térünk vissza False-al, inkább mentsük el, amit eddig tudunk!
|
|
|
|
# Ha még kombinálva sincs meg a CCM (és nem elektromos)
|
|
if merged_ccm == 0 and "electric" not in fuel and "elektric" not in fuel and v_class != "trailer":
|
|
logger.warning("Sane-check figyelmeztetés: Hiányzó CCM egy belsőégésű motornál. Engedélyezve részleges adatként.")
|
|
# Ezt is átengedjük, hogy kitörjünk a végtelen hurokból.
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Sane check hiba: {e}")
|
|
return False
|
|
|
|
async def process_single_record(self, db, record_id: int, base_info: dict, current_attempts: int):
|
|
try:
|
|
logger.info(f"🧠 AI dúsítás indul: {base_info['make']} {base_info['m_name']}")
|
|
|
|
# 1. LÉPÉS: AI Hívás (Rábízzuk az adatokat a modellre)
|
|
ai_data = await AIService.get_clean_vehicle_data(
|
|
base_info['make'],
|
|
base_info['m_name'],
|
|
base_info
|
|
)
|
|
|
|
# 2. LÉPÉS: Validáció (Ha az AI rossz adatot ad, NEM megyünk ki a webre, hanem dobjuk az aktát!)
|
|
if not ai_data or not self.is_data_sane(ai_data, base_info):
|
|
raise ValueError("Az AI hiányos adatot adott vissza vagy hallucinált.")
|
|
|
|
# 3. LÉPÉS: HIBRID MERGE (Az RDW adatok felülbírálják az AI-t a hatósági paramétereknél)
|
|
final_kw = base_info['rdw_kw'] if base_info['rdw_kw'] > 0 else (ai_data.get("kw") or 0)
|
|
final_ccm = base_info['rdw_ccm'] if base_info['rdw_ccm'] > 0 else (ai_data.get("ccm") or 0)
|
|
|
|
# Üzemanyag tisztítása
|
|
fuel_rdw = base_info.get('rdw_fuel', '')
|
|
final_fuel = fuel_rdw if fuel_rdw and fuel_rdw != "Unknown" else ai_data.get("fuel_type", "petrol")
|
|
|
|
final_engine = base_info['rdw_engine'] if base_info['rdw_engine'] else ai_data.get("engine_code", "Unknown")
|
|
final_euro = base_info['rdw_euro'] or ai_data.get("euro_classification")
|
|
final_cylinders = base_info['rdw_cylinders'] or ai_data.get("cylinders")
|
|
|
|
# 4. LÉPÉS: Mentés az Arany Katalógusba
|
|
clean_model = str(ai_data.get("marketing_name", base_info['m_name']))[:50].upper()
|
|
|
|
cat_stmt = text("""
|
|
INSERT INTO data.vehicle_catalog
|
|
(master_definition_id, make, model, power_kw, engine_capacity, fuel_type, factory_data)
|
|
VALUES (:m_id, :make, :model, :kw, :ccm, :fuel, :factory)
|
|
RETURNING id;
|
|
""")
|
|
|
|
await db.execute(cat_stmt, {
|
|
"m_id": record_id,
|
|
"make": base_info['make'].upper(),
|
|
"model": clean_model,
|
|
"kw": final_kw,
|
|
"ccm": final_ccm,
|
|
"fuel": final_fuel,
|
|
"factory": json.dumps(ai_data)
|
|
})
|
|
|
|
# 5. LÉPÉS: Staging tábla (VMD) lezárása
|
|
await db.execute(
|
|
update(VehicleModelDefinition)
|
|
.where(VehicleModelDefinition.id == record_id)
|
|
.values(
|
|
status="gold_enriched",
|
|
engine_capacity=final_ccm,
|
|
power_kw=final_kw,
|
|
fuel_type=final_fuel,
|
|
engine_code=final_engine,
|
|
euro_classification=final_euro,
|
|
cylinders=final_cylinders,
|
|
specifications=ai_data, # Elmentjük az AI teljes outputját a mestertáblába is
|
|
updated_at=func.now()
|
|
)
|
|
)
|
|
await db.commit()
|
|
logger.info(f"✨ ARANY REKORD KÉSZ: {base_info['make'].upper()} {clean_model}")
|
|
self.ai_calls_today += 1
|
|
|
|
except Exception as e:
|
|
await db.rollback()
|
|
logger.warning(f"⚠️ Alkimista hiba ({base_info['make']} {base_info['m_name']}): {e}")
|
|
|
|
# Visszaküldés a sorba vagy felfüggesztés
|
|
new_status = 'suspended' if current_attempts + 1 >= self.max_attempts else 'unverified'
|
|
|
|
await db.execute(
|
|
update(VehicleModelDefinition)
|
|
.where(VehicleModelDefinition.id == record_id)
|
|
.values(
|
|
attempts=current_attempts + 1,
|
|
last_error=str(e)[:200],
|
|
status=new_status,
|
|
updated_at=func.now()
|
|
)
|
|
)
|
|
await db.commit()
|
|
if new_status == 'unverified':
|
|
logger.info("♻️ Akta visszaküldve a Robot-2-nek (Kutató).")
|
|
|
|
async def run(self):
|
|
logger.info(f"🚀 Alchemist Pro HIBRID ONLINE (Atomi Zárolás Patch)")
|
|
while True:
|
|
if not self.check_budget():
|
|
logger.warning("💸 Napi AI limit kimerítve! Pihenés...")
|
|
await asyncio.sleep(3600); continue
|
|
|
|
try:
|
|
async with AsyncSessionLocal() as db:
|
|
# ATOMI ZÁROLÁS (A "Szent Grál" a race condition ellen)
|
|
# A Robot-1 (ACTIVE) és a Robot-2 (awaiting_ai_synthesis) aktáit is felveszi!
|
|
query = text("""
|
|
UPDATE data.vehicle_model_definitions
|
|
SET status = 'ai_synthesis_in_progress'
|
|
WHERE id = (
|
|
SELECT id FROM data.vehicle_model_definitions
|
|
WHERE status IN ('awaiting_ai_synthesis', 'ACTIVE')
|
|
AND attempts < :max_attempts
|
|
ORDER BY
|
|
CASE WHEN status = 'awaiting_ai_synthesis' THEN 1 ELSE 2 END,
|
|
priority_score DESC
|
|
FOR UPDATE SKIP LOCKED
|
|
LIMIT 1
|
|
)
|
|
RETURNING id, make, marketing_name, vehicle_class, power_kw, engine_capacity,
|
|
fuel_type, engine_code, euro_classification, cylinders, raw_search_context, attempts;
|
|
""")
|
|
|
|
result = await db.execute(query, {"max_attempts": self.max_attempts})
|
|
task = result.fetchone()
|
|
await db.commit()
|
|
|
|
if task:
|
|
# Szétbontjuk a lekérdezett rekordot a base_info dict-be
|
|
r_id = task[0]
|
|
base_info = {
|
|
"make": task[1], "m_name": task[2], "v_type": task[3] or "car",
|
|
"rdw_kw": task[4] or 0, "rdw_ccm": task[5] or 0,
|
|
"rdw_fuel": task[6] or "petrol", "rdw_engine": task[7] or "",
|
|
"rdw_euro": task[8], "rdw_cylinders": task[9],
|
|
"web_context": task[10] or ""
|
|
}
|
|
attempts = task[11]
|
|
|
|
# Külön adatbázis kapcsolat a feldolgozáshoz (hosszú AI hívás miatt)
|
|
async with AsyncSessionLocal() as process_db:
|
|
await self.process_single_record(process_db, r_id, base_info, attempts)
|
|
|
|
# GPU hűtés / Ollama rate limit
|
|
await asyncio.sleep(random.uniform(1.5, 3.5))
|
|
else:
|
|
logger.info("😴 Nincs feldolgozandó akta, az Alkimista pihen...")
|
|
await asyncio.sleep(15)
|
|
|
|
except Exception as e:
|
|
logger.error(f"💀 Kritikus hiba a főciklusban: {e}")
|
|
await asyncio.sleep(10)
|
|
|
|
if __name__ == "__main__":
|
|
import os # Import az AI limit környezeti változóhoz
|
|
asyncio.run(TechEnricher().run()) |