AI

Créez un agent TPRM d’entreprise avec Bright Data et OpenHands SDK

Ce guide vous montre comment créer un agent TPRM évolutif avec les API Bright Data et le SDK OpenHands afin d’automatiser la vérification des risques liés aux fournisseurs.
30 min de lecture
OpenHands Agent SDK with Bright Data

Dans ce guide, vous apprendrez :

  • Qu’est-ce que la gestion des risques liés aux tiers (TPRM) et pourquoi le filtrage manuel échoue
  • Comment créer un agent IA autonome qui recherche les informations négatives sur les fournisseurs dans les médias
  • Comment intégrer l’API SERP et le Web Unlocker de Bright Data pour une collecte de données fiable et actualisée
  • Comment utiliser OpenHands SDK pour la génération de scripts d’agent et OpenAI pour l’analyse des risques
  • Comment améliorer l’agent avec l’API Browser pour les scénarios complexes tels que les registres judiciaires

C’est parti !

Le problème du filtrage manuel des fournisseurs

Les équipes de conformité des entreprises sont confrontées à une tâche impossible : surveiller des centaines de fournisseurs tiers à la recherche de signaux de risque sur l’ensemble du web. Les approches traditionnelles impliquent :

  • Recherches manuelles sur Google pour chaque nom de fournisseur, associées à des mots-clés tels que « procès », « faillite » ou « fraude »
  • La présence de paywalls et de CAPTCHAs lors de la tentative d’accès à des articles de presse et à des dossiers judiciaires
  • Une documentation incohérente, sans processus standardisé pour enregistrer les résultats
  • Aucune surveillance continue, la sélection des fournisseurs se fait une seule fois lors de l’intégration, puis plus jamais

Cette approche échoue pour trois raisons essentielles :

  1. Échelle: un seul analyste peut enquêter de manière approfondie sur 5 à 10 fournisseurs par jour
  2. Accès: les sources protégées telles que les registres judiciaires et les sites d’information premium bloquent l’accès automatisé
  3. Continuité: les évaluations ponctuelles ne permettent pas de détecter les risques qui apparaissent après l’intégration

La solution : un agent TPRM autonome

Un agent TPRM automatise l’ensemble du processus d’enquête sur les fournisseurs à l’aide de trois couches spécialisées :

  • Découverte (API SERP): l’agent recherche sur Google les signaux d’alerte tels que les poursuites judiciaires, les mesures réglementaires et les difficultés financières.
  • Accès (Web Unlocker): lorsque les résultats pertinents sont protégés par des paywalls ou des CAPTCHA, l’agent contourne ces barrières pour extraire l’intégralité du contenu
  • Action (OpenAI + OpenHands SDK): l’agent analyse le contenu pour déterminer la gravité du risque à l’aide d’OpenAI, puis utilise OpenHands SDK pour générer des scripts de surveillance Python qui vérifient quotidiennement l’apparition de nouveaux articles négatifs dans les médias

Ce système transforme des heures de recherche manuelle en quelques minutes d’analyse automatisée.

Prérequis

Avant de commencer, assurez-vous de disposer des éléments suivants :

Architecture du projet

L’agent TPRM suit un pipeline en trois étapes :

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   DÉCOUVERTE     │────▶│     ACCÈS      │────▶│     ACTION      │
│   (API SERP)    │     │ (Web Unlocker)  │     │ (OpenAI + SDK)  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                       │                       │
   Rechercher sur Google          Contourner les paywalls         Analyser les risques
   pour les signaux d'alerte          et les CAPTCHA           Générer des scripts

Créer la structure de projet suivante :

tprm-agent/
├── src/
│   ├── __init__.py
│   ├── config.py         # Configuration
│   ├── discovery.py      # Intégration de l'API SERP
│   ├── access.py         # Intégration de Web Unlocker
│   ├── actions.py        # OpenAI + OpenHands SDK
│   ├── agent.py          # Orchestration principale
│   └── browser.py        # API du navigateur (amélioration)
├── api/
│   └── main.py           # Points de terminaison FastAPI
├── scripts/
│   └── generated/        # Scripts de surveillance générés automatiquement
├── .env
├── requirements.txt
└── README.md

Configuration de l’environnement

Créez un environnement virtuel et installez les dépendances requises :

python -m venv venv
source venv/bin/activate  # Windows : venvScriptsactivate

pip install requests fastapi uvicorn python-dotenv pydantic openai beautifulsoup4 playwright openhands-sdk openhands-tools

Créez un fichier .env pour stocker vos identifiants API :

# Jeton API Bright Data (pour l'API SERP)
BRIGHT_DATA_API_TOKEN=votre_jeton_api

# Zone SERP Bright Data
BRIGHT_DATA_SERP_ZONE=votre_nom_de_zone_serp

# Identifiants Bright Data Web Unlocker
BRIGHT_DATA_CUSTOMER_ID=votre_identifiant_client
BRIGHT_DATA_UNLOCKER_ZONE=votre_nom_de_zone_unlocker
BRIGHT_DATA_UNLOCKER_PASSWORD=votre_mot_de_passe_zone

# OpenAI (pour l'analyse des risques)
OPENAI_API_KEY=votre_clé_api_openai

# OpenHands (pour la génération de scripts agentics)
# Utilisez OpenHands Cloud : openhands/claude-sonnet-4-5-20260929
# Ou apportez le vôtre : anthropic/claude-sonnet-4-5-20260929
LLM_API_KEY=votre_llm_api_key
LLM_MODEL=openhands/claude-sonnet-4-5-20260929

Configuration Bright Data

Étape 1 : Créez votre compte Bright Data

