Marylene commited on
Commit
e65e7d4
·
verified ·
1 Parent(s): fb0c9fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +280 -268
app.py CHANGED
@@ -1,268 +1,280 @@
1
- # app.py — Formulaire EAN/Libellé + logs (UI & Space) — style "IPC / Insee"
2
- import io, sys, json
3
- import gradio as gr
4
- from contextlib import redirect_stdout, redirect_stderr
5
-
6
- # 👉 adapte le module si ton fichier agent a un autre nom
7
- from quick_deploy_agent import build_agent, parse_result
8
-
9
-
10
- # --- en haut de app.py, ajoute cet util ---
11
- import re
12
-
13
- def _extract_json(text: str):
14
- """
15
- Essaie de parser un JSON valable depuis une chaîne.
16
- - d'abord json.loads direct
17
- - sinon, extrait le contenu d'un bloc ```json ... ``` puis parse
18
- - renvoie None si rien de valable
19
- """
20
- if isinstance(text, dict):
21
- return text
22
- if not isinstance(text, str):
23
- return None
24
- # 1) direct
25
- try:
26
- return json.loads(text)
27
- except Exception:
28
- pass
29
- # 2) dans des backticks
30
- try:
31
- m = re.search(r"```(?:json)?\s*(\{[\s\S]*\})\s*```", text)
32
- if m:
33
- return json.loads(m.group(1))
34
- except Exception:
35
- pass
36
- return None
37
-
38
-
39
- # ---------- util "tee" pour dupliquer les logs ----------
40
- class _Tee(io.TextIOBase):
41
- def __init__(self, *streams): self.streams = streams
42
- def write(self, s):
43
- for st in self.streams:
44
- try: st.write(s)
45
- except Exception: pass
46
- return len(s)
47
- def flush(self):
48
- for st in self.streams:
49
- try: st.flush()
50
- except Exception: pass
51
-
52
- # ---------- instancie l'agent + logs verbeux ----------
53
- agent = build_agent()
54
- if hasattr(agent, "verbosity_level"):
55
- agent.verbosity_level = 3 # 0..3 (3 = très verbeux)
56
-
57
- # ---------- prompt interne (l'utilisateur ne tape rien) ----------
58
- TASK_TMPL = """\
59
- Classe ce produit en COICOP:
60
- EAN: {ean}
61
- Libellé: {label}
62
-
63
- Outils autorisés UNIQUEMENT : validate_ean, openfoodfacts_product_by_ean,
64
- map_off_to_coicop, coicop_regex_rules, coicop_semantic_similarity, resolve_coicop_candidates.
65
- N'UTILISE PAS python_interpreter. N'ÉCRIS PAS DE CODE. N'INDEXE JAMAIS la sortie d'un tool.
66
-
67
- Pipeline :
68
- 1) validate_ean(ean)
69
- 2) openfoodfacts_product_by_ean(ean)
70
- 3) map_off_to_coicop(off_payload=<sortie brute de (2)>) ou, si nécessaire,
71
- map_off_to_coicop(product_name, categories_tags, ingredients_text)
72
- 4) coicop_regex_rules(text=LIBELLÉ UTILISATEUR)
73
- 5) coicop_semantic_similarity(text=LIBELLÉ UTILISATEUR, topk=5)
74
- 6) resolve_coicop_candidates([...], topn=3)
75
-
76
- Règles strictes :
77
- - Les outils renvoient des objets Python (dict/list). Tu peux indexer directement.
78
- - Pour (4) et (5), utilise le libellé utilisateur (pas besoin de lire la réponse d’OFF).
79
- - Retourne uniquement un JSON valide (objet), sans backticks.
80
- """
81
-
82
-
83
- def classify(label: str, ean: str):
84
- import re
85
- label = (label or "").strip()
86
- ean = (ean or "").strip()
87
-
88
- if not label and not ean:
89
- return json.dumps({"error": "Veuillez saisir un libellé ou un EAN."}, ensure_ascii=False, indent=2), "—"
90
-
91
- # Construire le message pour l’agent
92
- task = TASK_TMPL.format(ean=ean or "N/A", label=label or "N/A")
93
-
94
- # Buffers UI + duplication vers logs Space (stdout/stderr)
95
- buf_out, buf_err = io.StringIO(), io.StringIO()
96
- tee_out, tee_err = _Tee(sys.stdout, buf_out), _Tee(sys.stderr, buf_err)
97
-
98
- # Utilitaire local : extraire un vrai JSON d'une chaîne (gère les ```json ... ```)
99
- def _extract_json(text: str):
100
- if isinstance(text, dict):
101
- return text
102
- if not isinstance(text, str):
103
- return None
104
- # 1) direct
105
- try:
106
- return json.loads(text)
107
- except Exception:
108
- pass
109
- # 2) JSON dans des backticks
110
- try:
111
- m = re.search(r"```(?:json)?\s*(\{[\s\S]*\})\s*```", text)
112
- if m:
113
- return json.loads(m.group(1))
114
- except Exception:
115
- pass
116
- return None
117
-
118
- print("\n===== Agent run start =====")
119
- print(task)
120
-
121
- try:
122
- # Exécuter l’agent en dupliquant les logs vers l’UI et le Space
123
- with redirect_stdout(tee_out), redirect_stderr(tee_err):
124
- res = agent.run(task)
125
-
126
- # Parsing robuste du résultat
127
- obj = None
128
- if isinstance(res, dict):
129
- obj = res
130
- if obj is None:
131
- obj = _extract_json(res)
132
- if obj is None:
133
- try:
134
- obj = parse_result(res) # ton parseur existant
135
- except Exception:
136
- obj = None
137
- if obj is None:
138
- obj = {"raw": str(res)} # dernier recours
139
-
140
- logs_ui = "\n".join(
141
- s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
142
- ) or "(aucun log)"
143
-
144
- print("===== Agent run end =====\n")
145
- return json.dumps(obj, ensure_ascii=False, indent=2), logs_ui
146
-
147
- except Exception as e:
148
- logs_ui = "\n".join(
149
- s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
150
- ) or "(aucun log)"
151
- print(f"✖ Agent error: {e}")
152
- return json.dumps({"error": str(e)}, ensure_ascii=False, indent=2), logs_ui
153
-
154
-
155
- def fill_example():
156
- # EAN réel OFF (Les p'tits crémeux Aldi – 216 g)
157
- return "Camembert au lait cru AOP 250g - ALDI", "2006050033638"
158
-
159
- # ---------- thème & CSS (style IPC / Insee) ----------
160
- theme = gr.themes.Soft(
161
- primary_hue="blue",
162
- secondary_hue="indigo",
163
- neutral_hue="slate",
164
- )
165
-
166
- custom_css = """
167
- :root{
168
- --insee-primary: #89c2d9;
169
- --insee-primary-700:#89c2d9;
170
- --insee-accent: #ff5c35;
171
- --insee-neutral-50:#f8fafc;
172
- --insee-neutral-100:#f1f5f9;
173
- --insee-neutral-900:#0f172a;
174
- --radius-lg: 16px;
175
- }
176
- .gradio-container{
177
- font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
178
- background: linear-gradient(180deg, var(--insee-neutral-50), #ffffff 30%);
179
- }
180
- #app-header{
181
- background: linear-gradient(90deg, var(--insee-primary), var(--insee-primary-700));
182
- color: white; padding: 18px 22px; border-radius: 18px;
183
- box-shadow: 0 10px 30px rgba(11,61,145,0.25); margin-bottom: 16px;
184
- }
185
- #app-header h1{ margin:0; font-size: 22px; letter-spacing: .3px; }
186
- #app-header p{ margin:6px 0 0; opacity:.92 }
187
-
188
- .card{
189
- background: #fff; border: 1px solid var(--insee-neutral-100); border-radius: var(--radius-lg);
190
- box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
191
- padding: 18px;
192
- }
193
-
194
- .gr-textbox textarea{
195
- font-size: 16px; line-height: 1.35; padding: 14px 16px;
196
- border: 2px solid #e5e7eb; border-radius: 14px;
197
- }
198
- .gr-textbox textarea:focus{
199
- outline: 3px solid color-mix(in oklab, var(--insee-primary) 22%, white);
200
- border-color: var(--insee-primary);
201
- box-shadow: 0 0 0 4px rgba(11,61,145,0.15);
202
- }
203
-
204
- .gr-button{
205
- font-weight: 700; letter-spacing: .2px; border-radius: 14px;
206
- transition: transform .06s ease, box-shadow .06s ease;
207
- }
208
- .gr-button:hover{ transform: translateY(-1px); }
209
- .gr-button:focus{
210
- outline: 3px solid color-mix(in oklab, var(--insee-primary) 30%, white);
211
- box-shadow: 0 0 0 4px rgba(11,61,145,.18);
212
- }
213
-
214
- /* primaire */
215
- button.primary{
216
- background: linear-gradient(180deg, var(--insee-primary), var(--insee-primary-700));
217
- color: white; border: none;
218
- }
219
- /* bouton secondaire "fantôme" */
220
- button.ghost{
221
- background: white; color: var(--insee-primary); border: 2px solid var(--insee-primary);
222
- }
223
- button.ghost:hover{
224
- background: color-mix(in oklab, var(--insee-primary) 6%, white);
225
- }
226
-
227
- .gr-code pre, .gr-code code{
228
- font-size: 13.5px; line-height: 1.45; border-radius: 14px;
229
- }
230
- """
231
-
232
-
233
- # ---------- UI ----------
234
- with gr.Blocks(title="OpenFoodFactsAgent (COICOP)", theme=theme, css=custom_css) as demo:
235
- gr.HTML("""
236
- <div id="app-header">
237
- <h1>🧮 Agent COICOP — Classification IPC</h1>
238
- <p>Analyse de libellé ou d’EAN, interrogation Open Food Facts, règles & sémantique, fusion des candidats.</p>
239
- </div>
240
- """)
241
-
242
- with gr.Row():
243
- with gr.Column(scale=1):
244
- with gr.Group(elem_classes=["card"]):
245
- gr.Markdown("### Saisie")
246
- label_in = gr.Textbox(label="Libellé de produit",
247
- placeholder="Ex. Camembert au lait cru AOP 250g - ALDI",
248
- lines=2)
249
- ean_in = gr.Textbox(label="EAN (optionnel)",
250
- placeholder="Ex. 2006050033638")
251
- with gr.Row():
252
- btn_go = gr.Button("Classifier en COICOP", elem_classes=["primary"])
253
- btn_ex = gr.Button("Exemple", elem_classes=["ghost"])
254
-
255
- with gr.Column(scale=1):
256
- with gr.Group(elem_classes=["card"]):
257
- gr.Markdown("### Résultat JSON")
258
- out_json = gr.Code(language="json")
259
-
260
- with gr.Group(elem_classes=["card"]):
261
- gr.Markdown("### Logs de l’agent")
262
- out_logs = gr.Textbox(lines=16)
263
-
264
- btn_go.click(fn=classify, inputs=[label_in, ean_in], outputs=[out_json, out_logs])
265
- btn_ex.click(fn=fill_example, outputs=[label_in, ean_in])
266
-
267
- if __name__ == "__main__":
268
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Formulaire EAN/Libellé + logs (UI & Space) — style "IPC / Insee"
2
+ import io, sys, json
3
+ import gradio as gr
4
+ from contextlib import redirect_stdout, redirect_stderr
5
+
6
+ # 👉 adapte le module si ton fichier agent a un autre nom
7
+ from quick_deploy_agent import build_agent, parse_result
8
+
9
+
10
+ # --- en haut de app.py, ajoute cet util ---
11
+ import re
12
+
13
+ def _extract_json(text: str):
14
+ """
15
+ Essaie de parser un JSON valable depuis une chaîne.
16
+ - d'abord json.loads direct
17
+ - sinon, extrait le contenu d'un bloc ```json ... ``` puis parse
18
+ - renvoie None si rien de valable
19
+ """
20
+ if isinstance(text, dict):
21
+ return text
22
+ if not isinstance(text, str):
23
+ return None
24
+ # 1) direct
25
+ try:
26
+ return json.loads(text)
27
+ except Exception:
28
+ pass
29
+ # 2) dans des backticks
30
+ try:
31
+ m = re.search(r"```(?:json)?\s*(\{[\s\S]*\})\s*```", text)
32
+ if m:
33
+ return json.loads(m.group(1))
34
+ except Exception:
35
+ pass
36
+ return None
37
+
38
+
39
+ # ---------- util "tee" pour dupliquer les logs ----------
40
+ class _Tee(io.TextIOBase):
41
+ def __init__(self, *streams): self.streams = streams
42
+ def write(self, s):
43
+ for st in self.streams:
44
+ try: st.write(s)
45
+ except Exception: pass
46
+ return len(s)
47
+ def flush(self):
48
+ for st in self.streams:
49
+ try: st.flush()
50
+ except Exception: pass
51
+
52
+ # ---------- instancie l'agent + logs verbeux ----------
53
+ agent = build_agent()
54
+ if hasattr(agent, "verbosity_level"):
55
+ agent.verbosity_level = 3 # 0..3 (3 = très verbeux)
56
+
57
+ # ---------- prompt interne (l'utilisateur ne tape rien) ----------
58
+ TASK_TMPL = """\
59
+ Classe ce produit en COICOP:
60
+ EAN: {ean}
61
+ Libellé: {label}
62
+
63
+ Outils autorisés :
64
+ - validate_ean
65
+ - openfoodfacts_product_by_ean
66
+ - map_off_to_coicop
67
+ - coicop_regex_rules
68
+ - coicop_semantic_similarity
69
+ - merge_candidates
70
+ - resolve_coicop_candidates
71
+ - web_search
72
+ - web_get
73
+ - python_interpreter # autorisé si besoin pour manipuler des listes/dicts
74
+
75
+ Règles strictes :
76
+ - Utilise python_interpreter uniquement pour manipuler des résultats (listes/dicts, filtrage, fusion).
77
+ - N’écris pas de code inutile (pas de fichiers, pas de réseaux hors outils).
78
+ - Pour (4) et (5), utilise le libellé utilisateur tel quel.
79
+ - Retourne uniquement un JSON valide (objet), sans backticks.
80
+
81
+ Pipeline :
82
+ 1) v = validate_ean(ean)
83
+ 2) off = openfoodfacts_product_by_ean(ean) # si EAN vide ou invalide, passe à l’étape 4
84
+ 3) offmap = map_off_to_coicop(off_payload=off) # si off.ok != True, ignorer
85
+ 4) rx = coicop_regex_rules(text=LIBELLÉ UTILISATEUR)
86
+ 5) sem = coicop_semantic_similarity(text=LIBELLÉ UTILISATEUR, topk=5)
87
+ 6) merged = merge_candidates(candidates_lists=[offmap, rx, sem], min_k=3, fallback_bias="cheese")
88
+ 7) res = resolve_coicop_candidates(json_lists=[merged], topn=3)
89
+
90
+ → Retourne res tel quel (objet contenant final, alternatives, candidates_top).
91
+ """
92
+
93
+
94
+
95
+ def classify(label: str, ean: str):
96
+ import re
97
+ label = (label or "").strip()
98
+ ean = (ean or "").strip()
99
+
100
+ if not label and not ean:
101
+ return json.dumps({"error": "Veuillez saisir un libellé ou un EAN."}, ensure_ascii=False, indent=2), "—"
102
+
103
+ # Construire le message pour l’agent
104
+ task = TASK_TMPL.format(ean=ean or "N/A", label=label or "N/A")
105
+
106
+ # Buffers UI + duplication vers logs Space (stdout/stderr)
107
+ buf_out, buf_err = io.StringIO(), io.StringIO()
108
+ tee_out, tee_err = _Tee(sys.stdout, buf_out), _Tee(sys.stderr, buf_err)
109
+
110
+ # Utilitaire local : extraire un vrai JSON d'une chaîne (gère les ```json ... ```)
111
+ def _extract_json(text: str):
112
+ if isinstance(text, dict):
113
+ return text
114
+ if not isinstance(text, str):
115
+ return None
116
+ # 1) direct
117
+ try:
118
+ return json.loads(text)
119
+ except Exception:
120
+ pass
121
+ # 2) JSON dans des backticks
122
+ try:
123
+ m = re.search(r"```(?:json)?\s*(\{[\s\S]*\})\s*```", text)
124
+ if m:
125
+ return json.loads(m.group(1))
126
+ except Exception:
127
+ pass
128
+ return None
129
+
130
+ print("\n===== Agent run start =====")
131
+ print(task)
132
+
133
+ try:
134
+ # Exécuter l’agent en dupliquant les logs vers l’UI et le Space
135
+ with redirect_stdout(tee_out), redirect_stderr(tee_err):
136
+ res = agent.run(task)
137
+
138
+ # Parsing robuste du résultat
139
+ obj = None
140
+ if isinstance(res, dict):
141
+ obj = res
142
+ if obj is None:
143
+ obj = _extract_json(res)
144
+ if obj is None:
145
+ try:
146
+ obj = parse_result(res) # ton parseur existant
147
+ except Exception:
148
+ obj = None
149
+ if obj is None:
150
+ obj = {"raw": str(res)} # dernier recours
151
+
152
+ logs_ui = "\n".join(
153
+ s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
154
+ ) or "(aucun log)"
155
+
156
+ print("===== Agent run end =====\n")
157
+ return json.dumps(obj, ensure_ascii=False, indent=2), logs_ui
158
+
159
+ except Exception as e:
160
+ logs_ui = "\n".join(
161
+ s for s in [buf_out.getvalue().strip(), buf_err.getvalue().strip()] if s
162
+ ) or "(aucun log)"
163
+ print(f"✖ Agent error: {e}")
164
+ return json.dumps({"error": str(e)}, ensure_ascii=False, indent=2), logs_ui
165
+
166
+
167
+ def fill_example():
168
+ # EAN réel OFF (Les p'tits crémeux – Aldi – 216 g)
169
+ return "Camembert au lait cru AOP 250g - ALDI", "2006050033638"
170
+
171
+ # ---------- thème & CSS (style IPC / Insee) ----------
172
+ theme = gr.themes.Soft(
173
+ primary_hue="blue",
174
+ secondary_hue="indigo",
175
+ neutral_hue="slate",
176
+ )
177
+
178
+ custom_css = """
179
+ :root{
180
+ --insee-primary: #89c2d9;
181
+ --insee-primary-700:#89c2d9;
182
+ --insee-accent: #ff5c35;
183
+ --insee-neutral-50:#f8fafc;
184
+ --insee-neutral-100:#f1f5f9;
185
+ --insee-neutral-900:#0f172a;
186
+ --radius-lg: 16px;
187
+ }
188
+ .gradio-container{
189
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
190
+ background: linear-gradient(180deg, var(--insee-neutral-50), #ffffff 30%);
191
+ }
192
+ #app-header{
193
+ background: linear-gradient(90deg, var(--insee-primary), var(--insee-primary-700));
194
+ color: white; padding: 18px 22px; border-radius: 18px;
195
+ box-shadow: 0 10px 30px rgba(11,61,145,0.25); margin-bottom: 16px;
196
+ }
197
+ #app-header h1{ margin:0; font-size: 22px; letter-spacing: .3px; }
198
+ #app-header p{ margin:6px 0 0; opacity:.92 }
199
+
200
+ .card{
201
+ background: #fff; border: 1px solid var(--insee-neutral-100); border-radius: var(--radius-lg);
202
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
203
+ padding: 18px;
204
+ }
205
+
206
+ .gr-textbox textarea{
207
+ font-size: 16px; line-height: 1.35; padding: 14px 16px;
208
+ border: 2px solid #e5e7eb; border-radius: 14px;
209
+ }
210
+ .gr-textbox textarea:focus{
211
+ outline: 3px solid color-mix(in oklab, var(--insee-primary) 22%, white);
212
+ border-color: var(--insee-primary);
213
+ box-shadow: 0 0 0 4px rgba(11,61,145,0.15);
214
+ }
215
+
216
+ .gr-button{
217
+ font-weight: 700; letter-spacing: .2px; border-radius: 14px;
218
+ transition: transform .06s ease, box-shadow .06s ease;
219
+ }
220
+ .gr-button:hover{ transform: translateY(-1px); }
221
+ .gr-button:focus{
222
+ outline: 3px solid color-mix(in oklab, var(--insee-primary) 30%, white);
223
+ box-shadow: 0 0 0 4px rgba(11,61,145,.18);
224
+ }
225
+
226
+ /* primaire */
227
+ button.primary{
228
+ background: linear-gradient(180deg, var(--insee-primary), var(--insee-primary-700));
229
+ color: white; border: none;
230
+ }
231
+ /* bouton secondaire "fantôme" */
232
+ button.ghost{
233
+ background: white; color: var(--insee-primary); border: 2px solid var(--insee-primary);
234
+ }
235
+ button.ghost:hover{
236
+ background: color-mix(in oklab, var(--insee-primary) 6%, white);
237
+ }
238
+
239
+ .gr-code pre, .gr-code code{
240
+ font-size: 13.5px; line-height: 1.45; border-radius: 14px;
241
+ }
242
+ """
243
+
244
+
245
+ # ---------- UI ----------
246
+ with gr.Blocks(title="OpenFoodFactsAgent (COICOP)", theme=theme, css=custom_css) as demo:
247
+ gr.HTML("""
248
+ <div id="app-header">
249
+ <h1>🧮 Agent COICOP — Classification IPC</h1>
250
+ <p>Analyse de libellé ou d’EAN, interrogation Open Food Facts, règles & sémantique, fusion des candidats.</p>
251
+ </div>
252
+ """)
253
+
254
+ with gr.Row():
255
+ with gr.Column(scale=1):
256
+ with gr.Group(elem_classes=["card"]):
257
+ gr.Markdown("### Saisie")
258
+ label_in = gr.Textbox(label="Libellé de produit",
259
+ placeholder="Ex. Camembert au lait cru AOP 250g - ALDI",
260
+ lines=2)
261
+ ean_in = gr.Textbox(label="EAN (optionnel)",
262
+ placeholder="Ex. 2006050033638")
263
+ with gr.Row():
264
+ btn_go = gr.Button("Classifier en COICOP", elem_classes=["primary"])
265
+ btn_ex = gr.Button("Exemple", elem_classes=["ghost"])
266
+
267
+ with gr.Column(scale=1):
268
+ with gr.Group(elem_classes=["card"]):
269
+ gr.Markdown("### Résultat JSON")
270
+ out_json = gr.Code(language="json")
271
+
272
+ with gr.Group(elem_classes=["card"]):
273
+ gr.Markdown("### Logs de l’agent")
274
+ out_logs = gr.Textbox(lines=16)
275
+
276
+ btn_go.click(fn=classify, inputs=[label_in, ean_in], outputs=[out_json, out_logs])
277
+ btn_ex.click(fn=fill_example, outputs=[label_in, ean_in])
278
+
279
+ if __name__ == "__main__":
280
+ demo.launch()