Files
rpa_vision_v3/agent_v0/server_v1/domain_context.py
Dom 99041f0117 feat: pipeline complet MACRO/MÉSO/MICRO — Critic, Observer, Policy, Recovery, Learning, Audit Trail, TaskPlanner
Architecture 3 niveaux implémentée et testée (137 tests unitaires + 21 visuels) :

MÉSO (acteur intelligent) :
- P0 Critic : vérification sémantique post-action via gemma4 (replay_verifier.py)
- P1 Observer : pré-analyse écran avant chaque action (api_stream.py /pre_analyze)
- P2 Grounding/Policy : séparation localisation (grounding.py) et décision (policy.py)
- P3 Recovery : rollback automatique Ctrl+Z/Escape/Alt+F4 (recovery.py)
- P4 Learning : apprentissage runtime avec boucle de consolidation (replay_learner.py)

MACRO (planificateur) :
- TaskPlanner : comprend les ordres en langage naturel via gemma4 (task_planner.py)
- Contexte métier TIM/CIM-10 pour les hôpitaux (domain_context.py)
- Endpoint POST /api/v1/task pour l'exécution par instruction

Traçabilité :
- Audit trail complet avec 18 champs par action (audit_trail.py)
- Endpoints GET /audit/history, /audit/summary, /audit/export (CSV)

Grounding :
- Fix parsing bbox_2d qwen2.5vl (pixels relatifs, pas grille 1000x1000)
- Benchmarks visuels sur captures réelles (3 approches : baseline, zoom, Citrix)
- Reproductibilité validée : variance < 0.008 sur 10 itérations

Sécurité :
- Tokens de production retirés du code source → .env.local
- Secret key aléatoire si non configuré
- Suppression logs qui leakent les tokens

Résultats : 80% de replay (vs 12.5% avant), 100% détection visuelle Citrix JPEG Q20

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:03:25 +02:00

202 lines
8.0 KiB
Python

