# ui/agent/synthesis.py from __future__ import annotations import json import re from typing import Any, TypedDict class Finding(TypedDict, total=False): todo_id: int todo_title: str todo_country: str todo_methods: str summary: str class TodoItem(TypedDict): id: int country: str methods: str URL_PATTERN = re.compile(r"https?://[^\s\)\]>\"']+") STEP_PATTERN = re.compile( r"(?:^|\n)\s*(?:\d+[\.\)]|[-*])\s+.+(?:\n\s*(?:\d+[\.\)]|[-*])\s+.+)*", re.MULTILINE, ) SECTION_ALIASES = { "eligibility": re.compile(r"(?:^|\n)#+\s*eligibility[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S), "documents": re.compile( r"(?:^|\n)#+\s*(?:required documents|documents)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), "timeline": re.compile( r"(?:^|\n)#+\s*(?:timeline|duration|processing)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), "costs": re.compile( r"(?:^|\n)#+\s*(?:costs?|fees?|budget)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), "risks": re.compile( r"(?:^|\n)#+\s*(?:risks?|tradeoffs?)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), "steps": re.compile( r"(?:^|\n)#+\s*(?:steps?|procedure|process)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), "pathway": re.compile( r"(?:^|\n)#+\s*(?:visa pathway|pathway|route)[^\n]*\n(.+?)(?=\n#+\s|\Z)", re.I | re.S, ), } def _split_country_title(title: str) -> tuple[str, str]: if "—" in title: country, pathway = title.split("—", 1) return country.strip(), pathway.strip() if " - " in title: country, pathway = title.split(" - ", 1) return country.strip(), pathway.strip() return title.strip(), "Skilled migration pathway" def _extract_section(text: str, key: str) -> str | None: pattern = SECTION_ALIASES.get(key) if not pattern: return None match = pattern.search(text) if not match: return None value = match.group(1).strip() return value or None def _extract_steps(text: str) -> str | None: section = _extract_section(text, "steps") if section: return section matches = STEP_PATTERN.findall(text) if matches: return "\n".join(match.strip() for match in matches[:1]) return None def _extract_urls(text: str) -> list[str]: seen: set[str] = set() urls: list[str] = [] for url in URL_PATTERN.findall(text): cleaned = url.rstrip(".,;") if cleaned not in seen: seen.add(cleaned) urls.append(cleaned) return urls def _needs_verification(value: str | None, *, fallback: str) -> str: if value and value.strip(): return value.strip() return f"{fallback} *(Needs verification on official immigration sources.)*" def build_country_report_sections( summary: str, *, country: str, pathway: str, ) -> dict[str, str]: """Structured sections for a single country report.""" pathway_text = _extract_section(summary, "pathway") or pathway timeline = _extract_section(summary, "timeline") costs = _extract_section(summary, "costs") risks = _extract_section(summary, "risks") urls = _extract_urls(summary) duration_parts = [] if timeline: duration_parts.append(f"**Timeline:** {timeline}") if costs: duration_parts.append(f"**Costs:** {costs}") if risks: duration_parts.append(f"**Risks:** {risks}") duration_block = "\n\n".join(duration_parts) if duration_parts else "" sources_block = "\n".join(f"- {url}" for url in urls[:8]) if urls else "" return { "why_recommended": _needs_verification( _extract_section(summary, "eligibility"), fallback=f"Potential fit for skilled migration to {country} via {pathway}.", ), "feasible_methods": _needs_verification( pathway_text, fallback=pathway, ), "procedure": _needs_verification( _extract_steps(summary), fallback=( f"Confirm the official application sequence on the immigration " f"authority website for {country}." ), ), "requirements": _needs_verification( _extract_section(summary, "documents"), fallback=( "Passport, education credentials, employment records, language " f"test results (if required), and proof of funds — verify exact " f"list for {pathway}." ), ), "duration_costs_risks": _needs_verification( duration_block or None, fallback="Typical processing time, fees, and risks not verified in research.", ), "official_sources": _needs_verification( sources_block or None, fallback="Check the country's official immigration authority website.", ), } def build_structured_final_answer( *, profile_summary: str, findings: list[Finding], todos: list[TodoItem] | None = None, preamble: str = "", ) -> str: """Build a deterministic immigration report from parallel research findings.""" ordered_findings = sorted(findings, key=lambda item: item["todo_id"]) todo_by_id = {todo["id"]: todo for todo in (todos or [])} if not ordered_findings and todos: ordered_findings = [ { "todo_id": todo["id"], "todo_title": f"{todo['country']} — {todo['methods']}", "todo_country": todo["country"], "todo_methods": todo["methods"], "summary": "No detailed findings were captured for this country yet.", } for todo in sorted(todos, key=lambda item: item["id"]) ] lines: list[str] = [ "# Migration Research Summary", "", preamble.strip() or ( "Based on the research gathered so far, here is a structured comparison " "of realistic migration options for your profile." ), "", "## Recommended Countries", ] country_entries: list[dict[str, str]] = [] all_urls: list[str] = [] for finding in ordered_findings: todo = todo_by_id.get(finding["todo_id"]) if todo: country = todo["country"] pathway = todo["methods"] title = f"{country} — {pathway}" elif finding.get("todo_country") and finding.get("todo_methods"): country = str(finding["todo_country"]) pathway = str(finding["todo_methods"]) title = f"{country} — {pathway}" else: title = str(finding.get("todo_title") or "") country, pathway = _split_country_title(title) summary = str(finding.get("summary") or "").strip() country_entries.append( { "country": country, "pathway": pathway, "title": title, "summary": summary, } ) lines.append(f"- **{country}** — {pathway}") all_urls.extend(_extract_urls(summary)) if not country_entries: lines.append("- Needs verification — rerun research to populate country shortlist.") lines.extend(["", "## Feasible Migration Pathways"]) for entry in country_entries: pathway_text = _extract_section(entry["summary"], "pathway") or entry["pathway"] lines.extend( [ "", f"### {entry['country']}", _needs_verification(pathway_text, fallback=entry["pathway"]), ] ) eligibility = _extract_section(entry["summary"], "eligibility") if eligibility: lines.extend(["", "**Eligibility notes:**", eligibility]) lines.extend(["", "## Step-By-Step Procedure"]) for entry in country_entries: steps = _extract_steps(entry["summary"]) lines.extend(["", f"### {entry['country']}"]) lines.append( _needs_verification( steps, fallback=( "Confirm the official application sequence on the immigration " f"authority website for {entry['country']}." ), ) ) lines.extend(["", "## Requirements And Documents"]) for entry in country_entries: documents = _extract_section(entry["summary"], "documents") lines.extend(["", f"### {entry['country']}"]) lines.append( _needs_verification( documents, fallback=( "Passport, education credentials, employment records, language " "test results (if required), and proof of funds — verify exact " f"list for {entry['pathway']}." ), ) ) lines.extend(["", "## Duration, Costs, And Risks"]) for entry in country_entries: timeline = _extract_section(entry["summary"], "timeline") costs = _extract_section(entry["summary"], "costs") risks = _extract_section(entry["summary"], "risks") lines.extend(["", f"### {entry['country']}"]) lines.append( "**Timeline:** " + _needs_verification( timeline, fallback="Typical processing time not verified in research.", ) ) lines.append( "**Costs:** " + _needs_verification( costs, fallback="Government fees and proof-of-funds requirements not verified.", ) ) lines.append( "**Risks:** " + _needs_verification( risks, fallback="Policy changes, job-market competition, and credential recognition.", ) ) lines.extend(["", "## Official Sources To Verify"]) unique_urls = list(dict.fromkeys(all_urls)) if unique_urls: for url in unique_urls[:20]: lines.append(f"- {url}") else: lines.append( "- No official URLs were captured in the research notes. " "Check each country's immigration authority website before applying." ) lines.extend( [ "", "## Next Steps", "1. Pick your top 1-2 countries from the shortlist above.", "2. Verify eligibility rules, fees, and document checklists on official sites.", "3. Start language tests or credential assessments if required.", "4. Gather employment and financial evidence within your budget and timeline.", "5. Consult a qualified immigration professional before submitting applications.", "", "## Profile Snapshot", profile_summary or "Profile summary unavailable.", "", "*This is research guidance, not legal advice. Rules change — verify everything " "on official government immigration websites.*", ] ) return "\n".join(lines) def findings_from_ui_messages(ui_messages: list[Any]) -> list[Finding]: """Best-effort extraction of country findings from legacy ChatMessage cards.""" findings: list[Finding] = [] todo_id = 1 for message in ui_messages: metadata = getattr(message, "metadata", None) or {} title = str(metadata.get("title") or "") if not title.startswith("Findings ·"): continue todo_title = title.removeprefix("Findings ·").strip() content = str(getattr(message, "content", "") or "").strip() findings.append( { "todo_id": todo_id, "todo_title": todo_title or f"Research task {todo_id}", "summary": content or "No findings captured.", } ) todo_id += 1 return findings def _parse_tool_payload(content: str) -> dict[str, Any]: try: parsed = json.loads(content) except json.JSONDecodeError: return {"text": content} return parsed if isinstance(parsed, dict) else {"text": content} def synthesize_finding_from_tool_messages( todo: TodoItem, tool_messages: list[Any], ) -> str: """Build a markdown finding from tool results when the LLM summary is empty.""" country = todo["country"] methods = todo["methods"] search_snippets: list[str] = [] scraped_snippets: list[str] = [] profile_notes: list[str] = [] urls: list[str] = [] for message in tool_messages: content = getattr(message, "content", message) payload = _parse_tool_payload(str(content or "")) if payload.get("error") and len(payload) <= 3: continue for result in payload.get("results") or []: if not isinstance(result, dict): continue url = str(result.get("url") or "") if url: urls.append(url) title = str(result.get("title") or url or "Search result") highlights = result.get("highlights") or [] summary = str(result.get("summary") or "") snippet = "\n".join(str(item) for item in highlights[:2]) if not snippet: snippet = summary if snippet: search_snippets.append(f"- **{title}**: {snippet[:500]}") markdown = str(payload.get("markdown") or "") if markdown: title = str(payload.get("title") or payload.get("url") or "Official page") url = str(payload.get("source_url") or payload.get("url") or "") if url: urls.append(url) scraped_snippets.append(f"### {title}\n{markdown[:1500]}") for page in payload.get("pages") or []: if not isinstance(page, dict): continue page_markdown = str(page.get("markdown") or "") if not page_markdown: continue title = str(page.get("title") or page.get("url") or "Page") url = str(page.get("source_url") or page.get("url") or "") if url: urls.append(url) scraped_snippets.append(f"### {title}\n{page_markdown[:800]}") for profile in payload.get("countries") or []: if not isinstance(profile, dict): continue name = str(profile.get("name") or country) domains = profile.get("official_immigration_domains") or [] domain_text = ", ".join(str(domain) for domain in domains[:5]) if domain_text: profile_notes.append(f"{name}: official domains include {domain_text}") if not search_snippets and not scraped_snippets and not profile_notes: return "" raw_parts = [ f"# Research Findings: {country} — {methods}", "", "*Compiled from tool results. Verify all details on official sources.*", "", "## Visa Pathway", methods, "", ] if search_snippets: raw_parts.extend(["## Search Results", "\n".join(search_snippets[:8]), ""]) if scraped_snippets: raw_parts.extend( ["## Official Page Excerpts", "\n\n".join(scraped_snippets[:3]), ""] ) if profile_notes: raw_parts.extend(["## Country Context", "\n".join(profile_notes), ""]) unique_urls = list(dict.fromkeys(url for url in urls if url)) if unique_urls: raw_parts.extend( ["## Official Sources", "\n".join(f"- {url}" for url in unique_urls[:10])] ) raw_text = "\n".join(raw_parts) sections = build_country_report_sections( raw_text, country=country, pathway=methods, ) return ( f"# {country} — {methods}\n\n" "*Automated fallback report from research tool outputs. " "Verify all details on official immigration websites.*\n\n" f"## Eligibility\n{sections['why_recommended']}\n\n" f"## Visa Pathway\n{sections['feasible_methods']}\n\n" f"## Required Documents\n{sections['requirements']}\n\n" f"## Steps\n{sections['procedure']}\n\n" f"## Timeline, Costs, and Risks\n{sections['duration_costs_risks']}\n\n" f"## Official Sources\n{sections['official_sources']}\n\n" f"## Raw Research Notes\n{raw_text[:2000]}" )