Inscrivez-vous sur Bright Data et accédez au tableau de bord.

Étape 2 : Configurez la zone API SERP

  1. Accédez à Proxies & Infrastructure de scraping
  2. Cliquez sur Ajouter et sélectionnez API SERP
  3. Nommez votre zone (par exemple, tprm_serp)
  4. Copiez le nom de votre zone et notez votre jeton API dans Paramètres > Jetons API

L’API SERP renvoie des résultats de recherche structurés provenant de Google sans être bloquée. Ajoutez brd_json=1 à votre URL de recherche pour obtenir une sortie JSON analysée.
A Bright Data Dashboard SERP API

Étape 3 : Configurer la zone Web Unlocker

  1. Cliquez sur Ajouter et sélectionnez Web Unlocker
  2. Nommez votre zone (par exemple, tprm_unlocker)
  3. Copiez les informations d’identification de votre zone (format du nom d’utilisateur : brd-customer-CUSTOMER_ID-zone-ZONE_NAME)

Web Unlocker gère automatiquement les CAPTCHA, les empreintes digitales et la rotation des adresses IP via un point de terminaison Proxy.
A Bright Data Dashboard Web Unlocker API

Création de la couche de découverte (API SERP)

La couche de découverte recherche sur Google les médias défavorables aux fournisseurs à l’aide de l’API SERP. Créez src/discovery.py:

import requests
from typing import Optional
from dataclasses import dataclass
from urllib.parse import quote_plus
from config import settings


@dataclass
class SearchResult:
    title: str
    url: str
    snippet: str
    source: str


class DiscoveryClient:
    """Rechercher des informations négatives à l'aide de l'API SERP de Bright Data (API directe)."""

    RISK_CATEGORIES = {
        "litigation": ["lawsuit", "litigation", "sued", "court case", "legal action"],
        "financial": ["bankruptcy", "insolvency", "debt", "financial trouble", "default"],
        « fraude » : [« fraude », « escroquerie », « enquête », « mise en accusation », « scandale »],
        « réglementation » : [« violation », « amende », « pénalité », « sanctions », « conformité »],
        « opérationnel » : [« rappel », « problème de sécurité », « chaîne d'approvisionnement », « perturbation »],
    }

    def __init__(self) :
        self.api_url = « https://api.brightdata.com/request »
        self.headers = {
            « Content-Type » : « application/json »,
            « Authorization » : f« Bearer {settings.BRIGHT_DATA_API_TOKEN} »,
        }

    def _build_queries(self, vendor_name: str, categories: Optional[list] = None) -> list[str]:
        """Construire des requêtes de recherche pour chaque catégorie de risque."""
        categories = categories or list(self.RISK_CATEGORIES.keys())
        queries = []

        for category in categories:
            keywords = self.RISK_CATEGORIES.get(category, [])
            keyword_str = " OR ".join(keywords)
            query = f'"{vendor_name}" ({keyword_str})'
            queries.append(query)

        return queries

    def search(self, query: str) -> list[SearchResult]:
        """Exécute une requête de recherche unique à l'aide de l'API SERP de Bright Data."""
        try:
            # Construire l'URL de recherche Google avec brd_json=1 pour le JSON analysé
            encoded_query = quote_plus(query)
            google_url = f"https://www.google.com/search?q={encoded_query}&hl=en&gl=us&brd_json=1"

            payload = {
                "Zone": settings.BRIGHT_DATA_SERP_ZONE,
                "url": google_url,
                "format": "raw",
            }

            response = requests.post(
                self.api_url,
                headers=self.headers,
                json=payload,
                timeout=30,
            )
            response.raise_for_status()
            data = response.json()

            results = []
            organic = data.get("organic", [])

            for item in organic:
                results.append(
                    SearchResult(
                        title=item.get("title", ""),
                        url=item.get("link", ""),
                        snippet=item.get("description", ""),
                        source=item.get("displayed_link", ""),
                    )
                )
            return results

        except Exception as e:
            print(f"Search error: {e}")
            return []

    def discover_adverse_media(
        self,
        vendor_name: str,
        categories: Optional[list] = None,
    ) -> dict[str, list[SearchResult]]:
        """Recherche de médias défavorables dans toutes les catégories de risques."""
        queries = self._build_queries(vendor_name, categories)
        category_names = categories or list(self.RISK_CATEGORIES.keys())

        categorized_results = {}
        for category, query in zip(category_names, queries):
            print(f"  Recherche : {category}...")
            results = self.search(query)
            categorized_results[category] = results

        return categorized_results

    def filter_relevant_results(
        self, results: dict[str, list[SearchResult]], vendor_name: str
    ) -> dict[str, list[SearchResult]]:
        """Filtrer les résultats non pertinents."""
        filtered = {}
        vendor_lower = vendor_name.lower()

        for category, items in results.items():
    relevant = []
    for item in items:
        if (
            vendor_lower in item.title.lower()
            or vendor_lower in item.snippet.lower()
        ):
            relevant.append(item)
    filtered[category] = relevant

return filtered

L’API SERP renvoie un JSON structuré avec des résultats organiques, ce qui facilite l’analyse des titres, des URL et des extraits pour chaque résultat de recherche.

Création de la couche d’accès (Web Unlocker)

Lorsque la couche de découverte trouve des URL pertinentes, la couche d’accès récupère le contenu complet à l’aide de l’API Web Unlocker. Créez src/access.py:

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import Optional
from config import settings


@dataclass
class ExtractedContent:
    url: str
    title: str
    text: str
    publish_date: Optional[str]
    author: Optional[str]
    success: bool
    error: Optional[str] = None


class AccessClient:
    """Accédez au contenu protégé à l'aide de Bright Data Web Unlocker (basé sur l'API)."""

    def __init__(self):
        self.api_url = "https://api.brightdata.com/request"
        self.headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {settings.BRIGHT_DATA_API_TOKEN}",
        }

    def fetch_url(self, url: str) -> ExtractedContent:
        """Récupère et extrait le contenu d'une URL à l'aide de l'API Web Unlocker."""
        try:
            payload = {
                "zone": settings.BRIGHT_DATA_UNLOCKER_ZONE,
                "url": url,
                "format": "raw",
            }

            response = requests.post(
                self.api_url,
                headers=self.headers,
                json=payload,
                timeout=60,
            )
            response.raise_for_status()

            # L'API Web Unlocker renvoie directement le code HTML.
            html_content = response.text
            content = self._extract_content(html_content, url)
            return content

        except requests.Timeout:
            return ExtractedContent(
                url=url,
                title="",
                text="",
                publish_date=None,
                author=None,
                success=False,
                error="Request timed out",
            )
        except Exception as e:
            return ExtractedContent(
                url=url,
                title="",
                text="",
                publish_date=None,
                author=None,
                success=False,
                error=str(e),
            )

    def _extract_content(self, html: str, url: str) -> ExtractedContent:
        """Extraire le contenu de l'article à partir du HTML."""
        soup = BeautifulSoup(html, "html.parser")

        # Supprimer les éléments indésirables
        for element in soup(["script", "style", "nav", "footer", "header", "aside"]):
            element.decompose()

        # Extraire le titre
        title = ""
        if soup.title:
            title = soup.title.string or ""
        elif soup.find("h1"):
            title = soup.find("h1").get_text(strip=True)

        # Extraire le contenu principal
        article = soup.find("article") or soup.find("main") or soup.find("body")
        text = article.get_text(separator="n", strip=True) if article else ""

        # Limiter la longueur du texte
        text = text[:10000] if len(text) > 10000 else text

        # Essayer d'extraire la date de publication
        publish_date = None
        date_meta = soup.find("meta", {"property": "article:published_time"})
        if date_meta:
            publish_date = date_meta.get("content")

        # Essayer d'extraire l'auteur
        auteur = None
        auteur_meta = soup.find("meta", {"name": "author"})
        si auteur_meta :
            auteur = auteur_meta.get("content")

        return ExtractedContent(
            url=url,
            title=title,
            text=text,
            publish_date=publish_date,
            author=author,
            success=True,
        )

    def fetch_multiple(self, urls: list[str]) -> list[ExtractedContent]:
        """Récupère plusieurs URL de manière séquentielle."""
        results = []
        for url in urls:
            print(f"  Récupération : {url[:60]}...")
            content = self.fetch_url(url)
            if not content.success:
                print(f"  Erreur : {content.error}")
            results.append(content)
        return results

