borderless / ui /agent /synthesis.py
spagestic's picture
fix empty research findings
fc6d1ba
Raw
History Blame Contribute Delete
16.9 kB
# 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]}"
)