"""Deterministic mock provider - the default (`LLM_PROVIDER=mock`) and what every automated test runs against. Never calls a network or costs money. Rather than a generic reflection-based filler, each of the six analysis tasks gets a purpose-built, deterministic builder that reads the same evidence block a real model would see (see app/prompts/base.py) and produces genuinely useful output from it - real counts, real titles, real severities - never fabricated facts. This is what makes the fixture demo (Phase 9) work end-to-end without a paid API key. """ from __future__ import annotations import re from typing import Any from pydantic import BaseModel from app.prompts.alert_summarization import AlertSummary from app.prompts.base import extract_evidence_block from app.prompts.change_significance import ChangeSignificanceAssessment from app.prompts.company_profile import CompanyProfileExtraction from app.prompts.extraction import ExtractionResult from app.prompts.relevance import RelevanceAssessment from app.prompts.report_generation import ReportContent from app.prompts.synthesis import SynthesisResult def _confidence_label(score: float) -> str: if score >= 0.85: return "confirmed" if score >= 0.65: return "strongly_indicated" if score >= 0.45: return "likely" if score >= 0.25: return "possible" if score > 0: return "unconfirmed" return "insufficient_evidence" def _build_relevance(evidence: dict[str, Any]) -> dict[str, Any]: text = (evidence.get("document_text") or "").lower() focus = (evidence.get("monitoring_focus") or "").lower() focus_words = [w for w in focus.split() if len(w) > 4] matches_focus = any(w in text for w in focus_words) if focus_words else False return { "is_relevant": True, "matches_focus": matches_focus, "topic_categories": [], "source_reliability": 0.7, "reasoning": "Mock provider: keyword-based heuristic (no live LLM configured).", } def _build_extraction(evidence: dict[str, Any]) -> dict[str, Any]: text = (evidence.get("document_text") or "").strip() if not text: return {"signals": []} first_sentence = text.split(".")[0][:200].strip() if not first_sentence: return {"signals": []} return { "signals": [ { "signal_type": "event", "description": first_sentence, "supporting_passage": first_sentence, "date": None, "entities": [], } ] } def _build_synthesis(evidence: dict[str, Any]) -> dict[str, Any]: signals = evidence.get("signals") or [] if len(signals) < 2: return {"conclusions": []} return { "conclusions": [ { "conclusion": ( "Multiple related signals were detected together; the mock provider " "does not attempt fine-grained synthesis - configure a live LLM provider " "for a specific conclusion." ), "evidence_summary": [s.get("description", "") for s in signals[:5]], "source_count": len(signals), "confidence": 0.3, "alternative_explanations": [ "A live LLM provider would assess this more precisely." ], "missing_information": [], } ] } def _build_report(evidence: dict[str, Any]) -> dict[str, Any]: profile = evidence.get("company_profile") or {} company_name = profile.get("name") or "The company" documents = evidence.get("source_documents") or [] changes = evidence.get("detected_changes") or [] failed = evidence.get("sources_that_failed_to_collect") or [] # The discovered profile (real, from onboarding) is genuine evidence even # when no monitoring run has collected source_documents/detected_changes # yet - build company_overview/market_positioning from it honestly # rather than defaulting straight to "insufficient evidence". overview_parts = [] if profile.get("description"): overview_parts.append(profile["description"]) facts = [] if profile.get("industry"): facts.append(f"industry: {profile['industry']}") if profile.get("headquarters"): facts.append(f"headquartered in {profile['headquarters']}") elif profile.get("country") or profile.get("region"): facts.append( f"based in {', '.join(f for f in (profile.get('country'), profile.get('region')) if f)}" ) if profile.get("aliases"): facts.append(f"also known as {', '.join(profile['aliases'])}") if facts: overview_parts.append(f"{company_name} ({'; '.join(facts)}).") company_overview = " ".join(overview_parts) or f"No description on file for {company_name}." if profile.get("competitors"): market_positioning = ( f"{company_name} operates in a space that includes " f"{', '.join(profile['competitors'])} as named competitors, per the discovered " "company profile. No comparative data (pricing, features, market share) is " "available to assess relative positioning." ) else: market_positioning = "Insufficient evidence to assess market positioning." executive_summary = ( f"Mock analysis (LLM_PROVIDER=mock) based on {len(documents)} collected document(s) " f"and {len(changes)} detected change(s) for {company_name}." ) if failed: executive_summary += ( f" {len(failed)} source(s) failed to collect this run and are excluded below." ) recent_developments = [ { "headline": change.get("summary") or "Change detected", "summary": ( f"{(change.get('change_type') or 'change').replace('_', ' ')} detected " f"with {change.get('severity') or 'unknown'} severity." ), "evidence": [ { "detected_change_id": change.get("id"), "description": change.get("summary") or "", } ], "confidence": _confidence_label(change.get("confidence_score") or 0.5), "category": change.get("change_type"), "date": change.get("created_at"), } for change in changes[:10] ] hiring_signals = [ { "headline": doc.get("title") or doc.get("url") or "Job posting", "summary": (doc.get("excerpt") or "")[:280], "evidence": [ { "source_document_id": doc.get("id"), "url": doc.get("url"), "description": "Collected source document", } ], "confidence": "confirmed", "category": "job_posting", "date": doc.get("retrieved_date"), } for doc in documents if doc.get("source_type") == "job_posting" ] unknowns = ( [f"{len(failed)} source(s) failed to collect this run: {', '.join(failed[:5])}"] if failed else [] ) return { "executive_summary": executive_summary, "company_overview": company_overview, "products_and_services": [], "market_positioning": market_positioning, "recent_developments": recent_developments, "strategic_initiatives": [], "key_inferred_projects": [], "leadership_changes": [ d for d in recent_developments if d["category"] == "leadership_change" ], "hiring_signals": hiring_signals, "technology_signals": [], "patent_signals": [], "manufacturing_and_expansion_signals": [], "partnerships_and_acquisitions": [], "financial_signals": [d for d in recent_developments if d["category"] == "filing_new"], "regulatory_and_legal_signals": [], "customer_sentiment": "Insufficient evidence to assess customer sentiment.", "competitor_comparison": "Insufficient evidence to compare against competitors.", "swot": {"strengths": [], "weaknesses": [], "opportunities": [], "threats": []}, "risks": [], "opportunities": [], "unknowns_and_missing_data": unknowns, "monitoring_recommendations": ["Continue monitoring configured sources on schedule."], "methodology": ( f"Generated by the mock LLM provider from {len(documents)} stored source " f"document(s) and {len(changes)} deterministic change-detection result(s). No " "external model was called." ), "limitations": ( "Generated by the deterministic mock provider, not a live LLM. Set " "LLM_PROVIDER=anthropic or LLM_PROVIDER=ollama for narrative synthesis." ), } def _build_change_significance(evidence: dict[str, Any]) -> dict[str, Any]: severity = evidence.get("deterministic_severity") or "low" confidence = evidence.get("deterministic_confidence") or 0.5 is_meaningful = severity in ("critical", "high", "medium") change_type = (evidence.get("change_type") or "change").replace("_", " ") return { "is_real_change": True, "is_meaningful": is_meaningful, "why_it_matters": ( f"Deterministic scoring classified this {change_type} as {severity} severity " f"with {confidence:.0%} confidence." ), "confidence": confidence, "should_notify": is_meaningful, } def _build_alert_summary(evidence: dict[str, Any]) -> dict[str, Any]: company = evidence.get("company_name") or "The company" change_type = (evidence.get("change_type") or "change").replace("_", " ") severity = evidence.get("severity") or "medium" confidence = evidence.get("confidence") or 0.5 return { "title": f"{company}: {change_type} detected"[:100], "summary": evidence.get("change_summary") or f"A {change_type} was detected for {company}.", "why_it_matters": f"Classified as {severity} severity with {confidence:.0%} confidence.", } _HQ_RE = re.compile(r"(?:headquartered|based) in ([A-Z][\w\s,]{2,60}?)(?:[.\n]|$)", re.IGNORECASE) _FORMERLY_RE = re.compile( r"formerly (?:known as|named) ([A-Z][\w&\s]{2,60}?)(?:[.,\n]|$)", re.IGNORECASE ) def _build_company_profile(evidence: dict[str, Any]) -> dict[str, Any]: """Mirrors app/change_detection/extractors.py's philosophy: cheap, deterministic regex heuristics over real evidence text, never a fabricated guess. Most fields (industry/country/region/public identifiers) stay empty since a name-only mock search has no real signal for them - see search/mock.py's docstring for why that's intentional, not a gap.""" homepage_text = evidence.get("homepage_text") or "" search_results = evidence.get("search_results") or [] combined_text = homepage_text + "\n" + "\n".join(r.get("snippet", "") for r in search_results) hq_match = _HQ_RE.search(combined_text) headquarters = hq_match.group(1).strip() if hq_match else None alias_match = _FORMERLY_RE.search(combined_text) aliases = [alias_match.group(1).strip()] if alias_match else [] # First real sentence of the fetched homepage, if any - an honest, # evidence-derived summary rather than a fabricated one. first_sentence = re.split(r"(?<=[.!?])\s", homepage_text.strip(), maxsplit=1)[0].strip() description = first_sentence[:280] if first_sentence and len(first_sentence) > 15 else None return { "description": description, "industry": None, "country": None, "region": None, "headquarters": headquarters, "aliases": aliases, "competitors": [], "public_identifiers": [], } _BUILDERS = { RelevanceAssessment: _build_relevance, ExtractionResult: _build_extraction, SynthesisResult: _build_synthesis, ReportContent: _build_report, ChangeSignificanceAssessment: _build_change_significance, AlertSummary: _build_alert_summary, CompanyProfileExtraction: _build_company_profile, } class MockLLMProvider: provider_name = "mock" async def generate_structured[T: BaseModel]( self, system_prompt: str, user_prompt: str, response_model: type[T] ) -> T: evidence = extract_evidence_block(user_prompt) builder = _BUILDERS.get(response_model) data = builder(evidence) if builder is not None else {} return response_model.model_validate(data) async def generate_text(self, system_prompt: str, user_prompt: str) -> str: evidence = extract_evidence_block(user_prompt) return ( "[mock provider] No live LLM configured. " f"{len(evidence)} evidence field(s) were provided for this request." )