feat(infra): Stabilized Docker env, fixed circular imports, enabled AI Enricher Robot v1.1

This commit is contained in:
2026-02-17 01:26:18 +00:00
parent d574d3297d
commit 2def6b2201
34 changed files with 1559 additions and 112 deletions

View File

@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
from app.api import deps
from app.models.identity import User, UserRole
from app.models.system_config import SystemParameter
from app.models import SystemParameter
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.schemas.admin_security import PendingActionResponse, SecurityStatusResponse

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm import sessionmaker
try:
from app.core.config import settings
from app.core.i18n import t
from app.models.system_config import SystemParameter
from app.models import SystemParameter
except ImportError as e:
print(f"❌ Import hiba: {e}")
print("Ellenőrizd, hogy a PYTHONPATH be van-e állítva!")

View File

@@ -32,10 +32,13 @@ from .translation import Translation
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
# Naplózás és Biztonság (HOZZÁADVA: audit.py modellek)
from .audit import SecurityAuditLog, OperationalLog, FinancialLedger # <--- KRITIKUS!
from .audit import SecurityAuditLog, ProcessLog, FinancialLedger # <--- KRITIKUS!
from .history import AuditLog, VehicleOwnership
from .security import PendingAction
# MDM (Master Data Management) Jármű modellek központ
from .vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
# Aliasok a kényelmesebb fejlesztéshez
Vehicle = Asset
UserVehicle = Asset
@@ -48,11 +51,12 @@ __all__ = [
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetFinancials",
"AssetTelemetry", "AssetReview", "ExchangeRate",
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
"Point_Rule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger",
"SystemParameter", "Document", "Translation", "PendingAction",
"SubscriptionTier", "OrganizationSubscription",
"CreditTransaction", "ServiceSpecialty", "AuditLog", "VehicleOwnership",
"SecurityAuditLog", "OperationalLog", "FinancialLedger", # <--- KRITIKUS!
"SecurityAuditLog", "ProcessLog", "FinancialLedger", # <--- KRITIKUS!
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging",
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord"
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition",
"VehicleType", "FeatureDefinition", "ModelFeatureMap"
]

View File

@@ -16,6 +16,9 @@ class AssetCatalog(Base):
)
id = Column(Integer, primary_key=True, index=True)
# Kapcsolat az MDM-hez
master_definition_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), nullable=True)
make = Column(String, index=True, nullable=False)
model = Column(String, index=True, nullable=False)
generation = Column(String, index=True)
@@ -24,7 +27,10 @@ class AssetCatalog(Base):
year_to = Column(Integer)
vehicle_class = Column(String)
fuel_type = Column(String, index=True)
# ÚJ MEZŐ: Kapcsolat az MDM-hez
master_definition = relationship("VehicleModelDefinition", back_populates="variants")
# --- ÚJ OSZLOPOK (Ezeket add hozzá!) ---
power_kw = Column(Integer, index=True)
engine_capacity = Column(Integer, index=True)

View File

