-Puter 98a4ec305e fix(events): normalize ingest URL, add source header, raise_for_status, dedupe keys
Grow event emitter fixes (wrong route / silent failure cannot recur):
- URL: /events/ingest -> /events/ingest/service (canonical service ingest path)
- Add x-growqr-source: matchmaking-service header
- Call resp.raise_for_status() so a 401 is caught and logged, not silently
  swallowed (401 is now observable; a wrong route surfaces immediately)
- Action-specific dedupe keys so retries collapse but later distinct user
  actions are NOT dropped:
    matches.generated  -> matchmaking:generated:{user}:{sig}
    feed.viewed        -> matchmaking:feed:{user}:{updated_at|none}
    match.viewed       -> matchmaking:viewed:{user}:{opp_id}
    match.applied      -> matchmaking:applied:{user}:{opp_id}  (already present)

All four emit handlers fire the event AFTER the persistent mutation
(set_opportunity_state / save_feed), so no event leaks when a mutation fails.

Tests (tests/test_grow_events.py, 12 cases, red->green TDD):
- URL normalizes to /events/ingest/service (with/without trailing slash)
- x-growqr-source header present
- 401 response raises_for_status -> logged warning; does not escape _post
- emit-after-mutation ordering; no emit when mutation raises
- dedupe keys stable for same evidence, distinct for different evidence
  across generated/feed/viewed/applied event types
2026-07-10 20:02:27 +05:30

matchmaking-v2 (Scout)

Fresh, on-demand matchmaking service replacing the dead nightly aggregator. Same agent-mesh mold as the other GrowQR services (FastAPI · a2a card · /a2a/tasks · orchestrator-routed). Not a drop-in — it keeps the existing 4 skills working and adds new actions from scratch.

Layout

app/
  main.py              FastAPI app (card + /a2a/tasks + /api/v1/health), lifespan worker
  config.py            lean settings (no corpus DB, no scrape schedule)
  a2a/                 card.py (discovery), auth.py (bearer), tasks.py (orchestrator entry)
  agent/session.py     Session: on_session_start / on_user_action dispatch  ← the brain
  adk/worker.py        Redis-Streams worker (graceful no-op without Redis)
  api/v1/health.py     /api/v1/health
  engine/
    board_adapters/    ScoutPrefs → Apify actor inputs (Naukri-first, verified maps)
    ...                the §3 cascade (normalize→filter→utility→fusion→rerank) lands here
  contracts/           Pydantic contracts (user_context, transport, …) — added per slice
research/              docs/ (ENGINE_DESIGN, SIGNAL_AUDIT_V2, ENGINE_INPUTS, …) + poc/ (auto-apply)

Contract (how it connects)

Frontend useAgentSession (page "job-matching") → orchestrator (routes by card name = matchmaking-service) → POST /a2a/tasks {action, params, user_context}Session pushes agent_data{action,data} → orchestrator → frontend latestData[action].

Skills: existing get_feed · sync_preferences · record_feedback · get_opportunity_detail; new run_search · tailor_resume · submit_application · get_apply_proof (stubbed). Each new action needs: card skill (here) + orchestrator action-map entry + frontend sendAction wiring.

Run (local)

pip install -r requirements.txt
uvicorn app.main:app --reload --port 8006
# card:    GET http://localhost:8006/.well-known/agent-card.json
# health:  GET http://localhost:8006/api/v1/health
# action:  POST http://localhost:8006/a2a/tasks  (Bearer dev-a2a-key)

Build order (full-stack slices)

  1. scaffold (this) — bootable skeleton, contract wired, handlers stubbed.
  2. on-demand run_search — board_adapters → Apify → engine cascade → ranked feed (+ frontend wire).
  3. feedback labels + record_feedback. 3. tailor_resume. 4. submit_application + get_apply_proof.
  4. cut over from old :8006, decommission corpus.
Description
GrowQR matchmaking v2 service synced from GitHub
Readme 9 MiB
Languages
Python 99.7%
Mako 0.2%
Dockerfile 0.1%