Skip to content

Instantly share code, notes, and snippets.

@JosephMaxwell
Last active March 24, 2026 20:29
Show Gist options
  • Select an option

  • Save JosephMaxwell/8ceea2dc59076964db4848c34e4ba6a5 to your computer and use it in GitHub Desktop.

Select an option

Save JosephMaxwell/8ceea2dc59076964db4848c34e4ba6a5 to your computer and use it in GitHub Desktop.
Unified WebSocket Integration Guide — /ws/unified/ endpoint migration

Unified WebSocket Integration Guide

Overview

The unified endpoint /ws/unified/{session_id} replaces three separate WebSocket endpoints with a single connection that handles all flows:

Old Endpoint New Message Type Flow
/ws/search/{session_id} query Free-text search with streaming LLM response
/ws/category-recommendations/{session_id} start (with current_page) Category page → discovery questions → recommendations
/ws/product-recommendations/{session_id} start (with current_page) Product page → discovery questions → fit evaluation
(new) resolve_platform Platform resolution → optional handoff to product discovery

Key change: the frontend sends current_page on every request. The server resolves the URL to determine if it's a product page, category page, or content page, and routes to the correct agent automatically. No need for category_url or sku fields — just the page URL.


Migration Checklist

  1. Replace endpoint URLs (one connection instead of three)
  2. Send current_page (the page URL) and website_code on every start message
  3. Remove all client-side logic that determines "am I on a product page or category page?"
  4. Handle platform_resolved and interface_clarification messages (new)
  5. Remove endpoint-selection logic — the server routes automatically via entity resolution

Connection

wss://api.example.com/ws/unified/{session_id}
  • session_id: Client-generated unique ID (max 255 chars)
  • Origin header must match CORS configuration
  • Connection is rate-limited per IP

Heartbeat

Send {"type": "ping"} periodically. Server responds with {"type": "pong"} and sends its own pings every 30 seconds. Idle connections (no activity for 30s) are closed.

Website Code Binding

The first message containing website_code binds the session. All subsequent messages inherit that value. Sending a different website_code returns an error.

// First message sets website_code for the session
{"type": "start", "website_code": "https://example.com/", "current_page": "/shoes/running/"}

// Later messages can omit website_code (but should still send current_page)
{"type": "answer", "question_id": 1, "answer_id": 10, "current_page": "/shoes/running/"}

Page-Based Routing

Send current_page on start and resume messages. The server resolves the URL against the product and category database to determine the page type:

  • Product URL → routes to Product Discovery Agent (questions → fit evaluation)
  • Category URL → routes to Category Discovery Agent (questions → recommendations)
  • Unknown URL → returns error

The frontend never needs to know which agent to use — just send the URL the user is on.


Flow 1: Search (replaces /ws/search)

Client Sends

{
  "type": "query",
  "content": "best running shoes for flat feet",
  "website_code": "https://example.com/",
  "current_page": "/running-shoes/",
  "conversation_id": "conv-abc123",
  "metadata": {}
}
Field Required Notes
content Yes Query text, 1–2000 chars
website_code First message Binds session
current_page No Current page URL for context
conversation_id No Omit to create new conversation
metadata No Arbitrary metadata passed to the agent

Server Sends (in order)

← {"type": "session_id", "session_id": "abc123"}
← {"type": "conversation_id", "conversation_id": "conv-xyz"}
← {"type": "status", "status": "thinking"}
← {"type": "status", "status": "searching"}
← {"type": "token", "content": "Based on your needs, "}
← {"type": "token", "content": "I'd recommend..."}
← {"type": "status", "status": "formatting"}
← {"type": "answer", "content": "Full response text..."}
← {"type": "products", "content": [...]}
← {"type": "recommendations", "content": [...]}
← {"type": "discovery_question", "content": {...}}
← {"type": "metadata", "content": {...}}
← {"type": "done"}

Key messages to handle:

  • session_id / conversation_id — store for subsequent queries
  • token — append content to streaming response display
  • answer — complete response text (render as final answer)
  • products — product cards to display
  • recommendations — related product suggestions
  • discovery_question — follow-up question widget
  • done — streaming complete, re-enable input

Flow 2: Category Discovery (replaces /ws/category-recommendations)

User lands on a category page. The server asks discovery questions, then returns recommendations.

Step 1: Start

{
  "type": "start",
  "website_code": "https://example.com/",
  "current_page": "/running-shoes/"
}

The server resolves /running-shoes/ → recognizes it as a category → routes to Category Discovery Agent.