Web Unlocker gère automatiquement les CAPTCHA, les empreintes digitales des navigateurs et la rotation des adresses IP. Il achemine simplement vos requêtes via le Proxy et s’occupe du reste.

Création de la couche d’action (OpenAI + OpenHands SDK)

La couche d’action utilise OpenAI pour analyser la gravité des risques et OpenHands SDK pour générer des scripts de surveillance qui utilisent l’API Bright Data Web Unlocker. OpenHands SDK fournit des capacités agentriques : l’agent peut raisonner, modifier des fichiers et exécuter des commandes pour créer des scripts prêts à être utilisés en production.

Créer src/actions.py:

import os
import json
from datetime import datetime, UTC
from dataclasses import dataclass, asdict
from openai import OpenAI
from pydantic import SecretStr
from openhands.sdk import LLM, Agent, Conversation, Tool
from openhands.tools.terminal import TerminalTool
from openhands.tools.file_editor import FileEditorTool
from config import settings


@dataclass
class RiskAssessment:
    vendor_name: str
    category: str
    severity: str
    summary: str
    key_findings: list[str]
    sources: list[str]
    recommended_actions: list[str]
    assessed_at: str


@dataclass
class MonitoringScript:
    vendor_name: str
    script_path: str
    urls_monitored: list[str]
    check_frequency: str
    created_at: str


