expAge commited on
Commit
fa45d53
·
1 Parent(s): fbc9866

fix(narration): parse plus robuste + 'vide' vs '(non parsable)'

Browse files

_parse_tool_result distingue maintenant 3 cas explicites :
- _PARSE_EMPTY : contenu vide ou whitespace -> '→ vide'
- _PARSE_UNPARSABLE: ni JSON ni Python literal -> '→ (résultat non parsable)'
- objet parse : dict/list/str/int... -> formate

Fallback ast.literal_eval ajoute apres json.loads pour gerer les
dict-repr Python avec single quotes (LangChain serialise parfois ainsi).

Nouveau helper _format_done(content, fmt_dict) qui factorise le dispatch :
- vide / non parsable -> messages dedies
- dict -> appelle fmt_dict(d)
- list -> '→ N element(s)'
- autre (str/int) -> '→ {valeur tronquee}'

Les 6 callbacks done de TOOL_NARRATION refactores pour utiliser ce
helper et ne plus dupliquer la logique de parse + fallback.

Files changed (1) hide show
  1. jarvis.py +79 -38
jarvis.py CHANGED
@@ -102,16 +102,66 @@ def _truncate(s: str, n: int = 60) -> str:
102
  return s if len(s) <= n else s[:n - 1] + "…"
103
 
104
 
