Passer au contenu principal

Documentation Index

Fetch the complete documentation index at: https://docs.lighton.ai/llms.txt

Use this file to discover all available pages before exploring further.

Derniere mise a jour : Avril 2026 — L’API Paradigm evolue rapidement. Consultez toujours la derniere reference API et privilegiez les cookbooks les plus recents.

Presentation

La boite de reception d’un associe VC est pleine de teasers d’investissement — pitch decks, emails transferes, memos de data-room. Avant de fixer le moindre rendez-vous, quelqu’un doit verifier l’opportunite au regard des criteres d’investissement du fonds : secteur, taille de ticket, calendrier. Ce cookbook construit un pipeline qui charge les documents dans Paradigm, extrait une analyse structuree en un seul appel document_search, evalue une grille de neuf criteres en code, et redige une reponse email dont le ton correspond au resultat du filtrage. Le pattern s’applique a tout workflow de triage base sur une grille qui combine une extraction LLM et un scoring deterministe : filtrage de reponses a appel d’offres, triage de candidatures a subvention, filtrage de RFP, classement de pipeline M&A.
Cet exemple est inspire d’un workflow de production pour un fonds d’investissement oriente GCC. Les neuf criteres refletent la politique reelle du fonds — vous pouvez les remplacer entierement en editant un seul module Python.

Demo

Decouvrez le pipeline analysant un pitch deck et un email transfere, les evaluant contre neuf criteres, et redigeant une reponse email au ton adapte au resultat :

Fonctionnement

  1. L’utilisateur charge un ou plusieurs documents d’opportunite (pitch deck, email transfere, memo de data-room).
  2. Chaque document est charge dans Paradigm et interroge jusqu’a ce qu’il atteigne le statut embedded.
  3. Un unique appel document_search agentique extrait une analyse structuree et libellee de l’opportunite — societe, geographie, financiers, conditions de deal, secteur, TRI, syndicat.
  4. Neuf evaluateurs de criteres deterministes s’executent en code, sans appel API supplementaire sur l’analyse extraite. Chacun retourne MET ou NOT MET avec une courte justification.
  5. Les criteres sont agreges en l’un des quatre paliers de recommandation (RECOMMEND, CONDITIONAL RECOMMEND, WEAK RECOMMEND, DO NOT RECOMMEND).
  6. Un unique appel chat/completions redige une reponse email dont le ton s’adapte au palier — demande de rendez-vous pour un recommend, relance avec questions specifiques pour un conditional, refus diplomatique pour un no.
Pipeline de filtrage d'opportunites VC — diagramme d'architecture montrant le chargement des documents, l'unique extraction d'analyse structuree, les neuf evaluateurs de criteres en code, et la redaction d'une reponse email au ton adapte

Prerequis

  • Une cle API Paradigm (obtenir une cle)
  • Python 3.10+
  • Un ou plusieurs documents d’opportunite (PDF et DOCX d’exemple fournis dans le depot GitHub)

Endpoints API utilises

EndpointRole dans ce pipeline
GET /api/v3/workspacesDecouvrir un workspace cible pour le chargement
POST /api/v3/filesCharger le pitch deck / email / memo (PDF ou DOCX)
GET /api/v3/files/{id}Attendre que chaque document termine son embedding
POST /api/v3/threads/turnsUnique extraction d’analyse structuree via document_search
POST /api/v2/chat/completionsRedaction de la reponse email au ton adapte

Implementation etape par etape

Etape 1 : Choisir un workspace et charger

L’endpoint /api/v3/files de Paradigm charge dans un workspace specifique. Plutot que d’en coder un en dur, on decouvre un defaut raisonnable au demarrage (personal → private → company → premier disponible) et on le met en cache sur le client. Les chargements sont asynchrones : chaque fichier passe par pending → parsing → embedded, on interroge donc GET /api/v3/files/{id} jusqu’a ce qu’il soit pret.
def upload_documents(self, file_paths: list[str]) -> list[int]:
    """Upload files, wait for embedding, return the list of file IDs."""
    workspace_id = self._discover_workspace_id()
    file_ids = [self._upload_one(p, workspace_id) for p in file_paths]
    self._wait_for_embedding(file_ids)
    return file_ids

