Ingestion de données RFP multi-sources

VérifiéSûr

Ingérez des opportunités RFP depuis plusieurs sources (SAM.gov, eMMA, RFPMart) avec normalisation de schéma et déduplication. Idéal pour ajouter de nouvelles sources, modifier la logique d'ingestion ou déboguer les problèmes de récupération.

Spar Skills Guide Bot
Data & IAIntermédiaire
11/03/2026
Claude Code
#data-ingestion#csv-parsing#api-integration#schema-normalization#rfp

Recommandé pour

Synthèse automatique

Guide d'implémentation pour ingérer des appels d'offres (RFP) depuis plusieurs sources (SAM.gov, eMMA, RFPMart) et les normaliser vers un schéma canonique unique.

Points forts

  • Tableau des sources avec priorités, type d'API et limites de débit, utile pour planifier l'ingestion
  • Définit une interface Opportunity canonique explicite vers laquelle toutes les sources doivent se normaliser
  • Fournit un parseur CSV gérant les champs entre guillemets, avec le format exact des alertes RFPMart sans en-tête
  • Inclut une correspondance préfixe d'ID vers catégorie et une liste des préfixes pertinents pour l'IT

Limites

  • Le contenu fourni est tronqué : l'exemple de requête SAM.gov s'arrête au milieu du code
  • Fortement lié à l'arborescence d'un projet précis (convex/ingestion, components/admin) et peu réutilisable tel quel
  • Le scraping d'eMMA et l'intégration GovTribe sont seulement mentionnés, sans procédure ni détails d'implémentation
Quand l'utiliser

À utiliser lors de l'ajout d'une nouvelle source de RFP, de la modification de la logique d'ingestion ou du débogage d'un import CSV RFPMart.

Quand l'éviter

À éviter si vous cherchez un guide générique d'ingestion de données ou si votre projet n'utilise pas la stack Convex décrite ici.

Analyse de sécurité

Sûr
Score qualité84/100

This is a documentation/reference skill describing an RFP ingestion pipeline: schema definitions, field mappings, Convex action/mutation examples, deduplication, and error handling. Its only network activity is legitimate calls to public procurement APIs using an environment-provided key. The one real security nit is the example placing the API key in a URL query string rather than a header, which is a credential-leakage hygiene issue rather than malicious behavior. No destructive, exfiltrating, or obfuscated operations are present.

Points d'attention
  • •Minor secret-handling anti-pattern in the SAM.gov example: the API key is interpolated into the request URL query string (`api_key=${apiKey}`) while the header-based `X-Api-Key` approach is documented earlier. Query-string keys can leak into proxy/CDN logs and error reports; recommend consistently using the header.
  • •Declared tools grant `Bash(npm:*)` and `Bash(npx:*)`, but the skill content is purely documentation and runs no scripts — no command execution is exercised, though npm/npx can install and run arbitrary packages if an agent improvises.
  • •No prompt-injection or instruction-override attempts found in the skill content; no obfuscated payloads, destructive commands, or outbound data exfiltration to third parties.
  • •The SAM.gov example places the API key in the query string (`api_key=${apiKey}`) rather than in the `X-Api-Key` header it documents as required; query-string secrets can leak via logs, proxies, and referrers.
  • •It includes a web-scraping data source (Maryland eMMA) with only 'respectful crawling' guidance — no rate-limit or terms-of-service handling described.
  • •Raw third-party payloads are persisted wholesale in `rawData`, which may retain sensitive or unbounded data; no size/sanitization limits are mentioned.
Comment cette analyse est faite

Exemples

Add a new source
Add GovTribe as a P2 source in the ingestion pipeline and normalize its response to the canonical Opportunity schema, including deduplication against existing records.
Debug CSV column shift
The RFPMart CSV upload is putting titles in the wrong column when the title contains a comma inside quotes. Fix the parser in convex/ingestion/rfpmartCsv.ts and verify with a sample file.
Filter IT-only imports
Extend the RFPMart CSV import so the 'Only import IT-relevant RFPs' toggle also covers the TELCOM and CSE prefixes, and show skipped counts in the results summary.

name: rfp-ingest description: Ingest RFP opportunities from multiple data sources (SAM.gov, eMMA, RFPMart). Use when adding new data sources, modifying ingestion logic, or debugging data fetching issues. allowed-tools: Read, Grep, Glob, Bash(npm:), Bash(npx:)

RFP Ingestion Skill

Overview

This skill helps implement multi-source RFP data ingestion with canonical schema normalization and deduplication.

Supported Data Sources

| Source | Priority | API Type | Rate Limits | |--------|----------|----------|-------------| | SAM.gov | P1 | REST API | 10 req/sec, 10k/day | | Maryland eMMA | P1 | Web scraping | Respectful crawling | | RFPMart API | Current | REST API | As documented | | RFPMart CSV | Current | Manual upload | N/A | | GovTribe | P2 | REST API (paid) | Per subscription |

CSV Upload (RFPMart Email Alerts)