class ActionsClient:
    """Analysez les risques à l'aide d'OpenAI et générez des scripts de surveillance à l'aide du SDK OpenHands."""

    def __init__(self):
        # OpenAI pour l'analyse des risques
        self.openai_client = OpenAI(api_key=settings.OPENAI_API_KEY)
        
        # OpenHands pour la génération de scripts agentifs
        self.llm = LLM(
            model=settings.LLM_MODEL,
            api_key=SecretStr(settings.LLM_API_KEY),
        )
        
        self.workspace = os.path.join(os.getcwd(), "scripts", "generated")
        os.makedirs(self.workspace, exist_ok=True)

    def analyze_risk(
        self,
        vendor_name: str,
        category: str,
        content: list[dict],
    ) -> RiskAssessment:
        """Analysez le contenu extrait pour déterminer la gravité du risque à l'aide d'OpenAI."""
        content_summary = "nn".join(
            [f"Source : {c['url']}nTitre : {c['title']}nContenu : {c['text'][:2000]}" for c in content]
        )

        prompt = f"""Analysez le contenu suivant concernant « {vendor_name} » pour l'évaluation des risques liés aux tiers.

Catégorie : {category}

Contenu :
{content_summary}

Fournissez une réponse JSON avec :
{{
    "severity": "low|medium|high|critical",
    « summary » : « résumé des conclusions en 2-3 phrases »,
    « key_findings » : [« conclusion 1 », « conclusion 2 », ...],
    « recommended_actions » : [« action 1 », « action 2 », ...]
}}

À prendre en compte :
- La gravité doit être basée sur l'impact potentiel sur l'activité
- Critique = action immédiate requise (fraude active, dépôt de bilan)
- Élevé = risque important nécessitant une enquête
- Moyen = préoccupation notable méritant d'être surveillée
- Faible = problème mineur ou question historique
"""

        response = self.openai_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"},
        )

        response_text = response.choices[0].message.content
        try:
            result = json.loads(response_text)
        except (json.JSONDecodeError, ValueError):
            result = {
                "severity": "medium",
                "summary": "Impossible d'analyser l'évaluation des risques",
                "key_findings": [],
                "recommended_actions": ["Révision manuelle requise"],
            }

        return RiskAssessment(
            vendor_name=vendor_name,
            category=category,
            severity=result.get("severity", "medium"),
            summary=result.get("summary", ""),
            key_findings=result.get("key_findings", []),
            sources=[c["url"] for c in content],
            recommended_actions=result.get("recommended_actions", []),
            assessed_at=datetime.now(UTC).isoformat(),
        )

    def generate_monitoring_script(
        self,
        vendor_name: str,
        urls: list[str],
        check_keywords: list[str],
    ) -> MonitoringScript:
        """Générer un script de surveillance Python à l'aide de l'agent OpenHands SDK."""
        script_name = f"monitor_{vendor_name.lower().replace(' ', '_')}.py"
        script_path = os.path.join(self.workspace, script_name)

        prompt = f"""Créer un script de surveillance Python à {script_path} qui :

1. Vérifie quotidiennement ces URL pour détecter tout nouveau contenu : {urls[:5]}
2. Recherche les mots-clés suivants : {check_keywords}
3. Envoie une alerte (impression sur la console) si un nouveau contenu pertinent est trouvé
4. Enregistre toutes les vérifications dans un fichier JSON nommé « monitoring_log.json »

Le script DOIT utiliser l'API Bright Data Web Unlocker pour contourner les paywalls et les CAPTCHAs :
- Point de terminaison de l'API : https://api.brightdata.com/request
- Utiliser la variable d'environnement BRIGHT_DATA_API_TOKEN pour le jeton Bearer
- Utiliser la variable d'environnement BRIGHT_DATA_UNLOCKER_ZONE pour le nom de la zone
- Effectuer des requêtes POST avec une charge utile JSON : {{"zone": "zone_name", "url": "target_url", "format": "raw"}}
- Ajoutez l'en-tête : « Authorization » : « Bearer <token> »
- Ajoutez l'en-tête : « Content-Type » : « application/json »

Le script doit :
- Charger les identifiants Bright Data à partir des variables d'environnement à l'aide de python-dotenv
- Utiliser l'API Bright Data Web Unlocker pour toutes les requêtes HTTP (PAS les requêtes simples requests.get)
- Gérer les erreurs de manière élégante avec try/except
- Inclure une fonction main() pouvant être exécutée directement
- Prendre en charge la planification via cron
- Stocker les hachages de contenu pour détecter les modifications

Écrire le script complet dans {script_path}.
"""

        # Créer un agent OpenHands avec des outils de terminal et d'édition de fichiers
        agent = Agent(
            llm=self.llm,
            tools=[
                Tool(name=TerminalTool.name),
                Tool(name=FileEditorTool.name),
            ],
        )

        # Exécuter l'agent pour générer le script
        conversation = Conversation(agent=agent, workspace=self.workspace)
        conversation.send_message(prompt)
        conversation.run()

        return MonitoringScript(
            vendor_name=vendor_name,
            script_path=script_path,
            urls_monitored=urls[:5],
            check_frequency="daily",
            created_at=datetime.now(UTC).isoformat(),
        )

    def export_assessment(self, assessment: RiskAssessment, output_path: str) -> None:
        """Exporter l'évaluation des risques vers un fichier JSON."""
        with open(output_path, "w") as f:
            json.dump(asdict(assessment), f, indent=2)

Le principal avantage de l’utilisation du SDK OpenHands par rapport à la simple génération de code basée sur des invites est que l’agent peut itérer son travail, tester le script, corriger les erreurs et l’affiner jusqu’à ce qu’il fonctionne correctement.

Orchestration des agents

Maintenant, connectons tout ensemble. Créez src/agent.py:

from dataclasses import dataclass
from datetime import datetime, UTC
from typing import Optional

from discovery import DiscoveryClient, SearchResult
from access import AccessClient, ExtractedContent
from actions import ActionsClient, RiskAssessment, MonitoringScript


@dataclass
class InvestigationResult:
    vendor_name: str
    started_at: str
    completed_at: str
    total_sources_found: int
    total_sources_accessed: int
    risk_assessments: list[RiskAssessment]
    monitoring_scripts: list[MonitoringScript]
    errors: list[str]


