""" Citation and sourcing utilities to reduce hallucinations. When presenting grant information, we cite the source data: - Grant ID and title - Specific fields (funding_max, deadline, etc.) - Field source (from grant record or extracted) This helps users verify information and trust the system. """ from typing import Dict, Any, List, Optional from datetime import datetime def cite_grant_fact(grant: Dict[str, Any], field: str, value: Any) -> str: """ Create a citation for a fact about a grant. Args: grant: The grant record dict field: Field name (e.g., "funding_max", "close_date") value: The value to cite Returns: Formatted citation like "£50,000 [Source: Grant ID #2315]" """ grant_id = grant.get("id") or grant.get("competition_id") or "unknown" # Special formatting for currency if field in ("funding_max", "funding_min", "total_pot") and isinstance(value, (int, float)): return f"£{value:,.0f} [Source: Grant ID #{grant_id}]" # Special formatting for dates if field in ("close_date", "open_date", "deadline") and value: return f"{value} [Source: Grant ID #{grant_id}]" # Default formatting if value is None: return "Not specified" return f"{value} [Source: Grant ID #{grant_id}]" def format_grant_with_citations(grant: Dict[str, Any]) -> Dict[str, Any]: """ Format a grant record with citations on key fields. Args: grant: The grant record Returns: Dict with cited versions of key fields """ grant_id = grant.get("id") or grant.get("competition_id") or "unknown" title = grant.get("title", "(untitled)") cited = { "id": grant_id, "title": title, "url": grant.get("url", ""), } # Cite monetary fields if grant.get("funding_max"): cited["funding_max"] = cite_grant_fact(grant, "funding_max", grant["funding_max"]) if grant.get("funding_min"): cited["funding_min"] = cite_grant_fact(grant, "funding_min", grant["funding_min"]) if grant.get("total_pot"): cited["total_pot"] = cite_grant_fact(grant, "total_pot", grant["total_pot"]) # Cite dates if grant.get("close_date"): cited["close_date"] = cite_grant_fact(grant, "close_date", grant["close_date"]) if grant.get("open_date"): cited["open_date"] = cite_grant_fact(grant, "open_date", grant["open_date"]) return cited def build_citation_summary(grant: Dict[str, Any]) -> str: """ Build a markdown summary of grant with citations. Args: grant: The grant record Returns: Markdown formatted summary with citations """ grant_id = grant.get("id") or grant.get("competition_id") or "unknown" title = grant.get("title", "(untitled)") url = grant.get("url", "") lines = [ f"## {title}", f"**Grant ID:** #{grant_id}", "", ] if url: lines.append(f"**Official Link:** {url}") lines.append("") # Funding details with citations if grant.get("funding_max") or grant.get("funding_min"): min_fund = grant.get("funding_min") max_fund = grant.get("funding_max") if min_fund and max_fund: funding_str = f"£{min_fund:,.0f} – £{max_fund:,.0f}" elif max_fund: funding_str = f"up to £{max_fund:,.0f}" elif min_fund: funding_str = f"from £{min_fund:,.0f}" else: funding_str = "See official page" lines.append(f"**Funding per project:** {funding_str} [Source: Grant ID #{grant_id}]") if grant.get("total_pot"): lines.append(f"**Total available:** £{grant['total_pot']:,.0f} [Source: Grant ID #{grant_id}]") lines.append("") # Dates with citations if grant.get("open_date") or grant.get("close_date"): close = grant.get("close_date") or grant.get("deadline") if close: lines.append(f"**Deadline:** {close} [Source: Grant ID #{grant_id}]") if grant.get("open_date"): lines.append(f"**Opens:** {grant['open_date']} [Source: Grant ID #{grant_id}]") lines.append("") # Duration with citation if grant.get("duration_min") or grant.get("duration_max"): min_dur = grant.get("duration_min") max_dur = grant.get("duration_max") if min_dur and max_dur: duration_str = f"{min_dur}–{max_dur} months" elif max_dur: duration_str = f"up to {max_dur} months" elif min_dur: duration_str = f"from {min_dur} months" else: duration_str = "Variable" lines.append(f"**Project duration:** {duration_str} [Source: Grant ID #{grant_id}]") return "\n".join(lines) def validate_fact_availability(grant: Dict[str, Any], fact_type: str) -> bool: """ Check if a fact exists in the grant data before citing it. Args: grant: The grant record fact_type: Type of fact ("funding", "deadline", "duration", "scope", "eligibility") Returns: True if the fact is available to cite Raises assertion/warning if fact is missing """ fact_fields = { "funding": ["funding_max", "funding_min", "total_pot"], "deadline": ["close_date", "deadline"], "duration": ["duration_min", "duration_max"], "scope": ["scope", "scope_raw", "sections"], "eligibility": ["eligibility", "eligibility_raw"], } required_fields = fact_fields.get(fact_type, []) for field in required_fields: if grant.get(field): return True return False