fix(core): signature de trajectoire stable malgre le moteur de grounding (by_text)
Le champ by_role remontait la methode de detection (yolo/ocr/vlm), instable entre sessions : deux apprentissages du meme parcours detectes differemment produisaient deux signatures -> fusion (create-or-update) ratee. On sort by_role de la signature et on s'appuie sur le texte semantique de la cible (by_text), independant du moteur de grounding. Fallback quand by_text vide : titre de fenetre / description VLM. Test TDD: test_signature_stable_despite_grounding_role_difference (RED->GREEN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,14 +39,18 @@ def trajectory_signature(steps: Iterable[Mapping[str, Any]]) -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _stable_target(target: Any) -> str:
|
def _stable_target(target: Any) -> str:
|
||||||
"""Descripteur de cible **stable** entre sessions (by_role/by_text, fallback window)."""
|
"""Descripteur de cible **stable** entre sessions.
|
||||||
|
|
||||||
|
S'appuie sur le texte sémantique de la cible (`by_text`), volontairement
|
||||||
|
indépendant du moteur de grounding : `by_role` peut valoir 'yolo'/'ocr'/'vlm'
|
||||||
|
(méthode de détection, instable entre sessions) et n'entre donc PAS dans la
|
||||||
|
signature. Fallback quand `by_text` est absent : titre de fenêtre / description VLM.
|
||||||
|
"""
|
||||||
if not isinstance(target, Mapping):
|
if not isinstance(target, Mapping):
|
||||||
return ""
|
return ""
|
||||||
by_role = str(target.get("by_role") or "").strip()
|
|
||||||
by_text = str(target.get("by_text") or "").strip()
|
by_text = str(target.get("by_text") or "").strip()
|
||||||
base = f"{by_role}:{by_text}".strip(":")
|
if by_text:
|
||||||
if base:
|
return by_text
|
||||||
return base
|
|
||||||
hints = target.get("context_hints")
|
hints = target.get("context_hints")
|
||||||
if isinstance(hints, Mapping):
|
if isinstance(hints, Mapping):
|
||||||
return str(hints.get("window_title") or hints.get("vlm_description") or "").strip()
|
return str(hints.get("window_title") or hints.get("vlm_description") or "").strip()
|
||||||
|
|||||||
@@ -69,3 +69,18 @@ def test_signature_follows_edge_chain_not_list_order():
|
|||||||
scrambled = {"entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}, {"node_id": "n3"}],
|
scrambled = {"entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}, {"node_id": "n3"}],
|
||||||
"edges": [e2, e1]} # liste inversée, même chaîne
|
"edges": [e2, e1]} # liste inversée, même chaîne
|
||||||
assert workflow_trajectory_signature(ordered) == workflow_trajectory_signature(scrambled)
|
assert workflow_trajectory_signature(ordered) == workflow_trajectory_signature(scrambled)
|
||||||
|
|
||||||
|
|
||||||
|
def test_signature_stable_despite_grounding_role_difference():
|
||||||
|
"""`by_role` peut porter le moteur de grounding (yolo/ocr/vlm) — instable entre
|
||||||
|
sessions. La signature doit rester identique si seul `by_role` change → elle
|
||||||
|
s'appuie sur le texte sémantique `by_text`, pas sur la méthode de détection."""
|
||||||
|
wf_yolo = {
|
||||||
|
"entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}],
|
||||||
|
"edges": [_edge("n1", "n2", "mouse_click", by_role="yolo", by_text="Fichier")],
|
||||||
|
}
|
||||||
|
wf_ocr = {
|
||||||
|
"entry_nodes": ["n1"], "nodes": [{"node_id": "n1"}, {"node_id": "n2"}],
|
||||||
|
"edges": [_edge("n1", "n2", "mouse_click", by_role="ocr", by_text="Fichier")],
|
||||||
|
}
|
||||||
|
assert workflow_trajectory_signature(wf_yolo) == workflow_trajectory_signature(wf_ocr)
|
||||||
|
|||||||
Reference in New Issue
Block a user