# /opt/docker/dev/service_finder/backend/app/workers/technical_enricher.py import asyncio import logging import os import datetime import random import sys # JAVÍTVA: case hozzáadva az importhoz from sqlalchemy import select, and_, update, text, func, case from app.db.session import AsyncSessionLocal from app.models.vehicle_definitions import VehicleModelDefinition from app.models.asset import AssetCatalog from app.services.ai_service import AIService from duckduckgo_search import DDGS # --- SZIGORÚ NAPLÓZÁS --- logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] Alchemist-v1.3: %(message)s', stream=sys.stdout ) logger = logging.getLogger("Robot-Enricher") class TechEnricher: """ Industrial TechEnricher (Alchemist Bot). Felelős az MDM (Master Data Management) 'Arany' rekordjainak előállításáért. """ def __init__(self): self.max_attempts = 5 self.batch_size = 10 self.daily_ai_limit = 500 self.ai_calls_today = 0 self.last_reset_date = datetime.date.today() def check_budget(self) -> bool: """ Ellenőrzi, hogy beleférünk-e még a napi AI keretbe. """ 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 def is_data_sane(self, data: dict) -> bool: """ Technikai józansági vizsgálat (Hallucináció elleni védelem). """ try: if not data: return False ccm = int(data.get("ccm", 0) or 0) kw = int(data.get("kw", 0) or 0) # Extrém értékek szűrése (pl. nem létezik 20 literes személyautó motor) if ccm > 16000 or (kw > 1500 and data.get("vehicle_type") != "truck"): return False return True except: return False async def get_web_wisdom(self, make: str, model: str) -> str: """ Ha az AI bizonytalan, ez a funkció gyűjt kontextust a netről. """ query = f"{make} {model} technical specifications oil capacity engine code" try: def sync_search(): with DDGS() as ddgs: # Az első 3 találat body részét gyűjtjük össze results = ddgs.text(query, max_results=3) return "\n".join([r['body'] for r in results]) if results else "" return await asyncio.to_thread(sync_search) except Exception as e: logger.warning(f"🌐 Web Search Error ({make}): {e}") return "" async def process_single_record(self, record_id: int): """ Egyetlen rekord dúsítása izolált folyamatban. Logika: Read -> AI Process -> Write Merge. """ # 1. ADAT LEKÉRÉSE async with AsyncSessionLocal() as db: res = await db.execute(select(VehicleModelDefinition).where(VehicleModelDefinition.id == record_id)) rec = res.scalar_one_or_none() if not rec: return make, m_name, v_type = rec.make, rec.marketing_name, (rec.vehicle_type or "car") # 2. AI FELDOLGOZÁS (DB kapcsolat nélkül!) try: # Elsődleges kísérlet a belső tudásbázis alapján ai_data = await AIService.get_clean_vehicle_data(make, m_name, v_type, {}) # Ha az AI bizonytalan, indítunk egy webes mélyfúrást if not ai_data or not ai_data.get("kw"): logger.info(f"🔍 AI bizonytalan, Web-Context hívása: {make} {m_name}") web_info = await self.get_web_wisdom(make, m_name) ai_data = await AIService.get_clean_vehicle_data(make, m_name, v_type, {"web_context": web_info}) if not ai_data or not self.is_data_sane(ai_data): raise ValueError("Hibás vagy hiányos AI válasz.") # 3. MENTÉS ÉS MERGE (Új session) async with AsyncSessionLocal() as db: # MDM Összefésülés: létezik-e már ez a variáns a katalógusban? clean_model = str(ai_data.get("marketing_name", m_name))[:50].upper() cat_stmt = select(AssetCatalog).where(and_( AssetCatalog.make == make.upper(), AssetCatalog.model == clean_model, AssetCatalog.power_kw == ai_data.get("kw") )).limit(1) existing_cat = (await db.execute(cat_stmt)).scalar_one_or_none() if not existing_cat: db.add(AssetCatalog( make=make.upper(), model=clean_model, power_kw=ai_data.get("kw"), engine_capacity=ai_data.get("ccm"), fuel_type=ai_data.get("fuel_type", "petrol"), factory_data=ai_data # Teljes technikai JSONB (olaj, gumi, stb.) )) logger.info(f"✨ ÚJ KATALÓGUS ELEM (Gold Data): {make} {clean_model}") # Staging (Discovery) állapot frissítése await db.execute( update(VehicleModelDefinition) .where(VehicleModelDefinition.id == record_id) .values( status="ai_enriched", technical_code=ai_data.get("technical_code") or f"REF-{record_id}", engine_capacity=ai_data.get("ccm"), power_kw=ai_data.get("kw"), updated_at=func.now() ) ) await db.commit() self.ai_calls_today += 1 except Exception as e: logger.error(f"🚨 Hiba a(z) {record_id} rekordnál: {e}") async with AsyncSessionLocal() as db: # Hibakezelés: ha sokszor bukik el, felfüggesztjük a rekordot await db.execute( update(VehicleModelDefinition) .where(VehicleModelDefinition.id == record_id) .values( attempts=VehicleModelDefinition.attempts + 1, last_error=str(e)[:200], status=case( (VehicleModelDefinition.attempts >= 4, "suspended"), else_="unverified" ), updated_at=func.now() ) ) await db.commit() async def run(self): logger.info(f"🚀 Alchemist Robot v1.3.0 ONLINE (Napi keret: {self.daily_ai_limit})") while True: if not self.check_budget(): logger.warning("💰 AI költségkeret kimerült mára. Alvás 1 órát.") await asyncio.sleep(3600); continue try: async with AsyncSessionLocal() as db: # Olyan rekordokat keresünk, amik még nincsenek dúsítva és nincs túl sok hiba rajtuk stmt = select(VehicleModelDefinition.id).where(and_( VehicleModelDefinition.status == "unverified", VehicleModelDefinition.attempts < self.max_attempts )).limit(self.batch_size) # JAVÍTVA: Fetchall és list comprehension res = await db.execute(stmt) ids = [r[0] for r in res.fetchall()] if not ids: await asyncio.sleep(60); continue logger.info(f"📦 Batch feldolgozása indul: {len(ids)} tétel.") for rid in ids: await self.process_single_record(rid) # VGA kímélése és API rate-limit védelem await asyncio.sleep(random.uniform(5.0, 15.0)) except Exception as e: logger.error(f"🚨 Főciklus hiba: {e}") await asyncio.sleep(30) if __name__ == "__main__": enricher = TechEnricher() asyncio.run(enricher.run())