@@ -13,28 +13,41 @@ class SecurityAuditLog(Base):
actor_id = Column(Integer, ForeignKey("data.users.id")) # Aki kezdeményezte
target_id = Column(Integer, ForeignKey("data.users.id")) # Akivel történt
# 4-szem elv: csak akkor válik élessé, ha ez nem NULL
confirmed_by_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
is_critical = Column(Boolean, default=False) # Szuperadmin hívásoknál True
is_critical = Column(Boolean, default=False)
payload_before = Column(JSON)
payload_after = Column(JSON)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class OperationalLog(Base):
""" Napi üzemi események (Operational). """
""" Felhasználói szintű napi üzemi események (Audit Trail). """
__tablename__ = "operational_logs"
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True, index=True) # <--- EZ HIÁNYZOTT!
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("data.users.id", ondelete="SET NULL"), nullable=True)
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE", "UPDATE_COST"
resource_type = Column(String(50)) # pl. "Asset", "Expense"
action = Column(String(100), nullable=False) # pl. "ADD_VEHICLE"
resource_type = Column(String(50))
resource_id = Column(String(100))
details = Column(JSON, server_default=text("'{}'::jsonb"))
ip_address = Column(String(45))
created_at = Column(DateTime(timezone=True), server_default=func.now())
class ProcessLog(Base):
""" Robotok és háttérfolyamatok futási naplója (A reggeli jelentésekhez). """
__tablename__ = "process_logs" # Külön tábla a tisztaság kedvéért
__table_args__ = {"schema": "data", "extend_existing": True}
id = Column(Integer, primary_key=True)
process_name = Column(String(100), index=True) # 'Master-Enricher'
start_time = Column(DateTime(timezone=True), server_default=func.now())
end_time = Column(DateTime(timezone=True))
items_processed = Column(Integer, default=0)
items_failed = Column(Integer, default=0)
details = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())
class FinancialLedger(Base):
""" Minden pénz- és kreditmozgás központi naplója. """
__tablename__ = "financial_ledger"
@@ -43,14 +56,9 @@ class FinancialLedger(Base):
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("data.users.id"))
person_id = Column(BigInteger, ForeignKey("data.persons.id"))
amount = Column(Numeric(18, 4), nullable=False)
currency = Column(String(10)) # 'HUF', 'CREDIT', 'COIN'
transaction_type = Column(String(50)) # 'PURCHASE', 'HUNTING_COMMISSION', 'FARMING_COMMISSION'
# Üzletkötői követhetőség
currency = Column(String(10))
transaction_type = Column(String(50))
related_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
details = Column(JSON, server_default=text("'{}'::jsonb"))
created_at = Column(DateTime(timezone=True), server_default=func.now())

View File

@@ -44,18 +44,22 @@ class PointsLedger(Base):
class UserStats(Base):
__tablename__ = "user_stats"
__table_args__ = SCHEMA_ARGS
__table_args__ = {"schema": "data", "extend_existing": True} # Biztosítjuk a sémát
# A ForeignKey-nek látnia kell a data sémát!
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("data.users.id"), primary_key=True)
total_xp: Mapped[int] = mapped_column(Integer, default=0)
social_points: Mapped[int] = mapped_column(Integer, default=0)
current_level: Mapped[int] = mapped_column(Integer, default=1)
# --- BÜNTETŐ RENDSZER (Strike System) ---
# JAVÍTÁS: server_default hozzáadva, hogy a meglévő sorok is 0-t kapjanak
# --- BÜNTETŐ RENDSZER ---
penalty_points: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
restriction_level: Mapped[int] = mapped_column(Integer, server_default=text("0"), default=0)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=func.now(), onupdate=func.now())
# VISSZAMUTATÁS A USER-RE: a back_populates értéke meg kell egyezzen a User osztály 'stats' mezőjével!
user: Mapped["User"] = relationship("User", back_populates="stats")

View File

@@ -85,6 +85,14 @@ class User(Base):
# Farming üzletkötő (Átruházható cégkezelő)
current_sales_agent_id = Column(Integer, ForeignKey("data.users.id"), nullable=True)
# Szervezeti kapcsolat
owned_organizations = relationship("Organization", back_populates="owner")
# Ez a sor felelős a gamification.py-val való hídért
stats = relationship("UserStats", back_populates="user", uselist=False, cascade="all, delete-orphan")
ownership_history = relationship("VehicleOwnership", back_populates="user")
is_active = Column(Boolean, default=False)
is_deleted = Column(Boolean, default=False)
folder_slug = Column(String(12), unique=True, index=True)

View File

