Skip to main content

Context Preservation & Merging

This document explains how the system preserves context from previous conversation turns and merges it with new user input to provide coherent multi-turn gift recommendations.

Overview

When a user says "show me valentine gifts" and then follows up with "do you have something edible", the system must:

  1. Remember the valentine occasion from turn 1
  2. Detect that turn 2 is a refinement (not a new topic)
  3. Merge the preserved occasion with the new "edible" constraint
  4. Search for valentine-appropriate edible gifts

Pipeline Stages

Stage 1: Preprocess & Fetch Stored Context

File: orchestrators/context-orchestrator/stages/preprocess.ts

On each turn, the system fetches any stored context from the previous turn:

// Preliminary fetch to check if we have context to restore
const preliminaryStoredContext = await fetchStoredContext({
conversationId: request.conversationId,
convexClient,
// ...
});

This gives the pipeline access to what was discussed before.


Stage 2: Context Extraction (Current Turn)

File: services/context-understanding/enhanced-semantic-prompt.ts

The LLM extracts structured data from the current user message:

Turn 1: "show me valentine gifts"

{
"intent": "valentines_day_gift",
"occasion": "valentinipäev",
"productType": null,
"categoryHints": ["Kaasaegne romantika", "Luule", "Kruusid"],
"productTypeHints": ["Raamat", "Kodu ja aed", "Ilu ja stiil", "Joodav ja söödav"]
}

Turn 2: "do you have something edible"

{
"intent": "product_search",
"occasion": null,
"productType": "Joodav ja söödav",
"categoryHints": ["Maiustused", "Tee, kohv ja kakao"]
}

Note: Turn 2 extraction does not contain the valentine occasion because it wasn't mentioned in that message.


Stage 3: Followup Router

File: orchestrators/context-orchestrator/followup-router.ts

The followup router uses an LLM to classify the type of followup:

const FOLLOWUP_KINDS = [
'pure_show_more', // "show me more"
'soft_refinement', // "something cheaper"
'new_constraint', // "but edible" ← THIS CASE
'hard_pivot', // "actually I want books instead"
'question_about_shown' // "tell me more about the first one"
];

For "do you have something edible":

{
"followupKind": "new_constraint",
"confidence": 0.85,
"preserve": {
"context": true,
"categoryHints": true,
"excludes": true
}
}

The router determines that:

  • This is a new constraint (adding "edible")
  • Previous context should be preserved (valentine occasion)
  • This is NOT a hard pivot (topic change)

Stage 4: Fetch & Prepare Preservation

File: stages/preservation/fetch-and-prepare.ts

Extracts all preservable values from stored context:

const preservedOccasion = storedContext?.giftContext?.occasion;      // "valentinipäev"
const preservedRecipient = storedContext?.giftContext?.recipient;
const preservedConstraints = storedContext?.giftContext?.constraints;
const preservedCategoryHints = storedContext?.giftContext?.categoryHints;
const preservedBudget = storedContext?.giftContext?.budget;

Also determines if we should reset the exclude list (shown products):

let shouldReset = shouldResetExcludeList(
previousIntent,
giftContext.intent,
userMessage,
// ...
);

Stage 5: Apply Preservation (THE MERGE)

File: stages/preservation/apply-preservation.ts

This is where the magic happens. The system merges preserved context with current extraction:

Occasion Preservation (Lines 423-431)

if (preservedOccasion && !giftContext.occasion) {
giftContext.occasion = preservedOccasion;
if (debug) {
console.log('✅ PRESERVED OCCASION FROM CONTEXT:', {
occasion: preservedOccasion,
intent: giftContext.intent
});
}
}

Recipient Preservation (Lines 414-422)

if (preservedRecipient && (!giftContext.recipient || giftContext.recipient === 'hobi_harrastaja')) {
giftContext.recipient = preservedRecipient;
}

Category Hints Preservation (Lines 351-385)

const shouldApplyCategoryHints =
preservedCategoryHints?.length &&
!preservationBlockedForShowMoreSwitch &&
!hasCategoryExclusion &&
(shouldForcePreservation || !giftContext.categoryHints || giftContext.categoryHints.length === 0);

if (shouldApplyCategoryHints) {
giftContext.categoryHints = preservedCategoryHints;
}

Constraints Preservation (Lines 586-597)

