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,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