105
- def _parse_tool_result(content: str) -> dict:
106
- """Parse défensif d'un retour de tool soit JSON soit dict-repr."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  import json
108
- if not content:
109
- return {}
 
 
 
110
  try:
111
- d = json.loads(content)
112
- return d if isinstance(d, dict) else {}
113
  except Exception:
114
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
  TOOL_NARRATION: dict[str, dict] = {
@@ -121,61 +171,52 @@ TOOL_NARRATION: dict[str, dict] = {
121
  f"« {_truncate(a.get('term'))} » pour la relation "
122
  f"`{a.get('relation_name') or a.get('relation') or '?'}`…"
123
  ),
124
- "done": lambda c: (
125
- lambda d: (f"→ {d.get('count', '?')} triplet(s) existant(s) trouvé(s)."
126
- if d else "→ (résultat non parsable)")
127
- )(_parse_tool_result(c)),
128
  },
129
  "validate_candidate": {
130
  "start": lambda a: (
131
  f"🧪 Je teste le candidat « {_truncate(a.get('term'))} | "
132
  f"{a.get('relation', '?')} | {_truncate(a.get('target'))} »…"
133
  ),
134
- "done": lambda c: (
135
- lambda d: (
136
- " consolidé" if d.get("consolidation_status") == "consolidated"
137
- else "⏸️ pas concluant" if d.get("consolidation_status") == "silent"
138
- else " rejeté par inférence" if d.get("consolidation_status") == "rejected"
139
- else f"→ {d.get('validation_status', '?')}"
140
- ) if d else "→ (résultat non parsable)"
141
- )(_parse_tool_result(c)),
142
  },
143
  "disambiguate": {
144
  "start": lambda a: f"🔎 Je cherche les sens de « {_truncate(a.get('term'))} »…",
145
- "done": lambda c: f"→ {len(_parse_tool_result(c).get('senses') or _parse_tool_result(c).get('refinements') or []) or '?'} sens trouvés."
146
- if _parse_tool_result(c) else "→ (résultat non parsable)",
 
147
  },
148
  "lookup_term": {
149
  "start": lambda a: f"📖 Je vérifie l'existence de « {_truncate(a.get('term'))} » dans JDM…",
150
- "done": lambda c: (
151
- (lambda d: ("→ trouvé." if d.get("found") or d.get("id") else "→ inconnu."))(
152
- _parse_tool_result(c))
153
- if _parse_tool_result(c) else "→ (résultat non parsable)"
154
- ),
155
  },
156
  "get_relations_of_type": {
157
  "start": lambda a: (
158
  f"🔗 Je regarde les triplets « {_truncate(a.get('term'))} | "
159
  f"{a.get('relation_name') or a.get('relation') or '?'} »…"
160
  ),
161
- "done": lambda c: (
162
- (lambda d: f"→ {d.get('count', len(d.get('triplets', [])) or '?')} relation(s) trouvée(s).")(
163
- _parse_tool_result(c))
164
- if _parse_tool_result(c) else "→ (résultat non parsable)"
165
- ),
166
  },
167
  "write_submission_file": {
168
  "start": lambda a: (
169
  f"💾 J'écris le fichier de soumission ({len(a.get('triplets') or [])} item(s))"
170
  + (" et je le pousse à JDM…" if a.get("upload") else "…")
171
  ),
172
- "done": lambda c: (
173
- (lambda d: (
174
- f" {d['error']}" if d.get("error")
175
- else f"→ écrit dans `{d.get('path', '?')}` ({d.get('count', '?')} ligne(s))."
176
- ))(_parse_tool_result(c))
177
- if _parse_tool_result(c) else "→ (résultat non parsable)"
178
- ),
179
  },
180
  }
181
 
 
102
  return s if len(s) <= n else s[:n - 1] + "…"
103
 
104
 
105
+ _PARSE_EMPTY = object() # sentinelle : contenu vide / blanc
106
+ _PARSE_UNPARSABLE = object() # sentinelle : ni JSON ni Python literal valide
107
+
108
+
109
+ def _parse_tool_result(content: str):
110
+ """Parse défensif d'un retour de tool.
111
+
112
+ Retourne :
113
+ - `_PARSE_EMPTY` si le contenu est vide ou whitespace
114
+ - l'objet parsé (dict / list / str / int / float / bool / None) si
115
+ le contenu est du JSON valide
116
+ - l'objet parsé via `ast.literal_eval` si le contenu est un
117
+ Python literal valide (dict-repr avec single quotes, etc. —
118
+ LangChain sérialise parfois ainsi)
119
+ - `_PARSE_UNPARSABLE` si rien de tout ça ne marche
120
+
121
+ Note : ne renvoie PLUS de dict vide `{}` pour signaler une erreur ;
122
+ on utilise les sentinelles dédiées.
123
+ """
124
  import json
125
+ import ast
126
+ if not content or not str(content).strip():
127
+ return _PARSE_EMPTY
128
+ s = str(content).strip()
129
+ # 1) JSON canonique
130
  try:
131
+ return json.loads(s)
 
132
  except Exception:
133
+ pass
134
+ # 2) Python literal (dict-repr avec single quotes, tuples, etc.)
135
+ try:
136
+ return ast.literal_eval(s)
137
+ except Exception:
138
+ pass
139
+ return _PARSE_UNPARSABLE
140
+
141
+
142
+ def _format_done(content: str, fmt_dict) -> str:
143
+ """Helper commun pour les callbacks 'done' du TOOL_NARRATION.
144
+
145
+ Parse le contenu, puis dispatche :
146
+ - vide → '→ vide'
147
+ - non parsable → '→ (résultat non parsable)'
148
+ - dict → délègue à `fmt_dict(d)`
149
+ - list → '→ N élément(s)'
150
+ - autre (str/int) → '→ {valeur tronquée}'
151
+ """
152
+ parsed = _parse_tool_result(content)
153
+ if parsed is _PARSE_EMPTY:
154
+ return "→ vide"
155
+ if parsed is _PARSE_UNPARSABLE:
156
+ return "→ (résultat non parsable)"
157
+ if isinstance(parsed, dict):
158
+ try:
159
+ return fmt_dict(parsed)
160
+ except Exception:
161
+ return "→ (format inattendu)"
162
+ if isinstance(parsed, list):
163
+ return f"→ {len(parsed)} élément(s)"
164
+ return f"→ {_truncate(str(parsed), 80)}"
165
 
166
 
167
  TOOL_NARRATION: dict[str, dict] = {
 
171
  f"« {_truncate(a.get('term'))} » pour la relation "
172
  f"`{a.get('relation_name') or a.get('relation') or '?'}`…"
173
  ),
174
+ "done": lambda c: _format_done(c, lambda d: (
175
+ f"→ {d.get('count', '?')} triplet(s) existant(s) trouvé(s)."
176
+ )),
 
177
  },
178
  "validate_candidate": {
179
  "start": lambda a: (
180
  f"🧪 Je teste le candidat « {_truncate(a.get('term'))} | "
181
  f"{a.get('relation', '?')} | {_truncate(a.get('target'))} »…"
182
  ),
183
+ "done": lambda c: _format_done(c, lambda d: (
184
+ "✅ consolidé" if d.get("consolidation_status") == "consolidated"
185
+ else "⏸️ pas concluant" if d.get("consolidation_status") == "silent"
186
+ else " rejeté par inférence" if d.get("consolidation_status") == "rejected"
187
+ else f" {d.get('validation_status', '?')}"
188
+ )),
 
 
189
  },
190
  "disambiguate": {
191
  "start": lambda a: f"🔎 Je cherche les sens de « {_truncate(a.get('term'))} »…",
192
+ "done": lambda c: _format_done(c, lambda d: (
193
+ f"→ {len(d.get('senses') or d.get('refinements') or []) or '?'} sens trouvés."
194
+ )),
195
  },
196
  "lookup_term": {
197
  "start": lambda a: f"📖 Je vérifie l'existence de « {_truncate(a.get('term'))} » dans JDM…",
198
+ "done": lambda c: _format_done(c, lambda d: (
199
+ "→ trouvé." if d.get("found") or d.get("id") else "→ inconnu."
200
+ )),
 
 
201
  },
202
  "get_relations_of_type": {
203
  "start": lambda a: (
204
  f"🔗 Je regarde les triplets « {_truncate(a.get('term'))} | "
205
  f"{a.get('relation_name') or a.get('relation') or '?'} »…"
206
  ),
207
+ "done": lambda c: _format_done(c, lambda d: (
208
+ f"→ {d.get('count', len(d.get('triplets', [])) or '?')} relation(s) trouvée(s)."
209
+ )),
 
 
210
  },
211
  "write_submission_file": {
212
  "start": lambda a: (
213
  f"💾 J'écris le fichier de soumission ({len(a.get('triplets') or [])} item(s))"
214
  + (" et je le pousse à JDM…" if a.get("upload") else "…")
215
  ),
216
+ "done": lambda c: _format_done(c, lambda d: (
217
+ f"❌ {d['error']}" if d.get("error")
218
+ else f" écrit dans `{d.get('path', '?')}` ({d.get('count', '?')} ligne(s))."
219
+ )),
 
 
 
220
  },
221
  }
222