classe TPRMAgent :
    """Agent autonome pour les enquêtes de gestion des risques tiers."""

    def __init__(self) :
        self.discovery = DiscoveryClient()
        self.access = AccessClient()
        self.actions = ActionsClient()

    def investigate(
        self,
        vendor_name: str,
        categories: Optional[list[str]] = None,
        generate_monitors: bool = True,
    ) -> InvestigationResult:
        """Exécuter une enquête complète sur le fournisseur."""
        started_at = datetime.now(UTC).isoformat()
        errors = []
        risk_assessments = []
        monitoring_scripts = []

        # Étape 1 : Découverte (API SERP)
        print(f"[Découverte] Recherche de médias défavorables concernant {vendor_name}...")
        try:
            raw_results = self.discovery.discover_adverse_media(vendor_name, categories)
            filtered_results = self.discovery.filter_relevant_results(raw_results, vendor_name)
        except Exception as e:
            errors.append(f"Échec de la découverte : {str(e)}")
            return InvestigationResult(
                vendor_name=vendor_name,
                started_at=started_at,
                completed_at=datetime.now(UTC).isoformat(),
                total_sources_found=0,
                total_sources_accessed=0,
                risk_assessments=[],
                monitoring_scripts=[],
                errors=errors,
            )

        total_sources = sum(len(results) for results in filtered_results.values())
        print(f"[Découverte] {total_sources} sources pertinentes trouvées")

        # Étape 2 : Accès (Web Unlocker)
        print(f"[Accès] Extraction du contenu des sources...")
        all_urls = []
        url_to_category = {}
        for category, results in filtered_results.items():
            for result in results:
                all_urls.append(result.url)
                url_to_category[result.url] = category

        try:
            extracted_content = self.access.fetch_multiple(all_urls)
            successful_extractions = [c for c in extracted_content if c.success]
        except Exception as e:
            error_msg = f"Échec de l'accès : {str(e)}"
            print(f"[Accès] {error_msg}")
            errors.append(error_msg)
            successful_extractions = []

        print(f"[Accès] {len(successful_extractions)} sources extraites avec succès")

        # Étape 3 : Action - Analyser les risques (OpenAI)
        print(f"[Action] Analyse des risques en cours...")
        category_content = {}
        for content in successful_extractions:
            category = url_to_category.get(content.url, "unknown")
            if category not in category_content:
                category_content[category] = []
            category_content[category].append({
                "url": content.url,
                "title": content.title,
                "text": content.text,
            })

        for category, content_list in category_content.items():
            if not content_list:
                continue
            try:
                assessment = self.actions.analyze_risk(vendor_name, category, content_list)
                risk_assessments.append(assessment)
            except Exception as e:
                errors.append(f"Échec de l'analyse des risques pour {category}: {str(e)}")

        # Étape 3 : Action - Générer des scripts de surveillance
        if generate_monitors and successful_extractions:
            print(f"[Action] Génération des scripts de surveillance...")
            try:
                urls_to_monitor = [c.url for c in successful_extractions[:10]]
                keywords = [vendor_name, "lawsuit", "bankruptcy", "fraud"]
                script = self.actions.generate_monitoring_script(
                    vendor_name, urls_to_monitor, keywords
                )
                monitoring_scripts.append(script)
            except Exception as e:
                errors.append(f"Échec de la génération du script : {str(e)}")

        completed_at = datetime.now(UTC).isoformat()
        print(f"[Terminé] Enquête terminée")

        return InvestigationResult(
            vendor_name=vendor_name,
            started_at=started_at,
            completed_at=completed_at,
            total_sources_found=total_sources,
            total_sources_accessed=len(successful_extractions),
            risk_assessments=risk_assessments,
            monitoring_scripts=monitoring_scripts,
            errors=errors,
        )


def main():
    """Exemple d'utilisation."""
    agent = TPRMAgent()
    result = agent.investigate("Acme Corp")

    print(f"n{'='*50}")
    print(f"Investigation terminée : {result.vendor_name}")
    print(f"Sources trouvées : {result.total_sources_found}")
    print(f"Sources consultées : {result.total_sources_accessed}")
    print(f"Évaluations des risques : {len(result.risk_assessments)}")
    print(f"Scripts de surveillance : {len(result.monitoring_scripts)}")

    pour l'évaluation dans result.risk_assessments :
        print(f"n[{assessment.category.upper()}] Gravité : {assessment.severity}")
        print(f"Résumé : {assessment.summary}")


if __name__ == "__main__":
    main()

L’agent orchestre les trois couches, gère les erreurs avec élégance et produit un résultat d’enquête complet.

Configuration

Créez src/config.py pour configurer tous les secrets et toutes les clés dont nous aurons besoin pour que l’application fonctionne correctement :

import os
from dotenv import load_dotenv

load_dotenv()


class Settings:
    # API SERP
    BRIGHT_DATA_API_TOKEN: str = os.getenv("BRIGHT_DATA_API_TOKEN", "")
    BRIGHT_DATA_SERP_ZONE: str = os.getenv("BRIGHT_DATA_SERP_ZONE", "")
    
    # Web Unlocker
    BRIGHT_DATA_CUSTOMER_ID: str = os.getenv("BRIGHT_DATA_CUSTOMER_ID", "")
    BRIGHT_DATA_UNLOCKER_ZONE: str = os.getenv("BRIGHT_DATA_UNLOCKER_ZONE", "")
    BRIGHT_DATA_UNLOCKER_PASSWORD : str = os.getenv("BRIGHT_DATA_UNLOCKER_PASSWORD", "")
    
    # OpenAI (pour l'analyse des risques)
    OPENAI_API_KEY : str = os.getenv("OPENAI_API_KEY", "")
    
    # OpenHands (pour la génération de scripts agentifs)
    LLM_API_KEY : str = os.getenv("LLM_API_KEY", "")
    LLM_MODEL : str = os.getenv("LLM_MODEL", "openhands/claude-sonnet-4-5-20260929")


settings = Settings()

Création de la couche API

À l’aide de FastAPI, vous allez créer api/main.py pour exposer l’agent via des points de terminaison REST :

from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing import Optional
import uuid
import sys
sys.path.insert(0, 'src')

from agent import TPRMAgent, InvestigationResult