RFPMart sends periodic email alerts with CSV attachments. These can be manually uploaded through the Admin UI.

CSV Format (No Header Row)

| Column | Index | Content | Example | |--------|-------|---------|---------| | ID | 0 | RFP identifier | SW-82097 | | Country | 1 | Country code | USA | | State | 2 | State name | Idaho | | Title | 3 | Full title with location | SW-82097 - USA (Idaho) - Data Concealment... | | Deadline | 4 | Due date | March 25,2026 | | URL | 5 | RFPMart link | https://www.rfpmart.com/... |

ID Prefix → Category Mapping

const categoryMap: Record<string, string> = {
  SW: "Software Development",
  ITES: "IT Services",
  NET: "Networking",
  TELCOM: "Telecommunications",
  DRA: "Data & Research",
  CSE: "Security Services",
  HR: "Human Resources",
  PM: "Project Management",
  MRB: "Marketing & Branding",
  // ... other prefixes default to "Other"
};

IT-Relevant Prefixes

When filtering for IT-relevant RFPs only, these prefixes are included:

  • SW - Software Development
  • ITES - IT Services
  • NET - Networking
  • TELCOM - Telecommunications
  • DRA - Data & Research
  • CSE - Security Services

Key Files

| File | Purpose | |------|---------| | convex/ingestion/rfpmartCsv.ts | CSV parser and Convex action | | components/admin/CsvUpload.tsx | Drag-and-drop upload UI |

Usage

  1. Navigate to Admin → Data Sources tab
  2. Scroll to RFPMart CSV Upload section
  3. Drop a CSV file or click to browse
  4. Toggle "Only import IT-relevant RFPs" if desired
  5. View results summary (new/updated/skipped/errors)

Implementation Example

// Parsing CSV with quoted fields
function parseCSVLine(line: string): string[] {
  const fields: string[] = [];
  let current = "";
  let inQuotes = false;

  for (let i = 0; i < line.length; i++) {
    const char = line[i];
    if (char === '"') {
      if (inQuotes && line[i + 1] === '"') {
        current += '"';
        i++;
      } else {
        inQuotes = !inQuotes;
      }
    } else if (char === "," && !inQuotes) {
      fields.push(current);
      current = "";
    } else {
      current += char;
    }
  }
  fields.push(current);
  return fields;
}

Canonical Schema

All sources must normalize to this schema:

interface Opportunity {
  externalId: string;         // Source-specific ID
  source: "sam.gov" | "emma" | "rfpmart" | "govtribe";
  title: string;
  description: string;
  summary?: string;
  location: string;
  category: string;
  naicsCode?: string;
  setAside?: string;          // "Small Business", "8(a)", etc.
  postedDate: number;         // Unix timestamp
  expiryDate: number;         // Unix timestamp
  url: string;
  attachments?: Attachment[];
  eligibilityFlags?: string[];
  rawData: Record<string, unknown>;
  ingestedAt: number;
}

SAM.gov Integration

API Endpoint

https://api.sam.gov/opportunities/v2/search

Required Headers

{
  "Accept": "application/json",
  "X-Api-Key": process.env.SAM_GOV_API_KEY
}

Example Query

const params = new URLSearchParams({
  postedFrom: "2024-01-01",
  postedTo: "2024-12-31",
  limit: "100",
  offset: "0",
  ptype: "o",  // Opportunities only
});

Field Mapping

| SAM.gov Field | Canonical Field | |---------------|-----------------| | noticeId | externalId | | title | title | | description | description | | postedDate | postedDate (parse to timestamp) | | responseDeadLine | expiryDate (parse to timestamp) | | placeOfPerformance.state | location | | naicsCode | naicsCode | | setAsideDescription | setAside |

Convex Implementation

Ingestion Action

// convex/ingestion.ts
import { action, internalMutation } from "./_generated/server";
import { v } from "convex/values";
import { internal } from "./_generated/api";

export const ingestFromSam = action({
  args: { daysBack: v.optional(v.number()) },
  handler: async (ctx, args) => {
    const apiKey = process.env.SAM_GOV_API_KEY;
    if (!apiKey) throw new Error("SAM_GOV_API_KEY not configured");

    const fromDate = new Date();
    fromDate.setDate(fromDate.getDate() - (args.daysBack ?? 7));

    const response = await fetch(
      `https://api.sam.gov/opportunities/v2/search?` +
      `api_key=${apiKey}&postedFrom=${fromDate.toISOString().split("T")[0]}&limit=100`,
      { headers: { Accept: "application/json" } }
    );

    if (!response.ok) {
      throw new Error(`SAM.gov API error: ${response.status}`);
    }

    const data = await response.json();
    let ingested = 0;
    let updated = 0;

    for (const opp of data.opportunitiesData ?? []) {
      const result = await ctx.runMutation(internal.rfps.upsert, {
        externalId: opp.noticeId,
        source: "sam.gov",
        title: opp.title ?? "Untitled",
        description: opp.description ?? "",
        location: opp.placeOfPerformance?.state ?? "USA",
        category: opp.naicsCode ?? "Unknown",
        postedDate: new Date(opp.postedDate).getTime(),
        expiryDate: new Date(opp.responseDeadLine).getTime(),
        url: `https://sam.gov/opp/${opp.noticeId}/view`,
        rawData: opp,
      });

      if (result.action === "inserted") ingested++;
      else updated++;
    }

    // Log ingestion
    await ctx.runMutation(internal.ingestion.logIngestion, {
      source: "sam.gov",
      status: "completed",
      recordsProcessed: data.opportunitiesData?.length ?? 0,
      recordsInserted: ingested,
      recordsUpdated: updated,
    });

    return { ingested, updated, source: "sam.gov" };
  },
});

