feat: Robot ecosystem v1.2.6 - Google Search RAG & Master-Merge logic stabilized

This commit is contained in:
2026-02-17 22:44:57 +00:00
parent 2def6b2201
commit b11b9bce87
25 changed files with 3192 additions and 789 deletions

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric, Index
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
@@ -41,10 +41,11 @@ class ModelFeatureMap(Base):
value = Column(String(100))
class VehicleModelDefinition(Base):
"""MDM Master rekordok"""
"""MDM Master rekordok - Kibővítve Deduplikációs és Évjárat mezőkkel (v1.2.5)"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = (
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
Index('idx_vmd_lookup', 'make', 'technical_code'),
{"schema": "data"}
)
@@ -58,28 +59,46 @@ class VehicleModelDefinition(Base):
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
vehicle_class = Column(String(50))
# --- LOGISZTIKAI ÉS TECHNIKAI FIX OSZLOPOK (v1.9) ---
# --- ÚJ MEZŐK AZ INTELLIGENS ÖSSZEFÉSÜLÉSHEZ ---
# Ha ez a rekord egy duplikátum, itt tároljuk, melyik az eredeti (Master) rekord
parent_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
# Gyártási intervallum meghatározása
year_from = Column(Integer, nullable=True, index=True)
year_to = Column(Integer, nullable=True, index=True)
# Alternatív elnevezések kereshetőséghez (pl. ["Tracer 9", "MT-09 Tracer"])
synonyms = Column(JSON, server_default=text("'[]'::jsonb"))
# -----------------------------------------------
# --- LOGISZTIKAI ÉS TECHNIKAI FIX OSZLOPOK ---
engine_capacity = Column(Integer, index=True)
power_kw = Column(Integer, index=True)
max_weight_kg = Column(Integer, index=True) # Össztömeg
max_weight_kg = Column(Integer, index=True)
axle_count = Column(Integer) # Tengelyek száma (Teher/Busz)
payload_capacity_kg = Column(Integer) # Teherbírás
cargo_volume_m3 = Column(Numeric(10, 2)) # Raktér térfogat
cargo_length_mm = Column(Integer) # Raktér méretek
axle_count = Column(Integer)
payload_capacity_kg = Column(Integer)
cargo_volume_m3 = Column(Numeric(10, 2))
cargo_length_mm = Column(Integer)
cargo_width_mm = Column(Integer)
cargo_height_mm = Column(Integer)
# --------------------------------------------------
# ----------------------------------------------
specifications = Column(JSON, server_default=text("'{}'::jsonb"))
features_json = Column(JSON, server_default=text("'{}'::jsonb")) # Összesített gyorseléréshez
features_json = Column(JSON, server_default=text("'{}'::jsonb"))
status = Column(String(20), server_default="unverified")
status = Column(String(20), server_default="unverified") # unverified, ai_enriched, duplicate, manual_check
is_master = Column(Boolean, default=False)
source = Column(String(50))
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Kapcsolatok
v_type_rel = relationship("VehicleType", back_populates="definitions")
# Önmagára hivatkozó kapcsolat a duplikációk kezeléséhez
master_record = relationship("VehicleModelDefinition", remote_side=[id], backref="merged_variants")
# Meglévő kapcsolatok megtartása
variants = relationship("AssetCatalog", back_populates="master_definition")

View File

@@ -1,72 +1,111 @@
import os
import json
import logging
import google.generativeai as genai
import asyncio
import re
from typing import Dict, Any, Optional
from google import genai
from google.genai import types
from sqlalchemy import select
from app.db.session import SessionLocal
from app.models import SystemParameter
logger = logging.getLogger("AI-Service")
class AIService:
# Konfiguráció a .env-ből
"""
AI Service v1.2.5 - Final Integrated Edition
- Robot 2: Technikai dúsítás (Search + Regex JSON parsing)
- Robot 3: OCR (Controlled JSON generation)
"""
api_key = os.getenv("GEMINI_API_KEY")
if api_key:
genai.configure(api_key=api_key)
# 1.5 Flash a legjobb ár/érték/sebesség arányú multimodális modell
model = genai.GenerativeModel('gemini-1.5-flash')
client = genai.Client(api_key=api_key) if api_key else None
PRIMARY_MODEL = "gemini-2.0-flash"
@classmethod
async def get_config_delay(cls) -> float:
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 1.0
except Exception: return 1.0
@classmethod
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
"""Robot 2: Technikai dúsítás és névtisztítás (pl. Yamaha 4HN)."""
"""Robot 2: Adatbányászat Google Search segítségével."""
if not cls.client: return None
await asyncio.sleep(await cls.get_config_delay())
search_tool = types.Tool(google_search=types.GoogleSearch())
prompt = f"""
Rendszer: Technikai gépjárműszakértő vagy.
Feladat: Tisztítsd meg a '{make} {raw_model}' ({v_type}) adatot.
Kimenet: Kizárólag JSON, magyarázat nélkül.
Formátum:
KERESS RÁ az interneten: {make} {raw_model} ({v_type}) pontos gyári modellkódja és technikai adatai.
Adj választ szigorúan csak egy JSON blokkban:
{{
"marketing_name": "Tiszta modellnév",
"technical_code": "Modellkód/Generáció",
"marketing_name": "tiszta név",
"synonyms": ["név1", "név2"],
"technical_code": "gyári kód",
"year_from": int,
"year_to": int_vagy_null,
"ccm": int,
"kw": int,
"maintenance": {{
"oil_type": "pl. 10W-40",
"oil_qty": float,
"spark_plug": "típus",
"coolant": "típus"
}}
"maintenance": {{ "oil_type": "string", "oil_qty": float, "spark_plug": "string", "coolant": "string" }}
}}
FONTOS: A 'technical_code' NEM lehet üres. Ha nem találod, adj 'N/A' értéket!
"""
# Search tool használata esetén a response_mime_type tilos!
config = types.GenerateContentConfig(
system_instruction="Profi járműtechnikai adatbányász vagy. Csak tiszta JSON-t válaszolsz markdown kódblokk nélkül.",
tools=[search_tool],
temperature=0.1
)
try:
response = cls.model.generate_content(prompt)
# A Gemini néha ```json ... ``` blokkba teszi, ezt le kell tisztítani
json_text = response.text.replace("```json", "").replace("```", "").strip()
return json.loads(json_text)
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
text = response.text
# Tisztítás: ha az AI mégis tenne bele markdown jeleket
clean_json = re.sub(r'```json\s*|```', '', text).strip()
res_json = json.loads(clean_json)
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f"❌ AI Dúsítás hiba: {e}")
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
return None
@classmethod
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
"""Robot 3: AI OCR - Forgalmi, Személyi, Számla, KM-óra elemzés."""
"""Robot 3: OCR funkció - Forgalmi, Személyi, Számla, Odometer."""
if not cls.client: return None
await asyncio.sleep(await cls.get_config_delay())
prompts = {
"identity": "Olvasd le az okmányról: vezetéknév, keresztnév, okmányszám, lejárati idő, születési dátum.",
"vehicle_reg": "Olvasd le a forgalmiból: rendszám, alvázszám (VIN), gyártmány, típus, kw, ccm, együttes tömeg, műszaki érvényesség.",
"invoice": "Olvasd le a számláról: eladó neve/adószáma, vevő neve, bruttó összeg, dátum, tételek (alkatrész/munkadíj).",
"odometer": "Olvasd le a képen látható műszerfalról a kilométeróra vagy üzemóra állását. Csak a számot add vissza."
"identity": "Személyes okmány adatok (név, szám, lejárat).",
"vehicle_reg": "Forgalmi adatok (rendszám, alvázszám, kW, ccm).",
"invoice": "Számla adatok (partner, végösszeg, dátum).",
"odometer": "Csak a kilométeróra állása számként."
}
prompt = f"Rendszer: Profi OCR és dokumentum-elemző vagy. {prompts.get(doc_type, 'Elemezd a képet.')} Válaszolj tiszta JSON formátumban."
# Itt maradhat a response_mime_type, mert nem használunk Search-öt
config = types.GenerateContentConfig(
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
response_mime_type="application/json"
)
try:
# A Gemini közvetlenül tud fogadni bytes adatot (képként)
contents = [
prompt,
{"mime_type": "image/jpeg", "data": image_data}
]
response = cls.model.generate_content(contents)
json_text = response.text.replace("```json", "").replace("```", "").strip()
return json.loads(json_text)
response = cls.client.models.generate_content(
model=cls.PRIMARY_MODEL,
contents=[
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, 'OCR')}",
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
],
config=config
)
res_json = json.loads(response.text)
if isinstance(res_json, list) and len(res_json) > 0: res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f" AI OCR hiba ({doc_type}): {e}")
logger.error(f"❌ OCR hiba: {e}")
return None

View File

@@ -0,0 +1,116 @@
import os
import json
import logging
import asyncio
from typing import Dict, Any, Optional
from google import genai
from google.genai import types
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.2.4 - Production Ready
- Robot 2 (Technical Enrichment) & Robot 3 (OCR)
- Fix: JSON response cleaning and array-to-dict transformation.
"""
api_key = os.getenv("GEMINI_API_KEY")
client = genai.Client(api_key=api_key) if api_key else None
PRIMARY_MODEL = "gemini-2.0-flash"
@classmethod
async def get_config_delay(cls) -> float:
"""Lekéri az adminisztrálható késleltetést 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 1.0
except Exception:
return 1.0
@classmethod
async def get_clean_vehicle_data(cls, make: str, raw_model: str, v_type: str) -> Optional[Dict[str, Any]]:
"""Robot 2: Gépjármű technikai adatok dúsítása."""
if not cls.client:
return None
await asyncio.sleep(await cls.get_config_delay())
prompt = f"""
Jármű: {make} {raw_model} ({v_type}).
Adj technikai adatokat JSON formátumban.
FONTOS: A 'technical_code' mező NEM lehet üres. Ha nem tudod a gyári kódot, adj 'N/A' értéket!
Várt struktúra:
{{
"marketing_name": "tiszta marketing név",
"technical_code": "gyári kód vagy N/A",
"ccm": egész szám,
"kw": egész szám,
"maintenance": {{
"oil_type": "viszkozitás",
"oil_qty": tizedes tört literben,
"spark_plug": "gyertya típus",
"coolant": "hűtőfolyadék"
}}
}}
"""
config = types.GenerateContentConfig(
system_instruction="Profi gépjárműtechnikus vagy. Kizárólag tiszta JSON-t válaszolsz.",
response_mime_type="application/json",
temperature=0.1
)
try:
response = cls.client.models.generate_content(model=cls.PRIMARY_MODEL, contents=prompt, config=config)
res_json = json.loads(response.text)
if isinstance(res_json, list) and len(res_json) > 0:
res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f"❌ AI hiba ({make} {raw_model}): {e}")
return None
@classmethod
async def analyze_document_image(cls, image_data: bytes, doc_type: str) -> Optional[Dict[str, Any]]:
"""Robot 3: Multimodális OCR elemzés (Képbeolvasás)."""
if not cls.client:
return None
await asyncio.sleep(await cls.get_config_delay())
prompts = {
"identity": "Személyes okmány adatok.",
"vehicle_reg": "Rendszám, alvázszám, technikai adatok.",
"invoice": "Számla adatok, összegek, dátumok.",
"odometer": "Csak a kilométeróra állása számként."
}
config = types.GenerateContentConfig(
system_instruction="Profi OCR dokumentum-elemző vagy. Csak tiszta JSON-t válaszolsz.",
response_mime_type="application/json"
)
try:
response = cls.client.models.generate_content(
model=cls.PRIMARY_MODEL,
contents=[
f"Elemezd ezt a képet ({doc_type}): {prompts.get(doc_type, '')}",
types.Part.from_bytes(data=image_data, mime_type="image/jpeg")
],
config=config
)
res_json = json.loads(response.text)
if isinstance(res_json, list) and len(res_json) > 0:
res_json = res_json[0]
return res_json if isinstance(res_json, dict) else None
except Exception as e:
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
return None

View File

@@ -3,133 +3,113 @@ import httpx
import logging
import os
import datetime
import json
from sqlalchemy import text, select, update
from sqlalchemy import select, and_
from sqlalchemy.exc import IntegrityError
from app.db.session import SessionLocal
from app.models.vehicle_definitions import VehicleModelDefinition
from app.models.audit import ProcessLog
from app.services.ai_service import AIService
from app.services.email_manager import EmailManager # Feltételezve, hogy létezik
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot-v1.1.0-Master-Enricher")
logger = logging.getLogger("Robot-Bulk-Master")
class TechEnricher:
"""
Master Enricher v1.1.0 - Hybrid RDW & AI Clean Edition
- Cél: vehicle_model_definitions (Master) tábla tisztítása és dúsítása.
- Megtartja a v1.0.4 RDW logikát, de kiegészíti AI-al a zajos adatokhoz (pl. Yamaha 4HN).
"""
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
@classmethod
def clean_num(cls, v):
try: return int(float(v)) if v else None
except: return None
@classmethod
async def fetch_rdw_tech_data(cls, make, model):
"""A v1.0.4-es RDW kereső logika."""
clean_model = str(model).upper().replace(str(make).upper(), "").strip()
if len(clean_model) < 2: return None
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
params = {"merk": make.upper(), "handelsbenaming": str(model).strip().upper(), "$limit": 1}
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
try:
await asyncio.sleep(1.1) # RDW Rate limit védelem
resp = await client.get(cls.API_URL, params=params, timeout=20)
if resp.status_code == 200:
data = resp.json()
return data[0] if data else None
return None
except Exception as e:
logger.error(f"❌ RDW API Hiba: {e}")
return None
resp = await client.get(cls.API_URL, params=params, timeout=15)
return resp.json()[0] if resp.status_code == 200 and resp.json() else None
except: return None
@classmethod
async def run(cls):
logger.info("🚀 Master Enricher v1.1.0 INDUL...")
start_time = datetime.datetime.now()
stats = {"processed": 0, "failed": 0, "cleaned": []}
async with SessionLocal() as db:
# Csak azokat a Master rekordokat nézzük, amik még nincsenek hitelesítve
stmt = select(VehicleModelDefinition).where(
VehicleModelDefinition.status == "unverified"
).limit(30) # Kisebb batch a biztonság érdekében
res = await db.execute(stmt)
masters = res.scalars().all()
if not masters:
logger.info("😴 Nincs dúsításra váró adat.")
return
for master in masters:
try:
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name}")
# 1. Lépés: RDW adatok lekérése (v1.0.4 logika)
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
# 2. Lépés: AI segítség kérése, ha az RDW nem elég vagy a név 'zajos' (pl. 4HN)
# Ha a névben gyanús kódok vannak, az AI tisztítja meg
if not rdw_data or "(" in master.marketing_name or len(master.marketing_name) < 5:
ai_data = await AIService.get_clean_vehicle_data(
master.make, master.marketing_name, master.vehicle_type
)
if ai_data:
old_name = master.marketing_name
master.marketing_name = ai_data.get("marketing_name", old_name)
master.technical_code = ai_data.get("technical_code", master.technical_code)
master.engine_capacity = ai_data.get("ccm", master.engine_capacity)
master.power_kw = ai_data.get("kw", master.power_kw)
master.specifications = ai_data.get("maintenance", {})
stats["cleaned"].append(f"{old_name} -> {master.marketing_name}")
# Ha volt RDW adatunk, de az AI nem írta felül, töltsük be az RDW-t
if rdw_data and master.status == "unverified":
master.power_kw = cls.clean_num(rdw_data.get("netto_maximum_vermogen_kw"))
master.engine_capacity = cls.clean_num(rdw_data.get("cilinderinhoud"))
master.axle_count = cls.clean_num(rdw_data.get("aantal_assen"))
master.status = "ai_enriched"
stats["processed"] += 1
await db.commit()
except Exception as e:
logger.error(f"❌ Hiba a(z) {master.id} rekordnál: {e}")
stats["failed"] += 1
await db.rollback()
# 3. JELENTÉS MENTÉSE ÉS EMAIL KÜLDÉS
end_time = datetime.datetime.now()
new_log = ProcessLog(
process_name="Master-Enricher",
start_time=start_time,
end_time=end_time,
items_processed=stats["processed"],
items_failed=stats["failed"],
details=stats
)
db.add(new_log)
await db.commit()
# Email küldés (Dummy hívás a meglévő EmailManager-hez)
await cls.send_report_email(stats)
@classmethod
async def send_report_email(cls, stats):
report_body = f"Reggeli Robot Jelentés - {datetime.date.today()}\n\n"
report_body += f"Sikeresen feldolgozva: {stats['processed']}\n"
report_body += f"Hibák: {stats['failed']}\n\n"
report_body += "Tisztított nevek:\n" + "\n".join(stats['cleaned'])
logger.info("🚀 Master-Merge Robot FOLYAMATOS ÜZEMMÓD INDUL...")
logger.info("📧 Email jelentés elküldve az adminnak.")
# EmailManager.send_admin_notification("Robot Report", report_body)
while True: # Folyamatos ciklus, amíg el nem fogy az adat
async with SessionLocal() as main_db:
stmt = select(VehicleModelDefinition.id).where(
VehicleModelDefinition.status == "unverified"
).limit(50) # Egyszerre 50 ID-t foglalunk le
res = await main_db.execute(stmt)
ids = res.scalars().all()
if not ids:
logger.info("🏁 Minden rekord feldolgozva. A robot megáll.")
break
logger.info(f"📦 Új csomag indítása: {len(ids)} rekord.")
for m_id in ids:
async with SessionLocal() as db:
try:
current = await db.get(VehicleModelDefinition, m_id)
if not current: continue
logger.info(f"🧪 Feldolgozás: {current.make} {current.marketing_name} (ID: {m_id})")
rdw_data = await cls.fetch_rdw_tech_data(current.make, current.marketing_name)
if rdw_data:
current.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or current.engine_capacity
current.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or current.power_kw
ai_data = await AIService.get_clean_vehicle_data(current.make, current.marketing_name, current.vehicle_type)
if ai_data:
tech_code = ai_data.get("technical_code") or "N/A"
new_ccm = ai_data.get("ccm") or current.engine_capacity
master_record = None
if tech_code and tech_code != "N/A":
stmt_master = select(VehicleModelDefinition).where(and_(
VehicleModelDefinition.make == current.make,
VehicleModelDefinition.technical_code == tech_code,
VehicleModelDefinition.engine_capacity == new_ccm,
VehicleModelDefinition.status == 'ai_enriched',
VehicleModelDefinition.id != m_id
))
master_record = (await db.execute(stmt_master)).scalar_one_or_none()
if master_record:
logger.info(f"🔗 Merge: ID:{m_id} -> Master ID:{master_record.id}")
syns = set(master_record.synonyms or [])
syns.update(ai_data.get("synonyms", []))
syns.add(current.marketing_name)
master_record.synonyms = list(syns)
current.status = "duplicate"
current.parent_id = master_record.id
else:
current.technical_code = tech_code if tech_code != "N/A" else f"N/A-{m_id}"
current.marketing_name = ai_data.get("marketing_name", current.marketing_name)
current.engine_capacity = new_ccm
current.power_kw = ai_data.get("kw") or current.power_kw
current.year_from = ai_data.get("year_from")
current.year_to = ai_data.get("year_to")
current.synonyms = ai_data.get("synonyms", [])
if ai_data.get("maintenance"):
old_spec = current.specifications or {}
old_spec.update(ai_data.get("maintenance"))
current.specifications = old_spec
current.status = "ai_enriched"
else:
if not current.technical_code:
current.technical_code = f"UNKNOWN-{m_id}"
current.updated_at = datetime.datetime.now()
await db.commit()
logger.info(f"✅ Mentve (ID: {m_id})")
except Exception as e:
await db.rollback()
logger.error(f"❌ Hiba ID:{m_id}: {e}")
finally:
await db.close()
if __name__ == "__main__":
asyncio.run(TechEnricher.run())

View File

@@ -0,0 +1,102 @@
import asyncio
import httpx
import logging
import os # <--- EZ HIÁNYZOTT!
import datetime
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from app.db.session import SessionLocal
from app.models.vehicle_definitions import VehicleModelDefinition
from app.services.ai_service import AIService
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot-v1.2.4-Fixed")
class TechEnricher:
API_URL = "https://opendata.rdw.nl/resource/kyri-nuah.json"
RDW_TOKEN = os.getenv("RDW_APP_TOKEN")
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
@classmethod
async def fetch_rdw_tech_data(cls, make, model):
"""Hibatűrő RDW lekérdezés tisztított paraméterekkel."""
clean_model = str(model).strip().upper()
params = {"merk": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
try:
resp = await client.get(cls.API_URL, params=params, timeout=15)
if resp.status_code == 200 and resp.json():
return resp.json()[0]
return None
except Exception:
return None
@classmethod
async def run(cls):
logger.info("🚀 Master Enricher INDUL (Atomi mentés üzemmód)...")
# 1. Csak az ID-kat kérjük le, hogy ne tartsuk nyitva a tranzakciót feleslegesen
async with SessionLocal() as main_db:
stmt = select(VehicleModelDefinition.id).where(
VehicleModelDefinition.status == "unverified"
).limit(50)
res = await main_db.execute(stmt)
ids = res.scalars().all()
if not ids:
logger.info("😴 Nincs dúsítandó adat.")
return
# 2. Egyesével dolgozzuk fel a rekordokat saját session-ben
for m_id in ids:
async with SessionLocal() as db:
try:
master = await db.get(VehicleModelDefinition, m_id)
if not master:
continue
logger.info(f"🧪 Feldolgozás: {master.make} {master.marketing_name} (ID: {m_id})")
data_found = False
# A: RDW fázis
rdw_data = await cls.fetch_rdw_tech_data(master.make, master.marketing_name)
if rdw_data:
master.engine_capacity = int(float(rdw_data.get("cilinderinhoud", 0))) or None
master.power_kw = int(float(rdw_data.get("netto_maximum_vermogen_kw", 0))) or None
data_found = True
# B: AI fázis (ha hiányzik adat vagy pontosítani kell)
if not data_found or master.engine_capacity is None:
ai_data = await AIService.get_clean_vehicle_data(
master.make, master.marketing_name, master.vehicle_type
)
if ai_data:
master.marketing_name = ai_data.get("marketing_name", master.marketing_name)
master.technical_code = ai_data.get("technical_code") or master.technical_code or "N/A"
master.engine_capacity = ai_data.get("ccm") or master.engine_capacity
master.power_kw = ai_data.get("kw") or master.power_kw
master.specifications = ai_data.get("maintenance", {})
data_found = True
# C: Mentés és véglegesítés
if data_found:
master.status = "ai_enriched"
master.updated_at = datetime.datetime.now()
await db.commit() # AZONNALI COMMIT A LEMEZRE
logger.info(f"✅ Sikeresen mentve: {master.marketing_name} (CCM: {master.engine_capacity})")
else:
logger.warning(f"⚠️ Nem találtam adatot az ID {m_id} esetében.")
except IntegrityError:
await db.rollback()
logger.warning(f"🚫 Duplikáció vagy Constraint hiba (ID: {m_id}). Kihagyva.")
except Exception as e:
await db.rollback()
logger.error(f"❌ Váratlan hiba az ID {m_id} esetében: {e}")
finally:
await db.close()
logger.info("🏁 50-es batch feldolgozva.")
if __name__ == "__main__":
asyncio.run(TechEnricher.run())