@@ -0,0 +1,85 @@
from sqlalchemy import Column, Integer, String, JSON, UniqueConstraint, text, Boolean, DateTime, ForeignKey, Numeric
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class VehicleType(Base):
"""Jármű főtípusok sémája (Séma-gazda)"""
__tablename__ = "vehicle_types"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
code = Column(String(30), unique=True, index=True) # car, motorcycle, truck, bus, boat, etc.
name = Column(String(50)) # Megjelenítendő név
icon = Column(String(50))
units = Column(JSON, server_default=text("'{\"power\": \"kW\", \"weight\": \"kg\", \"cargo\": \"m3\"}'::jsonb"))
features = relationship("FeatureDefinition", back_populates="vehicle_type")
definitions = relationship("VehicleModelDefinition", back_populates="v_type_rel")
class FeatureDefinition(Base):
"""Globális felszereltség szótár"""
__tablename__ = "feature_definitions"
__table_args__ = {"schema": "data"}
id = Column(Integer, primary_key=True)
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
category = Column(String(50)) # Műszaki, Beltér, Kültér, Multimédia
name = Column(String(100), nullable=False)
data_type = Column(String(20), default="boolean")
vehicle_type = relationship("VehicleType", back_populates="features")
class ModelFeatureMap(Base):
"""Modell-szintű felszereltségi sablon (Alap vs Extra)"""
__tablename__ = "model_feature_maps"
__table_args__ = {"schema": "data"}
model_id = Column(Integer, ForeignKey("data.vehicle_model_definitions.id"), primary_key=True)
feature_id = Column(Integer, ForeignKey("data.feature_definitions.id"), primary_key=True)
availability = Column(String(20), default="standard") # standard, optional, accessory
value = Column(String(100))
class VehicleModelDefinition(Base):
"""MDM Master rekordok"""
__tablename__ = "vehicle_model_definitions"
__table_args__ = (
UniqueConstraint('make', 'technical_code', 'vehicle_type', name='uix_make_tech_type'),
{"schema": "data"}
)
id = Column(Integer, primary_key=True)
make = Column(String(50), nullable=False, index=True)
technical_code = Column(String(50), nullable=False, index=True)
marketing_name = Column(String(100), index=True)
family_name = Column(String(100))
vehicle_type = Column(String(30), index=True)
vehicle_type_id = Column(Integer, ForeignKey("data.vehicle_types.id"))
vehicle_class = Column(String(50))
# --- LOGISZTIKAI ÉS TECHNIKAI FIX OSZLOPOK (v1.9) ---
engine_capacity = Column(Integer, index=True)
power_kw = Column(Integer, index=True)
max_weight_kg = Column(Integer, index=True) # Össztömeg
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
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
status = Column(String(20), server_default="unverified")
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())
v_type_rel = relationship("VehicleType", back_populates="definitions")
variants = relationship("AssetCatalog", back_populates="master_definition")

View File

@@ -0,0 +1,20 @@
# app/core/schemas/vehicle_categories.py
VEHICLE_SCHEMAS = {
"motorcycle": {
"features": ["ABS", "Markolatfűtés", "Szélvédő", "Bukócső/gomba", "Automata váltó", "Gyári dobozok", "Zárható doboz", "Veterán"],
"service_items": ["motorolaj", "olajszűrő", "levegőszűrő", "lánc_szett", "fékfolyadék", "gyújtógyertya", "szelephézag_ellenőrzés"]
},
"car": {
"features": ["Automata", "Tempomat", "Összkerékhajtás", "Alufelni", "Elektromos ablak", "Vonóhorog", "ISOFIX rendszer", "ESP", "Szervizkönyv", "Veterán"],
"service_items": ["motorolaj", "olajszűrő", "levegőszűrő", "pollenszűrő", "vezérlés_szett", "hosszbordásszíj", "váltóolaj", "fagyálló"]
},
"truck": {
"features": ["Légrugó", "Hálófülke", "Retarder/Intarder", "Emelőhátfal", "Tengelysúly-mérő", "AdBlue", "Állóhelyzeti klíma"],
"service_items": ["motorolaj", "légfék_szárító_patron", "üzemanyagszűrő", "érintésvédelmi_vizsga", "tengely_zsírozás"]
},
"boat": {
"features": ["Utánfutó", "Takaróponyva", "Orrsugárkormány", "Halradar", "Kormányállás", "Üzemanyagtartály", "Sólyakocsi", "Zárható tároló", "Elektromos horgonycsörlő"],
"service_items": ["motorolaj", "hajómotor_anód", "vízpumpa_lapát", "téliesítés", "algagátlózás"]
}
}

