Spaces:
Sleeping
Sleeping
ajout outil merge
Browse files- 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 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 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.
|