Spaces:
Sleeping
Sleeping
Probleme de Localisation et nouvel attribut Role Media
Browse files- Check_claim_gemini.py +128 -0
- appsheet_client.py +7 -0
- extract_claim_from_email.py +20 -11
- finalize_claim.py +20 -2
Check_claim_gemini.py
CHANGED
|
@@ -281,6 +281,18 @@ ZONE_FR_TO_PART = {
|
|
| 281 |
"pare-choc arriere": "back-bumper",
|
| 282 |
"pare choc arriere": "back-bumper",
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
# Capot / coffre / toit
|
| 285 |
"capot": "hood",
|
| 286 |
"coffre": "trunk",
|
|
@@ -302,6 +314,13 @@ ZONE_FR_TO_PART = {
|
|
| 302 |
"portiere arriere droite": "right_back-door",
|
| 303 |
"porte arriere droite": "right_back-door",
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
# Rétroviseurs
|
| 306 |
"retroviseur gauche": "left_mirror",
|
| 307 |
"retro gauche": "left_mirror",
|
|
@@ -365,6 +384,47 @@ ZONE_FR_TO_PART = {
|
|
| 365 |
# Tri par longueur de clé décroissante : on veut matcher "aile arriere droite" avant un éventuel motif plus générique qui s'y trouverait inclus.
|
| 366 |
_ZONE_KEYS_SORTED = sorted(ZONE_FR_TO_PART.keys(), key=len, reverse=True)
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
EXTRACTION_PROMPT_TEMPLATE = """Tu es un extracteur d'informations strict. On te donne une phrase décrivant une réclamation client sur un véhicule.
|
| 369 |
|
| 370 |
Extrait UNIQUEMENT deux informations :
|
|
@@ -1283,6 +1343,74 @@ def save_claim_result(result, vehicle_id, output_dir):
|
|
| 1283 |
return out_path
|
| 1284 |
|
| 1285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1286 |
# ─── TRAITEMENT EN LOT, MODÈLES CHARGÉS UNE SEULE FOIS ──────────
|
| 1287 |
|
| 1288 |
def process_claims_batch(claims, output_dir):
|
|
|
|
| 281 |
"pare-choc arriere": "back-bumper",
|
| 282 |
"pare choc arriere": "back-bumper",
|
| 283 |
|
| 284 |
+
# Pare-choc(s) SANS précision avant/arrière (ex: "le pare-chocs est
|
| 285 |
+
# abîmé") -> on ne devine PAS lequel des deux : on couvre les DEUX
|
| 286 |
+
# (avant ET arrière) plutôt que de tomber sur "zone non spécifiée"
|
| 287 |
+
# (qui élargirait la vérification à toute la voiture au lieu de la
|
| 288 |
+
# restreindre aux pare-chocs). Placées après les variantes avant/arrière
|
| 289 |
+
# ci-dessus : peu importe, le tri par longueur de clé (_ZONE_KEYS_SORTED)
|
| 290 |
+
# garantit que "pare-choc avant"/"pare-choc arriere" sont essayées avant.
|
| 291 |
+
"pare-choc": ["front-bumper", "back-bumper"],
|
| 292 |
+
"pare choc": ["front-bumper", "back-bumper"],
|
| 293 |
+
"pare-chocs": ["front-bumper", "back-bumper"],
|
| 294 |
+
"pare chocs": ["front-bumper", "back-bumper"],
|
| 295 |
+
|
| 296 |
# Capot / coffre / toit
|
| 297 |
"capot": "hood",
|
| 298 |
"coffre": "trunk",
|
|
|
|
| 314 |
"portiere arriere droite": "right_back-door",
|
| 315 |
"porte arriere droite": "right_back-door",
|
| 316 |
|
| 317 |
+
# Porte(s)/portière(s) SANS précision de côté NI d'axe avant/arrière
|
| 318 |
+
# (ex: "une porte est enfoncée") -> comme pour le pare-choc, on ne
|
| 319 |
+
# devine pas laquelle : on couvre les 4 portes plutôt que d'élargir la
|
| 320 |
+
# vérification à toute la voiture.
|
| 321 |
+
"portiere": ["left_front-door", "right_front-door", "left_back-door", "right_back-door"],
|
| 322 |
+
"porte": ["left_front-door", "right_front-door", "left_back-door", "right_back-door"],
|
| 323 |
+
|
| 324 |
# Rétroviseurs
|
| 325 |
"retroviseur gauche": "left_mirror",
|
| 326 |
"retro gauche": "left_mirror",
|
|
|
|
| 384 |
# Tri par longueur de clé décroissante : on veut matcher "aile arriere droite" avant un éventuel motif plus générique qui s'y trouverait inclus.
|
| 385 |
_ZONE_KEYS_SORTED = sorted(ZONE_FR_TO_PART.keys(), key=len, reverse=True)
|
| 386 |
|
| 387 |
+
|
| 388 |
+
# ─── CORRESPONDANCE CODE PIÈCE (nomenclature CV) -> LIBELLÉ FRANÇAIS ─────────
|
| 389 |
+
# Utilisé pour afficher un libellé lisible (colonne AppSheet "Localisation
|
| 390 |
+
# Carrosserie") plutôt que le code technique interne (ex: "front-bumper").
|
| 391 |
+
CODE_TO_FR_LABEL = {
|
| 392 |
+
"front-bumper": "Pare-choc avant",
|
| 393 |
+
"back-bumper": "Pare-choc arrière",
|
| 394 |
+
"hood": "Capot",
|
| 395 |
+
"trunk": "Coffre",
|
| 396 |
+
"roof": "Toit",
|
| 397 |
+
"license-plate": "Plaque d'immatriculation",
|
| 398 |
+
"grille": "Calandre",
|
| 399 |
+
"windshield": "Pare-brise avant",
|
| 400 |
+
"back-windshield": "Pare-brise arrière",
|
| 401 |
+
"left_front-door": "Portière avant gauche",
|
| 402 |
+
"right_front-door": "Portière avant droite",
|
| 403 |
+
"left_back-door": "Portière arrière gauche",
|
| 404 |
+
"right_back-door": "Portière arrière droite",
|
| 405 |
+
"left_mirror": "Rétroviseur gauche",
|
| 406 |
+
"right_mirror": "Rétroviseur droit",
|
| 407 |
+
"left_headlight": "Phare avant gauche",
|
| 408 |
+
"right_headlight": "Phare avant droit",
|
| 409 |
+
"left_tail-light": "Feu arrière gauche",
|
| 410 |
+
"right_tail-light": "Feu arrière droit",
|
| 411 |
+
"left_front-wheel": "Roue avant gauche",
|
| 412 |
+
"right_front-wheel": "Roue avant droite",
|
| 413 |
+
"left_back-wheel": "Roue arrière gauche",
|
| 414 |
+
"right_back-wheel": "Roue arrière droite",
|
| 415 |
+
"left_front-window": "Vitre avant gauche",
|
| 416 |
+
"right_front-window": "Vitre avant droite",
|
| 417 |
+
"left_back-window": "Vitre arrière gauche",
|
| 418 |
+
"right_back-window": "Vitre arrière droite",
|
| 419 |
+
"left_rocker-panel": "Bas de caisse gauche",
|
| 420 |
+
"right_rocker-panel": "Bas de caisse droit",
|
| 421 |
+
"left_fender": "Aile avant gauche",
|
| 422 |
+
"right_fender": "Aile avant droite",
|
| 423 |
+
"left_quarter-panel": "Aile arrière gauche",
|
| 424 |
+
"right_quarter-panel": "Aile arrière droite",
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
|
| 428 |
EXTRACTION_PROMPT_TEMPLATE = """Tu es un extracteur d'informations strict. On te donne une phrase décrivant une réclamation client sur un véhicule.
|
| 429 |
|
| 430 |
Extrait UNIQUEMENT deux informations :
|
|
|
|
| 1343 |
return out_path
|
| 1344 |
|
| 1345 |
|
| 1346 |
+
# ─── RÔLE DES MÉDIAS (table AppSheet "Medias Cameras", colonne "Role Media") ──
|
| 1347 |
+
# Valeurs possibles de l'enum "Role Media" :
|
| 1348 |
+
# - "Dommages" : le dommage réclamé est confirmé visible sur ce média.
|
| 1349 |
+
# - "Parties Visibles" : ce média montre la pièce réclamée, mais sans dommage
|
| 1350 |
+
# confirmé dessus (utile pour visualiser l'état du
|
| 1351 |
+
# véhicule à l'entrée).
|
| 1352 |
+
# - "Inutile" : ce média ne montre pas la pièce réclamée.
|
| 1353 |
+
ROLE_DOMMAGES = "Dommages"
|
| 1354 |
+
ROLE_PARTIES_VISIBLES = "Parties Visibles"
|
| 1355 |
+
ROLE_INUTILE = "Inutile"
|
| 1356 |
+
|
| 1357 |
+
|
| 1358 |
+
def build_media_role_resolver(claim_result: dict):
|
| 1359 |
+
"""
|
| 1360 |
+
À partir du résultat de process_claim()/process_multi_claim(), construit
|
| 1361 |
+
une fonction `resolver(file_id, is_image) -> role` qui donne le rôle
|
| 1362 |
+
("Dommages" / "Parties Visibles" / "Inutile") d'un média d'entrée à
|
| 1363 |
+
partir de son ID Drive (utilisé comme nom de fichier local par
|
| 1364 |
+
drive_enrichment.py / drive_lookup.py, ex: "abcd1234.png") et de son
|
| 1365 |
+
type (photo ou vidéo).
|
| 1366 |
+
|
| 1367 |
+
Règles :
|
| 1368 |
+
- Photo : "Dommages" si un dommage y a ��té confirmé (damage_visible=True
|
| 1369 |
+
sur au moins un "check" qui ne vient pas d'une frame vidéo) ; sinon
|
| 1370 |
+
"Parties Visibles" si elle couvre la pièce réclamée (présente dans
|
| 1371 |
+
candidate_images d'au moins un dommage) ; sinon "Inutile" (ne montre
|
| 1372 |
+
pas la pièce réclamée par la réclamation).
|
| 1373 |
+
- Vidéo : suit la même logique que leur upload dans Preuves / Parties
|
| 1374 |
+
Visibles (finalize_claim.executer_bilan_final /
|
| 1375 |
+
report_builder.construire_bilan_final) : "Dommages" si le verdict
|
| 1376 |
+
global est OUI, "Parties Visibles" si la zone n'a pas pu être
|
| 1377 |
+
précisée (mapped_part=None -> toutes les vidéos sont ajoutées pour
|
| 1378 |
+
vérification à l'oeil nu), "Inutile" sinon.
|
| 1379 |
+
"""
|
| 1380 |
+
sous_resultats = claim_result.get("damages") or [claim_result]
|
| 1381 |
+
|
| 1382 |
+
photos_avec_dommage = set()
|
| 1383 |
+
photos_visibles = set()
|
| 1384 |
+
for r in sous_resultats:
|
| 1385 |
+
for check in r.get("checks", []):
|
| 1386 |
+
if check.get("from_video"):
|
| 1387 |
+
continue
|
| 1388 |
+
file_id = Path(check["image_path"]).stem
|
| 1389 |
+
photos_visibles.add(file_id)
|
| 1390 |
+
if check.get("damage_visible") is True:
|
| 1391 |
+
photos_avec_dommage.add(file_id)
|
| 1392 |
+
for chemin_image in r.get("candidate_images", []):
|
| 1393 |
+
photos_visibles.add(Path(chemin_image).stem)
|
| 1394 |
+
|
| 1395 |
+
if claim_result.get("verdict") == "OUI":
|
| 1396 |
+
role_video = ROLE_DOMMAGES
|
| 1397 |
+
elif claim_result.get("mapped_part") is None:
|
| 1398 |
+
role_video = ROLE_PARTIES_VISIBLES
|
| 1399 |
+
else:
|
| 1400 |
+
role_video = ROLE_INUTILE
|
| 1401 |
+
|
| 1402 |
+
def resolver(file_id: str, is_image: bool) -> str:
|
| 1403 |
+
if not is_image:
|
| 1404 |
+
return role_video
|
| 1405 |
+
if file_id in photos_avec_dommage:
|
| 1406 |
+
return ROLE_DOMMAGES
|
| 1407 |
+
if file_id in photos_visibles:
|
| 1408 |
+
return ROLE_PARTIES_VISIBLES
|
| 1409 |
+
return ROLE_INUTILE
|
| 1410 |
+
|
| 1411 |
+
return resolver
|
| 1412 |
+
|
| 1413 |
+
|
| 1414 |
# ─── TRAITEMENT EN LOT, MODÈLES CHARGÉS UNE SEULE FOIS ──────────
|
| 1415 |
|
| 1416 |
def process_claims_batch(claims, output_dir):
|
appsheet_client.py
CHANGED
|
@@ -129,6 +129,13 @@ def ajouter_medias(rows: list):
|
|
| 129 |
appsheet_action(TABLE_MEDIAS, "Add", rows)
|
| 130 |
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
def find_rows(table: str, selector: str = None) -> list:
|
| 133 |
"""Lit des lignes d'une table AppSheet (action Find), avec un filtre
|
| 134 |
optionnel au format expression AppSheet, ex:
|
|
|
|
| 129 |
appsheet_action(TABLE_MEDIAS, "Add", rows)
|
| 130 |
|
| 131 |
|
| 132 |
+
def edit_media(row_id: str, id_media: str, champs: dict):
|
| 133 |
+
"""Met à jour une ligne existante de Medias Cameras (ex: colonne
|
| 134 |
+
"Role Media"). `champs` est un dict {nom_colonne_appsheet: valeur}."""
|
| 135 |
+
row = {"Row ID": row_id, "ID Media": id_media, **champs}
|
| 136 |
+
appsheet_action(TABLE_MEDIAS, "Edit", [row])
|
| 137 |
+
|
| 138 |
+
|
| 139 |
def find_rows(table: str, selector: str = None) -> list:
|
| 140 |
"""Lit des lignes d'une table AppSheet (action Find), avec un filtre
|
| 141 |
optionnel au format expression AppSheet, ex:
|
extract_claim_from_email.py
CHANGED
|
@@ -182,13 +182,14 @@ valide, sans aucun texte avant ou après, avec exactement ces clés :
|
|
| 182 |
"vehicle_model": "Marque et modèle du véhicule si mentionné, sinon null",
|
| 183 |
"claim_message": "Reformulation neutre, factuelle et concise du/des dommage(s), en français naturel, sans code technique. Si PLUSIEURS dommages distincts sont mentionnés (zones et/ou types différents), ils doivent TOUS être repris, sous forme d'une phrase par dommage séparée par un point-virgule (ex: 'Dégradation constatée sur le pare-choc avant.' ou 'Rayure sur le pare-choc avant ; enfoncement sur la portière arrière droite.'). Ne jamais en omettre un au profit d'un seul.",
|
| 184 |
"claim_demand": "Ce que le client demande explicitement (ex: dédommagement, remboursement, informations sur les démarches, prise en charge), en une phrase courte. Si rien n'est explicitement demandé, mets null.",
|
| 185 |
-
"
|
| 186 |
}}
|
| 187 |
|
| 188 |
Règles :
|
| 189 |
- Si une information est absente du mail, mets la valeur JSON `null` (pas une chaîne vide).
|
| 190 |
- Ne devine jamais une plaque ou un nom qui ne serait pas explicitement présent dans le texte.
|
| 191 |
- "claim_message" doit rester factuel et concis, utilisable directement dans un rapport.
|
|
|
|
| 192 |
"""
|
| 193 |
|
| 194 |
|
|
@@ -201,8 +202,8 @@ def extract_claim_info_from_email(email_text):
|
|
| 201 |
"""
|
| 202 |
client = get_genai_client()
|
| 203 |
# Codes pièce (nomenclature CV), dédoublonnés en gardant l'ordre —
|
| 204 |
-
# ce sont ces codes que le modèle doit renvoyer dans "
|
| 205 |
-
# ensuite traduits en
|
| 206 |
allowed_parts = list(dict.fromkeys(ZONE_FR_TO_PART.values()))
|
| 207 |
|
| 208 |
prompt = EXTRACTION_PROMPT.format(
|
|
@@ -232,10 +233,12 @@ def extract_claim_info_from_email(email_text):
|
|
| 232 |
"vehicle_model",
|
| 233 |
"claim_message",
|
| 234 |
"claim_demand",
|
| 235 |
-
"
|
| 236 |
]
|
| 237 |
for key in expected_keys:
|
| 238 |
data.setdefault(key, None)
|
|
|
|
|
|
|
| 239 |
|
| 240 |
# Sécurise le retraitement : si le client ne précise pas explicitement
|
| 241 |
# sa demande, on pose une valeur par défaut plutôt que de laisser le
|
|
@@ -243,13 +246,19 @@ def extract_claim_info_from_email(email_text):
|
|
| 243 |
if not data.get("claim_demand"):
|
| 244 |
data["claim_demand"] = DEFAULT_CLAIM_DEMAND
|
| 245 |
|
| 246 |
-
# Traduction
|
| 247 |
-
# pour la colonne AppSheet "Localisation Carrosserie".
|
| 248 |
-
#
|
| 249 |
-
#
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
return data
|
| 255 |
|
|
|
|
| 182 |
"vehicle_model": "Marque et modèle du véhicule si mentionné, sinon null",
|
| 183 |
"claim_message": "Reformulation neutre, factuelle et concise du/des dommage(s), en français naturel, sans code technique. Si PLUSIEURS dommages distincts sont mentionnés (zones et/ou types différents), ils doivent TOUS être repris, sous forme d'une phrase par dommage séparée par un point-virgule (ex: 'Dégradation constatée sur le pare-choc avant.' ou 'Rayure sur le pare-choc avant ; enfoncement sur la portière arrière droite.'). Ne jamais en omettre un au profit d'un seul.",
|
| 184 |
"claim_demand": "Ce que le client demande explicitement (ex: dédommagement, remboursement, informations sur les démarches, prise en charge), en une phrase courte. Si rien n'est explicitement demandé, mets null.",
|
| 185 |
+
"damaged_parts": "Un TABLEAU des pièces endommagées. Chaque élément DOIT être EXACTEMENT l'une des valeurs suivantes : {allowed_parts}. Si le mail précise suffisamment la zone (ex: 'pare-choc avant', 'portière arrière droite'), mets UNIQUEMENT la pièce exacte correspondante (tableau d'un seul élément). Si le mail mentionne une catégorie SANS préciser laquelle des pièces de cette catégorie est concernée (ex: 'pare-chocs' sans dire avant ou arrière, 'une portière' sans dire laquelle), mets TOUTES les pièces de cette catégorie dans le tableau plutôt que d'en deviner une seule au hasard (ex: pour 'pare-chocs' -> [\"front-bumper\", \"back-bumper\"] ; pour 'portière' -> [\"left_front-door\", \"right_front-door\", \"left_back-door\", \"right_back-door\"]). Si aucune partie n'est identifiable dans le mail, mets un tableau vide []."
|
| 186 |
}}
|
| 187 |
|
| 188 |
Règles :
|
| 189 |
- Si une information est absente du mail, mets la valeur JSON `null` (pas une chaîne vide).
|
| 190 |
- Ne devine jamais une plaque ou un nom qui ne serait pas explicitement présent dans le texte.
|
| 191 |
- "claim_message" doit rester factuel et concis, utilisable directement dans un rapport.
|
| 192 |
+
- Ne choisis JAMAIS arbitrairement une seule pièce quand le mail ne permet pas de trancher entre plusieurs pièces d'une même catégorie (avant/arrière, gauche/droite) : liste-les toutes dans "damaged_parts" (voir consigne ci-dessus).
|
| 193 |
"""
|
| 194 |
|
| 195 |
|
|
|
|
| 202 |
"""
|
| 203 |
client = get_genai_client()
|
| 204 |
# Codes pièce (nomenclature CV), dédoublonnés en gardant l'ordre —
|
| 205 |
+
# ce sont ces codes que le modèle doit renvoyer dans "damaged_parts",
|
| 206 |
+
# ensuite traduits en libellés français via CODE_TO_FR_LABEL.
|
| 207 |
allowed_parts = list(dict.fromkeys(ZONE_FR_TO_PART.values()))
|
| 208 |
|
| 209 |
prompt = EXTRACTION_PROMPT.format(
|
|
|
|
| 233 |
"vehicle_model",
|
| 234 |
"claim_message",
|
| 235 |
"claim_demand",
|
| 236 |
+
"damaged_parts",
|
| 237 |
]
|
| 238 |
for key in expected_keys:
|
| 239 |
data.setdefault(key, None)
|
| 240 |
+
if not isinstance(data.get("damaged_parts"), list):
|
| 241 |
+
data["damaged_parts"] = []
|
| 242 |
|
| 243 |
# Sécurise le retraitement : si le client ne précise pas explicitement
|
| 244 |
# sa demande, on pose une valeur par défaut plutôt que de laisser le
|
|
|
|
| 246 |
if not data.get("claim_demand"):
|
| 247 |
data["claim_demand"] = DEFAULT_CLAIM_DEMAND
|
| 248 |
|
| 249 |
+
# Traduction des codes pièce (nomenclature CV) en libellés français
|
| 250 |
+
# lisibles, pour la colonne AppSheet "Localisation Carrosserie". Une
|
| 251 |
+
# catégorie ambiguë (ex: "pare-chocs" sans avant/arrière, "portière" sans
|
| 252 |
+
# précision) donne PLUSIEURS codes dans "damaged_parts" (cf. prompt
|
| 253 |
+
# d'extraction) : on les liste TOUS plutôt que d'en choisir un seul. Si
|
| 254 |
+
# aucune pièce n'est identifiable dans le mail, on pose "Non Spécifié"
|
| 255 |
+
# plutôt que de laisser le champ vide.
|
| 256 |
+
labels = [
|
| 257 |
+
CODE_TO_FR_LABEL[code]
|
| 258 |
+
for code in dict.fromkeys(data["damaged_parts"])
|
| 259 |
+
if code in CODE_TO_FR_LABEL
|
| 260 |
+
]
|
| 261 |
+
data["localisation_carrosserie"] = " / ".join(labels) if labels else DEFAULT_LOCALISATION_CARROSSERIE
|
| 262 |
|
| 263 |
return data
|
| 264 |
|
finalize_claim.py
CHANGED
|
@@ -61,11 +61,11 @@ from pathlib import Path
|
|
| 61 |
import gspread
|
| 62 |
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
| 63 |
|
| 64 |
-
from appsheet_client import edit_reclamation, find_rows, TABLE_RECLAMATIONS, TABLE_MEDIAS
|
| 65 |
from drive_enrichment import get_drive_service, trouver_dossier_plaque
|
| 66 |
from oauth_drive import get_user_drive_service, get_user_credentials
|
| 67 |
from Inspect_car_parts import run_parts_inspection
|
| 68 |
-
from Check_claim_gemini import process_multi_claim
|
| 69 |
|
| 70 |
logging.basicConfig(level=logging.INFO)
|
| 71 |
logger = logging.getLogger("finalize_claim")
|
|
@@ -250,6 +250,24 @@ def executer_bilan_final(id_reclamation: str):
|
|
| 250 |
claim_result = process_multi_claim(claim_message, str(parts_json), videos_dir=videos_dir)
|
| 251 |
verdict = claim_result.get("verdict") # "OUI" / "NON" / None (cas d'erreur)
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
# 5. Dossiers Drive sous BDD Claim/Plaque/<plaque>/Bilan Final/ -------
|
| 254 |
plaque = reclamation.get("Vehicule Plaque", "")
|
| 255 |
dossier_plaque = trouver_dossier_plaque(service, plaque)
|
|
|
|
| 61 |
import gspread
|
| 62 |
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload
|
| 63 |
|
| 64 |
+
from appsheet_client import edit_reclamation, edit_media, find_rows, TABLE_RECLAMATIONS, TABLE_MEDIAS
|
| 65 |
from drive_enrichment import get_drive_service, trouver_dossier_plaque
|
| 66 |
from oauth_drive import get_user_drive_service, get_user_credentials
|
| 67 |
from Inspect_car_parts import run_parts_inspection
|
| 68 |
+
from Check_claim_gemini import process_multi_claim, build_media_role_resolver
|
| 69 |
|
| 70 |
logging.basicConfig(level=logging.INFO)
|
| 71 |
logger = logging.getLogger("finalize_claim")
|
|
|
|
| 250 |
claim_result = process_multi_claim(claim_message, str(parts_json), videos_dir=videos_dir)
|
| 251 |
verdict = claim_result.get("verdict") # "OUI" / "NON" / None (cas d'erreur)
|
| 252 |
|
| 253 |
+
# 4bis. Rôle de chaque média (colonne "Role Media" de Medias Cameras) :
|
| 254 |
+
# "Dommages" si le dommage réclamé y est confirmé, "Parties Visibles"
|
| 255 |
+
# si le média montre la pièce réclamée sans dommage confirmé,
|
| 256 |
+
# "Inutile" sinon (cf. Check_claim_gemini.build_media_role_resolver).
|
| 257 |
+
resolver_role_media = build_media_role_resolver(claim_result)
|
| 258 |
+
for m in medias:
|
| 259 |
+
file_id = _extraire_id_drive(m.get("Fichier Media", ""))
|
| 260 |
+
if not file_id:
|
| 261 |
+
continue
|
| 262 |
+
role = resolver_role_media(file_id, m.get("Type Media") == "Image")
|
| 263 |
+
try:
|
| 264 |
+
edit_media(m.get("Row ID"), m.get("ID Media"), {"Role Media": role})
|
| 265 |
+
except Exception:
|
| 266 |
+
logger.exception(
|
| 267 |
+
"Échec de la mise à jour de 'Role Media' pour le média %s (réclamation %s)",
|
| 268 |
+
file_id, id_reclamation,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
# 5. Dossiers Drive sous BDD Claim/Plaque/<plaque>/Bilan Final/ -------
|
| 272 |
plaque = reclamation.get("Vehicule Plaque", "")
|
| 273 |
dossier_plaque = trouver_dossier_plaque(service, plaque)
|