Optional: first_answer — if the first question was already shown on the page:

{
  "type": "start",
  "website_code": "https://example.com/",
  "current_page": "/running-shoes/",
  "first_answer": {
    "question_id": 1,
    "answer_id": 10,
    "answer_text": "Trail running"
  }
}

Server Response to Start

One of these sequences:

Normal (has questions):

← {"type": "question", "question": {...}, "question_number": 1, "is_last": false}

Expert user (questions skipped):

← {"type": "status", "status": "questions_complete"}
← {"type": "status", "status": "analyzing_preferences"}
← ... recommendation messages ...

No product type for this category:

← {"type": "status", "status": "no_product_type", "message": "No product type found for this category"}

Resuming existing session:

← {"type": "resumed", "answered_count": 2, "message": "Resuming from where you left off"}
← {"type": "question", "question": {...}, "question_number": 3, "is_last": true}

Step 2: Answer Questions

{"type": "answer", "question_id": 1, "answer_id": 10, "answer_text": "Trail running"}

Server responds with either the next question or terminal:

← {"type": "question", "question": {...}, "question_number": 2, "is_last": true}

or (when is_last was true on the previous question, or no more children):

← {"type": "status", "status": "questions_complete"}
← {"type": "status", "status": "analyzing_preferences"}
← {"type": "status", "status": "finalizing"}
← {"type": "recommendations", "products": [...], "fallback_used": false, ...}
← {"type": "done"}

Question Shape

{
  "type": "question",
  "question": {
    "id": 1,
    "question": "What will you primarily use these for?",
    "summary": "Usage type",
    "answers": [
      {
        "id": 10,
        "answer": "Trail running",
        "explanation": "Rugged terrain with uneven surfaces",
        "examples": "Mountain trails, forest paths"
      },
      {
        "id": 11,
        "answer": "Road running",
        "explanation": null,
        "examples": null
      }
    ]
  },
  "question_number": 1,
  "is_last": false
}

Use is_last to show "Get Recommendations" instead of "Next" on the last question.

Recommendations Shape

{
  "type": "recommendations",
  "products": [
    {
      "id": 123,
      "sku": "RUN-TRAIL-01",
      "name": "Trail Runner Pro",
      "display_name": "Trail Runner Pro X",
      "product_line": "Pro Series",
      "critical_attributes": [
        {"code": "weight", "name": "Weight", "value": "280g"}
      ],
      "price": 149.99,
      "url": "/products/trail-runner-pro/",
      "score": 0.95,
      "match_reasons": ["Excellent traction", "Lightweight"],
      "tag": {"value": "Recommended", "type": "recommended"},
      "benefits": ["Waterproof membrane", "Rock plate"],
      "use_cases": [{"name": "Trail Running", "score": 0.95}],
      "why": "Best match for trail running with superior grip",
      "variant_match": null,
      "review_insights": {
        "average_rating": 4.7,
        "review_count": 342,
        "summary": "Highly rated for durability and comfort",
        "likes": ["Great grip", "Comfortable fit", "Durable"]
      }
    }
  ],
  "fallback_used": false,
  "fallback_reason": null,
  "fallback_scope": null,
  "fallback_explanation": null
}

Tags: First product gets {"value": "Recommended", "type": "recommended"}, second gets {"value": "Runner Up", "type": "runner_up"}, rest get null.

Step 3 (optional): Resume

If the user navigates away and returns, send their previous answers for validation:

{
  "type": "resume",
  "website_code": "https://example.com/",
  "category_url": "/running-shoes/",
  "answered_intents": {
    "1": {"answer_id": 10, "answer_text": "Trail running"},
    "2": {"answer_id": 25, "answer_text": "Under 300g"}
  }
}

Server validates each answer against the current question tree and responds:

← {"type": "resumed", "answered_count": 2, "session_id": "abc123"}
← {"type": "question", ...}   // Next unanswered question

or if some answers are no longer valid:

← {"type": "resumed", "answered_count": 1, "invalidated_questions": ["2"], "reason": "Some questions or answers are no longer valid"}
← {"type": "question", ...}   // Re-ask from the invalidated point

Flow 3: Product Discovery (replaces /ws/product-recommendations)

User is on a product page. The server asks discovery questions, then evaluates product fit.

Step 1: Start

{
  "type": "start",
  "website_code": "https://example.com/",
  "current_page": "/products/trail-runner-pro/"
}

