CoolFace
Apppublic

emmixam/ai_workflow_discovery_agent

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
advisor_validators.py237 linesDownload Raw Back to validators
1from __future__ import annotations2 3from typing import Any, Iterable, Optional4 5from schemas import RapportAdvisor, WorkflowOptimise6 7 8class AdvisorValidationError(ValueError):9    """Erreur de validation métier sur la sortie Advisor."""10 11 12def _require_non_empty_str(value: Any, field_name: str) -> None:13    if not isinstance(value, str) or not value.strip():14        raise AdvisorValidationError(f"{field_name} vide ou invalide.")15 16 17def _normalize_catalog_names(tool_catalog: Iterable[Any]) -> set[str]:18    """19    Accepte soit :20    - un set/list de noms d'outils21    - une liste de dicts issus de tools_catalog.json avec clé 'name'22    """23    names: set[str] = set()24 25    for item in tool_catalog:26        if isinstance(item, str):27            name = item.strip()28            if name:29                names.add(name)30        elif isinstance(item, dict):31            raw_name = item.get("name")32            if isinstance(raw_name, str) and raw_name.strip():33                names.add(raw_name.strip())34        else:35            raise AdvisorValidationError(36                f"Entrée de catalogue non supportée: {type(item)!r}"37            )38 39    if not names:40        raise AdvisorValidationError("Le catalogue d'outils est vide ou illisible.")41 42    return names43 44 45def _validate_workflow_refs(report: RapportAdvisor, workflow: WorkflowOptimise) -> None:46    node_ids = {n.id for n in workflow.noeuds}47    node_labels = {n.label for n in workflow.noeuds}48 49    if not report.recommandations:50        raise AdvisorValidationError("Aucune recommandation retournée par l'Advisor.")51 52    seen_node_ids: set[str] = set()53 54    for rec in report.recommandations:55        _require_non_empty_str(rec.noeud_id, "recommandations[].noeud_id")56        _require_non_empty_str(rec.noeud_label, f"recommandation {rec.noeud_id}.noeud_label")57 58        if rec.noeud_id not in node_ids:59            raise AdvisorValidationError(60                f"Recommandation liée à un noeud inconnu: {rec.noeud_id}"61            )62 63        if rec.noeud_label not in node_labels:64            raise AdvisorValidationError(65                f"noeud_label inconnu ou incohérent avec le workflow: {rec.noeud_label}"66            )67 68        if rec.noeud_id in seen_node_ids:69            raise AdvisorValidationError(70                f"Recommandations dupliquées pour le noeud {rec.noeud_id}"71            )72        seen_node_ids.add(rec.noeud_id)73 74 75def _validate_tools_against_catalog(76    report: RapportAdvisor,77    catalog_names: set[str],78) -> None:79    for rec in report.recommandations:80        if not rec.outils:81            raise AdvisorValidationError(82                f"Aucun outil recommandé pour le noeud {rec.noeud_id}"83            )84 85        seen_tools_for_node: set[str] = set()86 87        for outil in rec.outils:88            _require_non_empty_str(outil.nom, f"{rec.noeud_id}.outils[].nom")89            _require_non_empty_str(outil.justification, f"{rec.noeud_id}.outils[{outil.nom}].justification")90 91            if outil.nom not in catalog_names:92                raise AdvisorValidationError(93                    f"Outil hors catalogue détecté: {outil.nom}"94                )95 96            if outil.nom in seen_tools_for_node:97                raise AdvisorValidationError(98                    f"Outil dupliqué pour le noeud {rec.noeud_id}: {outil.nom}"99                )100            seen_tools_for_node.add(outil.nom)101 102 103def _validate_stack_fields(report: RapportAdvisor, catalog_names: set[str]) -> None:104    if not report.stack_minimale:105        raise AdvisorValidationError("stack_minimale vide.")106    if not report.stack_complete:107        raise AdvisorValidationError("stack_complete vide.")108 109    for name in report.stack_minimale:110        _require_non_empty_str(name, "stack_minimale[]")111        if name not in catalog_names:112            raise AdvisorValidationError(f"Outil hors catalogue dans stack_minimale: {name}")113 114    for name in report.stack_complete:115        _require_non_empty_str(name, "stack_complete[]")116        if name not in catalog_names:117            raise AdvisorValidationError(f"Outil hors catalogue dans stack_complete: {name}")118 119    if len(report.stack_minimale) != len(set(report.stack_minimale)):120        raise AdvisorValidationError("Doublons détectés dans stack_minimale.")121 122    if len(report.stack_complete) != len(set(report.stack_complete)):123        raise AdvisorValidationError("Doublons détectés dans stack_complete.")124 125    if not set(report.stack_minimale).issubset(set(report.stack_complete)):126        raise AdvisorValidationError(127            "stack_minimale doit être incluse dans stack_complete."128        )129 130 131def _validate_implementation_guidance(report: RapportAdvisor) -> None:132    _require_non_empty_str(report.conseil_implementation, "conseil_implementation")133 134 135def _validate_recommendation_coverage(136    report: RapportAdvisor,137    workflow: WorkflowOptimise,138) -> None:139    """140    Version volontairement stricte :141    - on exige une recommandation pour chaque noeud non trivial du workflow142    - on ignore trigger et fin143    """144    expected_ids = {145        n.id for n in workflow.noeuds146        if n.type_noeud not in {"trigger", "fin"}147    }148    got_ids = {rec.noeud_id for rec in report.recommandations}149 150    missing = expected_ids - got_ids151    if missing:152        raise AdvisorValidationError(153            f"Certains noeuds du workflow n'ont aucune recommandation: {sorted(missing)}"154        )155 156 157def validate_advisor_report_or_raise(158    report: RapportAdvisor,159    workflow: WorkflowOptimise,160    tool_catalog: Iterable[Any],161) -> None:162    """163    Validation métier complète de la sortie Advisor.164 165    Vérifie :166    - cohérence des références vers le workflow167    - conformité des outils au catalogue autorisé168    - cohérence de stack_minimale / stack_complete169    - présence de recommandations pour les noeuds utiles170    """171    if not isinstance(report, RapportAdvisor):172        raise AdvisorValidationError(173            f"RapportAdvisor attendu, reçu: {type(report)!r}"174        )175 176    if not isinstance(workflow, WorkflowOptimise):177        raise AdvisorValidationError(178            f"WorkflowOptimise attendu, reçu: {type(workflow)!r}"179        )180 181    catalog_names = _normalize_catalog_names(tool_catalog)182 183    _validate_workflow_refs(report, workflow)184    _validate_tools_against_catalog(report, catalog_names)185    _validate_stack_fields(report, catalog_names)186    _validate_implementation_guidance(report)187    _validate_recommendation_coverage(report, workflow)188 189 190def audit_advisor_report(191    report: RapportAdvisor,192    workflow: WorkflowOptimise,193    tool_catalog: Optional[Iterable[Any]] = None,194) -> list[str]:195    """196    Audit non bloquant : signale les zones fragiles sans rejeter la sortie.197    """198    warnings: list[str] = []199 200    if len(report.stack_complete) > 8:201        warnings.append("stack_complete volumineuse (>8 outils) : risque de sur-architecture.")202 203    if len(report.stack_minimale) == len(report.stack_complete):204        warnings.append("stack_minimale et stack_complete sont identiques : peu de différenciation de rollout.")205 206    # Détection simple de redondance conceptuelle207    orchestration_keywords = ("hubspot", "notion", "jira", "linear", "airtable", "monday", "clickup")208    orchestration_hits = [209        name for name in report.stack_complete210        if any(k in name.lower() for k in orchestration_keywords)211    ]212    if len(orchestration_hits) > 2:213        warnings.append(214            f"Plusieurs outils centraux potentiels détectés dans la stack_complete : {orchestration_hits}"215        )216 217    if tool_catalog is not None:218        catalog_names = _normalize_catalog_names(tool_catalog)219        unknown_in_complete = [x for x in report.stack_complete if x not in catalog_names]220        if unknown_in_complete:221            warnings.append(222                f"Outils non reconnus dans stack_complete : {unknown_in_complete}"223            )224 225    non_terminal_nodes = [226        n for n in workflow.noeuds if n.type_noeud not in {"trigger", "fin"}227    ]228    if len(report.recommandations) < max(1, len(non_terminal_nodes) // 2):229        warnings.append(230            "Couverture de recommandations faible par rapport au nombre de noeuds du workflow."231        )232 233    if len(report.conseil_implementation.split()) < 12:234        warnings.append("conseil_implementation très court : justification potentiellement insuffisante.")235 236    return warnings237