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())