matchmaking-v2: heartbeat progress during the long search

Emit a rotating progress message every ~5s while the Apify scrape + Opus curation run,
so the long (30-40s) silent gap can't idle the response relay and improves the wait UX.
This commit is contained in:
raulgupta
2026-06-22 12:53:40 +05:30
parent be921559e7
commit b72c34db2e

View File

@@ -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']}"