feat: Léa chat + IRBuilder enrichi (stratégies V4 complètes)

Aspect 2/4 Léa : interface conversationnelle
- chat_interface.py : ChatSession thread-safe, états idle/planning/awaiting/executing/done
- 5 endpoints REST : /api/v1/chat/* (session, message, history, confirm, sessions)
- web_dashboard/chat.html + chat.js : UI minimaliste, polling 2s, pas de framework
- Proxy Flask /api/chat/* → serveur streaming
- 34 tests (happy path, abandon, refus, erreurs, gemma4 down)

IRBuilder enrichi pour plans V4 complets
- _event_to_action() appelle enrich_click_from_screenshot() quand session_dir dispo
- Chaque clic porte _enrichment (by_text OCR, anchor_image_base64, vlm_description)
- ExecutionCompiler consomme l'enrichissement pour produire 3 stratégies par clic
  Avant : [ocr] uniquement, target="unknown_window"
  Après : [ocr, template, vlm] avec vrai texte OCR ("Rechercher", "Ouvrir")

Validé sur session réelle : 10/10 clics enrichis (by_text + anchor + vlm_description)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-10 09:01:13 +02:00
parent a6eb4c168f
commit f541bb8ce4
8 changed files with 2241 additions and 26 deletions

View File

@@ -1876,7 +1876,7 @@ def load_system_config():
"version": "1.0.0",
"services": {},
"llm": {"provider": "ollama", "base_url": "http://localhost:11434", "model": "qwen2.5:7b"},
"vlm": {"provider": "ollama", "base_url": "http://localhost:11434", "model": "qwen2.5vl:7b"},
"vlm": {"provider": "ollama", "base_url": "http://localhost:11434", "model": "gemma4:e4b"},
"detection": {"owl_model": "google/owlv2-base-patch16-ensemble", "confidence_threshold": 0.3},
"database": {"type": "sqlite", "path": "data/training/workflows.db"},
"security": {"enable_encryption": True, "require_authentication": False}
@@ -2371,6 +2371,93 @@ def proxy_streaming(endpoint):
return jsonify({'error': str(e)}), 500
# =============================================================================
# Chat conversationnel — Léa
# =============================================================================
CHAT_BASE_URL = 'http://localhost:5005/api/v1/chat'
@app.route('/chat')
def chat_page():
"""Page de chat conversationnel avec Léa."""
return render_template('chat.html')
@app.route('/api/chat/session', methods=['POST'])
def proxy_chat_session():
"""Proxy : créer une session de chat côté serveur streaming."""
return _proxy_chat(
method='POST',
path='/session',
payload=request.get_json(silent=True) or {},
)
@app.route('/api/chat/<session_id>/message', methods=['POST'])
def proxy_chat_message(session_id):
"""Proxy : envoyer un message dans une session."""
return _proxy_chat(
method='POST',
path=f'/{session_id}/message',
payload=request.get_json(silent=True) or {},
)
@app.route('/api/chat/<session_id>/history', methods=['GET'])
def proxy_chat_history(session_id):
"""Proxy : récupérer l'historique."""
return _proxy_chat(method='GET', path=f'/{session_id}/history')
@app.route('/api/chat/<session_id>/confirm', methods=['POST'])
def proxy_chat_confirm(session_id):
"""Proxy : confirmer l'exécution d'un plan."""
return _proxy_chat(
method='POST',
path=f'/{session_id}/confirm',
payload=request.get_json(silent=True) or {},
)
def _proxy_chat(method, path, payload=None):
"""Helper pour proxyfier les requêtes vers le serveur streaming (:5005)."""
import urllib.request
import urllib.error
url = f'{CHAT_BASE_URL}{path}'
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
}
# Token Bearer (lu depuis l'env — même token que le serveur streaming)
token = os.environ.get('RPA_API_TOKEN', '')
if token:
headers['Authorization'] = f'Bearer {token}'
try:
data_bytes = None
if payload is not None and method != 'GET':
data_bytes = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(url, data=data_bytes, headers=headers, method=method)
with urllib.request.urlopen(req, timeout=15) as response:
body = response.read().decode('utf-8')
try:
return jsonify(json.loads(body))
except json.JSONDecodeError:
return body, response.status, {'Content-Type': 'application/json'}
except urllib.error.HTTPError as e:
try:
detail = json.loads(e.read().decode('utf-8'))
except Exception:
detail = {'error': str(e)}
return jsonify(detail), e.code
except urllib.error.URLError as e:
return jsonify({'error': f'Serveur chat inaccessible : {e}'}), 502
except Exception as e:
return jsonify({'error': str(e)}), 500
# =============================================================================
# Main
# =============================================================================