Marylene commited on
Commit
75fc807
·
verified ·
1 Parent(s): 95568e0

ajout outil merge

Browse files
Files changed (1) hide show
  1. quick_deploy_agent.py +108 -29
quick_deploy_agent.py CHANGED
@@ -466,6 +466,84 @@ class WebGet(Tool):
466
  except Exception as e:
467
  return {"ok": False, "url": url, "error": str(e), "text": ""}
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  # ---- Resolve ----
470
  class Resolve(Tool):
471
  name, description = "resolve_coicop_candidates", "Fusionne candidats → choix final + alternatives + explication."
@@ -559,6 +637,7 @@ def build_agent(model_id: str | None = None) -> CodeAgent:
559
  SemSim(),
560
  WebSearch(), # <-- autorise recherche web
561
  WebGet(), # <-- autorise lecture de pages
 
562
  Resolve(),
563
  ],
564
  model=model,
@@ -579,35 +658,35 @@ if __name__ == "__main__":
579
 
580
  agent = build_agent()
581
  task = f"""
582
- Classe ce produit en COICOP:
583
- EAN: {ean}
584
- Libellé: {label}
585
-
586
- Outils autorisés UNIQUEMENT :
587
- - validate_ean
588
- - openfoodfacts_product_by_ean
589
- - map_off_to_coicop
590
- - coicop_regex_rules
591
- - coicop_semantic_similarity
592
- - web_search
593
- - web_get
594
- - resolve_coicop_candidates
595
-
596
- RÈGLES:
597
- - TU PEUX interroger Internet via web_search puis web_get pour récupérer infos produit (fiche marque, page drive, comparateurs, etc.).
598
- - N'UTILISE PAS python_interpreter. N'ÉCRIS PAS DE CODE.
599
- - N'INDEXE JAMAIS la sortie d'un tool (copie-colle uniquement ce qui est utile).
600
-
601
- Pipeline :
602
- 1) validate_ean(ean)
603
- 2) openfoodfacts_product_by_ean(ean)
604
- 3) map_off_to_coicop(off_payload=<sortie brute de (2)>) ou, si nécessaire, map_off_to_coicop(product_name, categories_tags, ingredients_text)
605
- 3bis) SI doute (peu d'infos ou contradictions), web_search(query = "EAN + libellé + marque" ou libellé seul) → choisir 1–2 urls pertinentes → web_get(url)
606
- 4) coicop_regex_rules(text = LIBELLÉ UTILISATEUR)
607
- 4bis) coicop_regex_rules(text = TEXTE DES PAGES WEB RÉCUPÉRÉES) # pour capter des mots-clés comme camembert/brie/emmental/etc.
608
- 5) coicop_semantic_similarity(text=LIBELLÉ UTILISATEUR, topk=5) # retourne un dict "candidates"
609
- 6) resolve_coicop_candidates(json_lists=[<sortie de (3)>, <sortie de (4)>, <sortie de (5)>], topn=3)
610
- → Retourne la sortie de l’étape 6 telle quelle (JSON objet complet).
611
 
612
 
613
  Retourne uniquement un JSON valide (objet), sans backticks.
 
466
  except Exception as e:
467
  return {"ok": False, "url": url, "error": str(e), "text": ""}
468
 
469
+ # ---- MergeCandidatesTool ----
470
+
471
+ class MergeCandidatesTool(Tool):
472
+ name = "merge_candidates"
473
+ description = ("Fusionne des listes de candidats COICOP (dédupe par code, prend le score max, "
474
+ "agrège les justifs) et garantit min_k éléments avec padding neutre.")
475
+ inputs = {
476
+ "candidates_lists": {"type": "array", "description": "Liste de dicts {'candidates':[...]} venant d'autres outils."},
477
+ "min_k": {"type": "integer", "description": "Taille minimale de la liste fusionnée (défaut 3).", "nullable": True},
478
+ "fallback_bias": {"type": "string", "description": "Indice métier pour le padding (ex: 'cheese' ou '').", "nullable": True},
479
+ "score_cap": {"type": "number", "description": "Clip des scores à [0, score_cap] (défaut 1.0).", "nullable": True},
480
+ }
481
+ output_type = "object"
482
+
483
+ def forward(self, candidates_lists, min_k: int = 3, fallback_bias: str = "", score_cap: float = 1.0):
484
+ # 1) Collecte
485
+ if not isinstance(candidates_lists, list):
486
+ return {"candidates": []}
487
+
488
+ bucket = {} # code -> {code, score, votes, why_list}
489
+ for obj in candidates_lists:
490
+ if not isinstance(obj, dict):
491
+ continue
492
+ for c in obj.get("candidates", []):
493
+ code = c.get("code")
494
+ if not code:
495
+ continue
496
+ score = float(c.get("score", c.get("score_final", 0.0)))
497
+ if score_cap is not None:
498
+ score = max(0.0, min(float(score_cap), score))
499
+ why = c.get("why", "") or c.get("label", "")
500
+ if code not in bucket:
501
+ bucket[code] = {"code": code, "score": score, "votes": 1, "why_list": [why] if why else []}
502
+ else:
503
+ # Garde le meilleur score, incrémente les votes, agrège les raisons
504
+ if score > bucket[code]["score"]:
505
+ bucket[code]["score"] = score
506
+ bucket[code]["votes"] += 1
507
+ if why:
508
+ bucket[code]["why_list"].append(why)
509
+
510
+ merged = list(bucket.values())
511
+
512
+ # 2) Tri primaire par score puis par votes
513
+ merged.sort(key=lambda x: (x["score"], x["votes"]), reverse=True)
514
+
515
+ # 3) Padding si < min_k
516
+ def _fallback_order(bias: str):
517
+ # Ordre neutre mais raisonnable pour les fromages
518
+ base = ["01.1.4.5.2", "01.1.4.5.3", "01.1.4.5.5", "01.1.4.5.1", "01.1.4.5"]
519
+ return base if (bias or "").lower() == "cheese" else base
520
+
521
+ if len(merged) < max(1, int(min_k or 3)):
522
+ present = {m["code"] for m in merged}
523
+ for code in _fallback_order(fallback_bias):
524
+ if len(merged) >= min_k:
525
+ break
526
+ if code in present:
527
+ continue
528
+ merged.append({
529
+ "code": code,
530
+ "score": 0.5 if (fallback_bias or "").lower() == "cheese" else 0.48,
531
+ "votes": 0,
532
+ "why_list": ["padding fallback"]
533
+ })
534
+ present.add(code)
535
+
536
+ # 4) Normalisation finale de forme (why synthétique)
537
+ out = []
538
+ for m in merged[:max(1, int(min_k or 3))]:
539
+ why = ", ".join(sorted(set([w for w in m.get("why_list", []) if w])))
540
+ if not why:
541
+ why = "fusion (pas d'explications)"
542
+ out.append({"code": m["code"], "score": m["score"], "votes": m["votes"], "why": why})
543
+
544
+ return {"candidates": out}
545
+
546
+
547
  # ---- Resolve ----
548
  class Resolve(Tool):
549
  name, description = "resolve_coicop_candidates", "Fusionne candidats → choix final + alternatives + explication."
 
637
  SemSim(),
638
  WebSearch(), # <-- autorise recherche web
639
  WebGet(), # <-- autorise lecture de pages
640
+ MergeCandidatesTool(),
641
  Resolve(),
642
  ],
643
  model=model,
 
658
 
659
  agent = build_agent()
660
  task = f"""
