Files
service-finder/backend/app/workers/technical_enricher.py
2026-02-26 08:19:25 +01:00

188 lines
8.1 KiB
Python

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