# ui/agent/graph/respond.py from __future__ import annotations import os import time from typing import Any import gradio as gr from gradio import ChatMessage from ui.globe_commands import apply_update_globe from ui.globe_details import attach_finding_to_globe, attach_research_progress_to_globe from ..messages import history_to_api_messages, multimodal_input_to_api_content from ..config import hub_token_available from ..respond import ( AUTH_REQUIRED_MESSAGE, SESSION_EXPIRED_MESSAGE, _auth_error_message, ) from ..streaming import yield_response from ..synthesis import build_structured_final_answer from .workflow import build_workflow def _format_plan_summary(todos: list[dict[str, Any]]) -> str: count = len(todos) if count == 0: return "No countries queued for research." noun = "country" if count == 1 else "countries" return f"{count} {noun} queued for parallel research." def _research_task_country_methods( research_task: dict[str, Any], ) -> tuple[str, str] | None: country = str(research_task.get("country") or "").strip() methods = str(research_task.get("methods") or "").strip() if country and methods: return country, methods title = str(research_task.get("title") or "").strip() if not title: return None from ui.globe_details import split_country_title country, methods = split_country_title(title) if country: return country, methods or "Skilled migration pathway" return None class _UiState: """Maps custom graph events onto the running ChatMessage list.""" def __init__(self, globe_state: dict[str, Any]): self.ui_messages: list[ChatMessage] = [] self.globe_state = globe_state self._pending_by_id: dict[str, int] = {} def _attach_research_progress(self, event: dict[str, Any]) -> None: research_task = event.get("research_task") if not research_task: return country_methods = _research_task_country_methods(research_task) if not country_methods: return country, methods = country_methods log = event.get("log") or {} self.globe_state = attach_research_progress_to_globe( self.globe_state, country=country, methods=methods, tool_name=str(log.get("tool") or ""), args=dict(log.get("arguments") or {}), message=str(event.get("message") or ""), ) def handle(self, event: dict[str, Any]) -> bool: kind = event.get("type") if kind == "thinking": thinking_text = str(event.get("text") or "").strip() self.ui_messages.append( ChatMessage( role="assistant", content=thinking_text, metadata={ "title": "Thinking", "status": "done", "display": "thinking", "thinking": thinking_text, }, ) ) return True if kind == "discovery": self.ui_messages.append( ChatMessage( role="assistant", content=str(event.get("summary") or ""), metadata={ "title": "Destination discovery", "status": "done", "compact": True, "log": event.get("log") or {}, }, ) ) return True if kind == "plan": todos = event.get("todos") or [] self.ui_messages.append( ChatMessage( role="assistant", content=_format_plan_summary(todos), metadata={ "title": "Research plan", "status": "done", "display": "plan", "plan_todos": todos, }, ) ) return True if kind == "globe": args = event.get("args") or {} countries = ", ".join(args.get("countries") or []) try: result, self.globe_state = apply_update_globe(self.globe_state, args) content = ( f"Marked {countries or 'recommended countries'} on the globe." ) status = "done" except Exception as exc: result = {"error": str(exc)} content = ( f"Could not update globe for {countries or 'recommended countries'}: " f"{exc}" ) status = "done" self.ui_messages.append( ChatMessage( role="assistant", content=content, metadata={ "title": "Updating globe", "status": status, "log": { "tool": "update_globe", "arguments": args, "result": result, }, }, ) ) return True if kind == "tool_start": research_task = event.get("research_task") self.ui_messages.append( ChatMessage( role="assistant", content=str(event.get("message") or ""), metadata={ "title": str(event.get("title") or "Tool"), "status": "pending", "log": event.get("log") or {}, "research_task": research_task, "compact": bool(research_task), }, ) ) self._pending_by_id[str(event.get("id"))] = len(self.ui_messages) - 1 self._attach_research_progress(event) return True if kind == "tool_end": index = self._pending_by_id.pop(str(event.get("id")), None) research_task = event.get("research_task") message = ChatMessage( role="assistant", content=str(event.get("message") or ""), metadata={ "title": str(event.get("title") or "Tool"), "status": "done", "duration": event.get("duration"), "log": event.get("log") or {}, "research_task": research_task, "compact": bool(research_task), }, ) if index is not None and index < len(self.ui_messages): self.ui_messages[index] = message else: self.ui_messages.append(message) self._attach_research_progress(event) return True if kind == "finding": research_task = event.get("research_task") or {} country_methods = _research_task_country_methods(research_task) if country_methods: country, methods = country_methods else: from ui.globe_details import split_country_title todo_title = str(event.get("todo_title") or "research") country, methods = split_country_title(todo_title) todo_label = f"{country} — {methods}" summary = str(event.get("summary") or "") self.globe_state = attach_finding_to_globe( self.globe_state, country=country, methods=methods, summary=summary, ) self.ui_messages.append( ChatMessage( role="assistant", content=summary, metadata={ "title": f"Findings · {todo_label}", "status": "done", "markdown": True, "research_task": research_task or { "id": event.get("todo_id"), "country": country, "methods": methods, }, "research_finding": True, }, ) ) return True return False def respond_with_graph( message: str | dict[str, Any], history: list[dict[str, Any]], system_message: str, max_tokens: int, temperature: float, top_p: float, globe_state: dict[str, Any], hf_token: gr.OAuthToken | None, ): """LangGraph-backed drop-in replacement for ui.agent.respond.respond.""" if hf_token is None and not hub_token_available(): yield from yield_response([], AUTH_REQUIRED_MESSAGE, globe_state) return if hf_token is not None and hf_token.expires_at <= time.time(): yield from yield_response([], SESSION_EXPIRED_MESSAGE, globe_state) return user_content = multimodal_input_to_api_content(message) if not user_content: yield from yield_response( [], "Please enter a message or attach a file.", globe_state, ) return workflow = build_workflow() ui_state = _UiState(globe_state) final_answer = "" last_state: dict[str, Any] = {} config = { "configurable": { "hf_token": ( hf_token.token if hf_token is not None else os.environ.get("HF_TOKEN", "") ), "max_tokens": max_tokens, "temperature": temperature, "top_p": top_p, }, "recursion_limit": 50, } graph_input = { "user_content": user_content, "history_messages": history_to_api_messages(history), "findings": [], } try: for mode, payload in workflow.stream( graph_input, config=config, stream_mode=["custom", "values"], ): if mode == "custom": if ui_state.handle(payload): yield list(ui_state.ui_messages), ui_state.globe_state elif mode == "values": final_answer = payload.get("final_answer") or final_answer last_state = payload except Exception as exc: error_message = _auth_error_message(exc) or ( f"Sorry, something went wrong while generating a response: {exc}" ) yield from yield_response( ui_state.ui_messages, error_message, ui_state.globe_state ) return if not final_answer: final_answer = build_structured_final_answer( profile_summary=str(last_state.get("profile_summary") or "").strip(), findings=last_state.get("findings") or [], todos=last_state.get("todos"), preamble=( "I could not complete model synthesis, but here is the structured " "research gathered so far." ), ) yield from yield_response( ui_state.ui_messages, final_answer, ui_state.globe_state, assistant_metadata={ "markdown": True, "thinking": str(last_state.get("consolidator_thinking") or "").strip(), }, )