átlagos kiegészítséek jó sok

This commit is contained in:
Roo
2026-03-22 11:02:05 +00:00
parent f53e0b53df
commit 5d44339f21
249 changed files with 20922 additions and 2253 deletions

View File

@@ -1,238 +1,203 @@
#!/usr/bin/env python3
import asyncio
import logging
import warnings
import os
import json
from datetime import datetime
from sqlalchemy import text, update, func
from app.database import AsyncSessionLocal
from app.models.vehicle_definitions import VehicleModelDefinition
warnings.filterwarnings("ignore", category=RuntimeWarning, module='duckduckgo_search')
import httpx
import re
from bs4 import BeautifulSoup
from duckduckgo_search import DDGS
from playwright.async_api import async_playwright
from sqlalchemy import text
from app.database import AsyncSessionLocal
# MB 2.0 Szabvány naplózás
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] Robot-2-Researcher: %(message)s')
logger = logging.getLogger("Vehicle-Robot-2-Researcher")
# Figyelmeztetések némítása (a csomag átnevezése miatti zaj elkerülésére)
warnings.filterwarnings("ignore", category=RuntimeWarning, module='duckduckgo_search')
class QuotaManager:
""" Szigorú napi limit figyelő a fizetős/hatósági API-khoz """
def __init__(self, service_name: str, daily_limit: int):
self.service_name = service_name
self.daily_limit = daily_limit
self.state_file = f"/app/temp/.quota_{service_name}.json"
self._ensure_file()
def _ensure_file(self):
os.makedirs(os.path.dirname(self.state_file), exist_ok=True)
if not os.path.exists(self.state_file):
with open(self.state_file, 'w') as f:
json.dump({"date": datetime.now().strftime("%Y-%m-%d"), "count": 0}, f)
def can_make_request(self) -> bool:
with open(self.state_file, 'r') as f:
data = json.load(f)
today = datetime.now().strftime("%Y-%m-%d")
if data["date"] != today:
data = {"date": today, "count": 0} # Új nap, kvóta nullázása
if data["count"] >= self.daily_limit:
return False
# Növeljük a számlálót
data["count"] += 1
with open(self.state_file, 'w') as f:
json.dump(data, f)
return True
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [R2-MASTER-EDITION] %(message)s'
)
logger = logging.getLogger("R2-Researcher")
class VehicleResearcher:
"""
Vehicle Robot 2.5: Sniper Researcher (Mesterlövész Adatgyűjtő)
Célzott keresésekkel és strukturált aktakészítéssel dolgozik az AI kímélése érdekében.
"""
def __init__(self):
self.max_attempts = 5
self.search_timeout = 15.0
def __init__(self, concurrency=5):
# Egyszerre 5 böngésző fület kezelünk a sebesség érdekében
self.semaphore = asyncio.Semaphore(concurrency)
self.ollama_url = "http://sf_ollama:11434/api/generate"
# Kvóta menedzserek beállítása (.env-ből olvasva)
dvla_limit = int(os.getenv("DVLA_DAILY_LIMIT", "1000"))
self.dvla_quota = QuotaManager("dvla", dvla_limit)
self.dvla_token = os.getenv("DVLA_API_KEY")
# FORDÍTÓ SZÓTÁR: Holland RDW -> Nemzetközi keresési nevek
self.translation_map = {
"ER REIHE": "Series",
"T-MODELL": "Estate",
"KLASSE": "Class",
"PERSONENAUTO": "Car",
"STATIONWAGEN": "Estate",
"MERCEDES-BENZ": "Mercedes",
"Vrachtwagen": "Truck",
"Oplegger": "Trailer"
}
async def fetch_ddg_targeted(self, label: str, query: str) -> str:
""" Célzott keresés szálbiztosan a DuckDuckGo-n. """
def clean_name(self, make, model):
"""Lefordítja a holland modellneveket, hogy a Google/Bing megtalálja őket."""
name = f"{make} {model}".upper()
for dutch, eng in self.translation_map.items():
name = name.replace(dutch, eng)
return name.title()
async def get_url(self, make, model, year, kw):
"""Keresés a DuckDuckGo-val. JAVÍTVA: 0kW fix és több találat."""
clean_n = self.clean_name(make, model)
# Ha a kW 0, None vagy érvénytelen, kihagyjuk a keresésből a találati arány javítására
kw_val = 0
try:
def search():
if kw and str(kw).replace('.','').isdigit():
kw_val = int(float(kw))
except: pass
kw_part = f"{kw_val}kW" if kw_val > 0 else ""
query = f"site:auto-data.net {clean_n} {year} {kw_part} specifications"
try:
def _search():
with DDGS() as ddgs:
# max_results=2: Nem kell sok zaj, csak a legrelevánsabb 2 találat
results = ddgs.text(query, max_results=2)
return [f"- {r.get('body', '')}" for r in results] if results else []
results = await asyncio.wait_for(asyncio.to_thread(search), timeout=self.search_timeout)
if not results:
return f"[SOURCE: {label}]\nNincs érdemi találat.\n"
content = f"[SOURCE: {label} | KERESÉS: {query}]\n"
content += "\n".join(results) + "\n"
return content
# Megnézzük az első 3 találatot, hátha az első nem direkt link
res = ddgs.search(query, max_results=3)
return [r.get('link', r.get('href', '')) for r in res if 'auto-data.net' in r.get('link', r.get('href', ''))]
links = await asyncio.to_thread(_search)
return links[0] if links else None
except Exception as e:
logger.debug(f"Keresési hiba ({label}): {e}")
return f"[SOURCE: {label}]\nKERESÉSI HIBA.\n"
logger.warning(f"Keresési hiba ({query}): {e}")
return None
def extract_specs_from_text(self, text: str) -> dict:
""" Regex alapú kinyerés a nyers szövegből: ccm, kW, motoradatok. """
import re
async def scrape_auto_data(self, url, browser):
"""Letölti az oldalt és kinyeri az összes technikai adatot."""
specs = {}
# CCM (köbcentiméter) minta: 1998 cc, 2.0 L, 2000 cm³
ccm_pattern = r'(\d{3,4})\s*(?:cc|ccm|cm³|cm3|cc\.)'
match = re.search(ccm_pattern, text, re.IGNORECASE)
if match:
specs['ccm'] = int(match.group(1))
else:
# Alternatív minta: 2.0 liter -> 2000 cc
liter_pattern = r'(\d+\.?\d*)\s*(?:L|liter|)'
match = re.search(liter_pattern, text, re.IGNORECASE)
if match:
liters = float(match.group(1))
specs['ccm'] = int(liters * 1000)
# KW (kilowatt) minta: 150 kW, 150kW, 150 KW
kw_pattern = r'(\d{2,4})\s*(?:kW|kw|KW)'
match = re.search(kw_pattern, text, re.IGNORECASE)
if match:
specs['kw'] = int(match.group(1))
else:
# Le (lóerő) átváltás: 150 LE -> 110 kW (kb)
hp_pattern = r'(\d{2,4})\s*(?:HP|hp|LE|le|Ps)'
match = re.search(hp_pattern, text, re.IGNORECASE)
if match:
hp = int(match.group(1))
specs['kw'] = int(hp * 0.7355) # hozzávetőleges átváltás
# Motor kód minta: motor kód: 1.8 TSI, engine code: N47
engine_pattern = r'(?:motor\s*kód|engine\s*code|motor\s*code)[:\s]+([A-Z0-9\.\- ]+)'
match = re.search(engine_pattern, text, re.IGNORECASE)
if match:
specs['engine_code'] = match.group(1).strip()
return specs
async def research_vehicle(self, db, vehicle_id: int, make: str, model: str, engine: str, year: str, current_attempts: int):
""" Egy jármű átvilágítása és a strukturált 'Akta' elkészítése a GPU számára. """
engine_safe = engine or ""
year_safe = str(year) if year else ""
logger.info(f"🔎 Mesterlövész Kutatás: {make} {model} (Motor: {engine_safe})")
# 1. TIER: Ingyenes, Célzott Keresések (A legmegbízhatóbb források)
queries = [
("ULTIMATE_SPECS", f"{make} {model} {engine_safe} {year_safe} site:ultimatespecs.com"),
("AUTO_DATA", f"{make} {model} {engine_safe} {year_safe} site:auto-data.net"),
("COMMON_ISSUES", f"{make} {model} {engine_safe} reliability common problems")
]
tasks = [self.fetch_ddg_targeted(label, q) for label, q in queries]
search_results = await asyncio.gather(*tasks)
# 2. TIER: Fizetős / Kvótás API-k (Példa a DVLA helyére)
# Ha a jövőben bejön brit rendszám, itt hívjuk meg a DVLA-t:
# if has_uk_plate and self.dvla_quota.can_make_request():
# uk_data = await self.fetch_dvla_data(plate)
# search_results.append(uk_data)
# 3. ÖSSZESÍTÉS (Az Akta összeállítása)
# Maximalizáljuk a szöveg hosszát, hogy az AI GPU ne fulladjon le!
full_context = "\n".join(search_results)
if len(full_context) > 2500:
full_context = full_context[:2500] + "\n...[TRUNCATED TO SAVE GPU TOKENS]"
# Regex alapú specifikáció kinyerés
extracted_specs = self.extract_specs_from_text(full_context)
full_text = ""
try:
if len(full_context.strip()) > 150: # Csökkentettük az elvárást, mert a célzott keresés tömörebb
await db.execute(
update(VehicleModelDefinition)
.where(VehicleModelDefinition.id == vehicle_id)
.values(
raw_search_context=full_context,
research_metadata=extracted_specs,
status='awaiting_ai_synthesis', # Kész az Akta, mehet az Alkimistának!
last_research_at=func.now(),
attempts=current_attempts + 1
)
)
logger.info(f"✅ Akta rögzítve ({len(full_context)} karakter): {make} {model}")
else:
new_status = 'suspended_research' if current_attempts + 1 >= self.max_attempts else 'unverified'
await db.execute(
update(VehicleModelDefinition)
.where(VehicleModelDefinition.id == vehicle_id)
.values(
status=new_status,
attempts=current_attempts + 1,
last_research_at=func.now()
)
)
if new_status == 'suspended_research':
logger.warning(f"🛑 Felfüggesztve (Nincs nyom a weben): {make} {model}")
else:
logger.warning(f"⚠️ Kevés adat: {make} {model}, visszatéve a sorba.")
page = await browser.new_page()
# Gyorsítás: képek, videók és stíluslapok tiltása
await page.route("**/*.{png,jpg,jpeg,gif,css,woff2}", lambda r: r.abort())
await page.goto(url, wait_until="domcontentloaded", timeout=20000)
html = await page.content()
# Kimentjük a tiszta szöveget is, ha az AI-nak kellene később
full_text = await page.evaluate("() => document.body.innerText")
await page.close()
soup = BeautifulSoup(html, 'html.parser')
# Végigfutunk minden táblázat soron
for row in soup.find_all('tr'):
th = row.find('th')
td = row.find('td')
if th and td:
k, v = th.get_text(strip=True).lower(), td.get_text(strip=True)
await db.commit()
# Minden fontos mező kinyerése
if "engine model/code" in k: specs["engine_code"] = v
elif "engine oil capacity" in k: specs["oil_l"] = v
elif "acceleration 0 - 100" in k: specs["acc_0_100"] = v
elif "maximum speed" in k: specs["max_speed"] = v
elif "fuel consumption" in k and "combined" in k: specs["cons_avg"] = v
elif "co2 emissions" in k: specs["co2"] = v
elif "generation" in k: specs["generation"] = v
elif "tires size" in k: specs["tires"] = v
elif "trunk (boot) space" in k: specs["trunk_l"] = v
elif "kerb weight" in k: specs["weight_kg"] = v
elif "drivetrain" in k: specs["drivetrain"] = v
elif "number of gears" in k: specs["transmission"] = v
return specs, full_text
except Exception as e:
await db.rollback()
logger.error(f"🚨 Adatbázis hiba az eredmény mentésénél ({vehicle_id}): {e}")
logger.error(f"Scraping hiba az oldalon ({url}): {e}")
return {}, ""
@classmethod
async def run(cls):
self_instance = cls()
logger.info("🚀 Vehicle Researcher 2.5 ONLINE (Sniper & Quota Manager)")
while True:
try:
async with AsyncSessionLocal() as db:
# ATOMI ZÁROLÁS
query = text("""
UPDATE vehicle.vehicle_model_definitions
SET status = 'research_in_progress'
WHERE id = (
SELECT id FROM vehicle.vehicle_model_definitions
WHERE status IN ('unverified', 'awaiting_research', 'ACTIVE')
AND attempts < :max_attempts
AND is_manual = FALSE
ORDER BY
CASE WHEN make = 'TOYOTA' THEN 1 ELSE 2 END,
attempts ASC
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, make, marketing_name, engine_code, year_from, attempts;
""")
result = await db.execute(query, {"max_attempts": self_instance.max_attempts})
task = result.fetchone()
await db.commit()
async def ask_ai_fallback(self, raw_text):
"""Ha a BeautifulSoup nem talál táblázatot, megkérjük az Ollamát."""
if not raw_text or len(raw_text) < 200: return {}
prompt = f"Extract vehicle specs (engine_code, oil_capacity, tires, generation) as JSON from this text: {raw_text[:2500]}"
try:
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.post(self.ollama_url, json={
"model": "qwen2.5-coder:14b",
"prompt": prompt,
"stream": False,
"format": "json"
})
return json.loads(r.json().get("response", "{}"))
except: return {}
if task:
v_id, v_make, v_model, v_engine, v_year, v_attempts = task
async with AsyncSessionLocal() as process_db:
await self_instance.research_vehicle(process_db, v_id, v_make, v_model, v_engine, v_year, v_attempts)
await asyncio.sleep(2) # Rate limit védelem a DDG felé
async def process_vehicle(self, v_id, make, model, year, kw, browser):
"""Egy jármű dúsításának teljes folyamata."""
async with self.semaphore:
logger.info(f"🔍 Kutatás: {make} {model} ({year}) | kW: {kw}")
url = await self.get_url(make, model, year, kw)
specs = {}
if url:
logger.info(f"🔗 Találat: {url}")
specs, raw_text = await self.scrape_auto_data(url, browser)
# Ha a táblázatból nem jött ki elég adat, jöhet az AI fallback
if len(specs) < 3:
ai_specs = await self.ask_ai_fallback(raw_text)
specs.update(ai_specs)
# MENTÉS: Minden szál saját adatbázis kapcsolatot használ a biztonság érdekében
async with AsyncSessionLocal() as db:
# Csak akkor validation_ready, ha találtunk adatot. Ha nem, külön státuszba tesszük.
new_status = 'validation_ready' if len(specs) > 0 else 'research_failed_empty'
update_query = text("""
UPDATE vehicle.vehicle_model_definitions
SET specifications = specifications || CAST(:specs AS JSONB),
status = :status,
last_research_at = now()
WHERE id = :id
""")
await db.execute(update_query, {
"specs": json.dumps(specs),
"status": new_status,
"id": v_id
})
await db.commit()
if len(specs) > 0:
logger.info(f"✅ SIKER: {make} {model} ({len(specs)} adat kinyerve)")
else:
await asyncio.sleep(30)
logger.warning(f"❌ SIKERTELEN: {make} {model} (nem találtunk adatot a neten)")
except Exception as e:
logger.error(f"💀 Kritikus hiba a főciklusban: {e}")
await asyncio.sleep(10)
async def run(self):
logger.info("🚀 R2-Kutató MASTER-EDITION (0kW fix + AI Fallback) ONLINE")
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
while True:
try:
async with AsyncSessionLocal() as db:
# 10 autó bekérése párhuzamos feldolgozásra
res = await db.execute(text("""
UPDATE vehicle.vehicle_model_definitions SET status = 'research_in_progress'
WHERE id IN (
SELECT id FROM vehicle.vehicle_model_definitions
WHERE status = 'enrich_ready'
LIMIT 10
) RETURNING id, make, marketing_name, year_from, power_kw
"""))
rows = res.fetchall()
await db.commit()
if not rows:
await asyncio.sleep(15)
continue
tasks = [self.process_vehicle(r[0], r[1], r[2], r[3], r[4], browser) for r in rows]
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"💀 Kritikus hiba a főciklusban: {e}")
await asyncio.sleep(10)
if __name__ == "__main__":
try:
asyncio.run(VehicleResearcher.run())
except KeyboardInterrupt:
logger.info("🛑 Kutató robot leállítva.")
asyncio.run(VehicleResearcher().run())