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:
@@ -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']
|
||||
|
||||
277
visual_workflow_builder/backend/api_v3/match.py
Normal file
277
visual_workflow_builder/backend/api_v3/match.py
Normal 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
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user