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:
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user