"""Build company-linked evidence groups from entities and evidence spans. Groups evidence spans by the company they are associated with. A span can belong to multiple groups if it mentions multiple companies. Relations can add additional evidence linkage (e.g., inferred exposure). """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol from services.intelligence_pipeline_v3.sentiment.models import EvidenceGroup class EntityLike(Protocol): """Protocol for entity objects that link to companies and evidence.""" @property def company_id(self) -> str | None: ... @property def evidence_id(self) -> str: ... @dataclass(frozen=True) class EvidenceSpanInput: """Minimal evidence span input for grouping.""" id: str text: str def build_evidence_groups( entities: list[dict[str, str | None]], evidence_spans: dict[str, str], relations: list[dict[str, str | None]] | None = None, ) -> dict[str, EvidenceGroup]: """Build company-linked evidence groups from entity-company associations. Groups evidence spans by the company they relate to, using both direct entity associations and relation-based linkages. A span can appear in multiple groups when it mentions multiple companies. Parameters ---------- entities List of dicts with keys: company_id (str or None), evidence_id (str). Each entity associates an evidence span with a resolved company. Entities without a company_id are skipped. evidence_spans Mapping of evidence_id -> text content for each evidence span. relations Optional list of dicts with keys: company_id (str or None), evidence_id (str or None), relation_type (str or None). Relations link additional evidence to companies (e.g., via directly_affects or inferred_exposure edges). Returns ------- dict[str, EvidenceGroup] Mapping of company_id -> EvidenceGroup containing all evidence associated with that company. """ # Accumulate evidence IDs per company company_evidence: dict[str, list[str]] = {} for entity in entities: company_id = entity.get("company_id") evidence_id = entity.get("evidence_id") if company_id is None or evidence_id is None: continue if company_id not in company_evidence: company_evidence[company_id] = [] # Avoid duplicate evidence IDs per company if evidence_id not in company_evidence[company_id]: company_evidence[company_id].append(evidence_id) # Process relations for additional evidence linkage if relations: for relation in relations: company_id = relation.get("company_id") evidence_id = relation.get("evidence_id") if company_id is None or evidence_id is None: continue if company_id not in company_evidence: company_evidence[company_id] = [] if evidence_id not in company_evidence[company_id]: company_evidence[company_id].append(evidence_id) # Build EvidenceGroup objects groups: dict[str, EvidenceGroup] = {} for company_id, evidence_ids in company_evidence.items(): texts = [] valid_ids = [] for eid in evidence_ids: text = evidence_spans.get(eid) if text is not None: valid_ids.append(eid) texts.append(text) if valid_ids: groups[company_id] = EvidenceGroup( company_id=company_id, evidence_ids=valid_ids, texts=texts, ) return groups