Comment j'ai construit RFP Search, un agrégateur de RFP optimisé par l'IA
Si vous avez déjà essayé de trouver des appels d'offres (RFP) gouvernementaux pour des services numériques, vous connaissez la douleur. Les opportunités sont dispersées sur SAM.gov, les portails d'approvisionnement des États, Grants.gov, l'ONU, l'UE et une douzaine d'autres endroits. Chaque site a sa propre interface de recherche, son propre format et ses propres particularités. Les vérifier tous manuellement est fastidieux, alors j'ai construit RFP Search pour le faire à ma place.
Ce que cela fait
RFP Search récupère 11 sources d'approvisionnement gouvernementales et à but non lucratif chaque nuit, traite chaque opportunité via un modèle d'IA pour en extraire des données structurées, et sert le tout via une interface de recherche unique avec des filtres et une carte interactive.
Les sources comprennent SAM.gov, Grants.gov, le Texas ESBD, Cal eProcure de Californie, le MFMP de Floride, le portail TED de l'UE, le Marché Mondial de l'ONU, NYC Open Data, le Federal Register, USAspending, et Brave Search pour une couverture plus large. Chaque matin, de nouveaux résultats vous attendent.
L'architecture
L'ensemble fonctionne sur Cloudflare. Trois Workers, une base de données D1, et zéro serveur traditionnel. Je l'ai structuré comme un monorepo avec Turborepo et les espaces de travail npm.
Les trois Workers ont chacun une tâche claire.
- rfp-web est le frontend, construit avec React Router v7 (le successeur de Remix). Il gère l'interface utilisateur de recherche, les filtres et une carte Leaflet interactive avec regroupement de marqueurs.
- rfp-api est une API REST Hono qui gère les requêtes de recherche, la pagination, les statistiques et les données de géolocalisation pour la carte.
- rfp-scraper est un Worker planifié qui exécute des tâches cron nocturnes pour récupérer et traiter les RFP de toutes les sources.
Le Worker web communique avec le Worker API via les liaisons de service Cloudflare. Pas de HTTP, pas de CORS, pas de point de terminaison d'API public. La liaison appelle l'API directement au sein du réseau de Cloudflare, ce qui est à la fois plus rapide et plus sécurisé.
Le scraper
C'est la partie la plus intéressante. Chaque source est un plugin qui implémente une interface simple. Vous voulez ajouter une nouvelle source ? Créez un fichier, implémentez l'interface, enregistrez-la et ajoutez une ligne dans la base de données. C'est tout.
Le scraper s'exécute en trois lots décalés (9h00, 9h15 et 9h30 UTC) pour rester dans les limites de temps CPU des Workers. Chaque lot traite un sous-ensemble de sources.
Lot 1 (9h00 UTC) SAM.gov, TED UE, USAspending
Lot 2 (9h15 UTC) Texas ESBD, Cal eProcure, Brave Search, Grants.gov, Federal Register
Lot 3 (9h30 UTC) Florida MFMP, UNGM, NYC Open DataChaque plugin de source sait comment récupérer et analyser son propre format de données. Certains appellent des API REST, d'autres font du scraping HTML, d'autres utilisent des flux RSS. L'architecture des plugins maintient cette complexité contenue. Si une source modifie son format, je n'ai besoin de mettre à jour qu'un seul fichier.
Le scraper suit également les échecs. Si une source échoue trois fois de suite, elle est automatiquement désactivée afin de ne pas gaspiller de cycles ou de polluer les journaux.
Extraction par IA
Les listes brutes de RFP sont désordonnées. Certaines ont des descriptions détaillées, d'autres n'ont qu'un titre et un lien. J'utilise Cloudflare Workers AI avec Llama 3.1 70B Instruct pour normaliser le tout dans une structure cohérente.
Chaque RFP reçoit un seul appel d'IA qui extrait des champs structurés (date limite, budget, localisation, coordonnées), des métadonnées de prise de décision (certifications requises, type de contrat, facilité de travail à distance, taille d'équipe estimée, pile technologique), des catégories (développement web, CMS, cloud, IA, migration, et plus encore), et un résumé de 2 à 3 phrases.
Un appel par RFP, une invite qui fait tout. Cela maintient les coûts de Workers AI bas tout en obtenant une bonne qualité d'extraction. Le modèle est étonnamment capable d'extraire des données structurées à partir de textes d'approvisionnement gouvernementaux non structurés.
La base de données
Cloudflare D1 (SQLite géré en périphérie) stocke tout. La table principale rfps contient plus de 30 champs couvrant les données de base, les champs extraits par l'IA et les métadonnées de prise de décision.
Pour la recherche, j'utilise FTS5 (Full-Text Search 5) de SQLite avec des déclencheurs qui synchronisent automatiquement l'index de recherche chaque fois qu'une ligne est insérée, mise à jour ou supprimée. Pas de réindexation manuelle, pas de service de recherche séparé. Ça fonctionne, tout simplement.
CREATE VIRTUAL TABLE rfps_fts USING fts5(
title, description, ai_summary, categories,
content=rfps, content_rowid=id
);La déduplication utilise une contrainte unique sur (source_name, external_id), donc si le même RFP apparaît dans plusieurs exécutions de scraping, il est mis à jour au lieu d'être dupliqué.
Le statut du RFP (ouvert, expiration prochaine, fermé) est calculé au moment de la requête à partir du champ de date limite plutôt que stocké. Cela signifie que le statut est toujours précis sans nécessiter de tâche d'arrière-plan pour mettre à jour les enregistrements obsolètes.
Le frontend
L'interface utilisateur est construite avec React Router v7 sur Cloudflare Workers, stylisée avec Tailwind CSS v4. Elle dispose d'une barre de recherche avec recherche plein texte sur les titres, descriptions, résumés et catégories. Des filtres déroulants vous permettent d'affiner par type d'organisation, catégorie, statut de la date limite et valeur estimée.
La carte interactive utilise Leaflet avec regroupement de marqueurs. Chaque RFP avec des données de localisation géocodées apparaît comme une épingle. La géocodification elle-même se produit dans le scraper en utilisant Nominatim (le service de géocodage d'OpenStreetMap) pour convertir les chaînes de localisation en paires latitude/longitude.
Une barre de statistiques en haut affiche le nombre de RFP actuellement ouverts et de sources actives, vous permettant de voir en un coup d'œil la fraîcheur des données.
Ce que je ferais différemment
Si je devais recommencer, j'ajouterais probablement des alertes par e-mail dès le premier jour. Actuellement, vous devez visiter le site pour vérifier les nouvelles opportunités. Un simple résumé quotidien pour les recherches enregistrées le rendrait beaucoup plus utile.
J'investirais également davantage dans les tests de fiabilité des sources. Les sites web gouvernementaux changent sans préavis, et leur scraping est intrinsèquement fragile. Une meilleure surveillance et des alertes automatiques lorsqu'une source commence à renvoyer des données inattendues feraient gagner du temps de débogage.
Essayez-le
La plateforme est en ligne à l'adresse rfp.davidloor.com. Elle est axée sur les opportunités dans les domaines des services numériques, du développement web, des CMS, du cloud et de l'IA, mais l'architecture pourrait prendre en charge n'importe quelle catégorie d'appels d'offres.