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.
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 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
import json
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
try:
|
| 111 |
-
|
| 112 |
-
return d if isinstance(d, dict) else {}
|
| 113 |
except Exception:
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 126 |
-
|
| 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 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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:
|
| 146 |
-
|
|
|
|
| 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 |
-
|
| 152 |
-
|
| 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 |
-
|
| 163 |
-
|
| 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 |
-
|
| 174 |
-
|
| 175 |
-
|
| 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 |
|