View File

@@ -0,0 +1,78 @@
import asyncio
from sqlalchemy import select, update, func
from app.db.session import SessionLocal
from app.models.asset import AssetCatalog
from app.models.vehicle_definitions import VehicleModelDefinition, VehicleType
async def link_catalog_to_mdm():
async with SessionLocal() as db:
try:
print("🔍 Meglévő variánsok elemzése...")
# 1. Lekérjük a típusokat a gyors kereséshez
type_res = await db.execute(select(VehicleType))
types = {t.code: t.id for t in type_res.scalars().all()}
# 2. Kigyűjtjük az egyedi márkákat és modelleket a katalógusból
# Itt csoportosítunk, hogy ne legyen duplikáció
stmt = select(
AssetCatalog.make,
AssetCatalog.model,
AssetCatalog.vehicle_class
).distinct()
raw_data = await db.execute(stmt)
unique_models = raw_data.all()
print(f"📊 Találtunk {len(unique_models)} egyedi modellt. Összefésülés indul...")
linked_count = 0
for make, model, v_class in unique_models:
# Meghatározzuk a típus ID-t (alapértelmezett: car)
t_code = v_class if v_class in types else "car"
t_id = types.get(t_code)
# Keressük, létezik-e már ilyen Master rekord
# A technical_code-ot itt ideiglenesen a modell nevével töltjük,
# amíg a robot/AI nem pontosítja
master_stmt = select(VehicleModelDefinition).where(
VehicleModelDefinition.make == make,
VehicleModelDefinition.marketing_name == model
)
master_res = await db.execute(master_stmt)
master = master_res.scalar_one_or_none()
if not master:
master = VehicleModelDefinition(
make=make,
technical_code=model, # Ideiglenes
marketing_name=model,
vehicle_type=t_code,
vehicle_type_id=t_id,
status="unverified",
source="initial_linking"
)
db.add(master)
await db.flush() # Hogy megkapjuk az ID-t
# 3. Összekötjük az összes variánst ezzel a Master rekorddal
update_stmt = update(AssetCatalog).where(
AssetCatalog.make == make,
AssetCatalog.model == model
).values(master_definition_id=master.id)
await db.execute(update_stmt)
linked_count += 1
if linked_count % 100 == 0:
print(f"⏳ Feldolgozva: {linked_count} modell...")
await db.commit()
print(f"✅ Kész! {linked_count} Master rekord létrehozva és összekötve.")
except Exception as e:
await db.rollback()
print(f"❌ Hiba az összefésülésnél: {e}")
if __name__ == "__main__":
asyncio.run(link_catalog_to_mdm())

View File

@@ -0,0 +1,42 @@
import asyncio
from sqlalchemy import select, func
from app.db.session import SessionLocal
from app.models.audit import ProcessLog
from datetime import datetime, timedelta
async def generate_morning_report():
async with SessionLocal() as db:
# Az elmúlt 24 óra logjai
yesterday = datetime.now() - timedelta(days=1)
stmt = select(ProcessLog).where(ProcessLog.start_time >= yesterday)
res = await db.execute(stmt)
logs = res.scalars().all()
report = f"📊 REGGELI ROBOT JELENTÉS - {datetime.now().date()}\n"
report += "="*40 + "\n"
total_proc = 0
total_fail = 0
cleaned_list = []
for log in logs:
total_proc += log.items_processed
total_fail += log.items_failed
if "cleaned" in log.details:
cleaned_list.extend(log.details["cleaned"])
report += f"✅ Feldolgozott modellek: {total_proc}\n"
report += f"❌ Hibás/Sikertelen: {total_fail}\n"
report += f"🧹 AI névtisztítások száma: {len(cleaned_list)}\n\n"
if cleaned_list:
report += "Példák a tisztított nevekre:\n"
for item in cleaned_list[:10]: # Csak az első 10-et listázzuk
report += f" - {item}\n"
print(report)
# Itt hívható az EmailManager.send(...)
return report
if __name__ == "__main__":
asyncio.run(generate_morning_report())

View File