app = FastAPI(
    title="TPRM Agent API",
    description="Autonomous Third-Party Risk Management Agent",
    version="1.0.0",)


investigations: dict[str, InvestigationResult] = {}
agent = TPRMAgent()


class InvestigationRequest(BaseModel):
    vendor_name: str
    categories: Optional[list[str]] = None
    generate_monitors: bool = True


classe InvestigationResponse(BaseModel) :
    investigation_id : str
    status : str
    message : str


@app.post("/investigate", response_model=InvestigationResponse)
def start_investigation(
    request : InvestigationRequest,
    background_tasks : BackgroundTasks,)
 :
    """Lancer une nouvelle enquête sur un fournisseur."""
    investigation_id = str(uuid.uuid4())

    def run_investigation():
        result = agent.investigate(
            vendor_name=request.vendor_name,
            categories=request.categories,
            generate_monitors=request.generate_monitors,
        )
        investigations[investigation_id] = result

    background_tasks.add_task(run_investigation)

    return InvestigationResponse(
        investigation_id=investigation_id,
        status="started",
        message=f"Investigation started for {request.vendor_name}",
    )


@app.get("/investigate/{investigation_id}")
def get_investigation(investigation_id: str):
    """Obtenir les résultats de l'enquête."""
    if investigation_id not in investigations:
        raise HTTPException(status_code=404, detail="Enquête introuvable ou toujours en cours")

    return investigations[investigation_id]


@app.get("/reports/{vendor_name}")
def get_reports(vendor_name: str):
    """Obtenir tous les rapports pour un fournisseur."""
    vendor_reports = [
        result
        for result in investigations.values()
        if result.vendor_name.lower() == vendor_name.lower()
    ]

    if not vendor_reports:
        raise HTTPException(status_code=404, detail="Aucun rapport trouvé pour ce fournisseur")

    return vendor_reports


@app.get("/health")
def health_check():
    """Point de contrôle de l'état de santé."""
    return {"status": "healthy"}

Exécutez l’API localement :

python -m uvicorn API.main:app --reload

Rendez-vous sur http://localhost:8000/docs pour explorer la documentation interactive de l’API.
FastAPI DOcumentation

Amélioration avec l’API du navigateur (Navigateur de scraping)

Pour les scénarios complexes tels que les registres judiciaires qui nécessitent la soumission de formulaires ou les sites riches en JavaScript, vous pouvez améliorer l’agent avec l’API du navigateur de Bright Data (Navigateur de scraping). Vous pouvez la configurer de la même manière que l’API Web Unlocker et l’API SERP.
A Bright Data Dashboard Web Unlocker API

L’API Browser fournit un navigateur hébergé dans le cloud que vous contrôlez via Playwright sur le protocole Chrome DevTools (CDP). Cela est utile pour :

  • Recherches dans les registres judiciaires nécessitant la soumission de formulaires et la navigation
  • Sites riches en JavaScript avec chargement de contenu dynamique
  • Les flux d’authentification en plusieurs étapes
  • Capturer des captures d’écran pour la documentation de conformité

Configuration

Ajoutez les informations d’identification de l’API du navigateur à votre fichier .env:

# API du navigateur
BRIGHT_DATA_BROWSER_USER: str = os.getenv("BRIGHT_DATA_BROWSER_USER", "")
BRIGHT_DATA_BROWSER_PASSWORD: str = os.getenv("BRIGHT_DATA_BROWSER_PASSWORD", "")

Implémentation du client navigateur

Créez src/browser.py:

import asyncio
from playwright.async_api import async_playwright
from dataclasses import dataclass
from typing import Optional
from config import settings


@dataclass
class BrowserContent:
    url: str
    title: str
    text: str
    screenshot_path: Optional[str]
    success: bool
    error: Optional[str] = None