The server resolves /products/trail-runner-pro/ → recognizes it as a product → routes to Product Discovery Agent.


### Step 2: Answer Questions

Same as category flow. Questions include an additional `sort_order` field:

```json
{
  "type": "question",
  "question": {
    "id": 5,
    "question": "What distances do you run?",
    "summary": null,
    "sort_order": 2,
    "answers": [...]
  },
  "question_number": 1,
  "is_last": false
}

Step 3: Fit Evaluation (automatic after terminal)

After the last answer, the server automatically evaluates product fit:

← {"type": "status", "status": "questions_complete"}
← {"type": "status", "status": "processing"}
← {"type": "fit_evaluation", ...}
← {"type": "done"}

Fit Evaluation Shape

{
  "type": "fit_evaluation",
  "current_product": {
    "id": 123,
    "sku": "RUN-TRAIL-01",
    "name": "Trail Runner Pro",
    "display_name": "Trail Runner Pro X",
    "product_line": "Pro Series",
    "critical_attributes": [
      {"code": "weight", "name": "Weight", "value": "280g"}
    ],
    "price": 149.99,
    "url": "/products/trail-runner-pro/",
    "tags": [{"value": "Good Fit", "type": "recommended"}],
    "variant_match": null,
    "review_insights": {
      "average_rating": 4.7,
      "review_count": 342,
      "summary": "Highly rated for durability",
      "likes": ["Great grip", "Comfortable"]
    }
  },
  "fit_evaluation": {
    "is_good_fit": true,
    "fit_score": 0.87,
    "fit_threshold": 0.6
  },
  "fit_explanation": {
    "why_good_fit": "Excellent traction for trail running with lightweight design",
    "why_not_good_fit": null
  },
  "alternative": {
    "id": 456,
    "sku": "RUN-ULTRA-02",
    "name": "Ultra Trail X",
    "display_name": "Ultra Trail X",
    "product_line": "Ultra Series",
    "critical_attributes": [...],
    "price": 189.99,
    "url": "/products/ultra-trail-x/",
    "score": 0.92,
    "match_reasons": ["Better cushioning", "More durable"],
    "why_better_for_you": ["Superior cushioning for long distances"],
    "tags": [],
    "variant_match": null,
    "review_insights": {...}
  },
  "fallback_reason": null
}

Tags on current_product: "Good Fit" / "recommended" if is_good_fit is true, "Poor Fit" / "warning" if false.

Tags on alternative: "Top Pick" / "recommended" only when the current product is a poor fit.

Direct Fit Evaluation (skip questions)

For expert users or when questions aren't needed:

{
  "type": "evaluate_fit",
  "website_code": "https://example.com/",
  "sku": "RUN-TRAIL-01",
  "discovery_answers": {
    "1": {"answer_id": 10, "answer_text": "Trail running"}
  }
}

Flow 4: Platform Resolution (new)

Resolve an unknown platform (e.g., user's equipment) to find compatible products.

Step 1: Resolve

{
  "type": "resolve_platform",
  "website_code": "https://example.com/",
  "make": "Acme",
  "model": "X-2000",
  "handoff_target": "product_discovery",
  "sku": "ACC-MOUNT-01"
}

handoff_target is optional. When set, the server automatically chains into that agent after resolution.

Server Response — Auto-resolved (no user input needed)

← {"type": "platform_resolved", "platform_id": 42, "archetype_id": 10}

If handoff_target was set, the product discovery flow starts automatically (questions appear next).

Server Response — Needs Clarification (Tier 3)

{
  "type": "interface_clarification",
  "archetype_id": 10,
  "question": "Which interfaces does your Acme X-2000 have?",
  "options": [
    {"id": 1, "label": "Standard Mount", "description": "Universal mounting system"},
    {"id": 2, "label": "Quick-Release", "description": "Tool-free attachment"},
    {"id": 3, "label": "Threaded Base", "description": "Screw-in attachment"}
  ],
  "make": "Acme",
  "model": "X-2000"
}

Step 2: Select Interfaces

{
  "type": "select_interfaces",
  "selected_interface_ids": [1, 3],
  "archetype_id": 10,
  "make": "Acme",
  "model": "X-2000",
  "website_code": "https://example.com/"
}

Server responds with platform_resolved (and optional handoff to product discovery).

Server Response — No Archetype Found

← {"type": "platform_resolved", "platform_id": null, "archetype_id": null}

The frontend should handle this gracefully — show a "we couldn't identify your platform" message.


Routing Rules

The server automatically determines which agent handles each message. For start and resume, the server resolves current_page against the entity database to determine the page type.

Message Type Routing
query Always → Search Agent
start / resume (with current_page) Resolved via entity DB: product URL → Product Agent, category URL → Category Agent
start (with explicit sku) → Product Discovery Agent (backward compat)
start (with explicit category_url) → Category Discovery Agent (backward compat)
answer → Whichever agent is currently active
get_recommendations → Category Discovery Agent
evaluate_fit → Product Discovery Agent
resolve_platform → Platform Resolver Agent
select_interfaces → Platform Resolver Agent
ping Handled directly (not routed to agent)

Concurrency

The server processes one message at a time per connection. If a message arrives while a previous one is still processing:

← {"type": "error", "content": "Still processing previous message"}

Wait for done or the final response before sending the next message.


Error Handling

All errors follow this shape:

{"type": "error", "content": "Human-readable error message"}

Some errors include an error_code for programmatic handling:

{"type": "error", "content": "website_code is required", "error_code": "INVALID_PAYLOAD"}
Error Code Meaning
INVALID_PAYLOAD Missing or malformed required fields
INVALID_WEBSITE Website code not recognized
INVALID_CATEGORY Category not found
INVALID_PRODUCT Product not found

Old → New Migration Map

/ws/search/{session_id}/ws/unified/{session_id}

No message changes. Same query type, same response stream.

/ws/category-recommendations/{session_id}/ws/unified/{session_id}

Replace category_url with current_page in start messages. The server resolves the URL automatically.

- {"type": "start", "website_code": "...", "category_url": "/shoes/"}
+ {"type": "start", "website_code": "...", "current_page": "/shoes/"}

Response messages are identical.

/ws/product-recommendations/{session_id}/ws/unified/{session_id}

Replace sku / current_page product resolution with just current_page. The server resolves the URL to find the product.

- {"type": "start", "website_code": "...", "sku": "SHOE-001"}
+ {"type": "start", "website_code": "...", "current_page": "/products/shoe-001/"}

Response messages are identical.

Backward Compatibility

The old field names (sku, category_url, category_id) still work as fallbacks. You can migrate incrementally — start sending current_page alongside the old fields, then remove the old fields once verified.

Feature Flag

The backend supports UNIFIED_WS_ENABLED=true to route /ws/search queries through the unified orchestrator. This allows gradual rollout of the search flow without changing the URL.


Sequence Diagrams

Category Discovery

Client                          Server
  |                               |
  |--- start {current_page} ---->|
  |     (resolved as category)    |
  |<---- question (Q1) ----------|
  |                               |
  |--- answer {q1, a1} --------->|
  |                               |
  |<---- question (Q2) ----------|
  |                               |
  |--- answer {q2, a2} --------->|
  |                               |
  |<---- status: questions_complete
  |<---- status: analyzing_preferences
  |<---- status: finalizing ------|
  |<---- recommendations --------|
  |<---- done -------------------|

Product Discovery

Client                          Server
  |                               |
  |--- start {current_page} ---->|
  |     (resolved as product)     |
  |<---- question (Q1) ----------|
  |                               |
  |--- answer {q1, a1} --------->|
  |                               |
  |<---- status: questions_complete
  |<---- status: processing ------|
  |<---- fit_evaluation ----------|
  |<---- done -------------------|

Platform Resolution → Product Discovery

Client                          Server
  |                               |
  |--- resolve_platform -------->|
  |         {make, model,         |
  |          handoff: product}    |
  |                               |
  |<---- interface_clarification -|
  |         {options: [...]}      |
  |                               |
  |--- select_interfaces ------->|
  |         {selected_ids: [...]} |
  |                               |
  |<---- platform_resolved -------|
  |         {platform_id: 42}     |
  |                               |
  |   (automatic handoff)         |
  |                               |
  |<---- question (Q1) ----------|
  |                               |
  |--- answer {q1, a1} --------->|
  |       ...                     |
  |<---- fit_evaluation ----------|
  |<---- done -------------------|

Search

Client                          Server
  |                               |
  |--- query {content} --------->|
  |                               |
  |<---- session_id --------------|
  |<---- conversation_id --------|
  |<---- status: thinking --------|
  |<---- token: "Based on..." ---|
  |<---- token: "I recommend..." |
  |<---- answer: "Full text..." -|
  |<---- products: [...] --------|
  |<---- recommendations: [...] -|
  |<---- discovery_question: {..}|
  |<---- done -------------------|
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment