diff --git a/app/agent/session.py b/app/agent/session.py index 9e33066..37b32f0 100644 --- a/app/agent/session.py +++ b/app/agent/session.py @@ -24,6 +24,23 @@ class MatchmakingAgentSession: async def push(self, msg_type: str, **kwargs): await self.ws.send_json({"type": msg_type, **kwargs}) + async def _with_heartbeat(self, awaitable, messages: list[str], interval: float = 5.0): + """Await `awaitable` while emitting a rotating progress message every `interval`s. + + A live search runs ~30-40s (Apify scrape + Opus curation). Without a steady trickle of + messages, the Redis pub/sub channel that relays the result back to the browser idles out and + the final `search_complete` is dropped — which is why users were seeing 0 matches. The + heartbeat keeps that channel warm AND gives the user real progress during the wait. + """ + task = asyncio.ensure_future(awaitable) + i = 0 + while True: + done, _ = await asyncio.wait({task}, timeout=interval) + if task in done: + return task.result() + await self.push("agent_thinking", message=messages[i % len(messages)]) + i += 1 + async def _stub(self, action: str, note: str): await self.push("agent_data", action=action, data={"status": "not_implemented", "note": note}) @@ -102,7 +119,11 @@ class MatchmakingAgentSession: sig = _repo.search_signature(prefs) cursors = await _repo.get_search_cursors(self.user_id, sig) prefs["_cursors"] = cursors - sweep = await search.run_sweep(prefs, fresh=fresh) + # Heartbeat through the long Apify scrape so the relay channel stays alive (see _with_heartbeat). + sweep = await self._with_heartbeat( + search.run_sweep(prefs, fresh=fresh), + ["Scanning live roles across the boards…", "Pulling the latest Naukri + LinkedIn + Foundit roles…", + "Gathering fresh postings for you…"]) # Safety net: drop anything already shown / applied / dismissed (covers boards that can't # paginate, so the same posting never reappears even if a board re-returns it). seen = await _repo.get_seen_ids(self.user_id) @@ -117,8 +138,11 @@ class MatchmakingAgentSession: # Run the blocking LLM work (embeddings + Opus) OFF the event loop, or the long sync OpenAI # call freezes the loop and the Redis response can't publish (→ the loader hangs forever). top, dbg = await asyncio.to_thread(_sift.sift, prefs, user_context, candidates) - await self.push("agent_thinking", message="Scout is reading your top roles…") - curated = await asyncio.to_thread(_curate.curate, prefs, user_context, top) # Opus's call is final + # Heartbeat through the long Opus curation (the other 30-40s gap that was dropping responses). + curated = await self._with_heartbeat( + asyncio.to_thread(_curate.curate, prefs, user_context, top), # Opus's call is final + ["Scout is reading your top roles…", "Scoring fit + writing your report cards…", + "Ranking your strongest matches…", "Almost there…"]) if curated is None: # safety net: sift + templated cards curated = _rank.select(top, prefs.get("stretch", "balanced")) engine = f"fallback:{dbg['mode']}"