661
+ Classe ce produit en COICOP:
662
+ EAN: {ean}
663
+ Libellé: {label}
664
+
665
+ Outils autorisés :
666
+ - validate_ean
667
+ - openfoodfacts_product_by_ean
668
+ - map_off_to_coicop
669
+ - coicop_regex_rules
670
+ - coicop_semantic_similarity
671
+ - merge_candidates
672
+ - resolve_coicop_candidates
673
+ - python_interpreter # ✅ autorisé si besoin pour fusionner/traiter les données
674
+
675
+ Règles strictes :
676
+ - Utilise python_interpreter uniquement pour manipuler des résultats (listes/dicts, filtrage, fusion).
677
+ - N’écris pas de code inutile : chaque appel doit servir à transformer ou agréger les sorties des outils.
678
+ - Ne télécharge rien en dehors des outils fournis.
679
+ - Retourne uniquement un JSON valide (objet), sans backticks.
680
+
681
+ Pipeline :
682
+ 1) v = validate_ean(ean) # si v.valid==False => expliquer via resolve (fallback générique)
683
+ 2) off = openfoodfacts_product_by_ean(ean)
684
+ 3) offmap = map_off_to_coicop(off_payload=off) # ou map_off_to_coicop(product_name, categories_tags, ingredients_text, ...)
685
+ 4) rx = coicop_regex_rules(text=LIBELLÉ UTILISATEUR)
686
+ 5) sem = coicop_semantic_similarity(text=LIBELLÉ UTILISATEUR, topk=5)
687
+ 6) merged = merge_candidates(candidates_lists=[offmap, rx, sem], min_k=3, fallback_bias="cheese")
688
+ 7) res = resolve_coicop_candidates(json_lists=[merged], topn=3)
689
+ → Retourne res tel quel (objet contenant final, alternatives, candidates_top le cas échéant).
690
 
691
 
692
  Retourne uniquement un JSON valide (objet), sans backticks.