class BrowserClient:
    """Accédez au contenu dynamique à l'aide de l'API Bright Data Browser (Navigateur de scraping).
    
    Utilisez cette fonctionnalité pour :
    - Les sites riches en JavaScript qui nécessitent un rendu complet
    - Les formulaires en plusieurs étapes (par exemple, les recherches dans les registres judiciaires)
    - Sites nécessitant des clics, des défilements ou des interactions
    - Capture de captures d'écran pour la documentation de conformité
    """

    def __init__(self) :
        # Créer un point de terminaison WebSocket pour la connexion CDP
        auth = f"{settings.BRIGHT_DATA_BROWSER_USER}:{settings.BRIGHT_DATA_BROWSER_PASSWORD}"
        self.endpoint_url = f"wss://{auth}@brd.superproxy.io:9222"

    async def fetch_dynamic_page(
        self,
        url: str,
        wait_for_selector: Optional[str] = None,
        take_screenshot: bool = False,
        screenshot_path: Optional[str] = None,
    ) -> BrowserContent:
        """Récupérer le contenu d'une page dynamique à l'aide de l'API du navigateur."""
        async with async_playwright() as playwright:
            try:
                print(f"Connexion au Navigateur de scraping Bright Data...")
                browser = await playwright.chromium.connect_over_cdp(self.endpoint_url)

                try:
                    page = await browser.new_page()
                    print(f"Navigation vers {url}...")
                    await page.goto(url, timeout=120000)

                    # Attendre le sélecteur spécifique s'il est fourni
                    if wait_for_selector:
                        await page.wait_for_selector(wait_for_selector, timeout=30000)

                    # Obtenir le contenu de la page
                    title = await page.title()

                    # Extraire le texte
                    text = await page.evaluate("() => document.body.innerText")

                    # Prendre une capture d'écran si demandé
                    if take_screenshot and screenshot_path:
                        await page.screenshot(path=screenshot_path, full_page=True)

                    return BrowserContent(
                        url=url,
                        title=title,
                        text=text[:10000],
                        screenshot_path=screenshot_path if take_screenshot else None,
                        success=True,
                    )

                finally :
                    await browser.close()

            except Exception as e :
                return BrowserContent(
                    url=url,
                    title="",
                    text="",
                    screenshot_path=None,
                    success=False,
                    error=str(e),
                )

    async def fill_and_submit_form(
        self,
        url: str,
        form_data: dict[str, str],
        submit_selector: str,
        result_selector: str,
    ) -> BrowserContent:
        """Remplir un formulaire et obtenir des résultats - utile pour les registres judiciaires."""
        async with async_playwright() as playwright:
            try:
                browser = await playwright.chromium.connect_over_cdp(self.endpoint_url)

                try:
                    page = await browser.new_page()
                    await page.goto(url, timeout=120000)

                    # Remplir les champs du formulaire
                    for selector, value in form_data.items():
                        await page.fill(selector, value)

                    # Soumettre le formulaire
                    await page.click(submit_selector)

                    # Attendre les résultats
                    await page.wait_for_selector(result_selector, timeout=30000)

                    title = await page.title()
                    text = await page.evaluate("() => document.body.innerText")

                    return BrowserContent(
                        url=url,
                        title=title,
                        text=text[:10000],
                        screenshot_path=None,
                        success=True,
                    )

                finally:
                    await browser.close()

            except Exception as e:
                return BrowserContent(
                    url=url,
                    title="",
                    text="",
                    screenshot_path=None,
                    success=False,
                    error=str(e),
                )

    async def scroll_and_collect(
        self,
        url: str,
        scroll_count: int = 5,
        wait_between_scrolls: float = 1.0,
    ) -> BrowserContent:
        """Gérer les pages à défilement infini."""
        async with async_playwright() as playwright:
            try:
                browser = await playwright.chromium.connect_over_cdp(self.endpoint_url)

                try:
                    page = await browser.new_page()
                    await page.goto(url, timeout=120000)

                    # Faire défiler plusieurs fois vers le bas
                    for i in range(scroll_count):
                        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
                        await asyncio.sleep(wait_between_scrolls)

                    title = await page.title()
                    text = await page.evaluate("() => document.body.innerText")

                    return BrowserContent(
                        url=url,
                        title=title,
                        text=text[:10000],
                        screenshot_path=None,
                        success=True,
                    )

                finally:
                    await browser.close()

            except Exception as e:
                return BrowserContent(
                    url=url,
                    title="",
                    text="",
                    screenshot_path=None,
                    success=False,
                    error=str(e),
                )


# Exemple d'utilisation pour la recherche dans le registre du tribunal
async def example_court_search():
    client = BrowserClient()

    # Exemple : recherche dans un registre judiciaire
    result = await client.fill_and_submit_form(
        url="https://example-court-registry.gov/search",
        form_data={
            "#party-name": "Acme Corp",
            "#case-type": "civil",
        },
        submit_selector="#search-button",
        result_selector=".search-results",
    )

    if result.success:
        print(f"Found court records: {result.text[:500]}")
    else:
        print(f"Error: {result.error}")


if __name__ == "__main__":
    asyncio.run(example_court_search())

Quand utiliser l’API du navigateur ou Web Unlocker

Scénario Utilisation
Requêtes HTTP simples Web Unlocker
Pages HTML statiques Web Unlocker
CAPTCHAs au chargement Web Unlocker
Contenu rendu en JavaScript API du navigateur
Soumissions de formulaires API du navigateur
Navigation en plusieurs étapes API du navigateur
Captures d’écran requises API du navigateur

Déploiement avec Railway

Votre agent TPRM peut être déployé en production à l’aide de Railway ou Render, qui prennent tous deux en charge les applications Python avec des dépendances de grande taille.

Railway est l’option la plus simple pour déployer des applications Python avec des dépendances lourdes comme OpenHands SDK. Vous devez vous inscrire et créer un compte pour que cela fonctionne.

Étape 1 : Installez Railway CLI globalement

npm i -g @railway/cli

Étape 2 : Ajoutez un fichier Procfile.

Dans le dossier racine de votre application, créez un nouveau fichier Procfile et ajoutez-y le contenu ci-dessous. Cela servira de configuration ou de commande de démarrage pour le déploiement

web: uvicorn API.main:app --host 0.0.0.0 --port $PORT

Étape 3 : Connectez-vous et initialisez Railway dans le répertoire du projet

railway login
railway init

Étape 4 : Déployez

railway up
Railway Initialization and Deployment

Étape 5 : Ajouter des variables d’environnement

Accédez au tableau de bord de votre projet Railway → ParamètresVariables partagées et ajoutez les variables et leurs valeurs comme indiqué ci-dessous :

BRIGHT_DATA_API_TOKEN
BRIGHT_DATA_SERP_ZONE
BRIGHT_DATA_UNLOCKER_ZONE
OPENAI_API_KEY
LLM_API_KEY
LLM_MODEL
Adding variables to your Railway app

Railway détectera automatiquement les modifications et vous demandera de redéployer sur le tableau de bord. Cliquez sur Déployer et votre application sera mise à jour avec les secrets.
Redeploy Railway app after adding variables

Après le redéploiement, cliquez sur la carte de service et sélectionnez Paramètres. Vous verrez où générer un domaine, car le service n’est pas encore accessible au public. Cliquez sur Générer un domaine pour obtenir votre URL publique.
Generate domain for Railway application