def _wait_for_embedding(self, file_ids: list[int]) -> None:
    deadline = time.time() + self.POLL_TIMEOUT  # 180s default
    pending = set(file_ids)
    while pending and time.time() < deadline:
        for fid in list(pending):
            status = self._get_file_status(fid)
            if status == "embedded":
                pending.discard(fid)
            elif status == "failed":
                raise RuntimeError(f"File {fid} failed to process on Paradigm.")
        if pending:
            time.sleep(self.POLL_INTERVAL)  # 3s
    if pending:
        raise TimeoutError(f"Files did not embed within {self.POLL_TIMEOUT}s")
Pour une petite demo (1 a 3 fichiers) les defauts suffisent. Pour traiter des dizaines de documents en parallele, augmentez POLL_TIMEOUT et baissez POLL_INTERVAL, ou passez a un pattern de job en arriere-plan.

Etape 2 : Tout extraire en une seule requete structuree

Au lieu de faire un appel par critere, on redige une unique “prompt d’extraction” qui demande a Paradigm de produire une analyse libellee par section couvrant toutes les informations dont on a besoin. La prompt demande explicitement au modele d’ecrire “not disclosed” quand une section manque — ainsi, on peut detecter les trous de facon deterministe en aval.
EXTRACTION_QUERY = """You are an expert venture-capital analyst. Provide a comprehensive,
structured analysis of this investment opportunity. Extract the following information
and name each section explicitly in your answer:

1. Target company name and legal entity structure.
2. Business model and core products / services.
3. Geographic presence and expansion plans, explicitly noting any GCC activity
   or joint-venture structures.
4. Financial position: current revenue and EBITDA, runway to profitability,
   funding requirements, dividend policy if mentioned.
5. Deal terms: proposed ticket size (USD), management fees, process timeline
   in weeks, round structure (primary vs secondary).
6. Sector and sub-sector classification.
7. Return projections, including IRR if disclosed.
8. Syndicate: is there a lead investor, and who are the co-investors.
9. Any mention of a group / strategic co-investment structure.

Be specific with numbers. If information is not disclosed in the document,
state "not disclosed" explicitly rather than omitting the section."""

Etape 3 : Forcer la recherche agentique et elargir la recuperation

L’outil document_search de l’endpoint V3 threads effectue la recuperation reelle. Deux drapeaux comptent ici. Premierement, force_tool="document_search" empeche l’agent de repondre depuis sa connaissance generale — il doit utiliser les fichiers charges. Deuxiemement, on remonte top_k et top_n bien au-dessus des defauts pour qu’une seule requete puisse tirer suffisamment de contexte pour repondre aux neuf sections d’un coup.
SEARCH_TOP_K = 40   # chunks retrieved by embedding similarity
SEARCH_TOP_N = 20   # chunks kept after re-ranking

def document_search(self, query: str, file_ids: list[int]) -> str:
    payload = {
        "query": query,
        "force_tool": "document_search",
        "file_ids": file_ids,
        "tool_parameters": {
            "document_search": {
                "top_k": self.SEARCH_TOP_K,
                "top_n": self.SEARCH_TOP_N,
            }
        },
    }
    resp = requests.post(
        f"{self.base_url}/api/v3/threads/turns",
        headers=self.headers, json=payload, timeout=240,
    )
    resp.raise_for_status()
    return self._extract_assistant_text(resp.json())
/api/v3/threads/turns peut retourner 202 Accepted pour des requetes longues. Cette demo reste synchrone — elle leve une exception plutot que de poller — ce qui convient pour des documents d’opportunite courts. Pour des memos de data-room de 50 pages, implementez une boucle de polling sur 202.

Etape 4 : Definir la grille comme de la donnee

Chaque critere est une entree d’un unique module — libelle, fonction d’evaluation, constantes de seuils en haut. Garder la grille au sommet de src/pipeline.py permet a un operateur du fonds de l’auditer ou de la modifier sans toucher a la plomberie API.
# Size thresholds (USD millions)
SIZE_MIN = 5.0
SIZE_STRONG = 8.0

# Return threshold (percent)
IRR_MIN = 15.0

# Timeline thresholds (weeks)
TIMELINE_MIN = 8
TIMELINE_MIN_COINVEST = 3

# Sector targeting
TARGET_SECTORS = [
    "healthcare", "healthtech", "education", "edtech",
    "data economy", "saas", "energy transition", "cleantech", "industrials",
]
EXCLUDED_SECTORS = ["consumer", "traditional infrastructure"]

Etape 5 : Rediger des evaluateurs deterministes