Upsert Mutation

// convex/rfps.ts (internal mutation)
export const upsert = internalMutation({
  args: {
    externalId: v.string(),
    source: v.string(),
    title: v.string(),
    description: v.string(),
    location: v.string(),
    category: v.string(),
    postedDate: v.number(),
    expiryDate: v.number(),
    url: v.string(),
    rawData: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("rfps")
      .withIndex("by_external_id", (q) =>
        q.eq("externalId", args.externalId).eq("source", args.source)
      )
      .first();

    const now = Date.now();

    if (existing) {
      await ctx.db.patch(existing._id, { ...args, updatedAt: now });
      return { id: existing._id, action: "updated" as const };
    }

    const id = await ctx.db.insert("rfps", {
      ...args,
      ingestedAt: now,
      updatedAt: now,
    });
    return { id, action: "inserted" as const };
  },
});

Deduplication Strategy

  1. Exact match: externalId + source combination
  2. Title similarity: Fuzzy match titles within same deadline window
  3. URL canonicalization: Normalize URLs before comparison

Eligibility Pre-Filtering

Detect disqualifiers during ingestion:

const DISQUALIFIER_PATTERNS = [
  { pattern: /u\.?s\.?\s*(citizen|company|organization)\s*only/i, flag: "us-org-only" },
  { pattern: /onshore\s*(only|required)/i, flag: "onshore-required" },
  { pattern: /on-?site\s*(required|mandatory)/i, flag: "onsite-required" },
  { pattern: /security\s*clearance\s*required/i, flag: "clearance-required" },
  { pattern: /small\s*business\s*set[- ]aside/i, flag: "small-business-set-aside" },
];

function detectEligibilityFlags(text: string): string[] {
  return DISQUALIFIER_PATTERNS
    .filter(({ pattern }) => pattern.test(text))
    .map(({ flag }) => flag);
}

Scheduled Ingestion

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

crons.interval(
  "ingest-sam-gov",
  { hours: 6 },
  internal.ingestion.ingestFromSam,
  { daysBack: 3 }
);

export default crons;

Error Handling

| Error Type | Action | |------------|--------| | Rate limit (429) | Exponential backoff, retry after delay | | Auth error (401/403) | Log error, alert admin | | Server error (5xx) | Retry up to 3 times | | Parse error | Log raw data, skip record |

Testing Approach

  1. Mock API responses for unit tests
  2. Use sandbox/test endpoints when available
  3. Validate schema transformation
  4. Test deduplication logic
  5. Verify eligibility flag detection

Du fichier au premier essai

Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.

Consulter la source et ses fichiers annexes

Ce téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.

1. Télécharger le fichier

Télécharger SKILL.md

Depuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.

Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.

mkdir -p '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/multi-source-rfp-data-ingestion-mmlj2nx7/download' --output '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md.part' && mv '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md.part' '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md'

2. Vérifier les prérequis

Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.

Outils déclarés par l’auteur (à vérifier dans votre agent) : Read, Grep, Glob, Bash(npm:*), Bash(npx:*)

Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.

Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.

3. Charger la skill

Après vérification, copiez le dossier complet dans le projet avec l’une des commandes ci-dessous, puis ouvrez une nouvelle session de l’outil. La commande s’arrête si un dossier du même nom existe déjà : comparez les fichiers avant de le remplacer.

Claude Code
test -f '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md' && test -s '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/rfp-ingest' && cp -R '.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7' '.claude/skills/rfp-ingest'

OpenSkills

OpenSkills doit déjà être installé. La procédure locale install/read est contrôlée avec OpenSkills 1.5.0, qui exige Node.js ≥ 20.6.0. Aucun slug du catalogue n’est résolu automatiquement.

Installation officielle d’OpenSkills
openskills install './.skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7'

openskills read 'multi-source-rfp-data-ingestion-mmlj2nx7'

La lecture doit afficher le contenu du fichier installé et son dossier de base. Elle ne lance ni la skill ni ses dépendances.

Pour un agent qui lit AGENTS.md, synchronisez ensuite l’index. Cette commande modifie AGENTS.md : relisez le diff avant de l’utiliser.

openskills sync

Mode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.

4. Faire un premier essai

Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :

Lis .skill-downloads/multi-source-rfp-data-ingestion-mmlj2nx7/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.

Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.

Skills similaires