v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution

- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

View File

@@ -14,5 +14,6 @@ from . import session
from . import workflow
from . import capture
from . import execute
from . import match # Matching sémantique des workflows
__all__ = ['api_v3_bp']

View File

@@ -0,0 +1,277 @@
"""
API v3 - Workflow Matching
Matching sémantique des workflows par commande en langage naturel
POST /api/v3/match/find → Trouver les workflows correspondant à une commande
GET /api/v3/match/suggest → Suggestions de workflows
POST /api/v3/match/reload → Recharger le cache des workflows
GET /api/v3/match/stats → Statistiques du matcher
"""
from flask import jsonify, request
from . import api_v3_bp
from services.workflow_matcher import get_workflow_matcher, WorkflowMatch
from dataclasses import asdict
@api_v3_bp.route('/match/find', methods=['POST'])
def find_matching_workflows():
"""
Trouver les workflows correspondant à une commande.
Request:
{
"command": "créer une facture pour le client Acme",
"limit": 5, // Optionnel, défaut: 5
"min_confidence": 0.3 // Optionnel, défaut: 0.3
}
Response:
{
"success": true,
"command": "créer une facture...",
"matches": [
{
"workflow_id": "wf_123",
"workflow_name": "Facturation Client",
"confidence": 0.85,
"match_reasons": ["trigger_example_exact:créer une facture", "tags:facturation"],
"extracted_params": {"client": "Acme"},
"description": "...",
"tags": ["facturation", "client"],
"step_count": 5
}
],
"best_match": { ... } ou null
}
"""
try:
data = request.get_json() or {}
command = data.get('command', '').strip()
if not command:
return jsonify({
'success': False,
'error': "Le champ 'command' est requis"
}), 400
limit = data.get('limit', 5)
min_confidence = data.get('min_confidence', 0.3)
# Trouver les workflows
matcher = get_workflow_matcher()
matches = matcher.find_workflows(command, limit=limit, min_confidence=min_confidence)
# Convertir en dict pour JSON
matches_dict = []
for match in matches:
match_data = {
'workflow_id': match.workflow_id,
'workflow_name': match.workflow_name,
'confidence': match.confidence,
'match_reasons': match.match_reasons,
'extracted_params': match.extracted_params,
'description': match.description,
'tags': match.tags or [],
'step_count': match.step_count
}
matches_dict.append(match_data)
print(f"🔍 [Match] Commande: '{command}'{len(matches)} résultats")
return jsonify({
'success': True,
'command': command,
'matches': matches_dict,
'best_match': matches_dict[0] if matches_dict else None,
'total_workflows': matcher.workflow_count()
})
except Exception as e:
import traceback
traceback.print_exc()
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/match/suggest', methods=['GET'])
def suggest_workflows():
"""
Obtenir des suggestions de workflows.
Query params:
q: Texte partiel (requis)
limit: Nombre max de suggestions (optionnel, défaut: 5)
Response:
{
"success": true,
"query": "fact",
"suggestions": [
{
"id": "wf_123",
"name": "Facturation Client",
"description": "Crée une facture...",
"tags": ["facturation"]
}
]
}
"""
try:
query = request.args.get('q', '').strip()
if not query:
return jsonify({
'success': False,
'error': "Le paramètre 'q' est requis"
}), 400
limit = int(request.args.get('limit', 5))
matcher = get_workflow_matcher()
suggestions = matcher.suggest_workflows(query, limit=limit)
return jsonify({
'success': True,
'query': query,
'suggestions': suggestions
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/match/reload', methods=['POST'])
def reload_matcher():
"""
Recharger le cache du matcher.
Utile après avoir ajouté/modifié des workflows.
Response:
{
"success": true,
"workflows_loaded": 10
}
"""
try:
matcher = get_workflow_matcher()
count = matcher.reload_workflows()
print(f"🔄 [Match] Cache rechargé: {count} workflows")
return jsonify({
'success': True,
'workflows_loaded': count
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/match/stats', methods=['GET'])
def matcher_stats():
"""
Obtenir les statistiques du matcher.
Response:
{
"success": true,
"stats": {
"total_workflows": 10,
"workflows_with_tags": 8,
"workflows_with_triggers": 5,
"workflows_with_description": 9
}
}
"""
try:
matcher = get_workflow_matcher()
workflows = matcher.get_all_workflows()
stats = {
'total_workflows': len(workflows),
'workflows_with_tags': sum(1 for w in workflows if w.tags),
'workflows_with_triggers': sum(1 for w in workflows if w.trigger_examples),
'workflows_with_description': sum(1 for w in workflows if w.description),
'total_tags': sum(len(w.tags) for w in workflows),
'total_trigger_examples': sum(len(w.trigger_examples) for w in workflows)
}
# Liste des workflows avec leurs métadonnées
workflow_list = []
for w in workflows:
workflow_list.append({
'id': w.workflow_id,
'name': w.name,
'tags_count': len(w.tags),
'triggers_count': len(w.trigger_examples),
'has_description': bool(w.description),
'step_count': w.step_count
})
return jsonify({
'success': True,
'stats': stats,
'workflows': workflow_list
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/match/test', methods=['GET'])
def test_matcher():
"""
Endpoint de test pour vérifier le fonctionnement du matcher.
Response:
{
"success": true,
"message": "Matcher opérationnel",
"example": { ... }
}
"""
try:
matcher = get_workflow_matcher()
count = matcher.workflow_count()
# Test avec une commande simple si des workflows existent
example = None
if count > 0:
workflows = matcher.get_all_workflows()
# Prendre le premier workflow avec des trigger_examples
for w in workflows:
if w.trigger_examples:
test_command = w.trigger_examples[0]
result = matcher.find_workflow(test_command)
if result:
example = {
'test_command': test_command,
'matched_workflow': result.workflow_name,
'confidence': result.confidence
}
break
return jsonify({
'success': True,
'message': 'Matcher opérationnel',
'workflows_loaded': count,
'example': example
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500

View File

@@ -33,26 +33,44 @@ def get_state():
try:
session = get_session_state()
# Workflow actif
# Workflow actif - nettoyer si n'existe plus
active_workflow = None
if session.active_workflow_id:
wf = Workflow.query.get(session.active_workflow_id)
if wf:
active_workflow = wf.to_dict()
else:
# Le workflow n'existe plus, nettoyer la session
print(f"⚠️ [Session] Workflow '{session.active_workflow_id}' n'existe plus, nettoyage session")
session.active_workflow_id = None
session.selected_step_id = None
# Exécution active
# Vérifier que l'étape sélectionnée existe toujours
if session.selected_step_id:
step = Step.query.get(session.selected_step_id)
if not step:
print(f"⚠️ [Session] Étape '{session.selected_step_id}' n'existe plus, nettoyage")
session.selected_step_id = None
# Exécution active - nettoyer si n'existe plus
active_execution = None
if session.active_execution_id:
exe = Execution.query.get(session.active_execution_id)
if exe:
active_execution = exe.to_dict()
else:
print(f"⚠️ [Session] Exécution '{session.active_execution_id}' n'existe plus, nettoyage")
session.active_execution_id = None
# Liste des workflows (résumé)
# Liste des workflows (résumé avec métadonnées)
workflows_list = []
for wf in Workflow.query.filter_by(is_active=True).order_by(Workflow.updated_at.desc()).all():
workflows_list.append({
'id': wf.id,
'name': wf.name,
'description': wf.description or '',
'tags': wf.tags or [],
'trigger_examples': wf.trigger_examples or [],
'step_count': wf.steps.count(),
'updated_at': wf.updated_at.isoformat() if wf.updated_at else None
})

View File

@@ -99,6 +99,66 @@ def get_workflow(workflow_id: str):
}), 500
@api_v3_bp.route('/workflow/<workflow_id>', methods=['PUT'])
def update_workflow(workflow_id: str):
"""
Met à jour les métadonnées d'un workflow.
Request:
{
"name": "Nouveau nom", // Optionnel
"description": "Description", // Optionnel
"tags": ["tag1", "tag2"], // Optionnel
"triggerExamples": ["phrase1"] // Optionnel
}
Response:
{
"success": true,
"workflow": { ... }
}
"""
try:
workflow = Workflow.query.get(workflow_id)
if not workflow:
return jsonify({
'success': False,
'error': f"Workflow '{workflow_id}' non trouvé"
}), 404
data = request.get_json() or {}
# Mettre à jour les champs fournis
if 'name' in data:
workflow.name = data['name']
if 'description' in data:
workflow.description = data['description']
if 'tags' in data:
workflow.tags = data['tags']
if 'triggerExamples' in data:
workflow.trigger_examples = data['triggerExamples']
workflow.updated_at = datetime.utcnow()
db.session.commit()
print(f"✅ [API v3] Workflow mis à jour: {workflow_id}")
return jsonify({
'success': True,
'workflow': workflow.to_dict()
})
except Exception as e:
db.session.rollback()
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_v3_bp.route('/workflow/<workflow_id>', methods=['DELETE'])
def delete_workflow(workflow_id: str):
"""Supprime un workflow"""