# ui/globe_details.py from __future__ import annotations import re import time from typing import Any from apis.rest_countries import lookup_country from ui.agent.synthesis import build_country_report_sections _COUNTRY_NAME_TO_ISO2 = { "canada": "CA", "germany": "DE", "australia": "AU", "ireland": "IE", "new zealand": "NZ", "united kingdom": "GB", "uk": "GB", "portugal": "PT", "netherlands": "NL", "singapore": "SG", "united states": "US", "usa": "US", } _INCOMPLETE_SUMMARIES = { "no findings could be produced for this to-do.", "no detailed findings were captured for this country yet.", } _TOOL_ACTIVITY_LABELS = { "search_immigration_info": "Searching immigration sources", "get_country_profile": "Loading country profile", "scrape_web_page": "Reading official page", "crawl_web_site": "Crawling official site", } _URL_PATTERN = re.compile(r"https?://[^\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 is_incomplete_summary(summary: str) -> bool: text = summary.strip().lower() if not text: return True return text in _INCOMPLETE_SUMMARIES 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 iso2_from_country_name(country: str, marker_isos: set[str] | None = None) -> str | None: lowered = country.strip().lower() if lowered in _COUNTRY_NAME_TO_ISO2: code = _COUNTRY_NAME_TO_ISO2[lowered] if marker_isos is None or code in marker_isos: return code candidates = marker_isos or set(_COUNTRY_NAME_TO_ISO2.values()) for iso2 in candidates: info = lookup_country(iso2) if info and str(info.get("name", "")).lower() == lowered: return iso2.upper() return None def iso2_from_todo_title(title: str, marker_isos: set[str] | None = None) -> str | None: country, _ = split_country_title(title) return iso2_from_country_name(country, marker_isos) def _preview_text(text: str, limit: int = 220) -> str: cleaned = re.sub(r"[#>*`_\-[\]()]", " ", text) cleaned = re.sub(r"\s+", " ", cleaned).strip() if len(cleaned) <= limit: return cleaned return f"{cleaned[: limit - 1]}…" def _activity_from_tool(tool_name: str, args: dict[str, Any], message: str) -> str: label = _TOOL_ACTIVITY_LABELS.get(tool_name, f"Running {tool_name}") if tool_name == "search_immigration_info": query = str(args.get("query") or "").strip() if query: return f"{label}: {query[:160]}" if tool_name == "scrape_web_page": url = str(args.get("url") or "").strip() if url: return f"{label}: {url[:120]}" if tool_name == "crawl_web_site": url = str(args.get("url") or "").strip() if url: return f"{label}: {url[:120]}" if message.strip(): return message.strip()[:220] return label def _base_detail(country: str, methods: str) -> dict[str, Any]: label = f"{country} — {methods}" return { "country": country, "title": label, "pathway": methods, "summary": "", "preview": "", "sources": [], "activities": [], "current_activity": "", "sections": {}, "updated_at": int(time.time()), } def _enrich_detail_with_iso2( detail: dict[str, Any], country: str, marker_isos: set[str], ) -> tuple[str | None, dict[str, Any]]: iso2 = iso2_from_country_name(country, marker_isos or None) if iso2: detail["iso2"] = iso2 info = lookup_country(iso2) if info: detail["country"] = str(info.get("name") or detail["country"]) key = iso2 or str(detail["country"]) return iso2, detail if iso2 else detail | {"iso2": None, "key": key} def _merge_detail_into_state( state: dict[str, Any], *, iso2: str | None, country_key: str, detail: dict[str, Any], ) -> dict[str, Any]: country_details = dict(state.get("country_details") or {}) country_details[country_key] = detail markers = [] for marker in state.get("markers", []): marker_copy = dict(marker) if iso2 and marker_copy.get("iso2") == iso2: marker_copy["detail"] = detail markers.append(marker_copy) return { **state, "markers": markers, "country_details": country_details, "details_version": int(state.get("details_version", 0)) + 1, } def build_research_progress_detail( country: str, methods: str, *, tool_name: str, args: dict[str, Any], message: str, existing: dict[str, Any] | None = None, ) -> dict[str, Any]: detail = dict(existing or _base_detail(country, methods)) activity = _activity_from_tool(tool_name, args, message) activities = list(detail.get("activities") or []) if activity and (not activities or activities[-1] != activity): activities.append(activity) activities = activities[-6:] detail.update( { "status": "researching", "current_activity": activity, "activities": activities, "preview": activity, "updated_at": int(time.time()), } ) return detail def build_country_detail(country: str, methods: str, summary: str) -> dict[str, Any]: detail = _base_detail(country, methods) if is_incomplete_summary(summary): detail.update( { "status": "incomplete", "preview": ( "Research could not produce verified findings for this country. " "Check the parallel research panel for tool activity." ), "sections": build_country_report_sections("", country=country, pathway=methods), } ) return detail sections = build_country_report_sections(summary, country=country, pathway=methods) detail.update( { "status": "complete", "summary": summary.strip(), "preview": _preview_text(summary), "sources": _extract_urls(summary), "sections": sections, "current_activity": "Research complete", } ) return detail def attach_research_progress_to_globe( state: dict[str, Any], *, country: str, methods: str, tool_name: str, args: dict[str, Any], message: str, todo_title: str | None = None, ) -> dict[str, Any]: if todo_title and not country: country, methods = split_country_title(todo_title) marker_isos = {str(marker.get("iso2", "")).upper() for marker in state.get("markers", [])} marker_isos.discard("") country_details = dict(state.get("country_details") or {}) iso2 = iso2_from_country_name(country, marker_isos or None) key = iso2 or country existing = country_details.get(key) if existing and existing.get("status") == "complete": return state detail = build_research_progress_detail( country, methods, tool_name=tool_name, args=args, message=message, existing=existing, ) iso2, detail = _enrich_detail_with_iso2(detail, country, marker_isos) return _merge_detail_into_state( state, iso2=iso2, country_key=iso2 or detail["country"], detail=detail, ) def attach_finding_to_globe( state: dict[str, Any], *, country: str, methods: str, summary: str, todo_title: str | None = None, ) -> dict[str, Any]: """Merge researcher findings into globe_state for marker popups.""" if todo_title and not country: country, methods = split_country_title(todo_title) marker_isos = {str(marker.get("iso2", "")).upper() for marker in state.get("markers", [])} marker_isos.discard("") detail = build_country_detail(country, methods, summary) iso2, detail = _enrich_detail_with_iso2(detail, country, marker_isos) return _merge_detail_into_state( state, iso2=iso2, country_key=iso2 or detail["country"], detail=detail, )