Marylene's picture
modifier format prompt
929058f verified
# app.py — Formulaire EAN/Libellé + logs (UI & Space) — style "IPC / Insee"
import io, sys, json, re
import gradio as gr
from contextlib import redirect_stdout, redirect_stderr
# 👉 imports utilitaires de ton agent (pas besoin de build_agent ici)
from quick_deploy_agent import parse_result, run_task_with_fallback
# ---------- util "tee" pour dupliquer les logs ----------
class _Tee(io.TextIOBase):
def __init__(self, *streams): self.streams = streams
def write(self, s):
for st in self.streams:
try: st.write(s)
except Exception: pass
return len(s)
def flush(self):
for st in self.streams:
try: st.flush()
except Exception: pass
# ---------- helper: extraire un JSON valide d'un texte ----------
def _extract_json(text: str):
"""
Essaie de parser un JSON valable depuis une chaîne.
- d'abord json.loads direct
- sinon, extrait le contenu d'un bloc ```json ... ``` puis parse
- renvoie None si rien de valable
"""
if isinstance(text, dict):
return text
if not isinstance(text, str):
return None
# 1) direct
try:
return json.loads(text)
except Exception:
pass
# 2) dans des backticks
try:
m = re.search(r"```(?:json)?\s*(\{[\s\S]*\})\s*```", text)
if m:
return json.loads(m.group(1))
except Exception:
pass
return None
# ---------- prompt interne (l'utilisateur ne tape rien) ----------
TASK_TMPL = """\
Classe ce produit en COICOP:
EAN: {ean}
Libellé: {label}
Outils autorisés :
- validate_ean
- openfoodfacts_product_by_ean
- map_off_to_coicop
- coicop_regex_rules
- coicop_semantic_similarity
- merge_candidates
- resolve_coicop_candidates
- python_interpreter # UNIQUEMENT pour lignes simples d’assignation ou d’appel d’outil (voir ci-dessous)
Règles STRICTES d’écriture de code :
- Pas de structures de contrôle Python : aucun if, else, for, while, try, with, def, class.
- Aucun print, aucun logging, aucune concaténation multi-ligne.
- Chaque bloc de code contient une seule instruction Python, sur une seule ligne.
- Commencer par définir deux variables :
1) EAN_STR = "{ean}" (chaîne simple)
2) LBL = \"\"\"{label}\"\"\" (triple guillemets pour éviter les erreurs de guillemets)
- Pour tous les outils qui prennent le libellé, utiliser LBL (ne jamais insérer le libellé en littéral).
- La fonction validate_ean renvoie un dictionnaire avec les clés 'valid' (booléen) et 'normalized' (chaîne). Ne pas la traiter comme un booléen directement.
Règles STRICTES de sortie :
- Terminer par un unique objet JSON valide en appelant final_answer avec cet objet (aucun texte hors JSON).
- Ne pas utiliser de backticks.
- Le JSON final doit contenir les clés : final, alternatives, candidates_top, explanation.
Branchements (décision prise sans écrire de if en code) :
- MODE AVEC EAN si EAN_STR n’est pas "N/A" et si validate_ean(EAN_STR) indique que 'valid' est vrai et si l’appel OpenFoodFacts réussit (champ ok vrai).
- Sinon, MODE SANS EAN.
Pipeline — MODE AVEC EAN :
1) v = validate_ean(EAN_STR)
2) off = openfoodfacts_product_by_ean(EAN_STR)
3) offmap = map_off_to_coicop(off_payload=off)
4) rx = coicop_regex_rules(text=LBL)
5) sem = coicop_semantic_similarity(text=LBL, topk=5)
6) merged = merge_candidates(candidates_lists=[offmap, rx, sem], min_k=3, fallback_bias="cheese")
7) res = resolve_coicop_candidates(json_lists=[merged], topn=3)
→ Appeler immédiatement final_answer avec res (objet JSON complet).
Pipeline — MODE SANS EAN :
1) rx = coicop_regex_rules(text=LBL)
2) sem = coicop_semantic_similarity(text=LBL, topk=5)
3) merged = merge_candidates(candidates_lists=[rx, sem], min_k=3, fallback_bias="cheese")
4) res = resolve_coicop_candidates(json_lists=[merged], topn=3)
→ Appeler immédiatement final_answer avec res (objet JSON complet).
Contraintes d’usage :
- N’utiliser python_interpreter que pour des lignes uniques d’assignation ou d’appel d’outil (format : var = tool(args) ou tool(args)).
- Ne créer aucun fichier et ne faire aucune entrée/sortie externe.
"""
def classify(label: str, ean: str):
label = (label or "").strip()
ean = (ean or "").strip()
if not label and not ean:
return json.dumps({"error": "Veuillez saisir un libellé ou un EAN."}, ensure_ascii=False, indent=2), "—"
# Construire le message pour l’agent
task = TASK_TMPL.format(ean=ean or "N/A", label=label or "N/A")
# Buffers UI + duplication vers logs Space (stdout/stderr)
buf_out, buf_err = io.StringIO(), io.StringIO()
tee_out, tee_err = _Tee(sys.stdout, buf_out), _Tee(sys.stderr, buf_err)
print("\n===== Agent run start =====")
print(task)
try:
# Exécuter l’agent via la fonction tolérante aux timeouts et modèles de repli
with redirect_stdout(tee_out), redirect_stderr(tee_err):
res = run_task_with_fallback(task)
# Parsing robuste du résultat
obj = None
if isinstance(res, dict):
obj = res
if obj is None:
obj = _extract_json(res)
if obj is None:
try:
obj = parse_result(res) # parseur du projet
except Exception:
obj = None
if obj is None:
obj = {"raw": str(res)} # dernier recours
logs_ui = "\n".join(
s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
) or "(aucun log)"
print("===== Agent run end =====\n")
return json.dumps(obj, ensure_ascii=False, indent=2), logs_ui
except Exception as e:
logs_ui = "\n".join(
s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
) or "(aucun log)"
print(f"✖ Agent error: {e}")
return json.dumps({"error": str(e)}, ensure_ascii=False, indent=2), logs_ui
def fill_example():
# EAN réel OFF (Les p'tits crémeux – Aldi – 216 g)
return "Camembert au lait cru AOP 250g - ALDI", "2006050033638"
# ---------- thème & CSS (style IPC / Insee) ----------
theme = gr.themes.Soft(
primary_hue="blue",
secondary_hue="indigo",
neutral_hue="slate",
)
custom_css = """
:root{
--insee-primary: #89c2d9;
--insee-primary-700:#89c2d9;
--insee-accent: #ff5c35;
--insee-neutral-50:#f8fafc;
--insee-neutral-100:#f1f5f9;
--insee-neutral-900:#0f172a;
--radius-lg: 16px;
}
.gradio-container{
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
background: linear-gradient(180deg, var(--insee-neutral-50), #ffffff 30%);
}
#app-header{
background: linear-gradient(90deg, var(--insee-primary), var(--insee-primary-700));
color: white; padding: 18px 22px; border-radius: 18px;
box-shadow: 0 10px 30px rgba(11,61,145,0.25); margin-bottom: 16px;
}
#app-header h1{ margin:0; font-size: 22px; letter-spacing: .3px; }
#app-header p{ margin:6px 0 0; opacity:.92 }
.card{
background: #fff; border: 1px solid var(--insee-neutral-100); border-radius: var(--radius-lg);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
padding: 18px;
}
.gr-textbox textarea{
font-size: 16px; line-height: 1.35; padding: 14px 16px;
border: 2px solid #e5e7eb; border-radius: 14px;
}
.gr-textbox textarea:focus{
outline: 3px solid color-mix(in oklab, var(--insee-primary) 22%, white);
border-color: var(--insee-primary);
box-shadow: 0 0 0 4px rgba(11,61,145,0.15);
}
.gr-button{
font-weight: 700; letter-spacing: .2px; border-radius: 14px;
transition: transform .06s ease, box-shadow .06s ease;
}
.gr-button:hover{ transform: translateY(-1px); }
.gr-button:focus{
outline: 3px solid color-mix(in oklab, var(--insee-primary) 30%, white);
box-shadow: 0 0 0 4px rgba(11,61,145,.18);
}
/* primaire */
button.primary{
background: linear-gradient(180deg, var(--insee-primary), var(--insee-primary-700));
color: white; border: none;
}
/* bouton secondaire "fantôme" */
button.ghost{
background: white; color: var(--insee-primary); border: 2px solid var(--insee-primary);
}
button.ghost:hover{
background: color-mix(in oklab, var(--insee-primary) 6%, white);
}
.gr-code pre, .gr-code code{
font-size: 13.5px; line-height: 1.45; border-radius: 14px;
}
"""
# ---------- UI ----------
with gr.Blocks(title="OpenFoodFactsAgent (COICOP)", theme=theme, css=custom_css) as demo:
gr.HTML("""
<div id="app-header">
<h1>🧮 Agent COICOP — Classification IPC</h1>
<p>Analyse de libellé ou d’EAN, interrogation Open Food Facts, règles & sémantique, fusion des candidats.</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
with gr.Group(elem_classes=["card"]):
gr.Markdown("### Saisie")
label_in = gr.Textbox(label="Libellé de produit",
placeholder="Ex. Camembert au lait cru AOP 250g - ALDI",
lines=2)
ean_in = gr.Textbox(label="EAN (optionnel)",
placeholder="Ex. 2006050033638")
with gr.Row():
btn_go = gr.Button("Classifier en COICOP", elem_classes=["primary"])
btn_ex = gr.Button("Exemple", elem_classes=["ghost"])
with gr.Column(scale=1):
with gr.Group(elem_classes=["card"]):
gr.Markdown("### Résultat JSON")
out_json = gr.Code(language="json")
with gr.Group(elem_classes=["card"]):
gr.Markdown("### Logs de l’agent")
out_logs = gr.Textbox(lines=16)
btn_go.click(fn=classify, inputs=[label_in, ean_in], outputs=[out_json, out_logs])
btn_ex.click(fn=fill_example, outputs=[label_in, ean_in])
if __name__ == "__main__":
# SSR est activé par défaut sur Spaces; ajuste si besoin:
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=True)