Skip to content

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

Resources