feat(workflow): variables runtime + extract_text serveur + t2a_decision LLM
Pipeline streaming étendu pour supporter des actions exécutées entièrement
côté serveur (jamais transmises à l'Agent V1) qui produisent des variables
réutilisables dans les steps suivants via templating {{var}} ou {{var.field}}.
== Variables d'exécution ==
- replay_state["variables"] : Dict[str, Any] initialisé vide à la création
- _resolve_runtime_vars() : résout {{var}} et {{var.field}} récursivement
dans str/dict/list. Variables absentes laissées intactes.
- /replay/next applique la résolution sur l'action AVANT toute interception
ou envoi à l'Agent V1.
== Boucle d'exécution serveur ==
- _SERVER_SIDE_ACTION_TYPES = {"extract_text", "t2a_decision"}
- /replay/next pop+execute en boucle ces actions jusqu'à trouver une action
visuelle (à transmettre Agent V1) ou un pause_for_human (qui bloque).
- Latence acceptable : t2a_decision = 5-10s côté serveur, l'Agent V1 attend
la réponse HTTP.
== Action extract_text ==
- Handler côté serveur réutilisant le dernier heartbeat (max 5s d'âge)
- core/llm/ocr_extractor.py : EasyOCR fr+en singleton + extract_text_from_image
- Stockage dans replay_state["variables"][output_var]
- Robuste : pas de heartbeat → variable = "" + log warning, pipeline continue
== Action t2a_decision ==
- core/llm/t2a_decision.py : refactor de demo_app.py query_model en module
importable. Prompt expert DIM T2A/PMSI, qwen2.5:7b par défaut (100% bench).
- Handler côté serveur appelle analyze_dpi(input_template_resolved)
- Stockage du JSON décision dans replay_state["variables"][output_var]
- Erreurs (Ollama down, parse) → variable = INDETERMINE + _error, pipeline continue
== VWB UI ==
- types.ts : nouveau type 't2a_decision' (icône 🧠 catégorie logic)
- extract_text refondu : needsAnchor=false, paramètre output_var (au lieu de
variable_name legacy — bridge accepte les deux pour compat)
- Bridge VWB→core : passthrough des deux types + paramètres préservés
== Tests ==
- tests/integration/test_t2a_extract.py : 25 tests verts
- templating runtime (8 tests)
- handler extract_text (3 tests, OCR mocké)
- handler t2a_decision (3 tests, analyze_dpi mocké)
- edge → action normalisée (2 tests)
- bridge VWB → core (5 tests)
- workflow chain extract→t2a→pause→clic (1 test)
Total branche : 82/82 verts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,10 @@ from .replay_engine import (
|
||||
_is_learned_workflow,
|
||||
_edge_to_normalized_actions,
|
||||
_substitute_variables,
|
||||
_resolve_runtime_vars,
|
||||
_SERVER_SIDE_ACTION_TYPES,
|
||||
_handle_extract_text_action,
|
||||
_handle_t2a_decision_action,
|
||||
_expand_compound_steps,
|
||||
_pre_check_screen_state as _pre_check_screen_state_impl,
|
||||
_detect_popup_hint as _detect_popup_hint_impl,
|
||||
@@ -2850,37 +2854,68 @@ async def get_next_action(session_id: str, machine_id: str = "default"):
|
||||
if not queue:
|
||||
return {"action": None, "session_id": session_id, "machine_id": machine_id}
|
||||
|
||||
# Peek à la prochaine action SANS la retirer (pour le pre-check)
|
||||
action = queue[0]
|
||||
# ── Boucle de traitement : actions serveur (extract_text, t2a_decision)
|
||||
# exécutées entièrement côté serveur jusqu'à trouver une action visuelle
|
||||
# à transmettre à l'Agent V1 ou un pause_for_human qui bloque le replay.
|
||||
action = None
|
||||
while queue:
|
||||
action = queue[0]
|
||||
|
||||
# ── pause_for_human : interception avant exécution ──
|
||||
# Cette action n'est jamais transmise à l'Agent V1. Elle bascule
|
||||
# le replay en paused_need_help avec le message custom, ce qui
|
||||
# déclenche la bulle interactive ChatWindow (J3.5).
|
||||
if action.get("type") == "pause_for_human" and owning_replay is not None:
|
||||
params = action.get("parameters") or {}
|
||||
message = params.get("message") or "Validation requise"
|
||||
queue.pop(0)
|
||||
_replay_queues[session_id] = queue
|
||||
owning_replay["status"] = "paused_need_help"
|
||||
owning_replay["pause_message"] = message
|
||||
owning_replay["failed_action"] = {
|
||||
"action_id": action.get("action_id", ""),
|
||||
"type": "pause_for_human",
|
||||
"reason": "user_request",
|
||||
}
|
||||
logger.info(
|
||||
f"Replay {owning_replay['replay_id']} pause supervisée demandée "
|
||||
f"par le workflow : {message[:80]}"
|
||||
)
|
||||
return {
|
||||
"action": None,
|
||||
"session_id": session_id,
|
||||
"machine_id": machine_id,
|
||||
"replay_paused": True,
|
||||
"pause_message": message,
|
||||
"replay_id": owning_replay["replay_id"],
|
||||
}
|
||||
# Résoudre les variables runtime ({{var}} et {{var.field}})
|
||||
if owning_replay is not None:
|
||||
runtime_vars = owning_replay.get("variables") or {}
|
||||
if runtime_vars:
|
||||
action = _resolve_runtime_vars(action, runtime_vars)
|
||||
|
||||
type_ = action.get("type")
|
||||
|
||||
# pause_for_human : bascule en paused_need_help, return action=None
|
||||
if type_ == "pause_for_human" and owning_replay is not None:
|
||||
params = action.get("parameters") or {}
|
||||
message = params.get("message") or "Validation requise"
|
||||
queue.pop(0)
|
||||
_replay_queues[session_id] = queue
|
||||
owning_replay["status"] = "paused_need_help"
|
||||
owning_replay["pause_message"] = message
|
||||
owning_replay["failed_action"] = {
|
||||
"action_id": action.get("action_id", ""),
|
||||
"type": "pause_for_human",
|
||||
"reason": "user_request",
|
||||
}
|
||||
logger.info(
|
||||
f"Replay {owning_replay['replay_id']} pause supervisée demandée "
|
||||
f"par le workflow : {message[:80]}"
|
||||
)
|
||||
return {
|
||||
"action": None,
|
||||
"session_id": session_id,
|
||||
"machine_id": machine_id,
|
||||
"replay_paused": True,
|
||||
"pause_message": message,
|
||||
"replay_id": owning_replay["replay_id"],
|
||||
}
|
||||
|
||||
# Actions serveur : exécuter, pop, continuer
|
||||
if type_ in _SERVER_SIDE_ACTION_TYPES and owning_replay is not None:
|
||||
try:
|
||||
if type_ == "extract_text":
|
||||
_handle_extract_text_action(
|
||||
action, owning_replay, session_id, _last_heartbeat
|
||||
)
|
||||
elif type_ == "t2a_decision":
|
||||
_handle_t2a_decision_action(action, owning_replay)
|
||||
except Exception as e:
|
||||
logger.warning(f"Action serveur {type_} a levé : {e}")
|
||||
queue.pop(0)
|
||||
_replay_queues[session_id] = queue
|
||||
continue # action suivante
|
||||
|
||||
# Action visuelle : sortir de la boucle pour la transmettre à l'Agent V1
|
||||
break
|
||||
|
||||
# Si la queue s'est vidée après les exécutions serveur, rien à transmettre
|
||||
if not queue or action is None:
|
||||
return {"action": None, "session_id": session_id, "machine_id": machine_id}
|
||||
|
||||
# ---- Pre-check écran (optionnel, non bloquant) ----
|
||||
# Ne s'applique qu'aux actions qui ont un from_node (actions de workflow,
|
||||
|
||||
Reference in New Issue
Block a user