Exécution d’une enquête complète

Exécution locale avec curl

Démarrez le serveur FastAPI :

# Activez votre environnement virtuel
source venv/bin/activate  # Sous Windows : venvScriptsactivate

# Exécutez le serveur
python -m uvicorn api.main:app --reload

Rendez-vous sur http://localhost:8000/docs pour explorer la documentation interactive de l’API.

Effectuer des requêtes API

  • Lancez une recherche :
curl -X POST "http://localhost:8000/investigate" 
  -H "Content-Type: application/json" 
  -d '{
    "vendor_name": "Acme Corp",
    "categories": ["litigation", "fraud"],
    "generate_monitors": true
  }'
  • Cela renvoie un identifiant d’enquête :
{
  "investigation_id": "f6af2e0f-991a-4cb7-949e-2f316e677b5c",
  "status": "started",
  "message": "Investigation started for Acme Corp"
}
  • Vérifier le statut de l’enquête :
curl http://localhost:8000/investigate/f6af2e0f-991a-4cb7-949e-2f316e677b5c

Exécution de l’agent en tant que script

Créez un fichier nommé run_investigation.py à la racine de votre projet :

import sys
sys.path.insert(0, 'src')

from agent import TPRMAgent

def investigate_vendor():
    """Lancer une enquête complète sur le fournisseur."""
    agent = TPRMAgent()
    
    # Lancer l'enquête
    result = agent.investigate(
        vendor_name="Acme Corp",
        categories=["litigation", "financial", "fraud"],
        generate_monitors=True,
    )
    
    # Imprimer le résumé
    print(f"n{'='*60}")
    print(f"Enquête terminée : {result.vendor_name}")
    print(f"{'='*60}")
    print(f"Sources trouvées : {result.total_sources_found}")
    print(f"Sources consultées : {result.total_sources_accessed}")
    print(f"Évaluations des risques : {len(result.risk_assessments)}")
    print(f"Scripts de surveillance : {len(result.monitoring_scripts)}")
    
    # Imprimer les évaluations des risques
    for assessment in result.risk_assessments:
        print(f"n{'─'*60}")
        print(f"[{assessment.category.upper()}] Gravité : {assessment.severity.upper()}")
        print(f"{'─'*60}")
        print(f"Résumé : {assessment.summary}")
        print("nPrincipales conclusions :")
        for finding in assessment.key_findings:
            print(f"  • {constat}")
        print("nActions recommandées :")
        pour action dans assessment.recommended_actions :
            print(f"  → {action}")
    
    # Imprimer les informations du script de surveillance
    pour script dans result.monitoring_scripts :
        print(f"n{'='*60}")
        print(f"Script de surveillance généré")
        print(f"{'='*60}")
        print(f"Chemin d'accès : {script.script_path}")
        print(f"Surveillance de {len(script.urls_monitored)} URL")
        print(f"Fréquence : {script.check_frequency}")
    
    # Imprimer les erreurs, le cas échéant
    if result.errors:
        print(f"n{'='*60}")
        print("Erreurs :")
        for error in result.errors:
            print(f"  ⚠️  {error}")

if __name__ == "__main__":
    investigate_vendor()

Exécutez le script d’investigation sur un nouveau terminal

# Activez votre environnement virtuel
source venv/bin/activate  # Sous Windows : venvScriptsactivate

# Exécutez le script d'investigation
python run_investigation.py

L’agent va :

  1. Rechercher dans Google les médias défavorables à l’aide de l’API SERP
  2. Accéder aux sources à l’aide de Web Unlocker
  3. Analyser le contenu pour déterminer la gravité du risque à l’aide d’OpenAI
  4. Générer un script de surveillance Python à l’aide du SDK OpenHands qui peut être planifié via cron
Terminal running the tprm-agent app

Exécution du script de surveillance généré automatiquement

Une fois l’enquête terminée, vous trouverez un script de surveillance dans le dossier scripts/generated:

cd scripts/generated
python monitor_acme_corp.py

Le script de surveillance utilise l’API Bright Data Web Unlocker pour vérifier toutes les URL surveillées et affichera :
Terminal monitoring the script from tprm-agent app

Vous pouvez désormais configurer une planification cron pour le script afin d’obtenir en permanence les informations correctes et actualisées sur l’entreprise.

Conclusion

Vous disposez désormais d’un cadre complet pour créer un agent TPRM d’entreprise qui automatise les enquêtes sur les médias défavorables aux fournisseurs. Ce système :

L’architecture modulaire facilite l’extension :

  • Ajoutez de nouvelles catégories de risques en mettant à jour le dictionnaire RISK_CATEGORIES
  • Intégrez-le à votre plateforme GRC en étendant la couche API
  • Évoluez vers des milliers de fournisseurs à l’aide de files d’attente de tâches en arrière-plan
  • Ajoutez des recherches dans les registres judiciaires à l’aide de l’amélioration de l’API du navigateur

Étapes suivantes

Pour améliorer encore cet agent, envisagez :

  • Intégrer des sources de données supplémentaires: documents déposés auprès de la SEC, listes de sanctions de l’OFAC, registres d’entreprises
  • Ajouter la persistance de la base de données: stocker l’historique des enquêtes dans PostgreSQL ou MongoDB
  • Mettre en place des notifications webhook: alerter Slack ou Teams lorsque des fournisseurs à haut risque sont détectés
  • Créer un tableau de bord: créer une interface React pour visualiser les scores de risque des fournisseurs
  • Planifier des analyses automatisées: utiliser Celery ou APScheduler pour la surveillance périodique des fournisseurs

Ressources