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:
- Remember the valentine occasion from turn 1
- Detect that turn 2 is a refinement (not a new topic)
- Merge the preserved occasion with the new "edible" constraint
- 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
| Field | Preserved When |
|---|---|
occasion | Current turn has no occasion |
recipient | Current turn has no/generic recipient |
recipientAge | Current turn has no age |
ageGroup | Current turn has unknown age group |
recipientGender | Current turn has unknown gender |
budget | Always merged |
constraints | Always merged (deduplicated) |
categoryHints | Current turn has no hints AND not blocked |
productType | Current turn has no/generic type AND not blocked |
authorName | Always preserved for author searches |
What Blocks Preservation
| Blocker | Effect |
|---|---|
hard_pivot followup | Resets all preservation |
| Context switch detected | Blocks taxonomy preservation |
| Explicit exclusion | Blocks that specific field |
| Type/category pivot | Blocks 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)
Related Documentation
- Context Extraction - LLM extraction details
- Gift Context System - Full context structure
- Intent Classification - Intent types
- Parallel Orchestrator - Pipeline execution
Last Updated: 2025-01-26
Version: 1.0