@@ -2,7 +2,7 @@ import asyncio
import json
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from app.models.system_config import SystemParameter
from app.models import SystemParameter
from app.core.config import settings
async def seed_system():

View File

@@ -0,0 +1,84 @@
import asyncio
from sqlalchemy import select
from sqlalchemy.orm import configure_mappers
from app.db.session import SessionLocal
# Fontos: Importálunk minden modellt a regisztrációhoz
import app.models
from app.models.vehicle_definitions import VehicleType, FeatureDefinition
async def seed_system_data():
# Kényszerített mapper konfiguráció a hiba ellen
try:
configure_mappers()
except Exception as e:
print(f"⚠️ Mapper figyelmeztetés (lehet, hogy már kész): {e}")
async with SessionLocal() as db:
try:
print("🚀 Kezdődik a rendszeradatok beoltása...")
# 1. Jármű Fajták (Blueprints)
types_data = [
{"code": "car", "name": "Személyautó", "icon": "directions_car"},
{"code": "motorcycle", "name": "Motorkerékpár", "icon": "moped"},
{"code": "truck", "name": "Teherautó/Kamion", "icon": "local_shipping"},
{"code": "bus", "name": "Autóbusz", "icon": "directions_bus"},
{"code": "boat", "name": "Hajó/Vitorlás", "icon": "sailing"},
{"code": "camper", "name": "Lakóautó", "icon": "rv_hookup"},
{"code": "machinery", "name": "Munkagép", "icon": "construction"},
{"code": "trailer", "name": "Utánfutó", "icon": "trailer"}
]
type_id_map = {}
for t_info in types_data:
stmt = select(VehicleType).where(VehicleType.code == t_info["code"])
res = await db.execute(stmt)
v_type = res.scalar_one_or_none()
if not v_type:
v_type = VehicleType(**t_info)
db.add(v_type)
await db.flush()
type_id_map[t_info["code"]] = v_type.id
# 2. Extrák (Features) betöltése - A te listád alapján
features = {
"car": [
("Műszaki", "ABS (blokkolásgátló)"), ("Műszaki", "ESP (menetstabilizátor)"),
("Műszaki", "távolságtartó tempomat"), ("Beltér", "ISOFIX rendszer"),
("Multimédia", "Android Auto"), ("Multimédia", "Apple CarPlay")
],
"truck": [
("Munkavégzés", "elektromos retarder"), ("Munkavégzés", "intarder"),
("Munkavégzés", "AdBlue"), ("Fülke", "hálófülke")
],
"boat": [
("Műszaki", "orrsugárkormány"), ("Műszaki", "halradar"),
("Műszaki", "elektromos horgonycsörlő")
]
}
for code, items in features.items():
t_id = type_id_map.get(code)
if not t_id: continue
for cat, name in items:
stmt = select(FeatureDefinition).where(
FeatureDefinition.name == name,
FeatureDefinition.vehicle_type_id == t_id
)
res = await db.execute(stmt)
if not res.scalar_one_or_none():
db.add(FeatureDefinition(
vehicle_type_id=t_id, category=cat, name=name, data_type="boolean"
))
await db.commit()
print("✅ Minden alapadat (Types & Features) sikeresen betöltve!")
except Exception as e:
await db.rollback()
print(f"❌ Végzetes hiba a feltöltés során: {e}")
raise e
if __name__ == "__main__":
asyncio.run(seed_system_data())

View File

@@ -0,0 +1,72 @@
import os
import json
import logging
import google.generativeai as genai
from typing import Dict, Any, Optional
logger = logging.getLogger("AI-Service")
class AIService:
# Konfiguráció a .env-ből
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')
@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)."""
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:
{{
"marketing_name": "Tiszta modellnév",
"technical_code": "Modellkód/Generáció",
"ccm": int,
"kw": int,
"maintenance": {{
"oil_type": "pl. 10W-40",
"oil_qty": float,
"spark_plug": "típus",
"coolant": "típus"
}}
}}
"""
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)
except Exception as e:
logger.error(f"❌ AI Dúsítás hiba: {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."""
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."
}
prompt = f"Rendszer: Profi OCR és dokumentum-elemző vagy. {prompts.get(doc_type, 'Elemezd a képet.')} Válaszolj tiszta JSON formátumban."
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)
except Exception as e:
logger.error(f"❌ AI OCR hiba ({doc_type}): {e}")
return None