Chaque critere est une petite fonction Python qui lit l’analyse extraite et retourne MET ou NOT MET avec une explication. Aucun appel LLM supplementaire — cela garde la grille peu couteuse, rapide et testable.
def _check_investment_size(analysis: str) -> CriterionResult:
    size = _ticket_size_usd_m(analysis)  # regex for "$Xm" near ticket/raise keywords
    if size >= SIZE_STRONG:
        return CriterionResult("MET", f"Ticket size ${size}m meets strong-preference threshold (>=${SIZE_STRONG}m).")
    if size >= SIZE_MIN:
        return CriterionResult("MET", f"Ticket size ${size}m meets minimum threshold (>=${SIZE_MIN}m).")
    if size > 0:
        return CriterionResult("NOT MET", f"Ticket size ${size}m below minimum threshold (${SIZE_MIN}m).")
    return CriterionResult("NOT MET", "Ticket size not disclosed.")


def _check_return_threshold(analysis: str) -> CriterionResult:
    irr = _first_irr_pct(analysis)
    low_risk = _has_any(analysis, ["low risk", "low-risk"])
    if irr >= IRR_MIN:
        return CriterionResult("MET", f"Projected IRR of {irr}% meets {IRR_MIN}% threshold.")
    if irr > 0 and low_risk:
        return CriterionResult("MET", f"Projected IRR of {irr}% below {IRR_MIN}% but justified as low-risk.")
    if irr > 0:
        return CriterionResult("NOT MET", f"Projected IRR of {irr}% below {IRR_MIN}% with no low-risk justification.")
    return CriterionResult("NOT MET", "Return projections / IRR not disclosed in the document.")
Les evaluateurs traitent deliberement “not disclosed” comme NOT MET pour la plupart des criteres — c’est un outil de filtrage, et l’information manquante est en elle-meme un signal. Changez ce comportement critere par critere si votre politique est plus souple.

Etape 6 : Agreger vers un palier de recommandation

Comptez les criteres respectes, puis choisissez l’un des quatre paliers. Les seuils (7 et 5 sur 9) refletent la politique reelle du client — ajustez pour votre propre organisation.
if met == total:
    recommendation = "RECOMMEND for further due diligence"
elif met >= 7:
    recommendation = "CONDITIONAL RECOMMEND — address the gaps before proceeding"
elif met >= 5:
    recommendation = "WEAK RECOMMEND — significant gaps, requires committee discussion"
else:
    recommendation = "DO NOT RECOMMEND — does not meet enough criteria"

Etape 7 : Rediger une reponse email au ton adapte

Le dernier appel est un unique Chat Completion. Le prompt systeme est fixe (“5 a 8 phrases, pas d’emojis, pas de marketing”), et le prompt utilisateur branche sur le palier de recommandation — un brief positif, mixte, ou de refus diplomatique. Cette separation garde la voix constante tout en laissant le contenu s’adapter au resultat.
SYSTEM_PROMPT = """You are an associate at a venture-capital firm preparing a concise
reply email to a banker who forwarded an investment opportunity. Write in
professional but warm English. No emojis. No marketing language. Keep the
email to 5-8 short sentences. Return only the email body — no subject line,
no commentary before or after."""

def _reply_prompt(company, recommendation, met, total, failed_criteria):
    if "DO NOT RECOMMEND" in recommendation:
        brief = ("Draft a diplomatic decline that thanks the counterparty, "
                 "acknowledges the merit of the business, does not itemise every "
                 "reason for declining, and leaves the door open for future opportunities.")
    elif "CONDITIONAL" in recommendation or "WEAK" in recommendation:
        failed_text = ", ".join(failed_criteria) or "a few gaps"
        brief = (f"The following criteria were not met: {failed_text}. "
                 "Draft a reply that thanks the counterparty, expresses interest, asks "
                 "for a follow-up call to discuss the specific open points, and proposes "
                 "a couple of dates next week.")
    else:
        brief = ("Draft a positive reply that expresses strong interest and asks for "
                 "a meeting with the founders plus access to the data room.")
    return f"Company under review: {company}.\n{brief}"

Etape 8 : Assembler le rapport

