| """Exa API client for immigration-focused web search."""
|
|
|
| from __future__ import annotations
|
|
|
| import os
|
| from functools import lru_cache
|
| from typing import Any
|
|
|
| from exa_py import Exa
|
|
|
| from apis.official_sources import classify_source, infer_official_domains
|
|
|
| HIGHLIGHT_CHAR_LIMIT = 2000
|
| SUMMARY_CHAR_LIMIT = 500
|
| DEFAULT_NUM_RESULTS = 8
|
| MAX_NUM_RESULTS = 15
|
|
|
|
|
| def _truncate(text: str | None, limit: int) -> str:
|
| if not text:
|
| return ""
|
| if len(text) <= limit:
|
| return text
|
| return text[:limit] + "\n… (truncated)"
|
|
|
|
|
| @lru_cache(maxsize=1)
|
| def _client() -> Exa:
|
| api_key = os.environ.get("EXA_API_KEY")
|
| if not api_key:
|
| raise RuntimeError("EXA_API_KEY is not set")
|
| return Exa(api_key=api_key)
|
|
|
|
|
| def _result_dict(result: Any) -> dict[str, Any]:
|
| highlights = result.highlights or []
|
| trimmed_highlights = [
|
| _truncate(highlight, HIGHLIGHT_CHAR_LIMIT) for highlight in highlights
|
| ]
|
| summary = getattr(result, "summary", None)
|
| source_quality = classify_source(result.url or "")
|
| return {
|
| "title": result.title or "",
|
| "url": result.url or "",
|
| "published_date": result.published_date,
|
| "score": result.score,
|
| "source_quality": source_quality,
|
| "highlights": trimmed_highlights,
|
| "summary": _truncate(summary, SUMMARY_CHAR_LIMIT) if summary else None,
|
| }
|
|
|
|
|
| def search_immigration(
|
| query: str,
|
| *,
|
| num_results: int = DEFAULT_NUM_RESULTS,
|
| country: str | None = None,
|
| include_domains: list[str] | None = None,
|
| ) -> dict[str, Any]:
|
| """
|
| Search the web for immigration-related information using Exa.
|
|
|
| Returns result titles, URLs, and query-relevant highlights so the agent
|
| can follow up with Firecrawl for full official page content.
|
|
|
| Args:
|
| query: Natural-language search query describing the ideal page.
|
| num_results: Number of results to return (capped at 15).
|
| country: Optional ISO-2 country code for geo-targeted results.
|
| include_domains: Optional list of domains to restrict results to.
|
| """
|
| normalized_query = (query or "").strip()
|
| if not normalized_query:
|
| return {"error": "query is required", "query": query}
|
|
|
| capped_results = max(1, min(num_results, MAX_NUM_RESULTS))
|
|
|
| try:
|
| kwargs: dict[str, Any] = {
|
| "num_results": capped_results,
|
| "type": "auto",
|
| "contents": {
|
| "highlights": {"maxCharacters": HIGHLIGHT_CHAR_LIMIT},
|
| },
|
| }
|
|
|
| if country:
|
| normalized_country = country.strip().upper()
|
| if normalized_country:
|
| kwargs["user_location"] = normalized_country
|
|
|
| if include_domains:
|
| kwargs["include_domains"] = include_domains
|
|
|
| official_domain_hints = infer_official_domains(normalized_query, country)
|
|
|
| response = _client().search(normalized_query, **kwargs)
|
| results = [_result_dict(result) for result in (response.results or [])]
|
| return {
|
| "query": normalized_query,
|
| "num_results": len(results),
|
| "official_domain_hints": official_domain_hints,
|
| "official_results": sum(
|
| 1
|
| for result in results
|
| if result.get("source_quality", {}).get("is_official")
|
| ),
|
| "results": results,
|
| }
|
| except Exception as exc:
|
| return {"error": str(exc), "query": normalized_query}
|
|
|