borderless / ui /agent /graph /nodes /discovery.py
spagestic's picture
Robust Planner Discovery Workflow
493a57c
Raw
History Blame Contribute Delete
3.5 kB
# ui/agent/graph/nodes/discovery.py
from __future__ import annotations
import json
from typing import Any
from langchain_core.runnables import RunnableConfig
from langgraph.config import get_stream_writer
from apis.exa import search_immigration
from ..state import AgentState, CandidateCountry
from .helpers import (
candidates_from_search_text,
discovery_queries,
heuristic_candidate_countries,
merge_candidates,
profile_summary_from_text,
user_text,
)
def _format_discovery_summary(
candidates: list[CandidateCountry],
search_notes: list[str],
) -> str:
lines = [
"Initial destination shortlist for parallel research:",
"",
]
for item in candidates:
lines.append(
f"- {item['name']} ({item['iso2']}): {item['pathway_hint']}"
)
if search_notes:
lines.extend(["", "Discovery search notes:"])
lines.extend(f"- {note}" for note in search_notes[:4])
lines.append(
"\nThese are starting candidates only — each country will be researched "
"in depth before the final recommendation."
)
return "\n".join(lines)
def discovery_node(state: AgentState, config: RunnableConfig) -> dict[str, Any]:
del config # discovery uses direct API calls, not the LLM
writer = get_stream_writer()
profile_text = user_text(state["user_content"])
profile_summary = profile_summary_from_text(profile_text)
heuristic = heuristic_candidate_countries(profile_text)
search_notes: list[str] = []
search_blob = ""
search_log: dict[str, Any] = {"queries": [], "results": []}
for query in discovery_queries(profile_text):
search_log["queries"].append(query)
try:
result = search_immigration(query=query, num_results=6)
search_log["results"].append(result)
titles = [
str(item.get("title") or "")
for item in result.get("results", [])
if isinstance(item, dict)
]
if titles:
search_notes.append(f"{query}{titles[0]}")
search_blob += "\n".join(
f"{item.get('title', '')} {item.get('url', '')}"
for item in result.get("results", [])
if isinstance(item, dict)
)
except Exception as exc:
search_notes.append(f"{query} → search unavailable ({exc})")
from_search = candidates_from_search_text(search_blob)
candidates = merge_candidates(from_search, heuristic)
if not candidates:
candidates = heuristic
summary = _format_discovery_summary(candidates, search_notes)
writer(
{
"type": "discovery",
"summary": summary,
"candidates": candidates,
"log": {
"tool": "destination_discovery",
"arguments": {"queries": search_log["queries"]},
"result": truncate_discovery_log(search_log),
},
}
)
return {
"profile_summary": profile_summary,
"candidate_countries": candidates,
"discovery_summary": summary,
}
def truncate_discovery_log(search_log: dict[str, Any]) -> Any:
serialized = json.dumps(search_log, default=str)
if len(serialized) <= 1500:
return search_log
return serialized[:1500] + "\n… (truncated)"