Le rapport final regroupe l’analyse extraite, les resultats critere par critere, la recommandation et l’email redige dans un unique payload JSON — pret pour une etape CI, une notification Slack ou une ecriture dans un CRM.
{
  "company": "MedSync Health FZ-LLC",
  "recommendation": "CONDITIONAL RECOMMEND — address the gaps before proceeding",
  "summary": { "met": 8, "total": 9, "failed_criteria": ["Return Threshold"] },
  "criteria": [
    { "key": "geography_structure", "label": "Geography / Structure",
      "status": "MET", "explanation": "GCC joint-venture / expansion structure identified." },
    { "key": "return_threshold", "label": "Return Threshold",
      "status": "NOT MET", "explanation": "Return projections / IRR not disclosed in the document." }
  ],
  "reply_email": "Dear Marcus, thank you for sharing the MedSync Health opportunity ...",
  "analysis": "1. Target company name and legal entity structure.\n   - Company Name: MedSync Health FZ-LLC ..."
}

Code complet

Code source complet

Clonez le depot pour executer le pipeline complet avec les documents d’exemple.

Reference API

Documentation complete de l’API Paradigm.

Personnalisation

ParametreDescriptionValeur par defautA ajuster si…
SIZE_MIN, SIZE_STRONGSeuils de taille de ticket (USD millions)5.0, 8.0Votre fonds ecrit des cheques de taille differente
IRR_MINTRI projete minimum15.0Vous avez un objectif de rendement different
TIMELINE_MIN, TIMELINE_MIN_COINVESTSemaines minimales jusqu’a signing8, 3Votre cycle de due diligence est plus long ou plus court
TARGET_SECTORS, EXCLUDED_SECTORSCiblage sectorielSante, edtech, SaaS, cleantech, industrielsVotre these vit dans d’autres secteurs
EXTRACTION_QUERY (dans src/pipeline.py)Prompt structure9 sections libelleesVous voulez extraire une grille differente
SEARCH_TOP_K, SEARCH_TOP_N (dans src/paradigm_client.py)Profondeur de recuperation40, 20Les documents sont tres longs (augmenter) ou tres courts (reduire)
SYSTEM_PROMPT (dans src/report.py)Voix de la reponse emailAnglais professionnel et chaleureux, 5 a 8 phrasesVotre societe a un ton distinctif
Seuils des paliers (dans run_screening)Combien de criteres pour chaque palier9 / 7 / 5 / en-dessousVotre appetit pour le risque differe

Ajouter votre propre critere

Chaque critere est une petite fonction plus une entree dans les maps d’ordre et de libelles. Etapes pour en ajouter un :
  1. Ajoutez la cle dans CRITERIA_ORDER et un libelle dans CRITERION_LABELS.
  2. Redigez une fonction _check_<nom> qui retourne un CriterionResult.
  3. Enregistrez-la dans _EVALUATORS.
  4. (Optionnel) Ajoutez dans EXTRACTION_QUERY ce dont votre nouveau check a besoin, pour que l’analyse l’inclue.
def _check_founder_background(analysis: str) -> CriterionResult:
    lc = analysis.lower()
    if _has_any(analysis, ["ex-founder", "second-time founder", "prior exit"]):
        return CriterionResult("MET", "Founder has prior entrepreneurial experience.")
    return CriterionResult("NOT MET", "No prior founder experience mentioned.")

Bonnes pratiques

  1. Extraire une fois, evaluer plusieurs fois — une unique grande requete d’extraction coute moins cher et est plus coherente qu’un appel par critere. Demandez tout au LLM d’emblee, puis appliquez la logique de la grille en code, la ou elle est deterministe et testable.
  2. Demandez au modele de libeller les trous explicitement — la phrase “state ‘not disclosed’ explicitly rather than omitting the section” dans la prompt d’extraction est cruciale. Elle transforme l’information manquante en signal detectable plutot qu’en passage silencieux.
  3. Gardez les evaluateurs deterministes — regex et keyword matching sur une analyse structuree extraite par LLM donne des decisions auditables. Si une opportunite est rejetee, le nom de l’evaluateur et son explication vous disent exactement pourquoi.
  4. Branchez le prompt systeme de l’email de reponse sur le resultat — gardez la voix constante, changez le contenu. Trois branches concises (positive / mixte / refus) produisent des emails toujours professionnels sans templating fragile.
  5. Gardez les seuils et les listes sectorielles en haut du module pipeline — votre politique va evoluer ; vos operateurs ne devraient pas avoir a lire du Python pour ajuster un chiffre.
  6. Remontez top_k / top_n pour les requetes d’extraction riches — les defauts sont cales pour des Q&A courts. Quand vous demandez neuf sections d’un coup, donnez au retriever assez de contexte pour tout trouver.