Personal Archivist
The Personal Archivist fosters personal connection through memory and perfect timing. It understands each user's planning journey and surfaces relevant vendors at exactly the right moment.
Overview
Mission: Foster personal connection through memory and timing
Key Insight: Wedding planning has distinct phases. The right vendor at the wrong time is the wrong vendor.
Phase: Introduced in Phase 2 (Week 7)
Planning Phase Intelligence
Phase Detection
enum PlanningPhase {
EARLY_DISCOVERY = 'early_discovery', // 12-18 months out
VENDOR_RESEARCH = 'vendor_research', // 6-12 months out
BOOKING = 'booking', // 3-6 months out
DETAIL_PLANNING = 'detail_planning', // 1-3 months out
FINAL_TOUCHES = 'final_touches' // 0-1 month out
}
async inferPlanningPhase(user: User): Promise<PlanningPhase> {
const monthsUntilWedding = this.getMonthsUntilWedding(user.weddingDate);
// Override with interaction signals
const recentInteractions = await this.getRecentInteractions(user, days: 30);
// What vendors are they engaging with?
const vendorTypes = recentInteractions.map(i => i.content.vendorType);
if (vendorTypes.includes('venue') && monthsUntilWedding > 6) {
return PlanningPhase.EARLY_DISCOVERY;
}
if (vendorTypes.includes('photographer') || vendorTypes.includes('videographer')) {
return PlanningPhase.VENDOR_RESEARCH;
}
if (recentInteractions.some(i => i.action === 'inquiry')) {
return PlanningPhase.BOOKING;
}
if (vendorTypes.includes('florist') || vendorTypes.includes('decor')) {
return PlanningPhase.DETAIL_PLANNING;
}
if (monthsUntilWedding < 1) {
return PlanningPhase.FINAL_TOUCHES;
}
// Fallback to timeline-based
if (monthsUntilWedding > 12) return PlanningPhase.EARLY_DISCOVERY;
if (monthsUntilWedding > 6) return PlanningPhase.VENDOR_RESEARCH;
if (monthsUntilWedding > 3) return PlanningPhase.BOOKING;
if (monthsUntilWedding > 1) return PlanningPhase.DETAIL_PLANNING;
return PlanningPhase.FINAL_TOUCHES;
}
Phase-Appropriate Recommendations
async getPhaseRelevantVendors(
user: User,
phase: PlanningPhase
): Promise<VendorType[]> {
const recommendations = {
[PlanningPhase.EARLY_DISCOVERY]: [
'venue', 'photographer', 'planner' // Core vendors
],
[PlanningPhase.VENDOR_RESEARCH]: [
'photographer', 'videographer', 'caterer', 'band'
],
[PlanningPhase.BOOKING]: [
'florist', 'decorator', 'makeup_artist', 'hair_stylist'
],
[PlanningPhase.DETAIL_PLANNING]: [
'stationery', 'favors', 'cake', 'transportation'
],
[PlanningPhase.FINAL_TOUCHES]: [
'day_of_coordinator', 'backup_vendors', 'rentals'
]
};
return recommendations[phase];
}
Timing Intelligence
Vendor Booking Windows
const OPTIMAL_BOOKING_WINDOWS = {
venue: { min: 12, max: 18, critical: 12 }, // 12-18 months
photographer: { min: 8, max: 12, critical: 9 }, // 8-12 months
videographer: { min: 6, max: 10, critical: 8 },
caterer: { min: 6, max: 9, critical: 6 },
florist: { min: 3, max: 6, critical: 4 },
makeup_artist: { min: 3, max: 6, critical: 4 },
band: { min: 6, max: 12, critical: 8 },
dj: { min: 3, max: 6, critical: 4 },
cake: { min: 2, max: 4, critical: 3 },
stationery: { min: 4, max: 6, critical: 5 }
};
async scoreTiming(
content: Content,
weddingDate: Date
): Promise<number> {
const monthsUntilWedding = this.getMonthsUntilWedding(weddingDate);
const window = OPTIMAL_BOOKING_WINDOWS[content.vendorType];
if (!window) return 0.5; // Unknown vendor type
// Perfect timing
if (monthsUntilWedding >= window.min && monthsUntilWedding <= window.max) {
return 1.0;
}
// Approaching critical window
if (monthsUntilWedding === window.critical) {
return 1.0; // Maximum urgency
}
// Too early
if (monthsUntilWedding > window.max) {
return 0.3; // Show a little, but not priority
}
// Too late (urgent!)
if (monthsUntilWedding < window.min) {
return 0.9; // High priority to book something
}
return 0.5;
}
Time-Sensitive Notifications
async identifyUrgentNeeds(user: User): Promise<UrgentNeed[]> {
const phase = await this.inferPlanningPhase(user);
const monthsUntil = this.getMonthsUntilWedding(user.weddingDate);
const urgentNeeds: UrgentNeed[] = [];
// Check each vendor type
for (const [vendorType, window] of Object.entries(OPTIMAL_BOOKING_WINDOWS)) {
const hasBooked = await this.hasBookedVendor(user, vendorType);
if (!hasBooked && monthsUntil <= window.critical) {
urgentNeeds.push({
vendorType,
urgency: 1.0 - (monthsUntil / window.critical), // Higher = more urgent
message: `Book your ${vendorType} soon! Only ${monthsUntil} months left.`,
recommendedVendors: await this.getTopVendors(user, vendorType, 5)
});
}
}
return urgentNeeds.sort((a, b) => b.urgency - a.urgency);
}
Style Evolution Tracking
Preference History
class StyleEvolutionTracker {
async trackStyleEvolution(user: User): Promise<StyleEvolution> {
const interactions = await this.getUserInteractions(user);
// Group by time period
const periods = {
month1: interactions.filter(i => i.daysAgo <= 30),
month2: interactions.filter(i => i.daysAgo > 30 && i.daysAgo <= 60),
month3: interactions.filter(i => i.daysAgo > 60 && i.daysAgo <= 90)
};
// Compute style embeddings per period
const styleEmbeddings = {
month1: await this.computeStyleEmbedding(periods.month1),
month2: await this.computeStyleEmbedding(periods.month2),
month3: await this.computeStyleEmbedding(periods.month3)
};
// Detect shifts
const shift1to2 = this.cosineSimilarity(
styleEmbeddings.month1,
styleEmbeddings.month2
);
const shift2to3 = this.cosineSimilarity(
styleEmbeddings.month2,
styleEmbeddings.month3
);
return {
currentStyle: styleEmbeddings.month1,
styleShift: shift1to2 < 0.8, // Significant change
direction: this.identifyStyleDirection(styleEmbeddings),
confidence: this.computeConfidence(interactions.length)
};
}
private identifyStyleDirection(
embeddings: Record<string, number[]>
): string {
// Compare to known style archetypes
const archetypes = {
modern: MODERN_STYLE_EMBEDDING,
bohemian: BOHEMIAN_STYLE_EMBEDDING,
classic: CLASSIC_STYLE_EMBEDDING,
rustic: RUSTIC_STYLE_EMBEDDING,
luxury: LUXURY_STYLE_EMBEDDING
};
// Find closest archetype for current style
let bestMatch = '';
let bestSimilarity = 0;
for (const [style, embedding] of Object.entries(archetypes)) {
const sim = this.cosineSimilarity(embeddings.month1, embedding);
if (sim > bestSimilarity) {
bestSimilarity = sim;
bestMatch = style;
}
}
return bestMatch;
}
}
Adaptive Recommendations
async computeStyleMatch(
content: Content,
styleHistory: StyleEvolution
): Promise<number> {
// Match to current style
const currentMatch = this.cosineSimilarity(
content.embedding,
styleHistory.currentStyle
);
// If style is shifting, also match the direction
if (styleHistory.styleShift) {
const directionMatch = this.matchesDirection(
content,
styleHistory.direction
);
// Blend current and direction
return currentMatch * 0.7 + directionMatch * 0.3;
}
return currentMatch;
}
Memory & Personalization
Remembering Preferences
class PersonalMemory {
async rememberPreference(
user: User,
preference: Preference
): Promise<void> {
await this.store({
userId: user.id,
type: preference.type,
value: preference.value,
strength: preference.strength, // How confident we are
source: preference.source, // Where we learned this
timestamp: Date.now()
});
}
async recallPreferences(
user: User,
context: string
): Promise<Preference[]> {
const allPreferences = await this.getUserPreferences(user);
// Filter by relevance to current context
const relevant = allPreferences.filter(p =>
this.isRelevant(p, context)
);
// Sort by strength and recency
return relevant.sort((a, b) => {
const scoreA = a.strength * this.recencyWeight(a.timestamp);
const scoreB = b.strength * this.recencyWeight(b.timestamp);
return scoreB - scoreA;
});
}
private recencyWeight(timestamp: number): number {
const daysAgo = (Date.now() - timestamp) / (1000 * 60 * 60 * 24);
return Math.exp(-0.01 * daysAgo); // Exponential decay
}
}
Milestone Reminders
async getMilestoneReminders(user: User): Promise<Reminder[]> {
const monthsUntil = this.getMonthsUntilWedding(user.weddingDate);
const milestones = [
{
months: 12,
message: "Time to book your venue and photographer!",
priority: 'high',
vendors: ['venue', 'photographer']
},
{
months: 6,
message: "Book your caterer and entertainment",
priority: 'high',
vendors: ['caterer', 'band', 'dj']
},
{
months: 3,
message: "Finalize flowers and beauty vendors",
priority: 'medium',
vendors: ['florist', 'makeup_artist', 'hair_stylist']
},
{
months: 1,
message: "Last chance for cake and stationery!",
priority: 'urgent',
vendors: ['cake', 'stationery']
}
];
return milestones
.filter(m => Math.abs(monthsUntil - m.months) < 0.5) // Within 2 weeks
.map(m => ({
...m,
recommendedVendors: this.getRecommendations(user, m.vendors)
}));
}
Scoring Implementation
Relevance Score
async identifyRelevantMoment(
user: User,
content: Content
): Promise<number> {
// 1. Planning phase relevance
const phase = await this.inferPlanningPhase(user);
const phaseRelevance = this.matchPhase(content, phase);
// 2. Timing relevance
const timingScore = await this.scoreTiming(
content,
user.weddingDate
);
// 3. Style match
const styleEvolution = await this.trackStyleEvolution(user);
const styleMatch = await this.computeStyleMatch(
content,
styleEvolution
);
// 4. Personal preferences
const preferences = await this.recallPreferences(
user,
content.vendorType
);
const preferenceMatch = this.matchPreferences(content, preferences);
// Weighted combination
return (
phaseRelevance * 0.3 +
timingScore * 0.3 +
styleMatch * 0.25 +
preferenceMatch * 0.15
);
}
API Endpoints
Get Planning Phase
Endpoint: GET /api/archivist/phase/:userId
Response:
{
"phase": "vendor_research",
"monthsUntilWedding": 8,
"confidence": 0.9,
"nextPhase": "booking",
"nextPhaseIn": "2 months"
}
Get Urgent Needs
Endpoint: GET /api/archivist/urgent/:userId
Response:
{
"urgentNeeds": [
{
"vendorType": "photographer",
"urgency": 0.85,
"message": "Book your photographer soon! Only 8 months left.",
"recommendedVendors": [...]
}
]
}
Performance
Targets
| Metric | Target |
|---|---|
| Phase detection accuracy | 85%+ |
| Timing relevance | 90%+ precision |
| Style tracking | 80%+ similarity |
| Latency | < 100ms |
Related Components
- Orchestrator - Uses timing scores
- Engagement Forecaster - Timing synergy
- Foundation Model - Style embeddings