View File

@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from app.models.asset import AssetCost, AssetTelemetry, ExchangeRate
from app.models.gamification import UserStats
from app.models.system_config import SystemParameter
from app.models import SystemParameter
from app.schemas.asset_cost import AssetCostCreate
from datetime import datetime

View File

@@ -6,7 +6,7 @@ from sqlalchemy import select
from app.models.gamification import UserStats, PointsLedger
from app.models.identity import User, Wallet
from app.models.core_logic import CreditTransaction
from app.models.system_config import SystemParameter
from app.models import SystemParameter
logger = logging.getLogger(__name__)

View File

@@ -6,7 +6,7 @@ from sqlalchemy import select, func, and_
from app.models.security import PendingAction, ActionStatus
from app.models.history import AuditLog, LogSeverity
from app.models.identity import User
from app.models.system_config import SystemParameter
from app.models import SystemParameter
logger = logging.getLogger(__name__)

View File

@@ -0,0 +1,66 @@
import asyncio
import os
import logging
from PIL import Image
from sqlalchemy import select, update
from app.db.session import SessionLocal
from app.models.document import Document # Feltételezve
from app.models.identity import User
from app.services.ai_service import AIService
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("Robot-OCR-V3")
NAS_BASE_PATH = os.getenv("NAS_STORAGE_PATH", "/mnt/nas/user_vault")
class OCRRobot:
@classmethod
async def process_queue(cls):
async with SessionLocal() as db:
# 1. Csak a várólistás és prémium jogosultságú dokumentumokat keressük
stmt = select(Document, User).join(User).where(
Document.status == "pending_ocr",
User.subscription_plan.in_(["PREMIUM_PLUS", "VIP_PLUS"])
).limit(10)
res = await db.execute(stmt)
tasks = res.all()
for doc, user in tasks:
try:
logger.info(f"📸 OCR feldolgozás: {doc.filename} (User: {user.id})")
# 2. AI OCR hívás
with open(doc.temp_path, "rb") as f:
image_bytes = f.read()
ocr_result = await AIService.analyze_document_image(image_bytes, doc.doc_type)
if ocr_result:
# 3. Kép átméretezése (Thumbnail és Standard)
target_dir = os.path.join(NAS_BASE_PATH, user.folder_slug, doc.doc_type)
os.makedirs(target_dir, exist_ok=True)
final_path = os.path.join(target_dir, f"{doc.id}.jpg")
cls.resize_and_save(doc.temp_path, final_path)
# 4. Adatbázis frissítése
doc.ocr_data = ocr_result
doc.file_link = final_path
doc.status = "processed"
# Ideiglenes fájl törlése
os.remove(doc.temp_path)
await db.commit()
except Exception as e:
logger.error(f"❌ OCR Hiba ({doc.id}): {e}")
await db.rollback()
@staticmethod
def resize_and_save(source, target):
with Image.open(source) as img:
img.convert('RGB').save(target, "JPEG", quality=85, optimize=True)
if __name__ == "__main__":
asyncio.run(OCRRobot.process_queue())

View File