if (preservedConstraints?.length && !preservationBlockedForShowMoreSwitch) {
const existingConstraints = giftContext.constraints ?? [];
const mergedConstraints = Array.from(new Set([...existingConstraints, ...preservedConstraints]));
giftContext.constraints = mergedConstraints;
}

Final Merged Context

After all preservation stages, turn 2's context becomes:

{
"intent": "product_search",
"occasion": "valentinipäev",
"productType": "Joodav ja söödav",
"categoryHints": ["Maiustused", "Tee, kohv ja kakao"],
"productTypeHints": ["Joodav ja söödav"],
"recipient": null,
"constraints": []
}

This merged context is then used for search, returning valentine-appropriate edible gifts (chocolates, tea, etc.) rather than just any food items.


Preservation Rules

What Gets Preserved

FieldPreserved When
occasionCurrent turn has no occasion
recipientCurrent turn has no/generic recipient
recipientAgeCurrent turn has no age
ageGroupCurrent turn has unknown age group
recipientGenderCurrent turn has unknown gender
budgetAlways merged
constraintsAlways merged (deduplicated)
categoryHintsCurrent turn has no hints AND not blocked
productTypeCurrent turn has no/generic type AND not blocked
authorNameAlways preserved for author searches

What Blocks Preservation

BlockerEffect
hard_pivot followupResets all preservation
Context switch detectedBlocks taxonomy preservation
Explicit exclusionBlocks that specific field
Type/category pivotBlocks productType preservation

Occasion-Aware Guards

File: stages/preservation/apply-preservation.ts (Lines 466-512)

The system applies occasion-aware guards to prevent inappropriate combinations:

// Valentine's Day should NOT suggest children's items
const OCCASION_INAPPROPRIATE_CATEGORY_PATTERNS = {
'valentinipäev': ['children', 'laste', 'abimaterjal', 'õpik', 'töövihik', 'school'],
'emadepäev': ['children', 'laste', 'abimaterjal', 'õpik'],
// ...
};

If a preserved category is inappropriate for the occasion, it gets filtered:

giftContext.categoryHints = (giftContext.categoryHints || []).filter(
h => !isChildCategory(h) && !isCategoryInappropriateForOccasion(giftContext.occasion, h)
);

Code Flow Summary

orchestrate.ts

├── preprocessOrchestration() → Fetch preliminary stored context

├── runFollowupRouter() → Detect followup type (new_constraint, etc.)

├── routeOrchestration() → Route query (author search, gift, etc.)

├── applyEnrichments() → Add occasion-based hints

├── fetchAndPreparePreservation() → Extract preservable values

├── applyShowMoreContextSwitch() → Handle "show more" special case

├── applyPreservation() → MERGE previous + current context

├── refineContext() → Negation, brainstorm, constraints

└── applyMemoryAndAuthorWorkflow() → Author resolution

Debugging

Enable debug logs to see preservation in action:

export CHAT_DEBUG_LOGS=true

Key log messages:

✅ PRESERVED OCCASION FROM CONTEXT: { occasion: "valentinipäev", intent: "product_search" }
✅ PRESERVED RECIPIENT FROM CONTEXT: { recipient: "kallim", intent: "product_search" }
✅ CATEGORY HINTS APPLIED TO GIFT CONTEXT: { categoryHints: [...], source: "preservedCategoryHints" }
🔄 CONTEXT PRESERVATION: { before: {...}, after: {...} }

Common Scenarios

Scenario 1: Valentine + Edible

Turn 1: "show me valentine gifts"
→ occasion: valentinipäev, productType: null

Turn 2: "do you have something edible"
→ Followup: new_constraint
→ Merged: occasion: valentinipäev, productType: Joodav ja söödav
→ Result: Valentine chocolates, tea, etc.

Scenario 2: Birthday + Cheaper

Turn 1: "gift for mom's birthday"
→ occasion: sünnipäev, recipient: ema

Turn 2: "something cheaper"
→ Followup: soft_refinement
→ Merged: occasion: sünnipäev, recipient: ema, budget: { max: 70% of avg }
→ Result: Same type of gifts, lower price range

Scenario 3: Topic Change (No Preservation)

Turn 1: "show me valentine gifts"
→ occasion: valentinipäev

Turn 2: "actually I need children's books"
→ Followup: hard_pivot
→ Merged: occasion: null (reset), productType: Raamat
→ Result: Children's books (no valentine context)


Last Updated: 2025-01-26
Version: 1.0