"""Task D: report generation. Turns accumulated evidence (source documents + detected changes) for a company into the structured CI report (spec section 17). The LLM only ever sees evidence this pipeline collected - it is explicitly instructed not to introduce outside knowledge, and every non-trivial claim must carry a confidence label and evidence references. """ from __future__ import annotations from pydantic import BaseModel, Field from app.analysis.llm.base import LLMProvider from app.prompts.base import build_user_prompt from app.prompts.schemas import ConfidenceLabel, EvidenceRef, Finding SYSTEM_PROMPT = ( "You are a competitive intelligence analyst producing a structured report about a " "company, using ONLY the evidence provided - the company_profile block (discovered once " "at onboarding from the company's real website and search results, not your own " "knowledge), the company_enrichment block (when present - fetched once from a paid " "third-party data provider at onboarding, also real evidence, not your own knowledge), " "stored source documents, and previously detected changes. Never introduce " "facts from outside knowledge, even if you recognize the company - if it isn't in the " "evidence block, it doesn't go in the report. The company_profile and company_enrichment " "fields ARE real evidence and should ground company_overview/market_positioning/" "financial_signals/leadership_changes/products_and_services/competitor_comparison/" "customer_sentiment/etc even when source_documents and detected_changes are sparse or " "empty - do not say 'insufficient evidence' for a field company_profile or " "company_enrichment already answers. Every finding must be " "traceable to the evidence given and must carry an honest confidence label: confirmed " "(the source states it directly), strongly_indicated, likely, possible, unconfirmed, or " "insufficient_evidence. When evidence is thin or missing for a section, say so explicitly " "in that section rather than inventing content. Distinguish clearly between what a source " "states and what you are inferring." ) class InferredProject(BaseModel): project_name: str status: ConfidenceLabel summary: str confidence: float = Field(ge=0.0, le=1.0) evidence: list[EvidenceRef] = Field(default_factory=list) signal_types: list[str] = Field(default_factory=list) alternative_explanations: list[str] = Field(default_factory=list) class SwotAnalysis(BaseModel): strengths: list[str] = Field(default_factory=list) weaknesses: list[str] = Field(default_factory=list) opportunities: list[str] = Field(default_factory=list) threats: list[str] = Field(default_factory=list) class ReportContent(BaseModel): executive_summary: str company_overview: str products_and_services: list[Finding] = Field(default_factory=list) market_positioning: str recent_developments: list[Finding] = Field(default_factory=list) strategic_initiatives: list[Finding] = Field(default_factory=list) key_inferred_projects: list[InferredProject] = Field(default_factory=list) leadership_changes: list[Finding] = Field(default_factory=list) hiring_signals: list[Finding] = Field(default_factory=list) technology_signals: list[Finding] = Field(default_factory=list) patent_signals: list[Finding] = Field(default_factory=list) manufacturing_and_expansion_signals: list[Finding] = Field(default_factory=list) partnerships_and_acquisitions: list[Finding] = Field(default_factory=list) financial_signals: list[Finding] = Field(default_factory=list) regulatory_and_legal_signals: list[Finding] = Field(default_factory=list) customer_sentiment: str competitor_comparison: str swot: SwotAnalysis risks: list[str] = Field(default_factory=list) opportunities: list[str] = Field(default_factory=list) unknowns_and_missing_data: list[str] = Field(default_factory=list) monitoring_recommendations: list[str] = Field(default_factory=list) methodology: str limitations: str async def generate_report( llm: LLMProvider, *, company_name: str, company_aliases: list[str], competitors: list[str], monitoring_focus: str | None, industry: str | None, documents: list[dict], detected_changes: list[dict], sources_failed: list[str], description: str | None = None, official_website: str | None = None, headquarters: str | None = None, country: str | None = None, region: str | None = None, public_identifiers: dict[str, str] | None = None, enrichment: dict | None = None, ) -> ReportContent: evidence = { # Discovered once at onboarding (app/services/discovery_service.py) # from the company's real website + search results - genuine # evidence, not the LLM's own background knowledge, and the only # evidence available before any monitoring run has collected # source_documents/detected_changes. "company_profile": { "name": company_name, "description": description, "official_website": official_website, "aliases": company_aliases, "competitors": competitors, "monitoring_focus": monitoring_focus, "industry": industry, "headquarters": headquarters, "country": country, "region": region, "public_identifiers": public_identifiers or {}, }, # Fetched once, at onboarding, from a paid third-party provider # (NinjaPear) - see app/services/enrichment_service.py. Absent for # any company created before this feature existed, or whose # enrichment never completed successfully - never fabricated. "company_enrichment": enrichment or {}, "source_documents": documents, "detected_changes": detected_changes, "sources_that_failed_to_collect": sources_failed, } user_prompt = build_user_prompt( "Produce a full structured competitive intelligence report from this evidence. " "If a section has no supporting evidence, say so explicitly instead of leaving it " "generic or inventing content.", evidence, ) return await llm.generate_structured(SYSTEM_PROMPT, user_prompt, ReportContent)