# agent_v0/server_v1/domain_context.py
"""
Contexte métier pour les appels VLM — rend Léa experte du domaine.
Chaque workflow est associé à un domaine métier (médical, comptable, etc.)
qui enrichit TOUS les prompts VLM (Observer, Critic, acteur, enrichissement).
Un gemma4 qui sait qu'il regarde un DPI et que l'utilisateur fait du codage
CIM-10 prend des décisions bien meilleures qu'un VLM générique.
Premier domaine : TIM (Technicien d'Information Médicale)
- Logiciels DPI/DMS (dossier patient informatisé)
- Codage CIM-10 / CCAM / GHM
- Lecture de comptes rendus médicaux
- Validation des séjours / RSS / RSA
Usage :
ctx = get_domain_context("tim_codage")
prompt = f"{ctx.system_prompt}\n\n{user_prompt}"
"""
import logging
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
@dataclass
class DomainContext:
"""Contexte métier pour un domaine spécifique."""
domain_id: str # Identifiant unique (tim_codage, comptabilite, etc.)
name: str # Nom lisible (Codage médical TIM)
description: str # Description courte du métier
# Prompt système injecté dans TOUS les appels VLM
system_prompt: str = ""
# Vocabulaire métier (termes que le VLM doit connaître)
vocabulary: List[str] = field(default_factory=list)
# Applications connues (noms de logiciels que le VLM peut rencontrer)
known_apps: List[str] = field(default_factory=list)
# Écrans types (descriptions des écrans courants du métier)
screen_patterns: Dict[str, str] = field(default_factory=dict)
def enrich_prompt(self, prompt: str, role: str = "") -> str:
"""Enrichir un prompt avec le contexte métier.
Args:
prompt: Le prompt original
role: Le rôle du VLM (observer, critic, actor, enrichment)
"""
parts = []
if self.system_prompt:
parts.append(self.system_prompt)
if role:
role_hint = _ROLE_HINTS.get(role, "")
if role_hint:
parts.append(role_hint.format(domain=self.name))
parts.append(prompt)
return "\n\n".join(parts)
def to_dict(self) -> Dict[str, Any]:
return {
"domain_id": self.domain_id,
"name": self.name,
"description": self.description,
"known_apps": self.known_apps,
"vocabulary_count": len(self.vocabulary),
}
# Hints par rôle VLM — adaptés au contexte métier
_ROLE_HINTS = {
"observer": (
"Tu observes un écran utilisé dans le domaine '{domain}'. "
"Cherche les popups, erreurs, ou états incohérents avec ce métier."
),
"critic": (
"Tu vérifies qu'une action dans le domaine '{domain}' a produit "
"le bon résultat. Sois précis sur ce que tu vois à l'écran."
),
"actor": (
"Tu décides si une action est nécessaire dans le contexte '{domain}'. "
"Utilise ta connaissance du métier pour juger si l'état est cohérent."
),
"enrichment": (
"Tu analyses un enregistrement de workflow dans le domaine '{domain}'. "
"Décris les intentions métier, pas juste les clics."
),
}
# =========================================================================
# Domaines pré-configurés
# =========================================================================
_TIM_CODAGE = DomainContext(
domain_id="tim_codage",
name="Codage médical TIM",
description=(
"Technicien d'Information Médicale : lecture de comptes rendus médicaux, "
"codage des diagnostics en CIM-10, codage des actes en CCAM, "
"validation des groupes homogènes de malades (GHM), "
"gestion des résumés de sortie standardisés (RSS/RSA)."
),
system_prompt=(
"Tu es un assistant expert en codage médical hospitalier. "
"L'utilisateur est un TIM (Technicien d'Information Médicale) qui utilise "
"un logiciel DPI (Dossier Patient Informatisé) ou DIM (Département d'Information Médicale). "
"Son travail : lire les comptes rendus médicaux des patients et coder les diagnostics "
"en CIM-10, les actes en CCAM, et valider les séjours pour le PMSI.\n\n"
"Vocabulaire du métier :\n"
"- DPI/DMS : logiciel de dossier patient (ex: Orbis, DxCare, Crossway, Easily, Hopital Manager)\n"
"- CIM-10 : Classification Internationale des Maladies, 10ème révision (codes diagnostics)\n"
"- CCAM : Classification Commune des Actes Médicaux (codes actes chirurgicaux/médicaux)\n"
"- GHM : Groupe Homogène de Malades (regroupement tarifaire)\n"
"- RSS : Résumé de Sortie Standardisé (données du séjour)\n"
"- RSA : Résumé de Sortie Anonyme (RSS anonymisé pour la T2A)\n"
"- DP : Diagnostic Principal (le code CIM-10 principal du séjour)\n"
"- DAS : Diagnostics Associés Significatifs\n"
"- CMA : Complication ou Morbidité Associée (augmente la sévérité)\n"
"- T2A : Tarification À l'Activité (financement des hôpitaux)\n"
"- PMSI : Programme de Médicalisation des Systèmes d'Information\n"
"- UM : Unité Médicale (service hospitalier)\n"
"- CR : Compte Rendu (document médical)\n\n"
"Écrans courants :\n"
"- Liste de patients / dossiers à coder\n"
"- Fiche patient (identité, séjour, UM)\n"
"- Écran de codage CIM-10 (recherche de codes, saisie DP/DAS)\n"
"- Visualiseur de comptes rendus médicaux\n"
"- Écran de validation / groupage GHM\n"
"- Recherche de codes (arborescence CIM-10 ou recherche textuelle)"
),
vocabulary=[
"CIM-10", "CCAM", "GHM", "RSS", "RSA", "PMSI", "T2A",
"diagnostic principal", "DAS", "CMA", "compte rendu",
"dossier patient", "séjour", "unité médicale", "codage",
"groupage", "valorisation", "exhaustivité",
],
known_apps=[
"Orbis", "DxCare", "Crossway", "Easily", "Hopital Manager",
"CORA", "AGFA", "Dedalus", "Maincare", "Softway Medical",
"WebPIMS", "CEPAGE", "Medimust",
],
screen_patterns={
"liste_patients": "Liste de dossiers patients avec colonnes (nom, prénom, date entrée, UM, statut codage)",
"fiche_patient": "Fiche d'identité patient avec numéro IPP, séjour, dates, UM",
"codage_cim10": "Écran de saisie des codes CIM-10 avec diagnostic principal et DAS",
"compte_rendu": "Visualiseur de compte rendu médical (texte libre, souvent PDF intégré)",
"recherche_code": "Recherche de code CIM-10 ou CCAM (champ de recherche + arborescence)",
"validation_ghm": "Écran de validation du groupage avec GHM calculé et valorisation",
},
)
_GENERIC = DomainContext(
domain_id="generic",
name="Bureautique générale",
description="Automatisation bureautique générale (Office, navigateur, etc.)",
system_prompt=(
"Tu es un assistant RPA qui observe des applications bureautiques. "
"Décris précisément ce que tu vois à l'écran."
),
)
# Registre des domaines disponibles
_DOMAINS: Dict[str, DomainContext] = {
"tim_codage": _TIM_CODAGE,
"generic": _GENERIC,
}
def get_domain_context(domain_id: str = "generic") -> DomainContext:
"""Récupérer le contexte métier par ID.
Args:
domain_id: Identifiant du domaine (tim_codage, generic, etc.)
Returns:
DomainContext correspondant, ou generic si non trouvé.
"""
ctx = _DOMAINS.get(domain_id, _GENERIC)
if ctx is _GENERIC and domain_id != "generic":
logger.warning(f"Domaine '{domain_id}' non trouvé, utilisation de 'generic'")
return ctx
def register_domain(context: DomainContext) -> None:
"""Enregistrer un nouveau domaine métier."""
_DOMAINS[context.domain_id] = context
logger.info(f"Domaine '{context.domain_id}' enregistré ({context.name})")
def list_domains() -> List[Dict[str, Any]]:
"""Lister tous les domaines disponibles."""
return [ctx.to_dict() for ctx in _DOMAINS.values()]