@@ -3,17 +3,22 @@ import httpx
import logging
import os
import datetime
from sqlalchemy import text
import json
from sqlalchemy import text, select, update
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.0.4-Master-Enricher")
logger = logging.getLogger("Robot-v1.1.0-Master-Enricher")
class TechEnricher:
"""
Master Enricher v1.0.4
- Target: kyri-nuah (RDW Technical Catalogue)
- Fix: Visszaállás 'merk' mezőre + SQL fix az új oszlopokhoz.
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"
@@ -21,105 +26,110 @@ class TechEnricher:
HEADERS = {"X-App-Token": RDW_TOKEN} if RDW_TOKEN else {}
@classmethod
async def fetch_tech_data(cls, make, model):
# Tisztítás: Ha a modell névben benne van a márka, levágjuk
clean_model = str(model).upper().replace(str(make).upper(), "").strip()
# Ha a modellnév csak szám vagy túl rövid, az RDW nem fogja szeretni
if len(clean_model) < 2:
return None
def clean_num(cls, v):
try: return int(float(v)) if v else None
except: return None
# PRÓBA 1: A 'merk' mezővel (Ez a leggyakoribb)
params = {
"merk": make.upper(),
"handelsbenaming": clean_model,
"$limit": 1
}
@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}
async with httpx.AsyncClient(headers=cls.HEADERS) as client:
try:
await asyncio.sleep(1.1)
await asyncio.sleep(1.1) # RDW Rate limit védelem
resp = await client.get(cls.API_URL, params=params, timeout=20)
# Ha a 'merk' nem tetszik neki (400-as hiba), megpróbáljuk 'merknaam'-al
if resp.status_code == 400:
params = {"merknaam": make.upper(), "handelsbenaming": clean_model, "$limit": 1}
resp = await client.get(cls.TECH_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"❌ API Hiba: {e}")
logger.error(f" RDW API Hiba: {e}")
return None
@classmethod
async def run(cls):
logger.info("🚀 Master Enricher v1.0.4 - Új oszlopok töltése indul...")
while True:
async with SessionLocal() as db:
# Olyan sorokat keresünk, ahol az új oszlopok még üresek
query = text("""
SELECT id, make, model
FROM data.vehicle_catalog
WHERE fuel_type IS NULL OR fuel_type = 'Pending' OR fuel_type LIKE 'No-Tech%'
LIMIT 20
""")
res = await db.execute(query)
tasks = res.fetchall()
logger.info("🚀 Master Enricher v1.1.0 INDUL...")
start_time = datetime.datetime.now()
stats = {"processed": 0, "failed": 0, "cleaned": []}
if not tasks:
logger.info("😴 Minden adat kész. Alvás 5 perc...")
await asyncio.sleep(300)
continue
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()
for t_id, make, model in tasks:
logger.info(f"🧪 Gazdagítás: {make} | {model}")
tech = await cls.fetch_tech_data(make, model)
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}")
if tech:
# RDW mezők kinyerése
kw = tech.get("netto_maximum_vermogen_kw")
ccm = tech.get("cilinderinhoud")
weight = tech.get("technisch_toelaatbare_maximum_massa")
axles = tech.get("aantal_assen")
euro = tech.get("milieuklasse_eg_goedkeuring_licht")
fuel = tech.get("brandstof_omschrijving_brandstof_stam", "Standard")
# 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}")
# Biztonságos konverzió
def clean_num(v):
try: return int(float(v)) if v else None
except: return None
update_query = text("""
UPDATE data.vehicle_catalog
SET fuel_type = :fuel,
power_kw = :kw,
engine_capacity = :ccm,
max_weight_kg = :weight,
axle_count = :axles,
euro_class = :euro,
factory_data = factory_data || jsonb_build_object('enriched_at', :now)
WHERE id = :id
""")
# 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"))
await db.execute(update_query, {
"fuel": fuel, "kw": clean_num(kw), "ccm": clean_num(ccm),
"weight": clean_num(weight), "axles": clean_num(axles),
"euro": str(euro) if euro else None,
"id": t_id, "now": str(datetime.datetime.now())
})
await db.commit()
logger.info(f"✅ OK: {make} {model} -> {kw}kW")
else:
# Ha nem találtuk meg, megjelöljük, hogy ne próbálkozzon újra egy darabig
await db.execute(text("UPDATE data.vehicle_catalog SET fuel_type = 'No-Tech-V4' WHERE id = :id"), {"id": t_id})
await db.commit()
master.status = "ai_enriched"
stats["processed"] += 1
await db.commit()
await asyncio.sleep(0.5)
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("📧 Email jelentés elküldve az adminnak.")
# EmailManager.send_admin_notification("Robot Report", report_body)
if __name__ == "__main__":
asyncio.run(TechEnricher.run())