"""Task B: fact and signal extraction. Pulls discrete, evidence-anchored signals (hiring, partnerships, leadership, financial, etc.) out of a single document - the atomic units Task C (synthesis) and Task D (report generation) later combine.""" from __future__ import annotations from pydantic import BaseModel, Field from app.analysis.llm.base import LLMProvider from app.prompts.base import build_user_prompt SYSTEM_PROMPT = ( "You are a competitive intelligence analyst extracting discrete factual signals from a " "single document. Extract only what the text actually states or clearly implies - never " "add outside knowledge. Every signal must include the exact passage that supports it." ) _SIGNAL_TYPES = ( "event, entity, date, location, product, leadership, partnership, hiring, investment, " "manufacturing, technology, financial, regulatory, sentiment" ) class ExtractedSignal(BaseModel): signal_type: str = Field(description=f"One of: {_SIGNAL_TYPES}") description: str supporting_passage: str = Field(description="The exact quote from the document backing this") date: str | None = None entities: list[str] = Field(default_factory=list) class ExtractionResult(BaseModel): signals: list[ExtractedSignal] = Field(default_factory=list) async def extract_signals( llm: LLMProvider, *, document_title: str | None, document_url: str, document_text: str, ) -> ExtractionResult: evidence = { "document_title": document_title, "document_url": document_url, "document_text": document_text[:6000], } user_prompt = build_user_prompt( "Extract every discrete factual signal from this document, each anchored to its " "exact supporting passage.", evidence, ) return await llm.generate_structured(SYSTEM_PROMPT, user_prompt, ExtractionResult)